Recipes / Recipe 14

Google Sheets: send an email when a cell changes (Apps Script)

Email yourself or the person on that row when a status column changes, for example to 'Approved'. Uses an installable trigger, because simple onEdit can't send mail.

Apps ScriptEmailTriggers Setup time: 10 minutes

You'll need: A Google Sheet you own or can edit, with a column to watch (a dropdown works best).

No-code option first

If you only need to know that something changed, Google Sheets has this built in: Tools → Notification settings → Edit notifications. It emails you about any change (immediately or as a daily digest), but it can't filter by column or value, and it can't email someone else. For that, use the script.

Script: email when a specific column changes to a specific value

  1. Extensions → Apps Script, paste the script.
  2. Set sheetName, watchColumn (4 = column D) and triggerValues (leave it as [] to email on any change).
  3. Set notifyTo, or set emailColumn to the column holding each row's email to notify that person instead.
  4. Select installEditTrigger and click Run once. Approve the permissions (it needs to send email as you).
  5. Change a watched cell to a trigger value. The email arrives within a minute.
Code.gsApps Script
/**
 * Email someone when a cell in a watched column changes (optionally only to a specific value,
 * e.g. Status -> "Approved"). Simple onEdit triggers can't send email, so this uses an
 * installable trigger: run installEditTrigger once.
 */
const CHANGE_CONFIG = {
  sheetName: 'Tasks',
  headerRows: 1,
  watchColumn: 4,             // 1-based column to watch (4 = D, e.g. Status)
  triggerValues: ['Approved'],// only these new values send email; [] = any change
  notifyTo: 'you@example.com',// who gets the email
  emailColumn: 0,             // or: 1-based column holding a per-row recipient (0 = use notifyTo)
  labelColumn: 1              // column used to name the row in the subject (1 = A)
};

function installEditTrigger() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const exists = ScriptApp.getProjectTriggers()
    .some((t) => t.getHandlerFunction() === 'emailOnChange');
  if (!exists) ScriptApp.newTrigger('emailOnChange').forSpreadsheet(ss).onEdit().create();
}

/** Pure function: should this edit send an email? */
function shouldNotify(sheetName, row, col, numRows, numCols, newValue, cfg) {
  if (sheetName !== cfg.sheetName) return false;
  if (numRows !== 1 || numCols !== 1) return false; // ignore pastes and multi-cell edits
  if (row <= cfg.headerRows || col !== cfg.watchColumn) return false;
  const v = String(newValue === undefined ? '' : newValue).trim().toLowerCase();
  if (v === '') return false;
  if (!cfg.triggerValues.length) return true;
  return cfg.triggerValues.some((t) => String(t).trim().toLowerCase() === v);
}

function emailOnChange(e) {
  const cfg = CHANGE_CONFIG;
  const range = e.range;
  const sheet = range.getSheet();
  const newValue = e.value !== undefined ? e.value : range.getValue();
  if (!shouldNotify(sheet.getName(), range.getRow(), range.getColumn(),
      range.getNumRows(), range.getNumColumns(), newValue, cfg)) return;
  const rowValues = sheet.getRange(range.getRow(), 1, 1, sheet.getLastColumn()).getValues()[0];
  const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  const to = cfg.emailColumn ? String(rowValues[cfg.emailColumn - 1]).trim() : cfg.notifyTo;
  if (!to) return;
  const label = rowValues[cfg.labelColumn - 1] || ('row ' + range.getRow());
  const details = headers.map((h, i) => (h || 'Column ' + (i + 1)) + ': ' + rowValues[i]).join('\n');
  MailApp.sendEmail(to,
    label + ' changed to "' + newValue + '"',
    'Row ' + range.getRow() + ' on "' + sheet.getName() + '" was updated' +
    (e.oldValue !== undefined ? ' (was "' + e.oldValue + '")' : '') + '.\n\n' + details +
    '\n\nOpen the sheet: ' + SpreadsheetApp.getActiveSpreadsheet().getUrl());
}

Good to know

Do it yourself

Recipe Club: $12/month

Prefer to set things up yourself? Join the Recipe Club and get access to a private GitHub repository of ready-made, tested automations for Sheets, Apps Script, Python and Node. Each one comes with setup notes and sample data.

  • A new recipe added every week
  • Everything in the free library, plus the longer, more complete versions
  • Use them in your own business as much as you like
  • Cancel anytime by email (or the Stripe customer portal once available)

Recipe Club opens soon: join the waitlist

You'll need a free GitHub account. After checkout, reply with your GitHub username and we'll send the repo invite, usually within one business day.

Or buy a one-time recipe pack ($19–$29)

  • Clean Data Pack $19
    Dedupe, cleaning, merging and normalizing scripts and formulas for messy exports, with a sample sheet.
    Coming soon
  • Forms & Inbox Pack $19
    Form auto-replies, request routing, a lead log and follow-up nudges, ready to paste into Apps Script.
    Coming soon
  • Money Ops Pack $29
    Invoice reminders, a monthly revenue dashboard with QUERY, a receipts log and nightly backups.
    Coming soon

Written with AI assistance. Script logic is unit-tested; formulas follow documented Google Sheets behavior. Always try new automations on a copy of your sheet first.