A teammate kicked off “Process all rows” from the menu. Two minutes in they realize the filter was wrong. There is no Stop button — only waiting for the 6-minute kill, or closing the tab and hoping. requestCancel(key) / isCancelRequested(key) / clearCancel(key) put a short-lived cancel flag in CacheService. A sidebar (or another menu item) sets it; your job loop checks it each iteration and breaks cleanly; finally clears it. If nobody clears it, the TTL expires on its own.
When to use
- Long menu / sidebar jobs a human might want to abort mid-run
- Time-driven jobs you occasionally need to pause from a “Cancel tonight’s run” control
- Any loop that already checks
underTimeBudget— add cancel as a second exit condition
How to use this snippet
var CANCEL_PREFIX_ = 'cancel:';
/** Bound: per-spreadsheet cache. Standalone: script-wide cache. */
function cancelCache_() {
return CacheService.getDocumentCache() || CacheService.getScriptCache();
}
/**
* Raise the cancel flag for this job key.
* @param {string} key short id shared by the UI and the job, e.g. 'nightlySync'
* @param {number=} ttlSeconds default 21600 (6h). Safety net if clearCancel never runs.
*/
function requestCancel(key, ttlSeconds) {
var ttl = Math.max(1, Math.min(ttlSeconds || 21600, 21600));
cancelCache_().put(CANCEL_PREFIX_ + String(key), '1', ttl);
}
/** True if a cancel has been requested for this key (and not yet cleared / expired). */
function isCancelRequested(key) {
return cancelCache_().get(CANCEL_PREFIX_ + String(key)) === '1';
}
/** Lower the flag. Call from the job's finally (and from a "Resume" control if you have one). */
function clearCancel(key) {
cancelCache_().remove(CANCEL_PREFIX_ + String(key));
}
Example
var JOB_KEY = 'processVisible';
// Menu / sidebar "Cancel" button calls this (google.script.run.requestCancel)
function cancelProcessVisible() {
requestCancel(JOB_KEY);
SpreadsheetApp.getActive().toast('Cancel requested — the job will stop after the current row.', 'Cancel', 5);
}
function processVisibleRows() {
clearCancel(JOB_KEY); // fresh run
var started = new Date();
try {
var rows = getVisibleRowObjects(SpreadsheetApp.getActiveSheet());
for (var i = 0; i < rows.length; i++) {
if (!underTimeBudget(started) || isCancelRequested(JOB_KEY)) break;
doWork_(rows[i]);
}
} finally {
clearCancel(JOB_KEY);
}
}
Tips:
- Use one stable key per job (
'nightlySync','processVisible'). Don’t invent a new key per run or Cancel can’t find the job. - Check the flag between units of work (per row / per file), not inside a single 30-second
UrlFetch— you can’t interrupt that mid-call. - Always
clearCancelinfinally. The TTL is a safety net for crashed UIs, not the happy path. - CacheService is best-effort and not instantaneous across all servers; a check every row is fine, don’t spin on it.
- Pair with under-time-budget so Cancel and the 6-minute wall share one loop exit.
Tip: NitroGAS Co-Pilot can wire a tiny HtmlService sidebar Cancel button to requestCancel and add the isCancelRequested check to an existing loop — paste the loop and say which key to use.
Happy Coding!
