Steps

Everything a handler can do durably: the step API - run, sleep, sleepUntil, waitForEvent, runWorkflow, emit, and approval - plus the handler context.

The handler context

Every handler receives a StepContext: the triggering event, the durable step API, and the run's metadata.

defineWorkflow<{ ticketId: string }>({
  name: "ticket.created",
  handler: async (ctx) => {
    ctx.log.info("refunding", { ticketId: ctx.event.data.ticketId, attempt: ctx.attempt });
    return ctx.step.run("refund", () => issueRefund(ctx.event.data.ticketId));
  },
});

Prop

Type

step

Every step takes a stable id, unique within the workflow. The result is recorded under that id, and on replay after a crash or retry a completed step returns its saved result instead of running again.

Prop

Type

run

The unit of durable work: fn executes once and its result is memoized under id. The three-argument overload also records an explicit input, which the run inspector shows on the step's Input tab and hands to fn.

const triage = await ctx.step.run("triage", () => classify(ctx.event.data.subject));

const reply = await ctx.step.run("reply", { ticketId, tone: "apologetic" }, (input) =>
  drafts.create(input.ticketId, input.tone),
);

sleep and sleepUntil

The run is suspended, not blocked: it holds no worker while it waits, and it survives a restart.

await ctx.step.sleep("cool-off", "30s");
await ctx.step.sleepUntil("follow-up", new Date("2026-08-01T00:00:00Z"));

waitForEvent

Suspends until an event with the given name arrives, returning its data - or null when timeout elapses first, which is how you branch on the timeout.

const paid = await ctx.step.waitForEvent<{ amount: number }>("await-payment", {
  event: "payment.received",
  timeout: "24h",
});
if (paid === null) return ctx.step.run("expire", () => expireOrder(ticketId));

poll

Re-checks an external resource until it is ready, sleeping durably between checks. The probe reads the resource and returns its value once ready, or a "not ready yet" signal otherwise; a not-ready check is a normal successful read, so it never spends the step's retry budget. Returns the ready value, or throws PollTimeoutError when timeout elapses first.

const record = await ctx.step.poll("provision", () => fetchRecordOrNull(), {
  every: "5s",
  timeout: "10m",
  until: (v) => v != null,
});

Prop

Type

A probe that throws is a genuine error, not a not-ready signal: it retries under the step's retry policy and fails the run if it exhausts its attempts. A PollTimeoutError fails the run and routes to onFailure; wrap the call in try/catch to treat a missed deadline as a non-fatal branch instead. See Poll until ready.

Each check costs two durable steps (a step.run probe plus a step.sleep gap), not a free suspension - every: "5s" over timeout: "10m" is up to 120 checks, 240 durable steps. Pick the widest every the resource's provisioning time tolerates.

runWorkflow

Invokes another workflow as a linked child run and waits for its result. Omit app to resolve the name in the caller's app first, then any app in the project; set runner to pin the child to a specific runner. Pass tags to attach run tags to the child - it also inherits the parent run's tags, with the child's value winning on a shared key.

const score = await ctx.step.runWorkflow<{ risk: number }>("risk-check", {
  name: "fraud.score",
  app: "risk",
  data: { ticketId },
  tags: { stage: "risk-check" },
});

emit

Emits an event from inside a run, which fans out to whatever triggers match it. Omit app to broadcast project-wide; set it to narrow the event to one app's triggers.

await ctx.step.emit("notify", {
  name: "ticket.shipped",
  app: "notifications",
  data: { ticketId, carrier: "ups" },
});

approval

Parks the run on a human decision. The run suspends in needs_attention - checkpoint kept, no worker held - until it is approved or denied, then resolves. args on the result are the effective arguments: the decider's edits when they changed them, otherwise the proposed ones. See Approvals.

const decision = await ctx.step.approval<{ amount: number }>("refund-gate", {
  tool: "issue-refund",
  args: { priority: "high" },
  risk: "high",
  summary: "Refund ticket A1 in full",
});
if (decision.status === "approved") {
  await ctx.step.run("refund", () => stripe.refunds.create({ amount: decision.args.amount }));
}

hashStepId

Returns the stable hash Duraton keys a step's memoized result by. It is exposed for tooling that correlates a step id with its recorded entry.

import { hashStepId } from "@duraton/sdk";

const key = hashStepId("triage"); // the hash the "triage" step's result is stored under

On this page