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

OptionDefaultNotes
pathVanity { handle, slug } or { format_id }.
bodySame body as createFormatRun* (input, caps, schema, attachments, …).
idempotencyKeySent as Idempotency-Key. A replay of a finished run returns immediately.
timeout20 minutesLonger than waitForRun's 10 — video Formats routinely run 10–20.
pollInterval2 secondsGap between status reads.
signalAborts the wait and the in-flight request.
timelinefalseAlso 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.
onCreatedCalled 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.

OptionDefaultNotes
familyRequired. "format", "action", or "agent".
clientmodule defaultThe client from createSumeClient(). Pass it — the module default has no base URL or key.
timeout10 minutesExceeding it throws SumeRunTimeoutError.
pollInterval2 secondsGap between status reads.
signalAborts the wait and the in-flight request; rejects with the signal's reason.
timelinefalseFormat 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: true doubles 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 onTransientError and 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.

OptionDefaultNotes
clientmodule defaultThe client from createSumeClient(). Pass it — the module default has no base URL or key.
timeout20 minutesLonger than waitForRun's 10 — video and avatar-video jobs routinely run minutes. Exceeding it throws SumeJobTimeoutError.
pollInterval2 secondsA floor. The status payload's next_poll_after_seconds wins when it asks for a longer gap.
signalAborts 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.

ErrorWhen
SumeRunTimeoutErrortimeout elapsed first. Carries runId and lastStatus.
SumeRunRequestErrorCreate refused, or a read failed and was not transient. Carries runId (or "(not created)") plus everything on SumeApiError.
the signal's reasonYou 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:

LayerDefaultOption
createSumeClient retries 408/429/5xx and transport failures2 retries, exponential backoff + jitter, honours retry-aftermaxRetries, timeout
waitForRun tolerates consecutive transient read failures6maxTransientFailures, 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