Runs
The run lifecycle, preflight and run drafts, events and artifacts, reruns, and promotion.
A run is one logical execution of a workflow with a JSON input. Its run_id is stable: retries, repairs, and recoveries re-run the same run as new immutable attempts under that id — there is never a second public run to discover or follow. Runs are created from the API, MCP, web app, CLI, a schedule, or a workflow endpoint; a runner claims each queued run and executes it, streaming events, logs, and artifacts back to the Hub as the durable record.
Lifecycle
| Status | Meaning |
|---|---|
waiting_approval | Held for a human decision before entering the queue (see Approvals). Never times out — waiting cannot fail a run. |
queued | Ready for a matching runner to claim. |
assigned | Claimed by a runner, not yet started. |
running | Executing on the runner. |
paused | Parked on a recoverable external interruption (provider credits or quota exhausted, or an operator pause). Non-terminal: the run keeps its engine checkpoint, frees its runner slot, is never reaped, and waits for resume or cancel. |
recovering | A retry superseded a still-live attempt; the new attempt waits (unclaimable) until the old attempt's runner acknowledges cancellation. Leaves only to queued/waiting_approval (released) or cancelled. |
succeeded | Finished with outputs. |
cancelled | Stopped by an operator or a rejected approval — a human "no" is a decision, not a failure. |
failed (+ classes) | Terminal failure; see failure classes below. |
Terminal states are final: late reports racing a terminal state are absorbed idempotently rather than overwriting the first decision.
Paused runs (recoverable interruptions)
When execution is interrupted by a condition a human can fix — the classic case is the provider account ran out of credits — the run parks as paused instead of failing. Pause metadata lives on run.pause:
{
"reason": "credits_exhausted",
"message": "Provider returned 402: credit balance is too low",
"pausedAt": "2026-07-09T12:00:00.000Z",
"pausedBy": "gateway",
"resumable": true,
"resume": { "smithersRunId": "run-1751234567", "strategy": "smithers_resume" },
"requiredAction": { "type": "add_credits", "label": "Add credits, then resume" }
}- How runs pause. The metering gateway pauses a gateway-metered run when the upstream provider answers with a credit/quota-exhaustion response (e.g. HTTP 402) — a structured signal at the inference boundary. The runner pauses a run when the engine parks it as
waiting-quota(a structured Smithers state since 0.27; the runner mirrors it as a pause withreason: "quota_exhausted"carrying the engine checkpoint), and still classifies engine failure text that clearly means exhausted credits/quota as a fallback. Operators can pause explicitly viaPOST /api/runs/{id}/pause(pause_run,runyard pause). - What paused means. The run releases its runner slot (a parked run never occupies capacity), keeps its
runner_idso resume lands on the runner holding the local engine checkpoint, and is exempt from liveness/stall/deadline reaping.run.paused/run.resumedtimeline events record the transitions. - Resume.
POST /api/runs/{id}/resume(resume_run,runyard resume) re-queues the same run. With a recorded checkpoint it continues via the engine's resume (smithers up --resume); without one it re-runs from scratch and the response (resume.strategy: "rerun_from_scratch") plus the run timeline say so explicitly. Cancel still works frompaused. - Resume strategies. The resume body optionally forces a strategy:
{"strategy": "rerun_from_scratch"}discards the recorded checkpoint and clears the runner pin so any live runner can take the run (runyard resume --from-scratch, the web "Restart from scratch" button);{"strategy": "smithers_resume"}insists on the checkpoint and answers409when none is recorded. Omitted, the strategy resolves automatically. - Offline checkpoint runner. A checkpointed resume only executes once the runner holding the checkpoint claims it. If that runner is offline, the resume still succeeds but the response carries a
warning(and therun.resumedevent records it): the run stays queued until the runner reconnects, or you resume again withrerun_from_scratchto run anywhere. - Resume failure. If the runner cannot find the recorded checkpoint in its local
.smithersstate (state cleaned, workspace replaced), it does not hang or fake a timeout: it emitsrunner.resume_checkpoint_missingand re-parks the run aspausedwithreason: "resume_failed"and the stale checkpoint dropped, so the next resume honestly re-runs from scratch. (The engine validates--resumeinside its detached child, so the Hub runner verifies the checkpoint withsmithers inspectbefore launching.) - Not a budget stop.
budget_exceededremains a distinct terminal status for the Hub's own hard spend ceiling; a budget breach is never converted into a pause.
Failure classes
Failures are classified into specific terminal statuses so operators can tell config problems from real failures: failed (generic), blocked_by_gate (a verification gate failed), blocked_by_preflight (the run could never start as configured), provider_limited (rate limits/quota), timed_out, invalid_output (schema mismatch), infra_unavailable (network/runner infrastructure), needs_human, and budget_exceeded (the run's metered usage reached its spend budget and it was hard-stopped).
Usage and budgets
Runs also meter the model calls they consume: per-call usage records stream as run.usage events and aggregate onto the run as usage (totalTokens, costMicros, byModel, ...), and run creation accepts an optional budget: { maxTokens?, maxCostMicros? } hard ceiling. Budgeted runs additionally carry a computed budgetStatus (spent vs limit, remaining, percent used, nearLimit at 80%) in list and detail payloads. See Usage metering & budgets for the data model, the fleet rollup (GET /api/usage/summary), the metering gateway, and budget enforcement.
The needs-attention queue
GET /api/runs/attention (list_attention_runs, runyard attention) returns every run whose next step is a human action, grouped with counts:
paused— resume them once the interruption is fixed;waitingApproval— a decision is pending on an approval card;budgetStopped— hit their spend ceiling in the last 7 days; raise the budget and re-run to finish the work.
counts also includes pendingApprovals (open decision cards). The web Home view renders this as a triage strip above the runs list with resume/review/inspect actions inline; it disappears entirely when nothing needs a human. GET /api/runs?status=paused (or any other status) remains the raw filter underneath.
Preflight and run drafts (negotiation)
Before anything is enqueued, Runyard can run a deterministic preflight: schema checks on required input fields, runner/tag availability, secret presence, hook eligibility, and workflow source resolution. The result is ready, needs_input (with questions[] the caller can answer), or blocked (with blockers[] only an operator can fix), plus warnings, suggested defaults, and a nextAction hint.
Three ways to use it:
POST /api/workflows/{id}/preflight— stateless report, nothing created.POST /api/workflows/{id}/runwithnegotiate: true— enqueue only when ready; otherwise the negotiation state and a saved draft come back instead of a doomed run.- Run drafts — a draft is a proposed run that has not been enqueued. Create it, patch its input to answer questions (each patch re-preflights), then submit; submit re-preflights and returns 202 with the run only when ready (422
needs_input/ 409blockedotherwise).
curl -X POST -H "Authorization: Bearer $RUNYARD_TOKEN" -H "Content-Type: application/json" \
-d '{"input": {"title": "Nightly digest", "topic": "release notes"}, "executionMode": "remote"}' \
https://hub.example.com/api/workflows/idea-to-product/runinput.title is recommended for agent-created runs: a short human-readable title used in run lists, approval cards, and handoff.
Events, timeline, logs, artifacts
Runners append structured events as the run progresses. Every event carries a monotonic per-run cursor (seq); GET /api/runs/{id}/events/stream serves them live over SSE — replaying persisted history after ?afterSeq= (or a standard Last-Event-ID reconnect header), tagging frames with id:<seq>, sending keepalive comments, and closing with a final run-terminal frame once a terminal run is fully drained. Run payloads expose the transport as eventsStreamUrl, and runyard run --follow / runyard logs --follow are the CLI front ends. GET /api/runs/{id}/timeline merges status transitions, events, and artifacts into one ascending feed (with since/limit paging). Log lines are at /logs, a condensed log summary and diagnostics at /diagnostics, and uploaded artifacts (outputs, traces, files) at /artifacts, downloadable by artifact id.
Stable run identity and attempts
A replacement/recovery is the same logical run, run again. Every execution try is an immutable attempt (att_…) under the stable run_id:
- Normal clients never change anything. Polling
GET /api/runs/{id}, the SSE event stream (whoseseqcursor stays monotonic across attempts), cancel, approvals, artifacts, webhooks, and work-item links all target the stable run id straight through a recovery.attemptId/attemptCounton the run payload can be ignored entirely. - Retry —
POST /api/runs/{id}/retry(retry_run,runyard retry) starts the next attempt on the same run. It answers409while an attempt is still active unlessforce: true, which supersedes the live attempt first (its pending approval cards are retired assuperseded, its workspace gets an ownership-checked cleanup intent, and its runner acknowledges cancellation before the new attempt is claimable — therecoveringstatus above). Recovery classification fields (failureClass,workspaceHealthy, …) select the strategy:gate_retry,same_workspace_repair,continuation(reuses the engine checkpoint and runner pin), orclean_room. - Attempt history —
GET /api/runs/{id}/attempts(get_run_attempts,runyard attempts) lists every try with status, recovery reason, runner, timestamps, archived input, failure evidence, and cleanup state;GET /api/runs/{id}/events?attemptId=filters one attempt's events. Prior attempts are auditable history and are never overwritten. The web run page shows the same under Attempts. - The lifecycle reads coherently — e.g.
running → recovering → running → succeeded; the terminal outcome is the final attempt's outcome. - Legacy ids keep resolving. Databases from v0.23.1 and earlier modeled each retry as a separate successor run; the migration collapses those chains into attempts under the original run, and the retired successor ids resolve to the same logical run forever (responses note
resolvedFrom).
Reruns and promotion
- Rerun —
POST /api/runs/{id}/rerunwith the same (or omitted) input retries the SAME stable run as a new attempt (retried: true,run.idunchanged) — an active attempt is superseded like v0.23.1 replaced its predecessor, and double-submits dedupe to the pending retry. An edited input is different work: it creates a new linked run withinput.rerunOfprovenance. - Promotion — for runs that mutated an isolated git worktree,
POST /api/runs/{id}/promotemerges the successful run's work into its target branch, runs gates, pushes, and cleans up the branch/worktree. Merging to the default branch always goes through promotion, never through a hook.
Response endpoints
A run request may include responseEndpoint: {type: "http" | "telegram", config} so the terminal-state reply is delivered to the caller when the run finishes. Delivery state is reported in responseEndpoints[] on GET /api/runs/{id}; polling that endpoint remains the canonical fallback.
API & MCP
| Endpoint | MCP tool |
|---|---|
POST /api/workflows/{id}/run | run_workflow |
POST /api/workflows/{id}/preflight | preflight_workflow |
GET/POST /api/run-drafts, GET/PATCH /api/run-drafts/{id}, POST .../submit, POST .../discard | list_run_drafts, create_run_draft, get_run_draft, update_run_draft, submit_run_draft, discard_run_draft |
GET /api/runs, GET /api/runs/{id} | list_runs, get_run_status |
GET /api/runs/attention | list_attention_runs |
GET /api/runs/{id}/usage, GET /api/usage/summary | get_run_usage, get_usage_summary |
GET /api/runs/{id}/events, /timeline, /diagnostics, /logs | get_run_events, get_run_timeline, get_run_diagnostics, get_run_logs |
GET /api/runs/{id}/artifacts, GET /api/artifacts, GET /api/artifacts/{id}/download | get_run_artifacts, search_artifacts, download_artifact |
POST /api/runs/{id}/cancel | cancel_run |
POST /api/runs/{id}/pause | pause_run |
POST /api/runs/{id}/resume | resume_run |
POST /api/runs/{id}/rerun | rerun_workflow_run |
POST /api/runs/{id}/retry | retry_run |
GET /api/runs/{id}/attempts, GET .../attempts/{attemptId} | get_run_attempts, get_run_attempt |
POST /api/runs/{id}/promote | promote_run |
GET /api/dashboard | get_dashboard |
The start/complete/fail/event-append and artifact-upload endpoints under /api/runs/{id} are the runner protocol (runner-scoped tokens, run ownership enforced) — see Runners.