Some work cannot finish in one go: a human has to approve something, a third party has to call
you back, a batch job elsewhere has to finish. this.pause() suspends the execution until
somebody resumes it.
async requestApproval() {
const hook = await this.pause();
await fetch("https://chat.example.com/messages", {
method: "POST",
body: JSON.stringify({
text: `Approve order ${this.payload.id}?`,
approve: hook.resumeUrl,
reject: hook.cancelUrl,
}),
});
}
async afterApproval() {
const decision = this.getResumeData<{ approved: boolean }>("requestApproval");
if (!decision?.approved) this.cancel();
}
The execution is dequeued while paused. It occupies no worker and consumes no time limit — a run can sit paused for days without costing anything.
pause() returns three things:
| Field | What it does |
|---|---|
resumeUrl | POST here to continue the run |
cancelUrl | POST here to abandon it (the execution becomes CANCELLED) |
expiresAt | After this instant the paused execution is auto-cancelled |
Set the window yourself when the default is wrong:
const hook = await this.pause({ expiresInMs: 7 * 24 * 60 * 60 * 1000 }); // a week
Both URLs are unguessable and specific to this one execution.
pause() marks the intent; the step keeps running to its end. That ordering is what makes the
example above work — the hook URLs have to be sent somewhere before the run suspends, and the
same step does both.
The paused step is re-entered on resume, so anything after the pause() call runs twice. Keep a
paused step small: mark, notify, return.
The resume request's body is available on re-entry, in whatever shape it arrived:
| The caller posted | Read it with |
|---|---|
| JSON | getResumeData() |
multipart/form-data | getResumeData() for fields, getResumeFiles() / getResumeFile(name) for uploads |
| Anything else | getResumeBytes() or getResumeStream(), described by getResumeInfo() |
Either way getResumeMeta() carries the resuming request's method, headers and query — so
an approver's identity or a correlation id travels with the call.
All of these default to the current step. A run that paused more than once can name an earlier
one: this.getResumeData("requestApproval").
That combination is what an approval callback usually needs — the decision as JSON, the signed document as a file, and the caller's identity in the headers:
const decision = this.getResumeData<{ approved: boolean }>();
const signed = this.getResumeFile("contract");
const who = (this.getResumeMeta() as { headers: Record<string, string> }).headers["x-user"];
Pausing is for waiting on something else. To simply wait, do not pause:
this.delay(ms) — finish this step, then wait before the next one.this.rerun(ms) — run this step again after the delay. This is the polling loop, and the
right answer to a rate limit.Both re-queue the execution rather than holding a worker open, so a long wait is free. See Retries and failure.
pause, cancel and exit all end the current step's run in different ways, and they cannot be
combined. Calling a second one after the first throws — an execution cannot both stop and wait.
PAUSED looks like from the
outside.