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.
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
- Extensions → Apps Script, paste the script.
- Set
sheetName,watchColumn(4 = column D) andtriggerValues(leave it as[]to email on any change). - Set
notifyTo, or setemailColumnto the column holding each row's email to notify that person instead. - Select
installEditTriggerand click Run once. Approve the permissions (it needs to send email as you). - Change a watched cell to a trigger value. The email arrives within a minute.
/**
* 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
- Why not just name the function
onEdit? Simple triggers run without authorization, so they can't call MailApp. That's the most common reason “my onEdit email doesn't send”. - Edit triggers fire for changes people make. They don't fire when a formula result changes, or when rows arrive from a Google Form or another script. For form rows, see email on form submit.
- Multi-cell pastes are ignored on purpose, so pasting 200 rows doesn't send 200 emails.
- Emails come from the account that ran
installEditTrigger, and count against its daily Apps Script email quota.
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.