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

> Wrap each unit of work in a step and it runs once: the result is saved, it retries on its own, and the run resumes from it after a crash.

Steps are how a workflow does durable work. Each step runs once, its result is saved, and it becomes
a checkpoint the workflow can resume from. Wrap every unit of real work in a step.

Every step takes a unique **id** as its first argument; Duraton keys the saved result by it.

| Method                        | Returns                      | Purpose                                                                                                   |
| ----------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------- |
| `step.run(id, fn)`            | the fn's value               | Run a function once and memoize its result.                                                               |
| `step.skip(id, reason?)`      | `void`                       | Record a deliberately bypassed step as terminal `skipped` (with an optional reason).                      |
| `step.sleep(id, duration)`    | `void`                       | Pause durably for a duration.                                                                             |
| `step.sleepUntil(id, at)`     | `void`                       | Pause durably until an absolute time.                                                                     |
| `step.waitForEvent(id, opts)` | the event payload, or `null` | Pause until a named event arrives or the timeout elapses.                                                 |
| `step.poll(id, probe, opts)`  | the ready value              | Re-check an external resource until it is ready, without spending the step's retry budget.                |
| `step.runWorkflow(id, opts)`  | the child's result           | Invoke another workflow as a child run and wait for it.                                                   |
| `step.emit(id, opts)`         | `void`                       | Publish an event from inside a run.                                                                       |
| `step.approval(id, opts)`     | the decision                 | Park the run until a human approves or rejects. See [Approvals](/ai/approvals).                           |
| `step.ai.*`                   | the call's result            | Model calls as durable, metered steps: `generate`, `wrap`, `embed`, `loop`. See [AI steps](/ai/ai-steps). |

The console renders a run's steps three ways - a **list**, a **flow** graph, and a **timeline** on a
shared time axis, where a long `sleep` or `waitForEvent` shows as a gap and parallel steps overlap.

<Frame>
  <img src="https://mintcdn.com/duraton-950ff1ca/C6sntRFXqHtJQroE/images/step-flow.png?fit=max&auto=format&n=C6sntRFXqHtJQroE&q=85&s=0acc7948d344d7529b798a176bbb96d1" alt="A run's steps as a flow graph" width="3200" height="2000" data-path="images/step-flow.png" />
</Frame>

## `step.run`

Run a function once and remember its result.

```ts theme={null}
const triage = await ctx.step.run("triage", () => triageTicket(ticket));
```

The first time, the function runs and its return value is saved. On any later pass, `step.run`
returns the saved value without running the function again. The return value is whatever your
function returns (it must be JSON-serializable, since it's stored).

Put anything with a side effect or a changing result inside a `step.run` - API calls, database
writes, payments, reading the clock. See [Durable execution](/core/durable-execution) for
why.

### Recording a step's input

Pass an explicit input to record it on the step, so it shows on the console's **Input** tab. The same
value is also handed to the function:

```ts theme={null}
const triage = await ctx.step.run("triage", { ticketId, amount: 4200 }, (input) =>
  triageTicket(input.ticketId, input.amount),
);
```

The recorded input shows on the step's **Input** tab in the console. It is optional: the bare
`step.run(id, fn)` form captures no input (its arguments live in the function's closure, which Duraton cannot see). The structural steps below record their input automatically - a `runWorkflow`'s
child input, an `emit`'s payload, a `waitForEvent`'s event and timeout, a `sleep`'s duration - so the
Input tab is backed wherever a step has a meaningful input.

## `step.skip`

Record a step you deliberately bypassed. Without it, a conditionally-omitted step simply never
appears, so a stage you chose not to run is indistinguishable from one that never existed. `step.skip`
records a terminal `skipped` step - with an optional reason - so the bypass is visible in the run's
steps, the flow graph, and the timeline.

```ts theme={null}
if (triage.priority !== "high") {
  await ctx.step.skip("page-on-call", "not high priority");
} else {
  await ctx.step.run("page-on-call", () => pageOnCall(ticket));
}
```

The reason is stored as the step's output. `step.skip` is durable and replay-safe (the `id` must be
stable across replays), and is available in the TypeScript SDK.

## `step.sleep`

Pause the workflow for a duration. The wait is durable: the process can restart during it and the
run still wakes up on time.

```ts theme={null}
await ctx.step.sleep("wait-for-settlement", "1h");
```

The duration is a string like `"10s"`, `"5m"`, `"1h"`, or a number of milliseconds. Sleeps can be
short or span days - Duraton owns the schedule, so nothing has to stay running in the meantime.

## `step.sleepUntil`

Pause until an absolute instant rather than for a relative duration. Use it when the wake time is a
fixed wall-clock target - midnight, a billing date, a scheduled send.

```ts theme={null}
await ctx.step.sleepUntil("resume-on-renewal", subscription.renewsAt);
```

| Argument | Type                       | Description                                                                  |
| -------- | -------------------------- | ---------------------------------------------------------------------------- |
| `at`     | `Date \| string \| number` | The absolute wake time: a `Date`, an ISO 8601 string, or epoch milliseconds. |

Reach for `sleep` when you mean "wait this long" and `sleepUntil` when you mean "wait until this
moment." Computing `target - Date.now()` to fake an absolute wait is wrong: it reads the clock outside
a step. A target already in the past wakes immediately.

## `step.waitForEvent`

Pause until a named event arrives, or until the timeout elapses. Returns the event's data on arrival,
or `null` on timeout.

```ts theme={null}
const approval = await ctx.step.waitForEvent("await-approval", {
  event: "ticket.approved",
  timeout: "24h",
});
if (approval === null) return { status: "expired" };
```

| Option    | Type               | Description                                                                                                                                                      |
| --------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event`   | `string`           | The event name that resumes this step.                                                                                                                           |
| `timeout` | `string \| number` | How long to wait before resolving to `null`.                                                                                                                     |
| `if`      | `string`           | Optional [CEL](/core/triggers#filters) predicate on the event payload. The run resumes only on an event whose name matches **and** whose payload satisfies `if`. |

An incoming event resumes every run waiting on that name. An event that arrives *shortly before* the
run parks still wakes it: on parking, the step also looks back over recently-received events and
resumes immediately if a matching one already arrived. This closes the race where a fast responder
emits its event before the waiting run reaches its `waitForEvent`, so a request/response pattern never
waits out its full timeout just because the reply came back first.

The look-back is bounded: only events received within a short window before the park - **60 seconds** -
and never older than the run itself are considered, and the same `if` predicate below still applies, so
a stale or unrelated event of the same name never wakes the wrong run. The window is fixed platform-wide
and is not a per-call, per-workflow, or per-project knob.

### Correlated waits

Use `if` to wait for *the* event that belongs to this run, rather than any event of that name. The
predicate uses the same CEL dialect as [event trigger filters](/core/triggers#filters) -
`event.name` and `event.data` are in scope:

```ts theme={null}
const payment = await ctx.step.waitForEvent("await-payment", {
  event: "payment.settled",
  timeout: "1h",
  if: `event.data.ticketId == "${ticketId}"`,
});
```

Without `if`, correlating a wait to a specific entity forces the id into the event name
(`payment.settled.<ticketId>`), which explodes event-name cardinality. The filter keeps one stable
event name and matches on the payload instead.

## `step.poll`

Re-check an external resource until it is ready. A resource that is still provisioning is not a
failure - it is a normal intermediate state - so `step.poll` re-checks on a fixed interval **without
spending the step's retry budget**, and gives up after an overall deadline.

Pass a **probe** that reads the resource and returns its value once ready, or a "not ready yet" signal
otherwise. Between checks the run is suspended durably, exactly like `step.sleep` - it holds no worker
and survives a restart. On the first ready check `step.poll` resolves with the value.

<Note>
  Each check is two durable steps, not a free suspension: a `step.run` probe call plus a `step.sleep`
  gap. A short `every` against a long `timeout` produces one checkpoint pair per interval - for example
  `every: "5s"` over a `timeout: "10m"` is up to 120 checks, so 240 durable steps. Prefer the widest
  `every` the resource's provisioning time tolerates.
</Note>

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

A probe that returns a value (or one that satisfies `until`) means **ready**. A probe that returns
`null`/`undefined` (or one `until` rejects) means **not ready**: the run waits `every` and checks
again.

| Option      | Type                 | Description                                                                       |
| ----------- | -------------------- | --------------------------------------------------------------------------------- |
| `every`     | `string \| number`   | Delay between checks - a duration string like `"5s"` or a number of milliseconds. |
| `timeout`   | `string \| number`   | Overall deadline for the whole wait. Once it passes, `step.poll` gives up.        |
| `until`     | `(value) => boolean` | Optional readiness predicate. When omitted, a non-null value is treated as ready. |
| `maxChecks` | `number`             | Optional cap on the number of checks, as a safety bound.                          |

A probe that **throws** is a real error, not a "not ready" signal: it retries under the normal
[step retry policy](/core/retries) and, if it exhausts its attempts, fails the run.
Readiness (a "not ready" return) and failure (a throw) stay fully separate, so waiting for a resource
never consumes the retry budget reserved for genuine errors.

### When the deadline passes

If the resource is still not ready when `timeout` elapses, `step.poll` throws `PollTimeoutError` and
the run fails, routing to [`onFailure`](/core/retries#onfailure) if one is declared. Giving
up on a required resource is a genuine failure, so this is the default.

To treat "not ready in time" as a normal branch rather than a failure, catch it:

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

try {
  const record = await ctx.step.poll("provision", () => fetchRecordOrNull(), {
    every: "5s",
    timeout: "10m",
  });
  return { status: "ready", record };
} catch (err) {
  if (err instanceof PollTimeoutError) return { status: "still-provisioning" };
  throw err;
}
```

### Poll vs retry

Polling and retrying look similar but answer different questions. A [retry](/core/retries)
handles a step that **failed** - it re-runs the same work after a backoff and spends an attempt from
the step's budget each time; [`RetryAfterError`](/core/retries#retryaftererror) only changes
*when* that next attempt runs, and the run still fails once the budget is exhausted. A poll handles a
resource that has not **become ready** yet - each check is a successful read, so it never spends the
retry budget, and the wait is bounded by a wall-clock deadline instead of an attempt count. Reach for
retry when a call can fail transiently; reach for poll when a call succeeds but the answer is "not yet."

## `step.runWorkflow`

Invoke another workflow as a **child run** and wait for its result. The parent blocks until the child
reaches a terminal state; if the child fails, the failure cascades to the parent.

```ts theme={null}
const verdict = await ctx.step.runWorkflow("fraud", {
  name: "ticket.fraud-check",
  data: { ticketId },
});
```

| Option   | Type      | Description                                                                                                                |
| -------- | --------- | -------------------------------------------------------------------------------------------------------------------------- |
| `name`   | `string`  | The child workflow to invoke.                                                                                              |
| `app`    | `string`  | Optional. Invoke the workflow in this specific app. Omit to resolve the name in the calling app first, then any other app. |
| `runner` | `string`  | Optional. Pin the child to a specific runner within the target app.                                                        |
| `data`   | `unknown` | Optional input passed to the child as its event data.                                                                      |

When two apps define a workflow with the same name, set `app` to target one exactly:

```ts theme={null}
const triage = await ctx.step.runWorkflow("triage", {
  name: "triage",
  app: "billing",
  data: { ticketId },
});
```

## `step.emit`

Publish an event from inside a run. It can trigger other workflows or resume `waitForEvent` steps.

```ts theme={null}
await ctx.step.emit("notify", {
  name: "notification.requested",
  data: { ticketId, kind: "triaged" },
});
```

| Option     | Type      | Description                                                                                                                                                                                |
| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`     | `string`  | The event name to publish.                                                                                                                                                                 |
| `app`      | `string`  | Optional. Deliver only to workflows in this app. Omit to deliver to every workflow that triggers on the event.                                                                             |
| `data`     | `unknown` | Optional event payload.                                                                                                                                                                    |
| `dedupeId` | `string`  | Optional. Drops a repeat of the same event (per app) within the dedupe window - the same idempotency key [`POST /events`](/reference/api/events) accepts. Available in the TypeScript SDK. |

## Step ids

The first argument to every step is its **id** (`"triage"`, `"wait-for-settlement"`). The id is how
Duraton matches a step to its saved result across passes, so:

* Keep ids **stable** across replays - don't compute them from changing values like timestamps, random
  values, or array contents. An id that changes between passes won't match the work already done, so the
  step runs again. This also bites when you rename a step in a new deploy while runs are in flight - see
  [changing step ids across deploys](/core/production#step-ids-across-deploys).
* Give distinct work distinct ids. Two *different* steps that happen to share an id would resolve to the
  same saved result.

### Reusing an id (loops)

Reusing the same id is legitimate and expected - a step inside a loop runs once per iteration under one
id, and that is not an error. The SDK disambiguates repeats **positionally**, in execution order: the
first occurrence of an id keeps it bare and each later occurrence gets a numeric suffix - `fetch-page`,
then `fetch-page:1`, `fetch-page:2`, and so on. Each occurrence is its own durable step with its own
saved result.

```ts theme={null}
for (const page of pages) {
  // "fetch-page", "fetch-page:1", "fetch-page:2", ... - one durable step per iteration
  await ctx.step.run("fetch-page", () => fetchPage(page));
}
```

Because the suffix is assigned by execution order, the loop must be **replay-deterministic**: it has to
run the same iterations in the same order on every pass, or the suffixes shift and later occurrences
stop matching their saved results. Drive the loop from already-durable data - the event payload or a
prior step's result - not from a live source that could return a different set on the next pass.

## Ordering

Steps run top to bottom, one after another. Each `await` completes before the next step begins, which
is what lets a workflow resume at exactly the right place.

```ts theme={null}
const triage = await ctx.step.run("triage", () => triageTicket(ticket));
await ctx.step.sleep("cool-off", "1h");
const refund = await ctx.step.run("refund", () => issueRefund(triage));
```

If this run is interrupted after `triage`, it resumes at the sleep; after the sleep, it resumes at
`refund`. Completed steps are never repeated.

## Parallel steps

Run independent steps concurrently with `Promise.all`. Duraton
discovers the whole batch in one pass and runs the branches together instead of one per round trip.

```ts theme={null}
const [user, prefs, plan] = await Promise.all([
  ctx.step.run("user", () => fetchUser(id)),
  ctx.step.run("prefs", () => fetchPrefs(id)),
  ctx.step.run("plan", () => fetchPlan(id)),
]);
```

Each branch is still its own durable step with its own id and saved result. The workflow continues
past the `Promise.all` only after **every** branch has completed - the join. Branches can mix step
kinds; a parallel `step.sleep` or `step.waitForEvent` parks alongside the others, and the run wakes
as each deadline arrives.

If one branch fails after exhausting its [retries](/core/retries), the run fails (the same
as `Promise.all` rejecting) and its still-running sibling steps are cancelled - nothing is left
dangling. Use `Promise.allSettled` instead when you want every branch to finish regardless.

One caveat: batching is best-effort. A branch that does its own `await` (an un-stepped `fetch`, say)
*before* calling its `step.run` may be discovered on the next pass rather than with its siblings. It
still runs correctly - it costs an extra round trip. Call your steps directly inside the
`Promise.all` to keep them in one batch.
