Read / Write a Job Checkpoint

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 → getCheckpoint returns null (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!

NitroGAS Chrome Extension

Want this snippet handy inside the Apps Script editor? NitroGAS gives you free themes and snippets — plus optional Co-Pilot when you want a boost (1-week, 1-month, or yearly passes — no subscription). Happy Coding!

Get the Extension