Google Sheets to video automation with Apps Script and Sume

Turn up to 100 sheet rows into Sume video runs with one Apps Script request, then poll the queue from a time-driven trigger and write URLs back.

5 min readSume
All posts

To automate video from Google Sheets, have Apps Script send the rows to Sume as one bulk run: a single UrlFetchApp.fetch POST to /v1/formats/{handle}/{slug}/bulk-runs queues up to 100 rows as Format runs, and a time-driven trigger polls the queue and writes each finished video URL back to its row.

Sume has no Google Sheets add-on; this is a plain HTTPS call from your own script. The Sume facts come from the Bulk runs and Runs and results docs, and the Apps Script facts from Google's reference pages, all read on 2026-09-27. For the request contract on its own, see Sume Format bulk runs.

How do I send the sheet to Sume from Apps Script?

Keep the API key out of cells and out of the code. Add it as a script property on the project settings page. Script properties are shared among all users of the script, so share the project only with people who may hold the key. Sume's docs add that keys never go into frontend JavaScript, support tickets, or screenshots.

The function below reads rows 2 to 101 of a Videos tab (instruction in A, image URL in B) and posts them as one queue. acme/product-promo stands in for your own Format's handle and slug. Sume publishes no field list for input, so send the keys your Format reads.

Sume's cookbook says to keep your own sheet-row ↔ index map, because items[i].index is the position you submitted. The code sends every row in order, so a row is always index + 2. Keep the rows contiguous: a blank row inside the range is still sent as an item.

  • Send contentType: "application/json". UrlFetchApp defaults to application/x-www-form-urlencoded.
  • muteHttpExceptions: true makes fetch return the response instead of throwing when the status code signals a failure, so you can log Sume's error body.
  • The Idempotency-Key comes from a BATCH property you bump for each new batch, so re-running submitSheet on an unchanged batch returns the existing queue with 202 instead of starting a second one.
  • The script tracks one batch at a time: one QUEUE_ID, and dropTrigger removes the old polling trigger before a new one is installed.
const API = "https://api.sume.com/v1";
const props = PropertiesService.getScriptProperties();

function submitSheet() {
  const sheet = SpreadsheetApp.openById(props.getProperty("SHEET_ID")).getSheetByName("Videos");
  const rows = sheet.getDataRange().getValues().slice(1, 101); // row 1 is the header
  const items = rows.map((r) => ({ instruction: r[0], input: { url: r[1] } }));
  const res = UrlFetchApp.fetch(API + "/formats/acme/product-promo/bulk-runs", {
    method: "post",
    contentType: "application/json",
    headers: {
      Authorization: "Bearer " + props.getProperty("SUME_API_KEY"),
      "Idempotency-Key": "sheet-batch-" + props.getProperty("BATCH"),
    },
    payload: JSON.stringify({ concurrency: 4, items: items }),
    muteHttpExceptions: true,
  });
  if (res.getResponseCode() !== 202) throw new Error(res.getContentText());
  props.setProperty("QUEUE_ID", JSON.parse(res.getContentText()).data.id);
  dropTrigger(); // a rerun must not leave a second polling trigger behind
  const trigger = ScriptApp.newTrigger("pollQueue").timeBased().everyMinutes(5).create();
  props.setProperty("TRIGGER_ID", trigger.getUniqueId());
}

How do I poll the queue without a long-running script?

Don't wait inside submitSheet. Google stops a script after 6 minutes per execution, and Sume's docs say each child is minutes of work when the Format makes video. So submitSheet installs a time-driven trigger, and pollQueue reads GET /v1/format-run-queues/{queue_id} on every tick.

  • everyMinutes(n) accepts only 1, 5, 10, 15, or 30 (ClockTriggerBuilder). Sume's docs note that polling a queue every second buys nothing and costs rate limit.
  • Queue completed means every item is terminal, not that every item succeeded. A row whose run failed gets its item status instead of a URL; read why on the child receipt at GET /v1/format-runs/{run_id}.
  • A 429 or 503 during a poll is transient and the queue keeps working, so the function returns and tries again on the next tick. Any other error throws, so it reaches you as a failure email.
  • primary_output_url is a durable media.sume.com URL. It does not expire and is public to anyone holding it, so anyone who can read the sheet can open the videos.
function pollQueue() {
  const auth = { Authorization: "Bearer " + props.getProperty("SUME_API_KEY") };
  const read = (path) => {
    const res = UrlFetchApp.fetch(API + path, { headers: auth, muteHttpExceptions: true });
    const code = res.getResponseCode();
    if (code === 429 || code === 503) return undefined; // transient: try the next tick
    if (code !== 200) throw new Error(res.getContentText()); // Apps Script emails the failure
    return JSON.parse(res.getContentText()).data;
  };
  const queue = read("/format-run-queues/" + props.getProperty("QUEUE_ID"));
  if (!queue || queue.status !== "completed") return; // a 429/503, or still running
  const sheet = SpreadsheetApp.openById(props.getProperty("SHEET_ID")).getSheetByName("Videos");
  for (const item of queue.items) {
    const run = item.run_id ? read("/format-runs/" + item.run_id) : null;
    if (run === undefined) return; // a 429/503: rewrite every row on the next tick
    sheet.getRange(item.index + 2, 3).setValue((run && run.primary_output_url) || item.status);
  }
  dropTrigger(); // every row is written
}

function dropTrigger() {
  for (const t of ScriptApp.getProjectTriggers()) {
    if (t.getUniqueId() === props.getProperty("TRIGGER_ID")) ScriptApp.deleteTrigger(t);
  }
}

Why not have Sume call an Apps Script web app instead?

The queue itself has no webhook; only each item's communication.webhook_url does. You could point that at an Apps Script web app, but content returned by the Content service is redirected to a one-time URL at script.googleusercontent.com. Sume does not follow redirects and counts a 3xx as a failed delivery attempt.

A trigger that polls needs no public endpoint, which makes it the pattern that works with Apps Script alone. For per-run webhooks, run a receiver outside Apps Script, as in Sume Format run lifecycle.

What are the limits on each side?

Google's quotas are per user and reset 24 hours after the first request, and an installable trigger always runs under the account of the person who created it.

  • When a triggered function throws, no error appears on screen. Apps Script sends a failure-summary email instead, with a link to deactivate or reconfigure the trigger.
  • To queue only the rows that failed, see Retry failed items in an AI video batch.
From Sume's Bulk runs page and Google's Apps Script quotas, read 2026-09-27.
LimitValueSide
Items per bulk request1–100Sume
Child runs in flight (concurrency)1–16Sume
Script runtime6 min per executionGoogle
Triggers total runtime90 min/day (consumer), 6 hr/day (Workspace)Google
URL Fetch calls20,000/day (consumer), 100,000/day (Workspace)Google
Triggers20 per user per scriptGoogle

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume