Year-in-review videos for 250 customers: bulk queues of 100, 100, 50

A Sume bulk run takes at most 100 items, so 250 year-in-review runs need three queues. Set concurrency 1 to 16, a spend cap per item and a new key per queue.

6 min readSume
All posts

Two hundred and fifty customers is three bulk queues on Sume: 100 + 100 + 50, because POST /v1/formats/{handle}/{slug}/bulk-runs accepts 1 to 100 items per request. Each queue takes a concurrency of 1 to 16, which is how many child runs stay in flight at once. Each item is an ordinary Format run, so each carries its own input, spend cap and webhook.

A personalised year-in-review is the case bulk runs were built for: the recipe is fixed and the data changes row by row. Keep the data in input as plain JSON rather than rewriting a prompt per customer.

The limits that shape the plan

These come from the Sume Formats bulk-runs and call docs.

Bulk-run limits (Sume docs, read 2026-10-13)
LimitValueWhat it means for 250 customers
Items per queue1 to 100Three queues
Concurrency1 to 16 per queuePick 4 to 8 to stay gentle on workspace concurrency
input size64 top-level keys, 2 MiBPer-customer stats fit easily
Queue webhookNone; webhook is per itemSet communication.webhook_url on each item, or poll the queue
IdempotencyA spent key returns the old queueUse one key per queue, never reuse one for changed data

Chunk and submit

The script splits the list and gives each queue its own key. Replaying a run of the same script with the same keys returns the same queues instead of making new ones, which is what you want after a crash and what you do not want after a data change, so include a data version in the key.

import asyncio, os
import httpx

def chunks(rows, n=100):
    for i in range(0, len(rows), n):
        yield rows[i:i + n]

async def main():
    rows = [{"customer_id": f"c{i}", "orders": 12, "favorite": "Oat latte"} for i in range(250)]
    headers = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
    url = "https://api.sume.com/v1/formats/acme/year-in-review/bulk-runs"
    async with httpx.AsyncClient(headers=headers, timeout=60) as client:
        for n, part in enumerate(chunks(rows), start=1):
            items = [{"input": r, "generation_spend_cap_credits": 400} for r in part]
            resp = await client.post(url, json={"concurrency": 6, "items": items},
                                     headers={"Idempotency-Key": f"yir-2026-v1-q{n}"})
            resp.raise_for_status()
            print(n, resp.json()["data"]["id"])

asyncio.run(main())

Poll the queues and read the result

Poll GET /v1/format-run-queues/{id}. A queue reports completed when every item is terminal, not when every item succeeded, so read counts.failed and re-queue the failed rows under a fresh key. A bad item fails the whole create with 400 invalid_request and an index before anything runs, so validate rows locally first.

A 250-row job is also the place to decide the cap per item before the run, not after. A cap that is too low fails the item without finishing the video; a cap that is high lets one bad row use a large part of the budget.

  • Validate that each row has a name and a number before you submit.
  • Store the queue id and the run ids against your customer ids.
  • Send the customer link only after a run is terminal and successful.

Related posts

More in Developers

All Developers posts

Written by Sume