Skip to Content

Steps

A step is a step.do or step.waitForScan call inside a run. The platform records one row per step per run, in the order they ran, with timing and outcome — that’s the run timeline you see on the run detail page.

What’s captured per step

FieldDescription
nameThe name you passed to step.do. Identifies the step across replays — must be unique within a single run().
statusOne of pending, running, succeeded, failed.
startedAtWhen the step began executing.
completedAtWhen the step finished. null while the step is still in flight.
returnThe value the step’s callback returned. Persisted so replays don’t re-run the step. Must be JSON-serializable.
errorThe error message and stack, if the step’s callback threw. Only present on failed steps.

waitForScan records the same fields — return is the scan payload that arrived, or empty on timeout.

Statuses

StatusMeaning
pendingThe step has been registered but hasn’t started yet. You’ll rarely see this in the UI — most steps move to running immediately.
runningThe step’s callback is executing. For waitForScan, the step is suspended waiting for the external event.
succeededThe callback returned (or the wait resolved). The return value is persisted.
failedThe callback threw, or the wait timed out. The error is recorded.

Read the timeline

The run detail page renders steps as a vertical timeline. Each entry shows:

  • A status glyph (check, X, spinner) so you can scan a failing run quickly.
  • The step’s name — exactly what you passed to step.do.
  • The step’s duration.
  • The step’s return value (and error, if it failed), rendered as formatted JSON beneath the row.

Steps appear in the order they ran. A replay-skipped step (one that succeeded on a previous attempt and was replayed from the record) shows the same way as a freshly-executed step — the timeline doesn’t distinguish them.

For the durability model that makes this work, see Runtime and organizations.

Examples

Use step names as the audit trail

Step names show up verbatim in the timeline, so name them like you’d name log lines — short, action-shaped, scannable:

await step.do("Load profile", async () => { /* … */ }); await step.do("Generate health scan", async ({ ctx }) => { /* … */ }); await step.waitForScan({ timeout: "30 minutes" }); await step.do("Send notification", async () => { /* … */ });

In the UI this reads as a four-line summary of what the loop did — no extra logging required.

Surface debugging context through return values

A step’s return value is persisted and shown in the timeline. Returning a structured object instead of a raw value gives you a free debugging breadcrumb:

const profile = await step.do("Load profile", async () => { const res = await fetch(`https://api.example.com/patients/${patientId}`); const data = await res.json(); return { id: data.id, region: data.region, plan: data.plan }; });

Now the step’s row in the timeline shows the patient’s region and plan — visible at a glance without re-running the loop.

Last updated on