Core concepts · Runs

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.

RUN STATUSno approval gate declaredpendingplannedorder childrenqueuedrunningfailedcancelledgatereviewapprovecompletedrejectedchanges_requestedterminal: completed · review · failed · cancelled · rejected · changes_requested
Run status transitionsplanned is how child runs of a Generation Order start life, before the order is started.
StatusMeaningTerminal?
pendingAccepted by the API, not yet picked up.No
queuedWaiting for an execution slot.No
runningExecuting steps.No
plannedA child of an order that has not been started yet.No
completedFinished. An artifact exists and is accepted.Yes
reviewProduced an artifact that is waiting on a human.Yes
failedTerminal error. Read error and errorCode.Yes
cancelledYou cancelled it.Yes
rejectedA reviewer rejected the output.Yes
changes_requestedA reviewer asked for changes.Yes
Poll on terminal, not on completed
A loop that waits for 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

GET /v1/runs/{run_id}json
{
  "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"
  }
}
FieldTypeRequiredDescription
idstringoptionalAlways run- plus eight hex characters.
statusenumoptionalOne of the nine above.
artifactIdstring | nulloptionalSet once the run has produced something.
workflowVersionstringoptionalThe version this run pinned at creation — not necessarily the live one now.
specVersionHashstringoptionalHash of the frozen definition plus intake. Identical hashes mean identical inputs.
createdViaenumoptionalsession, api_key, or worker. Accompanied by apiKeyId and apiKeyName where relevant.
totalCostsobjectoptionalEstimated and actual, plus the rollup cost state.
parentRunIdstring | nulloptionalSet when this run was spawned by a sub-workflow step.
parentOrderIdstring | nulloptionalSet when this run is a child of a Generation Order.
failureDetailsobject | nulloptionalStructured 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, from the run abovejson
"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.

a verdict step from generate-coloring-pagejson
{
  "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"]
  }
}
FieldTypeRequiredDescription
kindenumrequiredllm, image, tool, subWorkflow, agent, or code.
rolestringoptionalWhat this step needs, abstractly — imageGenerator, classifier, textGate. The workflow binds roles to models.
inputPathstringoptionalA dotted reference into the run context, such as step-2.url. Steps read each other’s outputs this way.
jsonSchemaobjectoptionalStructured output the model must conform to. Not a suggestion — it is enforced.
requireTruestringoptionalMakes the step a verdict: if the named field is not true, the step fails and failMessage explains why.
emitsArtifactbooleanoptionalShips 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.

ROLEMODEL REGISTRY IDimageGeneratorclassifiertextGateopenai/gpt-image-2.5-sunburst-2026-09-08anthropic/claude-haiku-4-5two roles, one model — bindings are a map, not a listrun.providers overrides a single role for one run only
Role bindingA run may override any binding for itself, and the override is validated against the registry.
POST /v1/runs — pinning one model for this runjson
{
  "templateId": "generate-illustration",
  "intake": { … },
  "providers": {
    "imageGenerator": "openai/gpt-image-2-2026-04-21"
  }
}
Pin dated snapshots for batch work
Registry ids like 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.

In short
  • There are nine statuses. Break your polling loop on any terminal one, not on completed.
  • runSteps tells 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.