Bulk runs
You can leave a list of Format runs to run overnight. You do not have to drive the 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.
The accurate 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. For new integrations, we recommend {handle}/{slug}. The opaque
skl_… path stays valid forever. The request body, headers, scopes, and queue receipt are
the same for both paths.
The API has no public list-queues or cancel-queue endpoint. To cancel a child, use
POST /v1/format-runs/{run_id}/cancel. Refer to Cancel.
Auth
The API-key rules are the same as for a single Format run:
- Use a Bearer API key (
Authorization: Bearer $SUME_API_KEY). - The key carries
formats:writeto create a queue andformats:readto poll it. - If a team workspace owns the Format, use a key 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 release of the Format API-call trigger do not carry these scopes. Create a new key. You cannot add scopes to a key that already exists. Refer to 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 } }. If the list is longer
than the window, the first concurrency items are already in flight on that receipt.
Worked example: a production batch
The small example above keeps the item bodies one line long. A real batch usually holds one long item for each spreadsheet row. The envelope still has only two keys:
Mobidoo uses this method for its live-commerce sheet against the vanity path
POST /v1/formats/mobidoo/live-commerce/bulk-runs: concurrency: 2 and 20 items. Each item
is one broadcast row. The client skips rows with no finished draft. Each item binds
its own output_schema and generation_spend_cap_usd.
Mobidoo → Bulk runs shows the unabridged item body
from that batch: full instruction, full script, schema, and per-item webhook.
That batch shape makes three items clear:
- The queue has no webhook.
communication.webhook_urlis per item. Pollstatus_urlfor queue-level progress. completedis not "all succeeded". It means that every item is terminal. Branch oncounts.failed.- Mint a fresh
Idempotency-Keyper batch. If you replay a spent key, the API returns202with the old queue.
Request body
| Field | Notes |
|---|---|
concurrency | Required integer 1–16. The number of child Format runs that stay in flight at the same time. |
items | Required array, 1–100 entries, in order. Each entry is the same body as POST …/runs. |
idempotency_key | Optional body form of the Idempotency-Key header. If you send both, the header wins. |
The API rejects unknown top-level fields. You must send items. { "concurrency": 3 } is
400, not an empty queue.
Each item must name at least one of instruction, input, previous_run_id, or
attachments. This is the same rule as for a single run. A bad item fails the create (400 invalid_request, details.index) before a queue exists. Sume dispatches nothing.
The bulk controller owns single-flight. The controller executes every item 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 for 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 URL for progress. |
Queue status
| Status | Meaning |
|---|---|
queued | The queue did not dispatch any item yet. |
running | The concurrency window drains the list. |
completed | Every item is terminal. Examine counts for failures. completed is not "all succeeded". |
counts.total is items.length. counts.running includes items that the API still treats as
in flight (a child that the API claimed but did not yet give 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 after dispatch. 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 | In 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 start. Terminal. Frees a slot. |
canceled | Child run is canceled. Terminal. Frees a slot. |
A failed item that never started a run still keeps 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." } |
To find why a child failed, read the run receipt (GET /v1/format-runs/{run_id}), not only
the queue item.
Concurrency window
The server keeps concurrency child runs in flight. When a slot opens, the server immediately
starts the next queued item, until the list is drained. This is not client-driven fan-out.
In-flight slots are items with the public status running. completed, failed, and
canceled are terminal and free a slot. When an in-flight child gets to completed or
failed (the usual drain path), the next queued item starts immediately. Thus, 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. When item 0 completes, item 3 starts. The window
stays at three until fewer than three items remain.
The window never goes above concurrency, even if a poll or an advance of progress occurs
more than once.
Child runs still go through ordinary Format-run admission (wallet, workspace generation
concurrency, spend caps). If a child fails to start, that item becomes failed. The
queue create already returned 202.
Poll the queue
Use 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 the API sets finished_at. Branch
on counts.failed and counts.canceled. Queue completed does not mean success.
A queue that you cannot see gets the same answer as a queue that does not exist:
404 format_run_queue_not_found (unknown id, or a queue that a different user owns).
Child runs
Each dispatched item is an ordinary Format run:
status_url / result_url / events_url / cancel_url on that receipt operate the same as
on Runs and results. The queue does not replace those endpoints. It adds
counts and per-item status on top.
When you cancel a child (POST /v1/format-runs/{run_id}/cancel), the queue marks that item
canceled and frees its slot for the next queued item.
Idempotency
Send Idempotency-Key on create as a header. The API also accepts body idempotency_key, but
the header wins. The scope of a key is one Format.
| Replay | Result |
|---|---|
Same key, same { concurrency, items } | 202 and the queue that already exists. |
| 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 add a separate idempotency-in-use lock. It uses only what the control plane already stores for that key. There is no queue-level webhook. There is no queue-level idempotency behavior other than 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 does not have formats:write, or it is a service-account key (details.reason is service_account_format_runs_unsupported). next_action is authenticate. You cannot patch scopes. Mint a new key. Missing scopes never give 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. Or, items is missing, empty, or longer than 100. Or, 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 that 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 | You already used that Idempotency-Key with a different bulk payload. |
invalid_attachment / attachment_not_found / attachment_too_large / attachment_fetch_failed | 400 / 413 / 502 | The API sends these codes when it resolves an item's attachments, before it creates the queue. These are the same codes as in 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 does not have formats:read. details.required_scope names the scope. |
format_run_queue_not_found | 404 | Unknown queue id, or a queue that a different owner has. |
rate_limited | 429 | Wait retry-after. A poll spends the read budget, which is separate from the create budget. |
studio_agent_upstream_unavailable | 503 | Retry later. The queue continues to drain. |
A 429 or 503 during a poll loop is transient. The queue continues to work. Back off.
Do not think of 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
In production, use exponential backoff, not a fixed five-second sleep. When the Format makes video, each child is still minutes of work. If you poll the queue every second, you get no benefit and you use your rate limit.
To read a finished child's media, get run_id from items[]. Then use the procedure in
Runs and results.
Next
- Calling a Format — the per-item invoke contract and every submit error
- Runs and results — child receipts, polls, 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