Send Bulk Emails from a Sheet

Sending personalised emails to a list of recipients is one of the most common Apps Script use cases. By reading from a Google Sheet and building dynamic email content per row, you can send hundreds of unique emails in a single script run — while tracking which ones have already been sent.

Basic Bulk Email Loop

function sendBulkEmails() { var sheet = SpreadsheetApp .getActiveSpreadsheet() .getSheetByName("Sales Tracker"); var data = sheet.getRange(2, 1, sheet.getLastRow() - 1, 7).getValues(); // Columns: [Customer Name, Email, Product, Amount, Region, Sales Rep, Status] data.forEach(function(row) { var customerName = row[0]; var email = row[1]; var product = row[2]; var amount = row[3]; var subject = "Welcome to Acme Corp – Your " + product + " is Active"; var body = "Hi " + customerName + ",\n\nYour " + product + " ($" + amount + "/mo) is now active.\n\nBest,\nAcme Corp Team"; GmailApp.sendEmail(email, subject, body); Logger.log("Sent to: " + email); }); }

Tracking Sent Emails with a Status Column

To avoid sending duplicates on re-runs, add an Email Sent column (column I) and check it before sending:

function sendBulkEmailsWithTracking() { var sheet = SpreadsheetApp .getActiveSpreadsheet() .getSheetByName("Sales Tracker"); var lastRow = sheet.getLastRow(); var data = sheet.getRange(2, 1, lastRow - 1, 9).getValues(); // Columns: [Customer Name, Email, Product, Amount, Region, Sales Rep, Status, Date, Email Sent] data.forEach(function(row, index) { var customerName = row[0]; var email = row[1]; var product = row[2]; var amount = row[3]; var salesRep = row[5]; var status = row[6]; var emailSent = row[8]; // Column I if (status !== "Closed") return; if (emailSent === "Sent") return; // Skip already-sent rows var firstName = customerName.split(" ")[0]; var subject = "Welcome to Acme Corp, " + firstName + "!"; var htmlBody = ` <p>Hi ${firstName},</p> <p>Thank you for choosing the <strong>${product}</strong> plan at <strong>$${amount}/month</strong>.</p> <p>Your dedicated account manager is <strong>${salesRep}</strong>. Don't hesitate to reach out with any questions.</p> <br> <p>Best regards,<br>The Acme Corp Team</p> `; GmailApp.sendEmail(email, subject, "", { htmlBody: htmlBody }); // Mark as sent in the sheet sheet.getRange(index + 2, 9).setValue("Sent"); Logger.log("Email sent to " + email); }); }

With the sample data, emails go to Sarah Johnson and Lisa Park (Closed). Mark Chen is skipped. On re-runs, the Sent flag prevents duplicates.

Sending a Weekly Digest to Sales Reps

This groups closed deals by sales rep and sends each rep their own summary:

function sendWeeklyDigestToReps() { var sheet = SpreadsheetApp .getActiveSpreadsheet() .getSheetByName("Sales Tracker"); var data = sheet.getRange(2, 1, sheet.getLastRow() - 1, 8).getValues(); // Group deals by sales rep var repData = {}; data.forEach(function(row) { var salesRep = row[5]; var status = row[6]; if (status !== "Closed") return; if (!repData[salesRep]) repData[salesRep] = []; repData[salesRep].push(row); }); // Sales rep email mapping var repEmails = { "Alex Martinez": "[email protected]", "Jordan Lee": "[email protected]", "Sam Rivera": "[email protected]" }; Object.keys(repData).forEach(function(repName) { var deals = repData[repName]; var email = repEmails[repName]; if (!email) return; var rows = deals.map(function(row) { return `<tr><td>${row[0]}</td><td>${row[2]}</td><td>$${row[3]}/mo</td></tr>`; }).join(""); var total = deals.reduce(function(sum, row) { return sum + row[3]; }, 0); var htmlBody = ` <h2>Weekly Closed Deals – ${repName}</h2> <table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse;"> <thead style="background:#4A90D9;color:#fff;"> <tr><th>Customer</th><th>Product</th><th>Amount</th></tr> </thead> <tbody>${rows}</tbody> </table> <p><strong>Total: $${total}/mo</strong></p> `; GmailApp.sendEmail(email, "Your Weekly Closed Deals Digest", "", { htmlBody: htmlBody }); Logger.log("Digest sent to " + repName); }); }

Gmail Daily Send Limits

Google imposes a daily sending limit:

  • Consumer Gmail: 500 emails/day
  • Google Workspace: 1,500 emails/day

For large lists, add Utilities.sleep(200) between sends to avoid hitting rate limits:

GmailApp.sendEmail(email, subject, body); Utilities.sleep(200); // 200ms pause between emails