// Sheets · Apps Script

Insert a timestamp when a row is added in Google Sheets.

Use an onEdit trigger in Apps Script to stamp a creation time into a column the moment a new row is entered, without overwriting it on every subsequent edit.

I want to record exactly when a row was first created in my spreadsheet, not when it was last touched.

The script

copy · paste · trigger
Code.gs
Apps Script
// Stamp column A with a creation time when any cell in the same row is first edited.
// Only writes if A is still empty — later edits leave the original timestamp alone.

const STAMP_COL = 1;   // column A
const SKIP_ROWS = 1;   // header rows to ignore

function onEdit(e) {
  var range = e.range;
  var sheet = range.getSheet();
  var row   = range.getRow();

  if (row <= SKIP_ROWS) return;

  var stampCell = sheet.getRange(row, STAMP_COL);
  if (stampCell.getValue() !== '') return;

  stampCell.setValue(new Date());
  stampCell.setNumberFormat('yyyy-MM-dd HH:mm:ss');
}

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

Walkthrough

Paste the script and save it

Open your spreadsheet, go to Extensions > Apps Script, delete the placeholder code, and paste the snippet above. Hit the floppy-disk icon (or Ctrl+S) to save. No deployment, no manifest changes — a plain onEdit function is a simple trigger, which Sheets installs automatically the moment the project is saved.

The two constants at the top are the only things you should touch. STAMP_COL = 1 targets column A; change it to 2 for column B, and so on. SKIP_ROWS = 1 skips your header row. If you have a two-row header, set it to 2.

Why the empty-cell check is the whole trick

Every onEdit call fires on every edit, including when someone corrects a typo in row 7 six months from now. Without the getValue() !== '' guard, the script would overwrite your creation timestamp with the correction time and you would never know.

The check costs one Sheets API read per edit. That is fine at human editing speeds. The first time I hit this pattern, I skipped the guard to keep the code short and spent an afternoon wondering why all my timestamps were clustering around 3 PM. The empty-cell check is not optional.

One nuance: if a user pastes a block of rows, onEdit fires once per paste, and e.range covers the whole pasted block rather than a single cell. The script reads row from the top-left cell of that range, so the first row in the paste gets stamped. Rows below it in the paste do not, because their stamp cells are not empty yet — they are empty but outside the single row the script checks. For paste-heavy workflows, consider switching to an installable trigger (Extensions > Apps Script > Triggers) that fires on onChange instead, which gives you more control over multi-row events.

Formatting and timezone

setNumberFormat('yyyy-MM-dd HH:mm:ss') applies a display pattern to the cell after setValue writes the raw Date object. Sheets stores all dates as UTC internally; the display timezone comes from File > Settings > Spreadsheet settings > Time zone. If your timestamps look off by a few hours, that setting is where to fix it, not in the script.

If you would rather store a plain text string instead of a true date value (so the cell sorts lexicographically and never shifts with timezone settings), replace setValue(new Date()) with setValue(Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss')). You lose date arithmetic on that column, but you gain a timestamp that looks identical on every machine regardless of viewer locale.

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
The script runs but nothing appears in column A — what is wrong?
Check that the editor you are testing in has edit permission on the sheet (view-only access silently skips simple triggers). Also confirm you saved after pasting — Apps Script does not auto-save, and an unsaved project runs the previous version. Finally, verify the cell you edited is not in row 1 or whichever row SKIP_ROWS covers.
I want to stamp a different column, not A. How?
Change STAMP_COL to the column number you want: 2 for B, 3 for C, and so on. Column letters map to integers sequentially. If you want to keep A for data and stamp into the last column dynamically, replace the constant with sheet.getLastColumn() + 1, though that shifts every time you add a column, so a fixed number is usually safer.
Can I stamp multiple sheets differently, or skip certain sheets?
Yes. Add a check at the top of onEdit: if (sheet.getName() !== 'Orders') return; swaps the trigger to only fire on a sheet named Orders. For different columns per sheet, use a plain object as a lookup: var config = {'Orders': 1, 'Returns': 3}; var col = config[sheet.getName()]; if (!col) return;
Will this work if someone uses a form to submit responses?
No. Form submissions do not fire onEdit — they fire onFormSubmit. Wire up an installable onFormSubmit trigger instead (Extensions > Apps Script > Triggers > Add Trigger > onFormSubmit), and use e.range.getRow() the same way. The empty-cell guard is still useful there in case the form itself pre-fills the stamp column.