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.

5 min readSume
All posts

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.

Lock calls (Apps Script reference, read 2026-10-10)
CallResultUse in a Sume submit
getScriptLock()Lock shared by all executions of the scriptDefault choice for triggers and web apps
getDocumentLock()Null in standalone scripts and web appsOnly for scripts bound to a document
tryLock(ms)Boolean, no exceptionSkip this run and let the next trigger try
waitLock(ms)Throws if the wait times outUse when the run must happen this time
releaseLock()Frees the lock nowCall 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

All Integrations posts

Written by Sume