> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duraton.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

```ts theme={null}
workflow<{ 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));
  },
});
```

<ResponseField name="event" type="{ name: string; data: TData }" required>
  The triggering event; event.data has the type passed to workflow.
</ResponseField>

<ResponseField name="step" type="Step" required>
  The durable step API - see below.
</ResponseField>

<ResponseField name="webhook" type="Webhook" required>
  ctx.webhook.send(id, \{ url, data? }): a durable outbound POST with retries and a per-attempt log.
</ResponseField>

<ResponseField name="log" type="Logger" required>
  Structured, replay-safe logging: ctx.log(msg, fields) plus .debug/.info/.warn/.error. See Logging.
</ResponseField>

<ResponseField name="runId" type="string" required>
  This run id.
</ResponseField>

<ResponseField name="attempt" type="number" required>
  Retry attempt, starting at 1 and increasing on each retry.
</ResponseField>

<ResponseField name="app" type="string" required>
  The app this run belongs to.
</ResponseField>

<ResponseField name="runner" type="string" required>
  The runner this run is pinned to; the empty string when the run is anycast.
</ResponseField>

<ResponseField name="events" type="Array<{ name: string; data: TData }>">
  Set only on a batched workflow: the coalesced events. event is events\[0].
</ResponseField>

<ResponseField name="error" type="StepError">
  Set only inside an onFailure handler: the terminal error that failed the source run.
</ResponseField>

## 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.

<ResponseField name="run" type="<T>(id, fn) => Promise<T>" required>
  Run fn once and memoize its result under id. Overload: run(id, input, fn) also records an explicit input.
</ResponseField>

<ResponseField name="ai" type="AIStep" required>
  Durable model calls: generate, wrap, embed, loop. See AI steps.
</ResponseField>

<ResponseField name="skip" type="(id, reason?: string) => Promise<void>" required>
  Record a deliberately bypassed step as a terminal "skipped" step; the optional reason is stored as its output. Durable: id must be stable across replays.
</ResponseField>

<ResponseField name="sleep" type="(id, duration: string | number) => Promise<void>" required>
  Suspend the run for a relative duration - a string like "30s"/"1h", or a number of ms.
</ResponseField>

<ResponseField name="sleepUntil" type="(id, at: Date | string | number) => Promise<void>" required>
  Suspend the run until an absolute time - a Date, an ISO string, or epoch ms.
</ResponseField>

<ResponseField name="waitForEvent" type="<T>(id, { event, timeout, if? }) => Promise<T | null>" required>
  Suspend until a matching event arrives, returning its data; resolves to null when timeout elapses first. Optional CEL if filters on the event payload so the run resumes only on the correlated event.
</ResponseField>

<ResponseField name="poll" type="<T>(id, probe, { every, timeout, until?, maxChecks? }) => Promise<T>" required>
  Re-check an external resource until it is ready, sleeping durably between checks, without spending the step's retry budget. Returns the ready value; throws PollTimeoutError when the deadline passes.
</ResponseField>

<ResponseField name="runWorkflow" type="<T>(id, { name, app?, runner?, data?, tags? }) => Promise<T>" required>
  Invoke another workflow as a child run and wait for its result.
</ResponseField>

<ResponseField name="emit" type="(id, { name, app?, data? }) => Promise<void>" required>
  Emit an event from inside a run.
</ResponseField>

<ResponseField name="approval" type="<A>(id, { tool, args?, risk?, summary?, policy?, context?, allow?, escalatesTo?, timeout?, onTimeout? }) => Promise<ApprovalResult<A>>" required>
  Park the run on a human decision and resolve to it on resume. onTimeout says what the timeout does: escalate (default), approve, reject, or fail.
</ResponseField>

### 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`.

```ts theme={null}
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.

```ts theme={null}
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.

```ts theme={null}
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.

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

<ResponseField name="every" type="string | number" required>
  Delay between checks - a duration string like "5s" or a number of milliseconds.
</ResponseField>

<ResponseField name="timeout" type="string | number" required>
  Overall deadline for the whole wait. Once it passes, poll throws PollTimeoutError.
</ResponseField>

<ResponseField name="until" type="(value: T) => boolean">
  Readiness predicate. When omitted, a non-null probe value is treated as ready.
</ResponseField>

<ResponseField name="maxChecks" type="number">
  Safety cap on the number of checks.
</ResponseField>

A probe that throws is a genuine error, not a not-ready signal: it retries under the step's
[retry policy](/core/retries) 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](/core/steps#step-poll).

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](/reference/api/runs#run-tags) to the child - it also inherits
the parent run's tags, with the child's value winning on a shared key.

```ts theme={null}
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.

```ts theme={null}
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. A `timeout` sets
a deadline and `onTimeout` says what reaching it does - by default it escalates and keeps waiting.
See [Approvals](/ai/approvals).

```ts theme={null}
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.

```ts theme={null}
import { hashStepId } from "@duraton/sdk";

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