// Apps Script

Persist global state across runs with PropertiesService.

A top-level const in Apps Script resets to its initial value on every execution. Use PropertiesService to store a cursor, flag, or counter that survives between trigger runs.

I want a variable in my Apps Script project to remember its value between separate trigger runs, but it keeps resetting to the default every time my script executes.

The script

copy · paste · trigger
cursor.gs
Apps Script
// Sync rows added since the last run, using a stored row cursor
const SHEET_NAME = 'Responses';
const CURSOR_KEY = 'lastProcessedRow';

function processNewRows() {
  const props = PropertiesService.getScriptProperties();
  const lastRow = Number(props.getProperty(CURSOR_KEY) || 1);

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  const currentLast = sheet.getLastRow();

  if (currentLast <= lastRow) {
    Logger.log('No new rows since row ' + lastRow);
    return;
  }

  const newData = sheet.getRange(lastRow + 1, 1, currentLast - lastRow, sheet.getLastColumn()).getValues();
  newData.forEach(function(row) { processRow(row); });

  props.setProperty(CURSOR_KEY, String(currentLast));
}

function processRow(row) {
  // your per-row logic here
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Why your top-level const resets every time

Apps Script does not keep a running process between executions. Every trigger fire, every manual run, every `doGet` call spins up a fresh V8 isolate, evaluates your files top to bottom, runs the target function, and then discards the entire runtime. A line like `const lastRow = 1;` at the top of your file is not a persistent value — it is an initializer that runs from scratch on every invocation. The first time I hit this, I spent an embarrassing amount of time adding `Logger.log` calls before realizing my variable wasn't drifting, it was being reset wholesale.

This is not a quirk you can work around with a global object or a module-level cache. There is no shared memory between executions. If a value needs to outlive the current run, it has to leave the runtime entirely and be stored somewhere durable before execution ends.

Three scopes, one right choice for most scripts

PropertiesService offers three stores: `getScriptProperties()`, `getUserProperties()`, and `getDocumentProperties()`. Script properties are shared across all users of the same script deployment and persist until you delete them or redeploy with a reset — that makes them the right home for state that belongs to the script itself, like a processing cursor or a last-sync timestamp. User properties are scoped to the person running the script, which matters for OAuth tokens or per-user preferences. Document properties only exist in container-bound scripts (spreadsheets, docs) and are tied to that specific file.

For a time-driven trigger doing incremental sync work, `getScriptProperties()` is almost always what you want. One property store, shared by every trigger execution, no auth context required.

The cursor pattern in practice

The snippet above is the shape I use for anything that processes a growing dataset incrementally. Read the stored cursor at the start (`getProperty` returns `null` if it has never been set, so the `|| 1` default handles first run). Do work only on the range beyond that cursor. Write the new cursor back at the end, and only if the work succeeded — if your `processRow` logic throws, the property never gets updated, so the next run retries from the same position.

A detail that bites people: `getProperty` always returns a string or null, and `setProperty` only accepts strings. Storing a number means converting on both ends — `Number(...)` coming in, `String(...)` going out. Miss either conversion and you get silent NaN comparisons that skip all rows forever. Keep both casts explicit and visible at the top of the function where they're easy to audit.

The 9 KB per-property and 500 KB total-per-store limits are generous for cursor values, flags, and small JSON blobs. If you are trying to cache a full API response, PropertiesService is the wrong tool — use Drive or Sheets instead.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Does PropertiesService work with time-driven triggers?
Yes. Script properties are stored server-side and survive between any two executions regardless of how they were triggered. A property set during a 6 AM trigger fire is readable by the 7 AM fire.
Can I store an object or array, not just a string?
Not directly. Serialize with JSON.stringify before calling setProperty, and parse with JSON.parse after getProperty. Stay under the 9 KB per-property limit — properties are not designed for large payloads.
How do I reset the cursor to start over from row 1?
Call PropertiesService.getScriptProperties().deleteProperty('lastProcessedRow') from the Script Editor, or add a resetCursor function you can run manually. You can also call deleteAllProperties() to wipe the entire store.
What is the difference between Script Properties and the old UserProperties?
UserProperties is deprecated and maps to getUserProperties() in the current API. Avoid it for new code. ScriptProperties (getScriptProperties) is the durable, shared store for anything tied to the script rather than a specific user session.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.