Runs and results
A Format run is asynchronous. POST .../runs hands you a receipt immediately; the receipt is
how you learn what happened afterwards.
One run is still one unit of work. A bulk request is a server-side queue of those runs,
not a new execution engine — see Bulk runs. Poll the queue at
GET /v1/format-run-queues/{queue_id} (the queue receipt's status_url) for counts and
per-item status. Each child still uses the Format-run endpoints on this page.
Polling always works. Run webhooks push the same receipt
without a loop on both api.dev.sume.com and api.sume.com.
Exact request and response schemas come from live OpenAPI
(https://api.sume.com/reference/json). The tables here are a readable summary, not a second
schema.
Lifecycle
| Status | Meaning |
|---|---|
queued | Accepted, not started. |
processing | The run is working. |
completed | Finished. output and artifacts are populated. |
failed | Finished with an error. |
canceled | Stopped by a cancel request. |
skipped | Never ran, because another run was already in flight. |
canceled is spelled with one l. These are the API's own status names, not the internal
job statuses — do not assume job-side strings transfer.
Bulk queues
POST /v1/formats/{format_id}/bulk-runs (and the vanity twin) returns a queue object
(object: "format.run_queue", id frq_…). Progress for that list is
GET /v1/format-run-queues/{queue_id} — not another Format-run poll. Queue status
completed means every item is terminal; inspect counts.failed / counts.canceled.
Child receipts stay at GET /v1/format-runs/{run_id}. Full contract:
Bulk runs.
Four read endpoints
The receipt carries its own URLs. Follow them rather than building paths.
| Field | Endpoint | Returns |
|---|---|---|
status_url | GET /v1/format-runs/{run_id}/status | id, status, started_at, finished_at, next_action, cancelable, expires_at, queue, and the URLs again — the poll payload. Once the run is terminal it also carries usage, and error when it failed, so the poll that sees failed already says why. |
result_url | GET /v1/format-runs/{run_id}/result | The full receipt, or 409 run_not_completed with the current status in details.status while the run is in flight. |
events_url | GET /v1/format-runs/{run_id}/events | The run's phase timeline — what it is doing and since when. |
cancel_url | POST /v1/format-runs/{run_id}/cancel | The current receipt. |
| — | GET /v1/format-runs/{run_id} | The full receipt at any status. |
The three URLs are echoed so a handle to status_url alone is enough to reach the result and the
cancel. You never need to build a path.
Watch a run progress
status answers whether a run is done. It does not answer what it is doing, which is the
question that matters during a ten-minute run. GET /v1/format-runs/{run_id}/events answers that:
| Phase | Meaning |
|---|---|
preparing | Sume is getting the run ready. Nothing of yours is executing yet. |
running | The agent is working the Format. This is where the time goes. |
finalizing | Teardown and output harvest. The result is nearly ready. |
Each entry carries a status (pending, running, done, warning, error, skipped) and a
duration_ms when the phase measured itself. Consecutive entries reporting the same phase and
status are collapsed into one, so a long phase is a single entry whose at keeps advancing.
at on the last entry is the run's progress clock. If it is advancing, the run is working; if
it has not moved in several minutes, the run is stalled rather than slow, and it will be finalized
at the bounds below.
An empty array is a normal answer — a run that has not started yet has no phases, and a skipped
run never will.
What this is not
It is not a log stream. Agent output, tool calls, sandbox identifiers and internal stage messages
are not published here and will not be. If you need the model's reasoning or its intermediate work,
that is not something the API exposes; read output and artifacts off the terminal receipt.
There is also no push channel — no SSE, no WebSocket. Poll this endpoint, or register a run webhook for the terminal event and skip polling entirely.
How long a run can take
A run is not allowed to hang forever, and expires_at on a non-terminal receipt is the deadline:
| Rule | Default |
|---|---|
| Hard ceiling, regardless of activity | 90 minutes from created_at |
| A run making no progress finalizes sooner: older than this and silent for the idle window | 25 minutes + 10 minutes idle |
Past either bound the run is force-finalized as failed — it does not sit in processing
indefinitely, and it stops spending. expires_at is null once the run is terminal.
Treat expires_at as an upper bound rather than an estimate. Most Formats finish in well under
it; Live Commerce runs are the long tail at ten to twenty minutes. If you are waiting
programmatically, use it as the ceiling for your own timeout rather than inventing one.
Is anything running my run?
status: "queued" used to answer two different questions with one word: the runtime is
about to pick this up and nothing is running. The queue block on the receipt (and on
the /status poll payload) separates them.
state | Means |
|---|---|
waiting | Queued, and pickup is still inside the normal window. Keep polling. |
runtime_unavailable | Queued well past that window with nothing having claimed it. Back off by retry_after_seconds; do not submit a second run for the same intent. |
processing | The runtime started it. |
done | The run is terminal. |
Two things this deliberately does not tell you:
positionis alwaysnull. Sume does not publish queue depth. It is not a number we can compute honestly, and a wrong position is worse than none. The field is declared so you can branch on it rather than discover its absence in production.runtime_not_picking_upsays nobody claimed the run, not why. From the API's side that is genuinely all that is known, and inventing a cause would be worse than saying so.
If you see runtime_unavailable for more than a few minutes, that is worth a support
ticket with the request_id.
The run receipt
GET /v1/format-runs/{run_id} returns the full shape.
| Field | Notes |
|---|---|
id, object | object is format.run. |
format | { "id", "slug", "title", "version" }. id is the opaque skl_…. |
status | See the table above. |
trigger | { "source": "cron" | "manual" | "api", "idempotency_key" }. API runs are api. |
created_at, started_at, finished_at | The last two are null until they happen. |
output_schema | { "name", "strict", "source" } — which schema shaped output. See Structured output. |
output | The structured result. null on every non-terminal status, and null whenever the run produced nothing that satisfies output_schema — not merely because the run failed. A failed run publishes whatever partial result it did author; see When output cannot be produced. |
output_error | { "code", "message", "details" } when the projection could not produce output. See When output cannot be produced. |
primary_output_key | The key in output whose URL is the one thing to show. null unless completed. |
primary_output_url | Resolved URL for primary_output_key. null unless completed. |
artifacts | Every durable file the run generated. Empty until the run is terminal. |
usage | { "currency": "USD", "billable_amount_usd_micros", "generation_spend_cap_usd_micros" }, or null when spend could not be read. |
error | { "code", "message" }. Non-null only when status is failed. |
skip_reason | Populated on a skipped run, otherwise null. |
request_id | Log this. It is what support will ask for. |
status_url, result_url, cancel_url | See above. |
events_url | The run's phase timeline — see Watch a run progress. Non-null on Format runs; Action and Agent Completion receipts still report null. |
expires_at | Deadline past which the run is force-finalized. null once terminal — see How long a run can take. |
queue | Whether anything is running this yet — see Is anything running my run?. |
cancelable | true while queued or processing. |
next_action | Recommended next step — see below. |
webhook_delivery | Delivery state for the callback you registered, or null if you registered none — see below. |
idempotency_hit | true when this receipt is an idempotency replay rather than a new run. |
thread_id | The agent conversation this run's turn happened in. Runs that continue one another share it. null on a skipped run. |
previous_run_id | The run this one continued, echoed from the request. null on a run that started fresh. |
artifacts[] is drawn from the same job ledger that fills output, so the two always agree.
Media fields are durable media.sume.com HTTPS URLs — they do not expire.
Continue a run
A Format run is one agent turn. Send previous_run_id on a new
POST .../runs and the next turn continues the same conversation instead of
starting over: the agent is replayed what it produced last time, so it can
revise part of a result and leave the rest alone.
The run you name must be continuable, which is two conditions you can read straight off its own receipt:
thread_idis notnull— there is a conversation to rejoin; and- it
completed, or itsartifacts[]is non-empty.
So a failed or canceled run can be continued when it left work behind.
A live-commerce show that produced 20 of 40 clips and then failed is incomplete,
not absent: the clips are on the thread, they were paid for, and the next turn
can fill the gaps rather than regenerate the lot. What stays refused is a
predecessor that left nothing — there the continuation would spend a turn
rediscovering that, and starting fresh is both cheaper and clearer.
A continuation is a new run. New id, new receipt, its own
generation_spend_cap_usd, its own single terminal webhook. Poll the id you
just received — the old run stays completed forever and will never change
again.
Both runs report the same thread_id, which is how you tell that two run ids
are two turns of one conversation. thread_id is read-only: continue by naming
previous_run_id, never by sending a thread id back.
Bind the same output_schema you bound on the first call. The schema is
per-run, not inherited, so omitting it gets you the default shape rather than
the one your backend expects.
| Refusal | Means |
|---|---|
404 previous_run_not_found | Unknown id, or a run belonging to another owner. |
400 previous_run_format_mismatch | That run was created on a different Format. A run can only be continued where it started. |
409 previous_run_not_terminal | It has not finished. Poll it, then call again. |
400 previous_run_not_resumable | It left nothing to continue: no thread_id (a skipped run never started one), or it did not complete and has no artifacts[]. details reports previous_run_status, has_thread and artifact_count so you can tell which. Start a fresh run. |
A continuation is a new run. It does not inherit the predecessor's outcome:
continuing a failed run does not repair that run's receipt, which stays
failed forever. You get a new id, and it succeeds or fails on its own.
Two honest details. artifacts[] on a continued run lists everything the whole
conversation has generated, not only this turn's — the media ledger is scoped to
the conversation. And usage stays per-run, so each turn reports only what that
turn spent.
Whether continuing does anything useful is up to the Format. A Format whose recipe has no notion of revising its own output will simply run again.
next_action
Receipts emit only these three values:
next_action | When | Do |
|---|---|---|
poll_status | queued or processing | Keep polling with backoff. |
retry_later | skipped | You asked for Format-level single-flight (on_active_run: "skip") and another run was active; try again. |
none | Every terminal run — completed, failed, canceled | Nothing left to fetch. On a failure, the reason is already on this receipt under error and output_error. |
Do not expect fix_input, contact_support, or inspect_events on Format run
receipts. The first two belong to the generation-job surface. inspect_events
stays retired even now that an events route exists: the
timeline tells you what a run is doing, and by the time next_action is set
the run is terminal and the reason is already on the receipt you hold. Read the
timeline while you wait, not after.
Check webhook delivery
If you created the run with communication.webhook_url, every receipt carries a
webhook_delivery block telling you what happened to that callback. It is null when the
run was created without one.
status | Means |
|---|---|
not_armed | The URL is stored but nothing is scheduled — either the run has not gone terminal yet, or run-webhook delivery is off in this environment. |
pending | Armed and waiting for its first attempt. |
retrying | An attempt failed; next_attempt_at is when the next one is due. HTTP 429 honours Retry-After (capped at 1h) on top of exponential backoff. |
delivered | Your endpoint returned 2xx. |
failed / exhausted | We gave up (10 attempts). Read last_status_code and last_error. A 429 here is the inbox rate-limiting us — the run itself is unchanged; poll result_url. |
not_armed means the URL is stored but nothing is scheduled yet (usually the run
has not gone terminal). After terminal, expect pending → delivered (or a
retry/failure status). Polling remains a valid backup.
last_error is our own transport error (a timeout, a connection failure, a non-2xx status),
never your response body.
usage
usage.billable_amount_usd_micros is the generation spend attributed to this run — the
same running total the run's generation_spend_cap_usd_micros is enforced against, counting
both reserved and captured amounts. It climbs while the run is in flight and settles when the
run terminates.
Two things it is not. It excludes the agent's own LLM turn, which bills a separate wallet, so
it is not the run's total cost. And it is a receipt figure, not an invoice —
GET /v1/usage remains the authoritative billing record.
usage itself is null when spend could not be read at all. That is distinct from 0,
which means the run really has spent nothing yet.
Which version ran
format.version records the Format version that produced the run. Editing a Format bumps its
version and does not touch any receipt already issued, so an old run always tells you what it
actually executed.
List runs for a Format
Newest first. limit accepts 1–100 and defaults to 20. A Format whose API trigger has never
been used returns an empty list, not a 404.
The response is a page:
To walk the whole history, pass next_cursor back as cursor until has_more is false:
The cursor is opaque — pass it back verbatim, never parse or construct one. It is a keyset
over (created_at, id), so runs created while you page do not shift rows onto a page you
already read. A cursor that is not one of ours is rejected with 400 invalid_request rather
than silently restarting from the newest run.
The vanity path works the same way: GET /v1/formats/{handle}/{slug}/runs.
Cancel
Requires formats:write, and is idempotent. Canceling a run that already reached a terminal
status is a no-op; the current receipt comes back either way. cancel_effect on the response
says which happened:
cancel_effect | Meaning |
|---|---|
canceled | This call stopped a run that was still in flight. |
no_op | The run had already finished; nothing changed. |
A canceled run never delivers a webhook. If your integration is webhook-only, a cancel is the one path where nothing will arrive; fall back to the receipt this call returns.
Errors on the run endpoints
| Code | Status | What to do |
|---|---|---|
unauthorized | 401 | Missing, malformed, revoked, or unknown API key. |
insufficient_scope | 403 | The key lacks formats:read (reads) or formats:write (cancel). details.required_scope names it. next_action is authenticate. Scopes cannot be patched — mint a new key. |
format_run_not_found | 404 | Unknown run id, or a run owned by another user. A run you cannot see reads the same as one that does not exist. |
run_not_completed | 409 | GET …/result before the run is terminal. details.status carries the current status; poll status_url and retry. |
rate_limited | 429 | Wait retry-after seconds. Polling spends the read budget, which is separate from — and far larger than — the one run creates spend. See Rate limits. |
studio_agent_upstream_unavailable | 503 | A Sume-side outage, not your key. Retry later. |
A 429 or a 503 during a poll loop is transient: the run is still executing. Back off and
poll again rather than treating it as a failed run — abandoning the loop does not stop the run or
its spend.
End to end
Use exponential backoff in production rather than a fixed five-second sleep. A run that makes video is minutes of work; polling every second buys you nothing and costs you rate limit.
In TypeScript, prefer subscribeFormatRun for Formats (create + wait).
Use waitForRun when you already have a run id. Prefer a
webhook when delivery is on — there is no push channel,
so everything else is a poll.
Next
- Structured output — shaping
output, and what to do when it is null - Calling a Format — the invoke contract and every submit error
- Bulk runs — a queue of these runs, plus
GET /v1/format-run-queues/{id} - Run webhooks — the terminal receipt, pushed instead of polled
- Waiting for runs —
subscribeFormatRun,waitForRun, and failure modes