Runs and steps
A run is one execution of one workflow, and the durable record of it: what happened, in what order, on which model, how long each part took, and what it cost. Runs are the thing you poll, stream, cancel, and audit.
The lifecycle
Nine statuses. Most runs only ever visit four of them — the right-hand column exists only for workflows that declare an approval gate.
planned is how child runs of a Generation Order start life, before the order is started.| Status | Meaning | Terminal? |
|---|---|---|
| pending | Accepted by the API, not yet picked up. | No |
| queued | Waiting for an execution slot. | No |
| running | Executing steps. | No |
| planned | A child of an order that has not been started yet. | No |
| completed | Finished. An artifact exists and is accepted. | Yes |
| review | Produced an artifact that is waiting on a human. | Yes |
| failed | Terminal error. Read error and errorCode. | Yes |
| cancelled | You cancelled it. | Yes |
| rejected | A reviewer rejected the output. | Yes |
| changes_requested | A reviewer asked for changes. | Yes |
completed will hang forever on a workflow that ends in review. Break on any terminal status, then decide what to do about the one you got.What a run records
{
"id": "run-fb0677b2",
"templateId": "generate-illustration",
"status": "completed",
"artifactId": "artifact-5a6a9501",
"durationMs": 16088,
"workflowVersion": "2026.09.09",
"specVersionHash": "sha256:eb3ced0c…",
"createdVia": "api_key",
"totalCosts": {
"estimatedUsd": 0.007749,
"actualUsd": 0.007749,
"currency": "USD",
"status": "provider_reported"
}
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | optional | Always run- plus eight hex characters. |
status | enum | optional | One of the nine above. |
artifactId | string | null | optional | Set once the run has produced something. |
workflowVersion | string | optional | The version this run pinned at creation — not necessarily the live one now. |
specVersionHash | string | optional | Hash of the frozen definition plus intake. Identical hashes mean identical inputs. |
createdVia | enum | optional | session, api_key, or worker. Accompanied by apiKeyId and apiKeyName where relevant. |
totalCosts | object | optional | Estimated and actual, plus the rollup cost state. |
parentRunId | string | null | optional | Set when this run was spawned by a sub-workflow step. |
parentOrderId | string | null | optional | Set when this run is a child of a Generation Order. |
failureDetails | object | null | optional | Structured evidence when a gate rejected the work. |
Step telemetry
Every run carries a runSteps array — one entry per unit of work, with its own provider, model, duration, and cost. This is where you look when a run was slow or expensive, because it tells you which part was.
"runSteps": [
{
"name": "Render illustration",
"status": "completed",
"provider": "openai",
"model": "gpt-image-2.5-sunburst",
"durationMs": 11178,
"costActualUsd": 0.0046
},
{
"name": "Classify asset",
"status": "completed",
"provider": "anthropic",
"model": "claude-haiku-4-5-20251001",
"durationMs": 1260,
"costActualUsd": 0.000884
},
{
"name": "Text gate",
"status": "completed",
"provider": "anthropic",
"model": "claude-haiku-4-5-20251001",
"durationMs": 3040,
"costActualUsd": 0.00226
},
{ "name": "artifact.create", "status": "completed", "durationMs": 26 }
]In that run the render took 11 seconds and cost $0.0046, while the two checks around it cost $0.0031 between them. Steps have their own statuses — pending, completed, failed, skipped — and a skipped step is informative: it usually means the engine determined the work was already delivered upstream.
How a step is defined
Steps live in the workflow’s runtimeSteps. Each one declares its kind, what it consumes, and — for anything that calls a model — the exact JSON it must return.
{
"id": "step-3b",
"name": "Text gate",
"kind": "llm",
"capability": "text",
"role": "textGate",
"inputPath": "step-2.url",
"requireTrue": "pass",
"failMessage": "OCR text gate rejected the render",
"jsonSchema": {
"type": "object",
"properties": {
"foundText": { "type": "string" },
"pass": { "type": "boolean" },
"reason": { "type": "string" }
},
"required": ["foundText", "pass", "reason"]
}
}| Field | Type | Required | Description |
|---|---|---|---|
kind | enum | required | llm, image, tool, subWorkflow, agent, or code. |
role | string | optional | What this step needs, abstractly — imageGenerator, classifier, textGate. The workflow binds roles to models. |
inputPath | string | optional | A dotted reference into the run context, such as step-2.url. Steps read each other’s outputs this way. |
jsonSchema | object | optional | Structured output the model must conform to. Not a suggestion — it is enforced. |
requireTrue | string | optional | Makes the step a verdict: if the named field is not true, the step fails and failMessage explains why. |
emitsArtifact | boolean | optional | Ships a real artifact mid-run, so partial work survives a later failure. |
Roles, not models
A step never names a model directly. It names a role, and the workflow’s providers map binds that role to a registry id. Two roles commonly share one model, and swapping a model is a binding change rather than a step rewrite.
{
"templateId": "generate-illustration",
"intake": { … },
"providers": {
"imageGenerator": "openai/gpt-image-2-2026-04-21"
}
}openai/gpt-image-2-2026-04-21 name a dated snapshot. For a catalog or a large order, pin one so every item in the batch was made by the same model — otherwise a mid-batch model change shows up as inconsistent output you cannot explain later.Watching a run
Polling works and is fine for scripts. For anything a person is waiting on, stream instead: GET /v1/runs/{run_id}/events sends a full snapshot on connect and then each transition as it happens, so reconnecting can never lose you a state change.
Cancelling
POST /v1/runs/{run_id}/cancel stops a run at its next step boundary. Work already paid for stays billed — cancellation stops future spend, it does not refund past spend.
- There are nine statuses. Break your polling loop on any terminal one, not on
completed. runStepstells you which part of a run was slow or expensive — the render is often not the biggest line.- Steps name roles; the workflow binds roles to models. A run can override a binding for itself.
- Every run pins its workflow version and a spec hash, so it stays explainable long after the workflow moves on.
