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

MethodPathScope
POST/v1/formats/{format_id}/bulk-runsformats:write
POST/v1/formats/{handle}/{slug}/bulk-runsformats: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:

  1. Bearer API key (Authorization: Bearer $SUME_API_KEY).
  2. The key carries formats:write to create a queue and formats:read to poll it.
  3. For a Format owned by a team workspace, the key was issued in that workspace.
  4. Service-account keys cannot create Format runs or bulk queues. They fail with 403 insufficient_scope and details.reason of service_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.

ScopeNeeded for
formats:writePOST …/bulk-runs (and single-run create / cancel).
formats:readGET /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

FieldNotes
concurrencyRequired integer 1–16. How many child Format runs stay in flight at once.
itemsRequired array, 1–100 entries, in order. Each entry is the same body as POST …/runs.
idempotency_keyOptional 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:

FieldNotes
idQueue id (frq_…).
objectAlways format.run_queue.
format{ "id", "slug", "title", "version" }. id is the opaque skl_….
concurrencyThe window you sent (1–16).
statusqueued / running / completed. See below.
countstotal, queued, running, completed, failed, canceled. All required.
itemsOne row per submitted item, in the same order.
created_at, updated_atISO-8601.
finished_atSet when the queue status becomes completed; otherwise null.
status_urlGET /v1/format-run-queues/{id} — poll this for progress.

Queue status

StatusMeaning
queuedNothing has dispatched yet.
runningThe concurrency window is draining the list.
completedEvery 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

FieldNotes
indexZero-based position in the submitted items array.
statusqueued / running / completed / failed / canceled.
run_idChild 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 statusMeaning
queuedNot started. run_id is null.
runningInside the concurrency window.
completedChild run completed. Terminal; frees a slot.
failedChild run failed (or skipped, recorded here as failed), or the child could not be started. Terminal; frees a slot.
canceledChild 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 runItem error
completednull
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.

ReplayResult
Same key, same { concurrency, items }202 and the existing queue.
Same key, different payload409 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):

CodeStatusWhat to do
unauthorized401Missing, malformed, revoked, or unknown API key.
insufficient_scope403The 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_required403Team Format, personal key. Use a key created in details.workspace_id.
invalid_request400concurrency 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_found404Unknown, archived, outside your key's workspace, or a team handle you are not a member of.
format_api_trigger_disabled409The API call trigger is off for this Format.
format_inactive409The Format is inactive.
idempotency_conflict409That Idempotency-Key was already used with a different bulk payload.
invalid_attachment / attachment_not_found / attachment_too_large / attachment_fetch_failed400 / 413 / 502Fired while resolving an item's attachments before the queue is created — same codes as Calling a Format.
rate_limited429Wait retry-after seconds. Create spends the write budget.
studio_agent_upstream_unavailable503A Sume-side outage. Retry later.

Poll (GET /v1/format-run-queues/{queue_id}):

CodeStatusWhat to do
unauthorized401Missing or invalid API key.
insufficient_scope403The key lacks formats:read. details.required_scope names it.
format_run_queue_not_found404Unknown queue id, or a queue belonging to another owner.
rate_limited429Wait retry-after. Polling spends the read budget, separate from create.
studio_agent_upstream_unavailable503Retry 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