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.

step.run
Run a function once and remember its result.
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 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: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.
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.
"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.
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.
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
Useif 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 -
event.name and event.data are in scope:
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.
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.until) means ready. A probe that returns
null/undefined (or one until rejects) means not ready: the run waits every and checks
again.
A probe that throws is a real error, not a “not ready” signal: it retries under the normal
step retry policy 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 whentimeout elapses, step.poll throws PollTimeoutError and
the run fails, routing to 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:
Poll vs retry
Polling and retrying look similar but answer different questions. A retry 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 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.
When two apps define a workflow with the same name, set
app to target one exactly:
step.emit
Publish an event from inside a run. It can trigger other workflows or resume waitForEvent steps.
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.
- 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.
Ordering
Steps run top to bottom, one after another. Eachawait completes before the next step begins, which
is what lets a workflow resume at exactly the right place.
triage, it resumes at the sleep; after the sleep, it resumes at
refund. Completed steps are never repeated.
Parallel steps
Run independent steps concurrently withPromise.all. Duraton
discovers the whole batch in one pass and runs the branches together instead of one per round trip.
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, 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.