Waiting for runs and jobs
Every Sume run is asynchronous: POST .../runs hands back a receipt with a status_url,
and you learn the outcome later. A bulk queue is a server-side list of those runs — poll
GET /v1/format-run-queues/{queue_id} for counts; the helpers on this page still wait on
one run id. See Bulk runs. In @sume-com/sdk@0.2.0 the partner-facing Format path
is subscribeFormatRun (create + wait). Use waitForRun when you already have a
run id, and waitForJob for generation jobs, which are a separate surface. Prefer
run webhooks when you can skip the wait entirely.
There is no SSE event stream today, so "subscribe" means create-then-poll. onStatus
reflects status polling (including a richer snapshot with next_action, timestamps, and
cancelable), not a live log feed. On a Format run you can also poll the phase
timeline — see Watching phases while you wait.
subscribeFormatRun
| Option | Default | Notes |
|---|---|---|
path | — | Vanity { handle, slug } or { format_id }. |
body | — | Same body as createFormatRun* (input, caps, schema, attachments, …). |
idempotencyKey | — | Sent as Idempotency-Key. A replay of a finished run returns immediately. |
timeout | 20 minutes | Longer than waitForRun's 10 — video Formats routinely run 10–20. |
pollInterval | 2 seconds | Gap between status reads. |
signal | — | Aborts the wait and the in-flight request. |
timeline | false | Also read the phase timeline on every poll and hand it to onStatus as snapshot.timeline. |
onStatus | — | (status, snapshot) on every status read, including the terminal one. |
onCreated | — | Called once with the accepted run, before polling starts. |
It resolves for any terminal status. It throws only when the create call itself
is refused (for example 403 workspace_key_required on a team Format called with a
personal key) — there is no run to wait for.
Field-by-field, the receipt is documented in Runs and results.
waitForRun
Use this for Action / Agent Completion runs, or when you created the Format run yourself and only need the poll loop.
| Option | Default | Notes |
|---|---|---|
family | — | Required. "format", "action", or "agent". |
client | module default | The client from createSumeClient(). Pass it — the module default has no base URL or key. |
timeout | 10 minutes | Exceeding it throws SumeRunTimeoutError. |
pollInterval | 2 seconds | Gap between status reads. |
signal | — | Aborts the wait and the in-flight request; rejects with the signal's reason. |
timeline | false | Format runs only. Also read the phase timeline on every poll. |
onStatus | — | (status, snapshot) on every status read, including the terminal one. |
family is required and cannot be inferred. A run id does not say which surface it
belongs to, and the three families live behind three different URL prefixes
(/v1/format-runs/…, /v1/action-runs/…, /v1/agent-runs/…). It is also what types the
return value: family: "format" resolves as PublicFormatRun.
The deadline is checked before sleeping, not after. A caller who asks for a 5-second timeout hears about it in 5 seconds, rather than 5 seconds plus one whole poll interval.
Watching phases while you wait
status tells you a Format run is processing. It does not tell you what it is doing,
which for a fifteen-minute video run is most of what you want to know. The phase
timeline does — preparing, running, finalizing, each with a timestamp and a status.
Pass timeline: true and it arrives on the onStatus snapshot:
Or read it directly, for a run you are not waiting on:
Three things to know:
- It is a phase timeline, not a log stream. Agent output, tool calls and sandbox internals are never published. See Watch a run progress.
timeline: truedoubles the request rate of the wait. Reads have their own rate-limit budget, so this cannot 429 your run creates — but it is still twice the requests for the same run, so it is off by default.- A timeline read that fails does not end the wait. The run is still executing and
still spending; the failure is reported through
onTransientErrorand the previous timeline is kept.
Format runs only — timeline: true is ignored for family: "action" and "agent",
whose receipts report events_url: null because they have no events route.
waitForJob
Runs and jobs are different surfaces. POST /v1/image-1.0/generate,
/v1/video-1.0/generate, the Avatar routes, and the /v1/models/sume/…/runs
aliases all create jobs at /v1/jobs/:id, not runs — so they need
waitForJob, not waitForRun. A job id and a run id are not interchangeable.
| Option | Default | Notes |
|---|---|---|
client | module default | The client from createSumeClient(). Pass it — the module default has no base URL or key. |
timeout | 20 minutes | Longer than waitForRun's 10 — video and avatar-video jobs routinely run minutes. Exceeding it throws SumeJobTimeoutError. |
pollInterval | 2 seconds | A floor. The status payload's next_poll_after_seconds wins when it asks for a longer gap. |
signal | — | Aborts the wait and the in-flight request. |
onStatus | — | (status, snapshot) on every status read, including the terminal one. |
Submit with mode: "async" (or omit mode). The server-side sync and
subscribe modes are the same bounded wait capped at 30 seconds, which is a
budget for the HTTP request rather than for the job — see
Communication modes. waitForJob is the
client-side wait that can outlast it.
It resolves with the job record read from /v1/jobs/:id, not from
/v1/jobs/:id/result — /result answers 409 job_not_completed for failed and
canceled jobs, and there would be nothing to hand back. Read status, result,
and error off the record. Errors mirror the run helpers:
SumeJobTimeoutError and SumeJobRequestError, both carrying jobId.
A timeout does not cancel the job. It keeps running and still bills; store the
job id and read it back later with getApiJob, or cancel it with
cancelApiJob.
Terminal is not the same as successful
Both run helpers resolve for any terminal status — completed, failed, canceled,
skipped. A failed run is a result you asked for, not an exception, so read status and
error off the receipt exactly as a webhook handler would:
skipped is worth a branch of its own: it means a run was already in flight and
you passed on_active_run: "skip" (Format runs allow concurrency by default).
See Runs and results for the whole lifecycle.
Errors they throw
Unlike the generated operations — which resolve with { data, error } — these helpers
throw, because a poll loop has nowhere to put a non-result.
| Error | When |
|---|---|
SumeRunTimeoutError | timeout elapsed first. Carries runId and lastStatus. |
SumeRunRequestError | Create refused, or a read failed and was not transient. Carries runId (or "(not created)") plus everything on SumeApiError. |
the signal's reason | You aborted. |
SumeRunRequestError extends SumeApiError, so the envelope is available as typed fields
rather than something to dig out of body:
SumeApiError subclasses: SumeAuthenticationError (401), SumeInsufficientCreditsError
(402), SumePermissionError (403), SumeNotFoundError (404), SumeConflictError (409),
SumeRateLimitError (429), SumeServerError (5xx). Every one carries code, requestId,
retryable, retryAfterSeconds, nextAction, details, and the raw body.
Transient failures do not end the wait
A 429 or a 5xx on a status read means the read failed, not the run. The run is still
executing and still spending, so the helpers back off and poll again rather than throwing —
losing the handle to a live run is far more expensive than waiting another second.
Two layers do this, and both are on by default:
| Layer | Default | Option |
|---|---|---|
createSumeClient retries 408/429/5xx and transport failures | 2 retries, exponential backoff + jitter, honours retry-after | maxRetries, timeout |
waitForRun tolerates consecutive transient read failures | 6 | maxTransientFailures, onTransientError |
A POST is only retried when it carries an Idempotency-Key — without one, a replay would
start and bill a second run. subscribeFormatRun generates one for you unless you pass your
own (or null).
Polls are jittered: reads have their own rate-limit budget, but several clients started together would otherwise stay in phase and hit that ceiling as a group.
A timeout does not cancel the run. The run keeps going; you have only stopped
watching. Store the run id and pick it up later with getFormatRun, or cancel it
explicitly with cancelFormatRun.
Prefer a webhook where you can
Polling is one timer and one open request per run in flight, for runs that routinely take
minutes. Run webhooks deliver the identical receipt without any of
that, and verifyWebhook is the receiver-side half.
Polling is the right tool when you are inside a job that can afford to block, when you
are prototyping, or while webhook delivery is still off for your environment. Build the
receiver now and keep subscribeFormatRun / waitForRun as the fallback.
Next
- Verifying webhooks — the push path's receiver check
- Runs and results — the receipt, field by field
- Bulk runs — queue many runs; poll
GET /v1/format-run-queues/{id} - Embed a Format in your product — the whole partner integration