Pausing and resuming

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.

The hook

pause() returns three things:

FieldWhat it does
resumeUrlPOST here to continue the run
cancelUrlPOST here to abandon it (the execution becomes CANCELLED)
expiresAtAfter 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.

The pause takes effect after the step finishes

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.

Reading what the resume delivered

The resume request's body is available on re-entry, in whatever shape it arrived:

The caller postedRead it with
JSONgetResumeData()
multipart/form-datagetResumeData() for fields, getResumeFiles() / getResumeFile(name) for uploads
Anything elsegetResumeBytes() 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"];

Waiting without pausing

Pausing is for waiting on something else. To simply wait, do not pause:

Both re-queue the execution rather than holding a worker open, so a long wait is free. See Retries and failure.

One control at a time

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.

Next