How to send emails automatically from Google Sheets
Put one row per recipient in a Google Sheet with a Status column. Paste a short Apps Script that sends one email per row with MailApp and writes "Sent" and a timestamp back, then add a time-driven trigger. Free Gmail accounts can send to 100 recipients a day this way. Workspace accounts get 1,500.
Runs in your browser. Your numbers stay here unless you press Send this estimate. Free, no sign-up.
01Methods
Which free method fits your setup
| Method | When it fits | Cost and versions | Where the data goes |
|---|---|---|---|
| Gmail mail merge (built in) | One-off personalised sends to a list you link from a Sheet, with no code. You start each send yourself. | Needs an eligible paid Google Workspace plan: Workspace Individual, Business Standard or Plus, Enterprise Standard or Plus, or Education Standard or Plus. | Stays inside your Google account. Limit of 1,500 recipients a day, shared with your other sending. |
| Apps Script (MailApp) | Recurring sends on a schedule, one email per row, with a Status column so nothing goes twice. | Free with any Google account. Limits: 100 recipients a day on gmail.com, 1,500 on Workspace. | Runs inside Google under your account. No third party sees the sheet or the emails. |
| n8n (Google Sheets Trigger + Gmail node) | Sends that should fire when a row is added or updated, or that feed other steps such as a CRM or Slack. | Free to self-host; n8n Cloud is a paid subscription. Gmail sending limits still apply. | Self-hosted keeps rows on your server. On n8n Cloud, sheet data passes through n8n's servers. |
| Sheets add-ons | People who want a point-and-click sender with templates and tracking inside Sheets. | Usually a free tier with limits; scheduled or automated sending is often on paid plans only. | The add-on gets access to your sheet and Gmail. Read its permission screen and privacy policy first. |
02Steps
Step by step
01Set up the sheet
Name the tab List. Put headers in row 1: Email, Name, Due, Status, Sent at (columns A to E). Add one recipient per row and leave Status and Sent at blank.
02Paste the script
In the sheet, open Extensions, then Apps Script. Delete the sample code, paste the script below and save. It skips any row already marked Sent, stops when the daily quota runs out, and writes Sent plus the time after each email. A row with a bad address is marked Error and skipped, so one typo cannot stop the rest of the list.
function sendRowEmails() { const sheet = SpreadsheetApp.getActive().getSheetByName('List'); const rows = sheet.getDataRange().getValues(); const tz = SpreadsheetApp.getActive().getSpreadsheetTimeZone(); for (let i = 1; i < rows.length; i++) { const [email, name, due, status] = rows[i]; if (!email || String(status).startsWith('Sent') || String(status).startsWith('Error')) continue; if (MailApp.getRemainingDailyQuota() < 1) break; const day = due ? Utilities.formatDate(new Date(due), tz, 'd MMM yyyy') : 'soon'; const body = 'Hi ' + name + ',\n\nYour renewal is due on ' + day + '.\nReply to this email if anything has changed.\n\nThanks'; try { MailApp.sendEmail(email, 'Your renewal is due ' + day, body); sheet.getRange(i + 1, 4, 1, 2).setValues([['Sent', new Date()]]); } catch (err) { sheet.getRange(i + 1, 4, 1, 2).setValues([['Error: ' + err.message, new Date()]]); } SpreadsheetApp.flush(); } }03Test on yourself first
Make a copy of the whole spreadsheet (File, then Make a copy), replace its rows with two that use your own address, open Apps Script from the copy and click Run. Google asks you to authorise the script the first time. Check both emails arrived and both rows now say Sent.
04Add a time-driven trigger
In the Apps Script editor, click Triggers (the alarm clock icon), then Add Trigger. Choose sendRowEmails, event source Time-driven, then a day or week timer and an hour. Google may run it at any point within that hour.
05Check the limits before you scale
Apps Script counts recipients, not emails: 100 a day on a gmail.com account and 1,500 on Workspace, with up to 50 recipients per message. A single run can last 6 minutes. If your list is bigger, the script stops and the next trigger run picks up the unsent rows.
06Or build it in n8n
Add a Google Sheets Trigger set to Row added, then a Gmail node with Resource Message and Operation Send. Map To, Subject and Message from the row. Turn off Append n8n attribution if you do not want the footer, and add a Google Sheets node after it that writes Sent back to the row.
07Decide if it is worth it
If you send the same kind of email every week, put the minutes you spend now into the free Automation ROI calculator, linked below, to see whether setting this up pays back.
03Worked example
Example: a 12-row renewal reminder list
- A sheet lists 12 customers with an email, first name and renewal date, and Status is blank on every row.
- The script runs from a weekly time-driven trigger on Monday morning on a free gmail.com account.
- It sends 12 emails, which uses 12 of the 100 daily recipients, and writes Sent with a timestamp on each row.
- Someone runs it again by hand the same day: every row already says Sent, so nothing is sent twice.
- Two new customers are added on Wednesday. Next Monday the script emails only those two rows.
- This is an illustration of the steps, not a measured result.
04Mistakes
Common problems and their fixes
Every run emails the whole list again.
Write Sent back to a Status column after each email and skip rows that already have it, as the script above does.
Dates in the email look like "Mon Dec 01 2026 00:00:00 GMT".
Format the date with Utilities.formatDate before you put it in the message body.
The script fails partway with a quota error.
Check MailApp.getRemainingDailyQuota() before each send and stop when it reaches zero. The Status column means the next run carries on where it stopped.
Emails go out from the wrong account.
Apps Script sends as the person who authorised it and created the trigger. Set up the trigger while signed in to the account that should send.
One row keeps showing Error instead of Sent.
That row usually has an invalid email address. Correct the address, clear the Status cell, and the next run sends it.
05FAQ
Questions people ask
Can Google Sheets send an email when a cell changes?
Not on its own. An Apps Script with an installable on-edit or time-driven trigger can, and so can an n8n workflow with the Google Sheets Trigger set to Row updated.
How many emails can I send from Google Sheets a day?
With Apps Script, Google counts recipients: 100 a day on a free gmail.com account and 1,500 on a Workspace account. Gmail's built-in mail merge allows 1,500 recipients a day on eligible Workspace plans.
Does Gmail have mail merge with Google Sheets for free?
No. Google lists it only for certain paid Workspace plans, such as Workspace Individual and Business Standard. On a free account, use the Apps Script method instead.
Will the script send duplicates if I run it twice?
No. It skips any row where Status starts with Sent or Error, with a capital letter. Clear that cell if you want a row to be sent again.
Does this work for Excel too?
This script works only in Google Sheets. For an Excel file, import it into Google Sheets first (File, then Import), then run the same steps.
06Sources
Sources, with the date we checked them
- Google Apps Script: Quotas for Google services
Checked
- Google Apps Script: Installable triggers
Checked
- Gmail Help: Send personalized emails with mail merge
Checked
- n8n docs: Google Sheets Trigger node
Checked
- n8n docs: Gmail node message operations
Checked
What changed
Published
First version: Apps Script with Sent status column, time trigger, n8n option and sourced limits.
07Next
Related pages
- Automation ROI calculator: is it worth automating?
- Can you automate sales follow-ups?
- Automate your weekly KPI report
- Extract tables from PDF to Excel for free
Every free tool in one place: Free tools that run in your browser