// Sheets · Apps Script

Generate a unique ID for each row in Google Sheets.

Use Apps Script and Utilities.getUuid() to stamp a permanent, collision-proof UUID into every new row — written once, never overwritten on re-run.

I need each row in my sheet to get a stable, unique ID when data is added, and I need it to stay put even if I run the script again.

The script

copy · paste · trigger
addRowIds.gs
Apps Script
// Stamp column A with a UUID on every row that doesn't already have one.
// Safe to re-run: skips rows where column A is populated.
function addRowIds() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var lastRow = sheet.getLastRow();
  if (lastRow < 2) return; // nothing to stamp

  var idCol = sheet.getRange(2, 1, lastRow - 1, 1).getValues();

  for (var i = 0; i < idCol.length; i++) {
    if (idCol[i][0] === '') {
      sheet.getRange(i + 2, 1).setValue(Utilities.getUuid());
    }
  }
}

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

Walkthrough

Why Utilities.getUuid() beats ROW() or RANDBETWEEN()

ROW() gives you a position, not an identity. Move the row, sort the sheet, or insert a blank above it and your "ID" silently changes. RANDBETWEEN() recalculates on every edit, so the value you logged this morning is gone by afternoon. Utilities.getUuid() produces a 128-bit RFC 4122 UUID — 36 characters, hyphen-separated, statistically impossible to collide — and setValue() writes it as a plain string that never recalculates.

The first time I hit this problem I used a CONCAT of timestamp plus RAND() and called it good. Three months later a collaborator sorted the sheet and half the IDs had silently re-rolled. UUID written via script is the only approach that actually holds.

Reading the whole column once, then writing cell by cell

The script calls getRange(2, 1, lastRow - 1, 1).getValues() to pull every existing ID in one network round-trip — that single read is fast even on 10,000 rows. Then it loops and calls setValue() only on the empty cells.

That asymmetry is deliberate. Batch reads are cheap; batch writes via setValues() would require building a parallel array and would overwrite cells you pass as empty strings. Individual setValue() calls on blank cells only is clearer and just as fast for the typical append-a-few-rows case. If you're stamping 5,000 rows at once, switch to building an output array and a single setValues() call to stay under the 30-second execution limit.

The guard if (idCol[i][0] === '') is the whole safety story. Any cell that already has a value — UUID or otherwise — is skipped unconditionally. Re-running the function on a sheet that already has IDs does nothing harmful.

Triggering it automatically on form submit or row append

Manual runs work for one-off backfills, but the real payoff is wiring it to an onFormSubmit or onChange trigger so every new row gets stamped within seconds of arriving. In the Apps Script editor go to Triggers (clock icon), add a trigger on the spreadsheet, choose "From spreadsheet" and "On form submit" (or "On change" if rows come from imports rather than Forms).

One gotcha: onFormSubmit passes an event object with a range property pointing at the new row. You can use e.range.getRow() to stamp only that row instead of scanning the whole sheet — a worthwhile optimization once your sheet has tens of thousands of rows. The addRowIds() function above is the safe backfill version; for the trigger handler, narrow the range to e.range.getRow() and write directly to column 1 of that row after checking it's empty.

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
Will the UUID change if I sort or filter the sheet?
No. setValue() writes a literal string into the cell. It has no formula to recalculate. Sort, filter, or move the row anywhere and the UUID stays exactly as written.
Can I put the ID in a column other than A?
Change the column index in getRange(2, 1, ...) and getRange(i + 2, 1). The second argument is the column number: 1 = A, 2 = B, and so on. Update both occurrences or the read and write will target different columns.
What if two people run the script at exactly the same time?
Utilities.getUuid() generates each UUID client-side inside the script execution, so two concurrent runs produce two different UUIDs. There is no shared counter to collide on. The only real race condition is if two executions both read the same blank cell before either writes to it — in practice the Apps Script execution model serializes spreadsheet writes, so duplicates don't happen, but if you're genuinely paranoid, use a Lock via LockService.getSpreadsheetLock() around the write loop.
The script times out on a large sheet. What do I do?
Replace the cell-by-cell setValue() loop with a single setValues() call. Build a 2D array the same size as idCol, fill each slot with either the existing value or a new Utilities.getUuid(), then call sheet.getRange(2, 1, idCol.length, 1).setValues(outputArray). One write call instead of thousands cuts execution time by an order of magnitude.
// one good script a week

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