Apps Script LockService tryLock stops a double Sume run
Wrap the Sume submit in an Apps Script lock so two triggers or edits cannot start the same paid run, then add an idempotency key so a retry is safe.

To stop an Apps Script from starting the same Sume run twice, call LockService.getScriptLock() and tryLock() before the submit, and release it afterwards. The lock stops two executions of your script from being inside the guarded section together. It does not make the Sume request idempotent, so also send an Idempotency-Key built from the sheet row and a version.
The Apps Script facts are from LockService and Lock, both read 2026-10-10. The Sume facts are from Create a run.
What does LockService offer?
The reference describes three lock kinds: script, user and document. The document lock returns null in standalone scripts and in web apps, so a standalone project or a web-app doPost should use the script lock. A lock is only acquired when you call tryLock or waitLock, not when you create it. tryLock(ms) returns a boolean; waitLock(ms) throws if it cannot get the lock in time; releaseLock() frees it; hasLock() reports state; and the lock is released automatically when the script ends.
A practical rule: use tryLock for timer triggers, where skipping a tick is fine because the next one runs soon, and waitLock for a button the person pressed, where silently doing nothing would look like a bug. Give waitLock a wait shorter than the script's own run limit, and catch the exception so the person sees a clear message instead of a stack trace.
| Call | Result | Use in a Sume submit |
|---|---|---|
| getScriptLock() | Lock shared by all executions of the script | Default choice for triggers and web apps |
| getDocumentLock() | Null in standalone scripts and web apps | Only for scripts bound to a document |
| tryLock(ms) | Boolean, no exception | Skip this run and let the next trigger try |
| waitLock(ms) | Throws if the wait times out | Use when the run must happen this time |
| releaseLock() | Frees the lock now | Call in finally after the request returns |
Where does the lock sit?
Guard the read of the row status, the request and the write of the run id back to the sheet. If you only guard the request, two executions both read an empty cell and both submit. Inside the lock: read the status cell, return if it already holds a run id, submit, write the id. The second execution then sees the id and exits.
Keep the guarded section short. Do not poll a render inside the lock; it holds everything else up. Write the id and leave.
function submitRow(row, sku) {
const lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) return;
try {
const sheet = SpreadsheetApp.getActiveSheet();
if (sheet.getRange(row, 3).getValue()) return;
const res = UrlFetchApp.fetch("https://api.sume.com/v1/formats/acme/promo/runs", {
method: "post", contentType: "application/json", muteHttpExceptions: true,
headers: { Authorization: "Bearer " + PropertiesService.getScriptProperties().getProperty("SUME_API_KEY"),
"Idempotency-Key": "sheet-" + sku + "-v1" },
payload: JSON.stringify({ instruction: "Make the promo.", input: { sku: sku } })
});
sheet.getRange(row, 3).setValue(JSON.parse(res.getContentText()).data.id);
} finally { lock.releaseLock(); }
}Why also send an idempotency key?
The lock protects against two executions, not against a request that succeeded while your script timed out or the write to the sheet failed. Create a run describes the answer: send an Idempotency-Key, and a replay returns 200 with idempotency_hit: true and the original run. The same key with a different body returns 409 idempotency_conflict, and a concurrent request with the same key returns 409 idempotency_key_in_use. The key is scoped to one Format and may be up to 255 characters.
Build the key from the item plus a version you change on purpose, not from the time. A key made from the clock never replays, which defeats it.
Also remember that the lock is released when the script ends, which the Lock reference states. That is a safety net, not a plan: a crash between the request and the sheet write still leaves a run without a stored id, which is exactly the gap the idempotency key closes on the next attempt.
What about the sleep after the submit?
Apps Script is a poor place to wait for a long render. Store the run id, then use a time-driven trigger or a Sume communication.webhook_url to learn the result. Runs and results explains that result_url gives 409 run_not_completed until the run is terminal, so a trigger that polls status_url on a minute-scale schedule is enough. Use muteHttpExceptions so a 409 or 429 arrives as a response you can read instead of an exception.
Sources
Related posts
More in Integrations
- Azure Logic Apps HTTP action times out at 120 s: long Sume runs
Logic Apps HTTP actions time out at 120 s. Start a Sume Format run (202 receipt), pass a webhook URL, and set an idempotency key so retries stay safe.
- Bitbucket merged-PR webhook to a Sume Format run: verify first
Verify Bitbucket's X-Hub-Signature (sha256=) with its published test values, then start a Sume Format run on pullrequest merged with a key built from the PR.
- Buildkite webhook: X-Buildkite-Token or Signature before a Sume run
Buildkite pipeline webhooks offer a clear-text token or an HMAC-SHA256 signature. Use the signature on build.finished before you start a paid Sume Format run.
- Cal.com BOOKING_CREATED webhook to a Sume video run, verified
Cal.com signs webhooks with x-cal-signature-256. Verify it, turn BOOKING_CREATED into a Sume Format run, and keep the Idempotency-Key stable on retries.
Written by Sume