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

StatusMeaning
queuedAccepted, not started.
processingThe run is working.
completedFinished. output and artifacts are populated.
failedFinished with an error.
canceledStopped by a cancel request.
skippedNever 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.

FieldEndpointReturns
status_urlGET /v1/format-runs/{run_id}/statusid, 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_urlGET /v1/format-runs/{run_id}/resultThe full receipt, or 409 run_not_completed with the current status in details.status while the run is in flight.
events_urlGET /v1/format-runs/{run_id}/eventsThe run's phase timeline — what it is doing and since when.
cancel_urlPOST /v1/format-runs/{run_id}/cancelThe 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:

PhaseMeaning
preparingSume is getting the run ready. Nothing of yours is executing yet.
runningThe agent is working the Format. This is where the time goes.
finalizingTeardown 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:

RuleDefault
Hard ceiling, regardless of activity90 minutes from created_at
A run making no progress finalizes sooner: older than this and silent for the idle window25 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.

stateMeans
waitingQueued, and pickup is still inside the normal window. Keep polling.
runtime_unavailableQueued 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.
processingThe runtime started it.
doneThe run is terminal.

Two things this deliberately does not tell you:

  • position is always null. 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_up says 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.

FieldNotes
id, objectobject is format.run.
format{ "id", "slug", "title", "version" }. id is the opaque skl_….
statusSee the table above.
trigger{ "source": "cron" | "manual" | "api", "idempotency_key" }. API runs are api.
created_at, started_at, finished_atThe last two are null until they happen.
output_schema{ "name", "strict", "source" } — which schema shaped output. See Structured output.
outputThe structured result. null on every non-terminal status, and null whenever the run produced nothing that satisfies output_schemanot 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_keyThe key in output whose URL is the one thing to show. null unless completed.
primary_output_urlResolved URL for primary_output_key. null unless completed.
artifactsEvery 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_reasonPopulated on a skipped run, otherwise null.
request_idLog this. It is what support will ask for.
status_url, result_url, cancel_urlSee above.
events_urlThe run's phase timeline — see Watch a run progress. Non-null on Format runs; Action and Agent Completion receipts still report null.
expires_atDeadline past which the run is force-finalized. null once terminal — see How long a run can take.
queueWhether anything is running this yet — see Is anything running my run?.
cancelabletrue while queued or processing.
next_actionRecommended next step — see below.
webhook_deliveryDelivery state for the callback you registered, or null if you registered none — see below.
idempotency_hittrue when this receipt is an idempotency replay rather than a new run.
thread_idThe agent conversation this run's turn happened in. Runs that continue one another share it. null on a skipped run.
previous_run_idThe 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_id is not null — there is a conversation to rejoin; and
  • it completed, or its artifacts[] 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.

RefusalMeans
404 previous_run_not_foundUnknown id, or a run belonging to another owner.
400 previous_run_format_mismatchThat run was created on a different Format. A run can only be continued where it started.
409 previous_run_not_terminalIt has not finished. Poll it, then call again.
400 previous_run_not_resumableIt 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_actionWhenDo
poll_statusqueued or processingKeep polling with backoff.
retry_laterskippedYou asked for Format-level single-flight (on_active_run: "skip") and another run was active; try again.
noneEvery terminal run — completed, failed, canceledNothing 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.

statusMeans
not_armedThe URL is stored but nothing is scheduled — either the run has not gone terminal yet, or run-webhook delivery is off in this environment.
pendingArmed and waiting for its first attempt.
retryingAn 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.
deliveredYour endpoint returned 2xx.
failed / exhaustedWe 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 pendingdelivered (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_effectMeaning
canceledThis call stopped a run that was still in flight.
no_opThe 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

CodeStatusWhat to do
unauthorized401Missing, malformed, revoked, or unknown API key.
insufficient_scope403The 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_found404Unknown 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_completed409GET …/result before the run is terminal. details.status carries the current status; poll status_url and retry.
rate_limited429Wait 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_unavailable503A 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