To send an email when a cell changes in Google Sheets, use an Apps Script onEdit function set up as an installable trigger — not a plain onEdit(e) function on its own, which cannot call MailApp at all. The script below watches one column, checks the new value against a list you define, and emails the address sitting in another column of the same row. It’s copy-paste ready and every scenario below was run against a mock spreadsheet in a script sandbox to confirm the logic actually branches the way it claims to.
Two ways to do this, and which one to use
Google Sheets can do this two ways: a built-in feature called Conditional notifications, or an Apps Script trigger you write yourself. They aren’t interchangeable — the built-in one has restrictions that will quietly rule it out for a lot of readers.
| Feature | Conditional notifications (built-in) | Apps Script installable trigger (this guide) |
|---|---|---|
| Who can use it | Work/school (Google Workspace) accounts only | Any Google account, including free gmail.com |
| Non-Google recipients | Not supported (no Outlook, Yahoo, etc.) | Supported — any email address |
| Group email addresses | Not supported | Supported |
| Delivery timing | “May not be immediate”; multiple changes can be batched into one email | Fires per edit, near-instant |
| Works with IMPORTRANGE / connected sheets | No — external data changes don’t trigger it | Depends on your logic, but the edit event still fires for direct edits |
| Setup effort | Menu-driven, no code | Paste a script, add one trigger |
Choose Conditional notifications if you’re on a Workspace account, every recipient has a Google address, and near-real-time delivery isn’t critical. Choose the Apps Script route below if you’re on a personal Gmail account, need to notify a non-Google address, need the email to include custom formatting or other row data, or the built-in batching is too imprecise for what you’re building. (Source: Google’s Conditional notifications documentation, checked July 2026.)
The script that sends the email
This function checks one column for a specific set of values (for example “Approved” or “Rejected”), and if it matches, emails the address stored in another column on the same row. It correctly handles multi-cell pastes — a common failure point in simpler versions of this script, since pasting a block of cells fires onEdit once for the whole range, not once per cell.
// CONFIG — change these four lines to match your sheet
const CONFIG = {
sheetName: "Requests", // exact tab name to watch
watchColumn: 4, // column D = Status
triggerValues: ["Approved", "Rejected"], // values that fire an email
emailColumn: 2, // column B = recipient's email address
subjectColumn: 1, // column A = a label used in the subject
};
function onFormEditInstallable(e) {
try {
const range = e.range;
const sheet = range.getSheet();
if (sheet.getName() !== CONFIG.sheetName) return;
// Skip entirely if the edited range doesn't touch the watched column
const firstCol = range.getColumn();
const lastCol = range.getLastColumn();
if (CONFIG.watchColumn < firstCol || CONFIG.watchColumn > lastCol) return;
// Loop every row in the edited range — handles single edits AND pastes
const numRows = range.getNumRows();
for (let i = 0; i < numRows; i++) {
const row = range.getRow() + i;
if (row === 1) continue; // never fire on the header row
const newValue = sheet.getRange(row, CONFIG.watchColumn).getValue();
if (CONFIG.triggerValues.indexOf(newValue) === -1) continue;
const recipient = sheet.getRange(row, CONFIG.emailColumn).getValue();
const label = sheet.getRange(row, CONFIG.subjectColumn).getValue();
if (!recipient) continue; // nothing to send to
MailApp.sendEmail({
to: recipient,
subject: `Status update: ${label} is now "${newValue}"`,
body: `Row ${row} in "${CONFIG.sheetName}" changed to "${newValue}".`,
});
}
} catch (err) {
console.error("onFormEditInstallable failed: " + err);
}
}
Note the function is named onFormEditInstallable, not onEdit. That’s deliberate — see the mistake section below for why.
How do I make this run automatically?
Pasting the code into the script editor does nothing by itself. You need to attach it to an installable trigger so Google runs it every time the sheet is edited.
- In your spreadsheet, open Extensions → Apps Script.
- Delete the default
myFunction(){}stub and paste in the script above. - Edit the four
CONFIGvalues to match your sheet’s tab name and columns. - Click the floppy-disk Save icon.
- Click the clock icon in the left sidebar (Triggers), then + Add Trigger.
- Set: Function —
onFormEditInstallable; Event source — From spreadsheet; Event type — On edit. Click Save. - Google will ask you to authorize the script (it needs permission to read the sheet and send email as you). Click Advanced → Go to [project name] (unsafe) — this warning appears because you wrote the script yourself and Google hasn’t reviewed it; it’s expected for a personal script.
- Edit a row so the watched column matches one of your
triggerValues, and confirm the email arrives.
Pointing it at your own sheet
The only part you need to change is the CONFIG block. sheetName must match your tab name exactly, including capitalization. watchColumn, emailColumn, and subjectColumn are column numbers, not letters — column A is 1, B is 2, C is 3, and so on. If you want any edit in the column to send an email regardless of value, set triggerValues to an empty check by replacing the indexOf condition with a simple non-blank check instead — but be aware that means every correction or typo fix will also send an email. If you want to go further — HTML-formatted emails, watching several columns at once, or adding conditions this guide doesn’t cover — you can describe the change in plain English and have ChatGPT adapt the script; see How to Use ChatGPT to Write Google Apps Script for Sheets for prompts that work well for this.
Why “nothing happens” is almost always this one mistake
The single most common failure reported for this exact task is naming the function onEdit and expecting it to send email on its own. It won’t. Google Apps Script treats a function literally named onEdit(e) as a simple trigger, and simple triggers are explicitly barred from calling any service that needs authorization — MailApp included. (Source: Google’s Installable Triggers documentation.) The script runs, hits the MailApp.sendEmail line, and throws a permission error that most people never see because it fails silently in the background.
The fix is exactly what the setup steps above do: give the function a name other than onEdit, and attach it manually as an installable trigger through the Triggers page. Installable triggers run with your authorization already granted, so they’re allowed to send email, read Gmail, call external URLs, and everything else a simple trigger can’t.
Common errors, explained
| Error message | What it means | Fix |
|---|---|---|
| “You do not have permission to call MailApp.sendEmail” | The function is still running as a simple trigger (literally named onEdit) |
Rename it and attach it as an installable trigger (see above) |
| “Service invoked too many times for one day: email” | You’ve hit the daily email quota | Check MailApp.getRemainingDailyQuota(); consumer accounts get 100 recipients/day, Workspace accounts get 1,500/day (verified July 2026) |
| Trigger never fires at all | The edit didn’t come from a person typing in the sheet UI — imports via IMPORTRANGE, API writes, or Zapier-style integrations don’t fire onEdit |
Switch to a time-based trigger that checks values on a schedule instead |
| One paste sends five emails at once | The script isn’t looping the full pasted range, so it’s being called once per affected cell in some versions, or once per row without checking each one properly | Use the range-looping pattern in the script above, which checks every row in the edit in a single pass |
| Email sent, but to the wrong row | Hard-coded row/column numbers instead of reading them from e.range |
Always derive the row from range.getRow() + i, never a fixed number |
| “Exceeded maximum execution time” | A simple trigger capped at 30 seconds, or a huge paste looping too much logic per row | Use an installable trigger (runs under the standard 6-minute script limit) and keep per-row logic light |
What I verified, and what I couldn’t
Verified: the script’s branching logic — single-cell edits, multi-cell pastes spanning several rows, edits outside the watched column, edits on a different sheet tab, header-row protection, and missing-recipient handling — by running the exact function above against a mock of the Apps Script Range/Sheet API in a Node.js sandbox and asserting the expected emails were queued in each case. The email quota numbers (100/day consumer, 1,500/day Workspace) and the simple-vs-installable trigger restriction are pulled directly from Google’s own developer documentation, fetched the same day this was written.
Not verified: the actual sending of a live email through MailApp, the exact wording of Google’s authorization dialogs, and the real-world behavior of Conditional notifications, since these require a live Google account and an active Workspace subscription that this process doesn’t have access to. Run the script in your own sheet to confirm delivery — the logic determining whether it should send has already been checked.
Looking for other ways to put Apps Script to work on a schedule instead of per-edit — like a daily digest email? See How to Automate Google Sheets with Apps Script + ChatGPT.
Need to route whole rows to different tabs based on an answer instead — like sorting form submissions by department? See routing Google Form responses to different sheets based on an answer.
Want a scheduled digest instead of an instant alert — one email each morning summarizing everything that changed, rather than a message per edit? See emailing yourself a daily Google Sheets summary with a time-driven trigger.
Need to copy matching rows into a completely separate spreadsheet file instead of just alerting on them — like archiving qualifying rows into another workbook? See copying rows to another Google spreadsheet without duplicates.
FAQ
Can I trigger the email from a Google Form response instead of a manual edit?
Yes — Form submissions write to the linked sheet as an edit, so the same installable “On edit” trigger fires. If you want it tied specifically to form submission rather than any edit, use an “On form submit” trigger instead and read the values from e.namedValues.
Will this work if I edit the sheet from the mobile app?
Yes. The trigger fires on the edit event regardless of which client made the change, as long as it’s a direct edit inside Sheets.
Can I send the email to multiple people at once?
Yes — set the to field to a comma-separated string of addresses, or read a column containing several addresses separated by commas and pass that value straight through.
Does this cost anything?
No. Apps Script and the email quota described above are included with any Google account at no extra cost; you’re only limited by the daily recipient caps listed in the error table.