Long jobs hit the ~6-minute wall mid-batch. A checkpoint in Script Properties — { key, cursor, updatedAt } — lets the next timed trigger pick up where you left off instead of restarting from row 1. Pair with chunked writes; don’t confuse this with locks (locks serialize; checkpoints resume).
What you'll need
- A container-bound or standalone Apps Script project
- Script Properties access (default for your project)
- A stable string key per job (e.g.
job:nightlySync) - A clear cursor meaning (row index, page token, Drive file offset)
How to use this snippet
/**
* Read a job checkpoint from Script Properties.
* @param {string} key property name
* @return {{key:string, cursor:*, updatedAt:string}|null}
*/
function getCheckpoint(key) {
var raw = PropertiesService.getScriptProperties().getProperty(key);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch (e) {
return null;
}
}
/**
* Write / update a job checkpoint.
* @param {string} key
* @param {*} cursor progress marker (number, string token, object — keep JSON-safe)
* @param {Object=} extra optional fields merged into the payload
* @return {{key:string, cursor:*, updatedAt:string}}
*/
function setCheckpoint(key, cursor, extra) {
var payload = Object.assign(
{ key: key, cursor: cursor, updatedAt: new Date().toISOString() },
extra || {}
);
PropertiesService.getScriptProperties().setProperty(key, JSON.stringify(payload));
return payload;
}
/** Clear when the job completes successfully. */
function clearCheckpoint(key) {
PropertiesService.getScriptProperties().deleteProperty(key);
}
/** Convenience facade bound to one job key (NitroGAS symbol: readWriteCheckpoint). */
function readWriteCheckpoint(key) {
return {
get: function () { return getCheckpoint(key); },
set: function (cursor, extra) { return setCheckpoint(key, cursor, extra); },
clear: function () { clearCheckpoint(key); }
};
}
Example
var CP_KEY = 'job:exportRows';
var BUDGET_MS = 4.5 * 60 * 1000; // exit before the hard wall
function exportRowsTrigger() {
var started = Date.now();
var cp = getCheckpoint(CP_KEY) || { cursor: 1 }; // 1-based data row
var sheet = SpreadsheetApp.getActive().getSheetByName('Data');
var last = sheet.getLastRow();
var cursor = Number(cp.cursor) || 1;
while (cursor <= last) {
if (Date.now() - started > BUDGET_MS) {
setCheckpoint(CP_KEY, cursor, { last: last });
console.log('checkpointed at row', cursor);
return;
}
// … process one row or a small block …
cursor += 50;
}
clearCheckpoint(CP_KEY);
console.log('export complete');
}
Tips:
- Name keys by job (
job:…) so multiple triggers don’t stomp each other. - Store the smallest cursor you need — not the whole working set.
- Clear on success; leave the checkpoint on failure so the next run retries.
- Corrupt JSON →
getCheckpointreturnsnull(treat as fresh start, or alert). - Still use a lock if two triggers can run the same job overlapping.
Tip: NitroGAS Co-Pilot can drop getCheckpoint / setCheckpoint next to your trigger loop and wire the budget exit — you keep the cursor semantics.
Happy Coding!
