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

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. 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:

  1. Use a 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. If a team workspace owns the Format, use a key 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 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.

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 } }. 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_url is per item. Poll status_url for queue-level progress.
  • completed is not "all succeeded". It means that every item is terminal. Branch on counts.failed.
  • Mint a fresh Idempotency-Key per batch. If you replay a spent key, the API returns 202 with the old queue.

Request body

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

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 URL for progress.

Queue status

StatusMeaning
queuedThe queue did not dispatch any item yet.
runningThe concurrency window drains the list.
completedEvery 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

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

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

CodeStatusWhat to do
unauthorized401Missing, malformed, revoked, or unknown API key.
insufficient_scope403The 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_required403Team Format, personal key. Use a key created in details.workspace_id.
invalid_request400concurrency 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_found404Unknown, archived, outside your key's workspace, or a team handle that you are not a member of.
format_api_trigger_disabled409The API call trigger is off for this Format.
format_inactive409The Format is inactive.
idempotency_conflict409You already used that Idempotency-Key with a different bulk payload.
invalid_attachment / attachment_not_found / attachment_too_large / attachment_fetch_failed400 / 413 / 502The 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_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 does not have formats:read. details.required_scope names the scope.
format_run_queue_not_found404Unknown queue id, or a queue that a different owner has.
rate_limited429Wait retry-after. A poll spends the read budget, which is separate from the create budget.
studio_agent_upstream_unavailable503Retry 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