How to Send Automatic Emails from Google Sheets with Apps Script
Step-by-step: MailApp.sendEmail in Google Apps Script, time-driven triggers, personalizing from row data, daily quotas, and common failure points.
Quick answer
Every Google Sheet ships with a free scripting layer — Google Apps Script — that can send email on its own, no Zapier, Make, or n8n subscription required. MailApp.sendEmail(recipient, subject, body) sends a message in one line, and a time-driven trigger runs your function on a schedule with nobody at the keyboard. This is also the DIY version of what people search for as "Gmail mail merge" or "Google Sheets email automation" — the paid add-ons (Yet Another Mail Merge, GMass, and similar) are, under the hood, doing roughly this same loop.
Below is a complete, working setup: a script that scans a sheet for new rows and emails only the people who haven't been emailed yet.
Step 1 — Set up the sheet
Create a Google Sheet with columns like:
| A: Email | B: Name | C: Status |
|---|---|---|
| ivan@example.com | Ivan | (empty) |
| olena@example.com | Olena | (empty) |
The Status column exists so the script never emails the same person twice.
Step 2 — Open the Apps Script editor
- In the Sheet: Extensions → Apps Script
- This opens a code editor bound to this specific spreadsheet
Step 3 — Write the send function
function sendEmails() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) { // i = 1 to skip the header row
const email = data[i][0];
const name = data[i][1];
const status = data[i][2];
if (status === 'sent' || !email) continue; // already handled or blank row
const subject = `Welcome, ${name}!`;
const body = `Hi ${name},\n\nThis is an automated email from a Google Sheet.`;
MailApp.sendEmail(email, subject, body);
sheet.getRange(i + 1, 3).setValue('sent'); // mark the row as processed
}
}
What MailApp.sendEmail actually accepts:
- Basic form:
MailApp.sendEmail(recipient, subject, body) - Extended form with an options object:
MailApp.sendEmail(recipient, subject, body, options), whereoptionssupportscc,bcc,htmlBody(an HTML version of the message),attachments,name(sender display name), andreplyTo - Unlike
GmailApp,MailAppcannot read your inbox — it can only send — which is why it asks for a narrower authorization scope the first time you run it
Step 4 — Test manually before automating
- In the Apps Script editor, select
sendEmailsfrom the function dropdown at the top - Click ▶ Run
- Google will prompt for authorization (permission to send email as you) — approve it once
- Confirm the email arrived and column C flipped to "sent"
Step 5 — Add a time-driven trigger
To make the function run itself, without clicking ▶ manually:
- In the Apps Script editor, click the clock icon on the left (Triggers)
- + Add Trigger
- Function to run:
sendEmails - Event source: Time-driven
- Type of time-based trigger: e.g. "Hour timer" (every hour) or "Day timer" (once a day, pick a time window)
- Save — Google asks for authorization a second time, specifically for the trigger
From this point on, the script checks the sheet on schedule and emails new rows without anyone opening the spreadsheet.
Quotas worth knowing before you rely on this
Apps Script is free but quota-capped. Daily quotas reset 24 hours after your first request in the current period — not at a fixed midnight:
| Limit | Consumer Gmail account | Google Workspace |
|---|---|---|
| Email recipients per day | 100 | 1,500 (2,000 within-domain) |
| Time-driven trigger runtime per day | 90 minutes | 6 hours |
| Max time-driven triggers per script | 20 | 20 |
| Single function execution time | 6 minutes | 6 minutes |
The recipient quota counts recipients, not emails. One email to 50 addresses costs 50 quota. 50 separate emails to one address each also costs 50. Check what's left before a big send with MailApp.getRemainingDailyQuota().
The limit that actually matters more than the quota
The 100/day (or 1,500/day on Workspace) ceiling is a hard technical cap, but it is not a safe sending volume if your list is cold outreach rather than people who already expect your email. Gmail's spam filtering looks at sending patterns — identical subject lines, near-identical bodies, bursts sent in a tight time window — and for genuinely cold contacts, the realistic safe volume is closer to 50-100 emails per mailbox per day, well under even the personal-account cap. Two things from the script above help avoid tripping that filtering:
- Space sends out instead of firing them all in one burst. Add
Utilities.sleep(1000)(a one-second pause) between calls toMailApp.sendEmailinside the loop — sending 100 identical-looking messages in two seconds flat is exactly the pattern spam filters key on. - Vary the subject line per recipient where you can. A subject built from row data (
Welcome, ${name}!as in the example above) already reads less "bulk" to a spam filter than a single static subject reused for every send.
None of this is enforced by Apps Script itself — it's Gmail's delivery-side filtering, so a script that runs without errors can still land in spam if the sending pattern looks automated at scale.
Failure points people hit
The script re-sends the same email on every run. This happens when there's no "already handled" marker column, so each trigger run reprocesses every row from scratch. Always write a status flag back to the sheet right after sending.
"Exceeded maximum execution time." The function is trying to process too many rows in one call and hits the 6-minute per-execution ceiling. Fix it by batching rows (say, 50-100 per trigger run) or using LockService to prevent two copies of the script from running at once.
No error, but the email never arrives. The most common cause is a maxed-out daily recipient quota — especially on a plain Gmail account capped at 100/day. Check MailApp.getRemainingDailyQuota() before sending.
The trigger silently stops firing. Changing your Google account password or revoking app access deactivates existing triggers without an obvious warning. Check the Triggers tab for execution errors periodically, particularly after a security-related account change.
When Apps Script isn't enough anymore
Apps Script is a good fit as long as everything stays inside Google's ecosystem (Sheets, Gmail, Drive, Calendar). Once you need to reach outside services — a CRM, Telegram, a payment processor — or build branching logic with retries, a dedicated no-code tool like n8n, Zapier, or Make gets you there faster with pre-built connectors instead of custom UrlFetchApp calls. See how the pricing and approach differ in this n8n vs Zapier vs Make comparison.
FAQ
Why build this myself instead of using a mail-merge add-on like Yet Another Mail Merge or GMass?
An add-on gets you a UI, open/click tracking, and a template editor without touching code — worth it if that's all you need. This script is worth writing yourself once you need logic the add-ons don't expose: conditional sending based on other columns, writing results back into other sheets, or chaining the send into a larger Apps Script workflow. Both approaches hit the same underlying Gmail/Workspace sending quotas either way.
Will Gmail flag this as spam if I email a list of people who don't know me?
It can, regardless of whether you're under the daily quota. Gmail's filtering keys on sending pattern, not just volume — see the limit-that-matters section above. Cold contacts should stay in the 50-100/day range with staggered send times, not the raw 100 (or 1,500) quota ceiling.
What's the difference between MailApp and GmailApp?
MailApp only sends mail and asks for minimal permissions. GmailApp can read your inbox, manage labels and folders, and search existing messages — but it needs a much more sensitive authorization scope. For a simple sheet-triggered send, MailApp is the safer and correct choice.
Does the trigger still fire if I close the spreadsheet or shut down my computer?
Yes. Time-driven triggers run on Google's servers, not your device — they fire on schedule regardless of whether the sheet is open or your machine is even on.
What if I need to send more than 100 emails a day?
Either move the script to a Google Workspace account (1,500/day), or switch from MailApp to a transactional email provider like SendGrid or Mailgun via UrlFetchApp, since those services scale recipient limits independently of Apps Script's quota — and, for cold outreach specifically, scale the sending infrastructure (warmed-up IPs, dedicated sending domains) in a way a personal Gmail account never will regardless of quota.
Last fact-checked: August 18, 2026. Apps Script quotas can change — verify against Google's official quotas documentation before relying on this for production sends.