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
| Field | Description |
|---|---|
name | The name you passed to step.do. Identifies the step across replays — must be unique within a single run(). |
status | One of pending, running, succeeded, failed. |
startedAt | When the step began executing. |
completedAt | When the step finished. null while the step is still in flight. |
return | The value the step’s callback returned. Persisted so replays don’t re-run the step. Must be JSON-serializable. |
error | The 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
| Status | Meaning |
|---|---|
pending | The step has been registered but hasn’t started yet. You’ll rarely see this in the UI — most steps move to running immediately. |
running | The step’s callback is executing. For waitForScan, the step is suspended waiting for the external event. |
succeeded | The callback returned (or the wait resolved). The return value is persisted. |
failed | The 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.