Spreadsheet to Format bulk queue in Node: 100-row limit and key rules

Turn a CSV of products into one bulk-runs request in Node. Items are capped at 100, concurrency at 16, and the Idempotency-Key must be new for each batch.

5 min readSume
All posts

To turn a spreadsheet into Format runs, read each row into one item and send the list to POST /v1/formats/{handle}/{slug}/bulk-runs. A queue takes 1 to 100 items and a concurrency of 1 to 16, so a longer sheet needs to be split into batches of 100. The Node script below does it for one batch with a header row. It uses the built-in fetch, which needs Node 18 or later.

The script

acme/sale-clip is a placeholder for a Format you own. The CSV is assumed to have two columns, sku,name, with no commas inside values; a real sheet needs a CSV parser. The key needs formats:write.

const fs = require("fs");

async function main() {
  const rows = fs.readFileSync("sale.csv", "utf8").trim().split("\n").slice(1);
  const items = rows.map((line) => {
    const [sku, name] = line.split(",");
    return { instruction: "15 second vertical sale clip", input: { sku, name },
             generation_spend_cap_usd: 10 };
  });
  const res = await fetch("https://api.sume.com/v1/formats/acme/sale-clip/bulk-runs", {
    method: "POST",
    headers: { Authorization: "Bearer " + process.env.SUME_API_KEY,
               "Content-Type": "application/json",
               "Idempotency-Key": "sale-clips-" + new Date().toISOString().slice(0, 10) },
    body: JSON.stringify({ concurrency: 4, items: items.slice(0, 100) }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(JSON.stringify(body));
  console.log(body.data.id, body.data.counts);
}

main().catch((e) => { console.error(e); process.exit(1); });

Rules that the script relies on

Each item has the same shape as a single run, so each can carry its own generation_spend_cap_usd. The table lists the queue's limits as of 2026-10-09.

Bulk queue limits from the Bulk runs docs, as of 2026-10-09
ItemLimit or rule
items1 to 100 entries, in order
concurrencyRequired integer, 1 to 16
Bad itemThe whole create fails with 400 invalid_request and details.index; no queue exists
WebhookNone on the queue; set it per item
Replay of a spent key202 with the old queue, so use a new key per batch
Scopeformats:write to create, formats:read to poll

After the request

A 202 returns data.id (frq_…) and counts. Poll GET /v1/format-run-queues/{id}. When the queue status is completed, all items are terminal, which is not the same as all succeeded, so check counts.failed and counts.canceled. To find why a row failed, read the child run at GET /v1/format-runs/{run_id}.

The date-based key in the script means a second run on the same day with the same body replays the old queue, which is what you want after a crash. For a deliberate re-run, change the key. For a sheet longer than 100 rows, add the batch number to the key.

Hardening the script

The script is short on purpose. For production, add four things. Parse the CSV with a real parser. Validate each row before you build the item, since one bad item fails the whole create with 400 invalid_request and details.index pointing at the row. Write the returned queue id and the row-to-index mapping to your database, because the queue items come back in the same order as you sent them. Split sheets over 100 rows into batches, each with its own key.

Poll status_url with a gap of a few seconds that grows toward a minute, like the single-run loop. Remember that the read budget is much larger than the write budget, but it is still per key. When the queue is completed, fetch the child receipts only for items with completed status, and handle the failed rows separately.

If the process stops after the create call, nothing is lost: the queue runs on the server. Read the frq_ id from your log, or send the same body with the same key to get the existing queue back with 202, and carry on polling from there.

Sources

Related posts

More in Formats

All Formats posts

Written by Sume