Bulk runs
Leave a list of Format runs overnight without driving fan-out from your laptop. A bulk
request is a server-side queue of ordinary Format runs, not a different execution
engine. Each item is the same unit of work as POST …/runs: one sandbox, one agent turn,
one run receipt.
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.
Endpoints
| Method | Path | Scope |
|---|---|---|
POST | /v1/formats/{format_id}/bulk-runs | formats:write |
POST | /v1/formats/{handle}/{slug}/bulk-runs | formats:write |
GET | /v1/format-run-queues/{queue_id} | formats:read |
The two POST paths are twins. Prefer {handle}/{slug} in new integrations; the opaque
skl_… path stays valid forever. Request body, headers, scopes, and the queue receipt
are identical.
There is no public list-queues or cancel-queue endpoint. Cancel a child with
POST /v1/format-runs/{run_id}/cancel — see Cancel.
Auth
Same API-key rules as a single Format run:
- Bearer API key (
Authorization: Bearer $SUME_API_KEY). - The key carries
formats:writeto create a queue andformats:readto poll it. - For a Format owned by a team workspace, the key was issued in that workspace.
- Service-account keys cannot create Format runs or bulk queues. They fail with
403 insufficient_scopeanddetails.reasonofservice_account_format_runs_unsupported.
Keys created before the Format API-call trigger shipped do not carry these scopes. Create a new key — scopes cannot be added to an existing one. See Calling a Format and Team Formats need a team key.
| Scope | Needed for |
|---|---|
formats:write | POST …/bulk-runs (and single-run create / cancel). |
formats:read | GET /v1/format-run-queues/{queue_id} (and Format / run reads). |
Create a queue
The opaque twin:
An accepted create returns 202 with { "data": { …queue } }. The first
concurrency items are already in flight on that receipt when the list is longer than
the window.
Request body
| Field | Notes |
|---|---|
concurrency | Required integer 1–16. How many child Format runs stay in flight at once. |
items | Required array, 1–100 entries, in order. Each entry is the same body as POST …/runs. |
idempotency_key | Optional body spelling of the Idempotency-Key header. The header wins when both are sent. |
Unknown top-level fields are rejected. items is required — { "concurrency": 3 } is
400, not an empty queue.
Each item must name at least one of instruction, input, previous_run_id, or
attachments — the same rule as a single run. A bad item fails the create (400 invalid_request, details.index) before a queue exists; nothing is dispatched.
The bulk controller owns single-flight. Every item is executed with on_active_run: "allow". Sending skip or reject on an item does not stall the window on the first
in-flight run of this Format. Workspace generation concurrency still applies to the
children.
Per-item communication.webhook_url is the same as a single run: each child can register
its own terminal webhook. The queue object has no webhook. Do not expect a
queue-level callback.
Queue receipt
data is a format.run_queue:
| Field | Notes |
|---|---|
id | Queue id (frq_…). |
object | Always format.run_queue. |
format | { "id", "slug", "title", "version" }. id is the opaque skl_…. |
concurrency | The window you sent (1–16). |
status | queued / running / completed. See below. |
counts | total, queued, running, completed, failed, canceled. All required. |
items | One row per submitted item, in the same order. |
created_at, updated_at | ISO-8601. |
finished_at | Set when the queue status becomes completed; otherwise null. |
status_url | GET /v1/format-run-queues/{id} — poll this for progress. |
Queue status
| Status | Meaning |
|---|---|
queued | Nothing has dispatched yet. |
running | The concurrency window is draining the list. |
completed | Every item is terminal. Inspect counts for failures — completed is not "all succeeded". |
counts.total is items.length. counts.running includes items the API still treats as
in flight (a child that has been claimed but not yet given a run_id still counts as
running).
Items
| Field | Notes |
|---|---|
index | Zero-based position in the submitted items array. |
status | queued / running / completed / failed / canceled. |
run_id | Child Format run id once dispatched. null while queued, and null if the item failed before a child run started. Poll GET /v1/format-runs/{run_id} for the full receipt. |
error | { "code", "message" } or null. |
| Item status | Meaning |
|---|---|
queued | Not started. run_id is null. |
running | Inside the concurrency window. |
completed | Child run completed. Terminal; frees a slot. |
failed | Child run failed (or skipped, recorded here as failed), or the child could not be started. Terminal; frees a slot. |
canceled | Child run was canceled. Terminal; frees a slot. |
A failed item that never started a run still occupies that index with run_id: null
and error set to the create-run failure (code / message from that attempt, for
example format_run_failed_to_start). The rest of the queue continues.
When a child run settles, error is:
| Child run | Item error |
|---|---|
completed | null |
failed | { "code": "format_run_failed", "message": "The Format run failed." } |
canceled | { "code": "format_run_canceled", "message": "The Format run was canceled." } |
Read why a child failed on the run receipt (GET /v1/format-runs/{run_id}), not only
on the queue item.
Concurrency window
The server keeps concurrency child runs in flight and starts the next queued item as
soon as a slot opens, until the list is drained. This is not client-driven fan-out.
In-flight slots are items whose public status is running. completed, failed, and
canceled are terminal and free a slot. When an in-flight child hits completed or
failed (the usual drain path), the next queued item starts immediately so the window
stays full. A canceled child does the same.
Create already fills the window: with concurrency: 3 and 8 items, the 202 receipt
shows three running and five queued. Completing item 0 starts item 3; the window
stays at three until fewer than three items remain.
The window never exceeds concurrency, even if progress is polled or advanced more than
once.
Child runs still go through ordinary Format-run admission (wallet, workspace generation
concurrency, spend caps). A child that fails to start is that item failed; the
queue create has already returned 202.
Poll the queue
Follow status_url, or build GET /v1/format-run-queues/{queue_id} from id:
200 returns the same queue object as create. Use counts for a dashboard; use items
when you need per-row run_id / error.
The queue is completed when every item is terminal. Then finished_at is set. Branch
on counts.failed and counts.canceled — do not treat queue completed as success.
A queue you cannot see reads as one that does not exist: 404 format_run_queue_not_found
(unknown id, or a queue owned by another user).
Child runs
Each dispatched item is an ordinary Format run:
status_url / result_url / events_url / cancel_url on that receipt behave exactly
as on Runs and results. The queue does not replace those endpoints; it
adds counts and per-item status on top.
Canceling a child (POST /v1/format-runs/{run_id}/cancel) marks that queue item
canceled and frees its slot for the next queued item.
Idempotency
Send Idempotency-Key on create (header; body idempotency_key is accepted, header
wins). Keys are scoped to one Format.
| Replay | Result |
|---|---|
Same key, same { concurrency, items } | 202 and the existing queue. |
| Same key, different payload | 409 idempotency_conflict (details.queue_id names the original). |
Unlike a single run, a bulk replay stays 202. The queue object has no
idempotency_hit field.
The queue does not grow a separate idempotency-in-use lock beyond what the control plane already stores for that key. There is no queue-level webhook, and no queue-level idempotency behaviour beyond the header / body key above.
Errors
Create (POST …/bulk-runs):
| Code | Status | What to do |
|---|---|---|
unauthorized | 401 | Missing, malformed, revoked, or unknown API key. |
insufficient_scope | 403 | The key lacks formats:write, or it is a service-account key (details.reason is service_account_format_runs_unsupported). next_action is authenticate. Scopes cannot be patched — mint a new key. Missing scopes are never format_not_found. |
workspace_key_required | 403 | Team Format, personal key. Use a key created in details.workspace_id. |
invalid_request | 400 | concurrency is not an integer 1–16; items is missing, empty, or longer than 100; an item is not an object; or items[i] names none of instruction / input / previous_run_id / attachments. details.index names a bad item. |
format_not_found | 404 | Unknown, archived, outside your key's workspace, or a team handle you are not a member of. |
format_api_trigger_disabled | 409 | The API call trigger is off for this Format. |
format_inactive | 409 | The Format is inactive. |
idempotency_conflict | 409 | That Idempotency-Key was already used with a different bulk payload. |
invalid_attachment / attachment_not_found / attachment_too_large / attachment_fetch_failed | 400 / 413 / 502 | Fired while resolving an item's attachments before the queue is created — same codes as Calling a Format. |
rate_limited | 429 | Wait retry-after seconds. Create spends the write budget. |
studio_agent_upstream_unavailable | 503 | A Sume-side outage. Retry later. |
Poll (GET /v1/format-run-queues/{queue_id}):
| Code | Status | What to do |
|---|---|---|
unauthorized | 401 | Missing or invalid API key. |
insufficient_scope | 403 | The key lacks formats:read. details.required_scope names it. |
format_run_queue_not_found | 404 | Unknown queue id, or a queue belonging to another owner. |
rate_limited | 429 | Wait retry-after. Polling spends the read budget, separate from create. |
studio_agent_upstream_unavailable | 503 | Retry later. The queue is still draining. |
A 429 or 503 during a poll loop is transient: the queue keeps working. Back off
rather than treating it as a failed queue.
Wallet / admission failures on a child after 202 do not fail the create. That item
becomes failed with the create-run error, and the window refills from the remaining
queued items.
End to end
Use exponential backoff in production rather than a fixed five-second sleep. Each child is still minutes of work when the Format makes video; polling the queue every second buys you nothing and costs you rate limit.
To read a finished child's media, take run_id off items[] and follow
Runs and results.
Next
- Calling a Format — the per-item invoke contract and every submit error
- Runs and results — child receipts, polling, cancelation
- Structured output — bind a schema on each item
- Run webhooks — per-child
communication.webhook_url, not a queue callback - Embed a Format in your product — partner integration around a single run