BigQuery product table to Sume bulk queues and a queue map
Read active SKUs from BigQuery, cast columns to plain JSON types, create one Sume bulk queue per 100 rows, and stream queue id, index and SKU to a map table.

Query the products you want as videos, cast each column to a plain string or integer, and send them to Sume 100 rows at a time. Each chunk becomes one bulk queue (POST /v1/formats/{handle}/{slug}/bulk-runs). Then write queue_id, idx and sku to a small BigQuery table. That table is how a finished run is joined back to a product, because the queue reports items by position.
Python's json.dumps raises TypeError on a Decimal or a date object. Casting in SQL means every value is already a string or an integer before it reaches the HTTP body, so the query is the one place to fix types.
Query and body shape
Order the query by SKU so the chunk boundaries are the same on every run. A different order moves rows between chunks, and a chunk key that was already used with a different body returns 409 idempotency_conflict.
Each item is the same shape as a single Format run. Put row data in input, which is a JSON object of at most 64 top-level keys and 2 MiB, and keep the task text short in instruction. Sume passes input to the agent as data and not as instructions, so scraped or supplier-written text belongs there.
| Column | Source | Reason |
|---|---|---|
queue_id | data.id of the create response (frq_...) | The API has no list-queues endpoint, so keep this yourself |
idx | Position in the items array you sent | Matches items[].index on the queue |
sku | Your row | input does not come back inside output |
run_id | items[].run_id after dispatch | Fill it on a later poll; it is null while queued |
The script
BATCH is part of every key. insert_rows_json returns a list of row errors and does not raise, so the script asserts on it.
import json, os, urllib.request
from google.cloud import bigquery
SQL = ("SELECT sku, product_url, CAST(price AS STRING) AS price "
"FROM `shop.catalog.bf_2026` WHERE active ORDER BY sku")
URL = f"https://api.sume.com/v1/formats/{os.environ['SUME_FORMAT']}/bulk-runs"
BATCH = "bf-2026-v1"
def create_queue(items, n):
req = urllib.request.Request(URL, method="POST",
data=json.dumps({"concurrency": 4, "items": items}).encode(),
headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": f"{BATCH}-chunk-{n}"})
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]["id"]
client = bigquery.Client()
rows = [dict(r) for r in client.query(SQL).result()]
for n in range(0, len(rows), 100):
chunk = rows[n:n + 100]
items = [{"instruction": "Make a 9:16 product ad.", "input": r} for r in chunk]
qid = create_queue(items, n // 100)
errs = client.insert_rows_json("shop.ops.sume_queue_items",
[{"queue_id": qid, "idx": i, "sku": r["sku"]} for i, r in enumerate(chunk)])
assert not errs, errsReading results later
Poll each queue at status_url and branch on counts. A queue is completed when every item is terminal, and that still includes failed items. For one failed row, the item error only says format_run_failed; the cause is on the run receipt at GET /v1/format-runs/{run_id}.
Reads and writes have separate rate budgets per key, and reads get forty times the write number. One create per chunk costs one write, so a 250-row table is three writes, and polls come out of the read budget.
If the script stops halfway
Run it again with the same BATCH. The queues that were already created come back as 202 with the original queue, so no second run starts. The map insert then writes the same rows again, so give that table a unique key on (queue_id, idx) or load it with a merge. A lost queue id can be recovered the same way, by replaying the create call with the same key and the same body.
The key and its scope matter here: an idempotency key belongs to one Format, so the same string sent to a second Format starts a second set of runs. If one table feeds two Formats, put the Format slug in the key as well as the batch label and chunk number.
Sources
Related posts
More in Integrations
- Deno.cron weekly Sume video run with a per-day idempotency key
A Deno.cron job that starts one Sume Format run each Monday, keyed by UTC date so a double fire replays, skips overlap, and hands the result to a webhook.
- GitHub Actions weekly Sume bulk queue that fails on counts.failed
A scheduled workflow that creates a Sume bulk queue, polls it through transient 429 and 503, and turns a completed queue with failed items into a red job.
- Let an agent pick talking video, Fabric or H3 Max over MCP
Rules for an agent on hosted MCP: avatar-videos_create for scripts, avatar-image-to-video_create for audio, and dry_run plus max_spend_usd before any paid call.
- How to add an MCP server to ChatGPT with developer mode
Turn on ChatGPT developer mode, create an app for the server's URL, and sign in with OAuth. The steps, with Sume's hosted MCP server as the example.
Written by Sume