Google Apps Script · Automation
A driver's license that quietly expired three weeks ago. A car registration renewal you only remembered because a traffic officer mentioned it. A birthday you missed because it wasn't on any calendar you actually check. None of that has to keep happening, and the fix costs nothing and runs entirely on Google's own servers.
This is a complete, start-to-finish guide to setting up a free automated email reminder system using Google Apps Script — covering exactly how to navigate to script.google.com, paste in the code, configure your own reminders, grant the one-time permissions Google requires, test it safely before trusting it, and set it running on autopilot every single day. It also includes an enhanced version of the original script with advance-notice reminders and built-in quota safety, so a license renewal reminder can arrive thirty days early instead of on the exact day it's already too late to act on.
📋 Table of Contents
- What This Script Actually Does, and Why It Beats a Phone Reminder
- Cool Things Worth Setting a Reminder For
- Getting Into script.google.com and Creating Your Project
- Configuring Your Own Reminders
- Running It for the First Time and Granting Permission
- Testing It Safely Before You Trust It
- Setting the Daily Trigger So It Runs Automatically
- The Enhanced Version: Advance Reminders and Quota Safety
- Honest Limits Worth Knowing
1. What This Script Actually Does, and Why It Beats a Phone Reminder
The core idea is simple: a list you define once, containing dates and messages, gets checked automatically every single day by a script running on Google's own infrastructure. When today's date matches something on your list, it emails you — or anyone else — the reminder. No app installed, no phone that needs to stay charged and turned on, no reminder silently dying because you switched phones eighteen months ago and forgot to reinstall something. As long as your Google account exists, this keeps running in the background indefinitely.
The script handles two types of reminders differently: Yearly, for things that repeat every single year on the same date — birthdays, anniversaries — and Exact, for one-time events tied to a specific year, like a project deadline or a single renewal that isn't recurring on a fixed annual cycle.
2. Cool Things Worth Setting a Reminder For
🪪 Driver's License Renewal
The single most common thing people forget until it's already expired.
🚗 Car Registration & Insurance
Both tend to renew annually on a fixed date — perfect for the Yearly type.
🛂 Passport Expiration
Passports run on multi-year cycles, making them an ideal fit for the Exact type.
🌐 Domain Name & Web Hosting Renewal
Losing a domain because an auto-renew card expired is a genuinely common, entirely avoidable disaster.
🎂 Birthdays and Anniversaries
Set it once, and it emails you every single year without needing to be re-entered.
🐾 Pet Vaccination Dates
Vet reminders often arrive too close to the date to actually book a slot in time.
🦷 Dental and Eye Checkups
Annual checkups are easy to let slide a full year without any reminder at all.
🧾 Tax Filing Deadlines
A fixed annual date that benefits enormously from a reminder well ahead of time, not on the day itself.
🔧 Home Maintenance
HVAC filter changes, water heater flushes, smoke detector battery swaps — all easy to forget, all cheap insurance against bigger problems.
3. Getting Into script.google.com and Creating Your Project
- Open a browser and go to script.google.com, signed into the Google account you want sending these reminders.
- Click New project in the top-left area of the page.
- You'll land in the code editor with a default empty function already there. Select all of that placeholder code and delete it.
- Paste in the full script provided further down this article.
- Click the untitled project name at the top of the page and give it a clear name, like "Email Reminders."
- Press Ctrl+S (or Cmd+S on Mac) to save.
4. Configuring Your Own Reminders
Everything you'll ever need to change day to day lives inside the EMAIL_TASKS list near the top of the script. Each entry needs these fields filled in:
- id — a unique label for that specific reminder. Only strictly required to be unique for "Exact" type reminders, since it's what the script uses to remember an email has already gone out.
- recipient — the email address that should receive it. Doesn't have to be your own address.
- subject — the email's subject line.
- message — the body text of the reminder.
- type — either
"Yearly"for anything repeating annually, or"Exact"for a specific one-time date. - targetDate — formatted as
"d-mon"for Yearly (like"5-sep"), or"d-mon-yyyy"for Exact (like"1-sep-2027").
To add a new reminder, copy an existing entry inside the curly braces, adjust each field, and make sure a comma separates it from the entry before it.
5. Running It for the First Time and Granting Permission
The very first time you run any function in this script, Google needs your explicit permission to send email on your behalf — this is a standard part of how Apps Script works for every project, not something specific to this script.
- At the top of the editor, use the function dropdown menu to select checkAndSendEmails.
- Click the Run button.
- A window titled Authorization required will appear. Click Review permissions.
- Choose the Google account you want running this script.
- You'll likely see a screen warning "Google hasn't verified this app" — this is completely normal and expected for a personal script you wrote yourself; it's not a warning about malicious code. Click Advanced, then click Go to [your project name] (unsafe).
- Review the permissions requested — sending email as you, and viewing your script's own stored data — then click Allow.
6. Testing It Safely Before You Trust It
Before setting this to run automatically, confirm it actually works the way you expect.
- Temporarily change one task's
targetDateto today's date, in the correct format for its type. - Run checkAndSendEmails again from the function dropdown.
- Check the Execution log at the bottom of the screen — a successful run shows a line confirming the email was sent.
- Check the actual inbox of the recipient address to confirm it physically arrived.
- If you're testing an "Exact" reminder and want to trigger it again after a successful test, run the resetAllSentStatuses function first — otherwise the script will correctly refuse to send it a second time, exactly as designed.
- Once confirmed, change the test date back to its real, intended value.
7. Setting the Daily Trigger So It Runs Automatically
- Select setupDailyAutoSend from the function dropdown at the top.
- Click Run once. This creates the automated daily schedule — you never need to run this particular function again unless you want to reset it.
- Click the clock icon on the left sidebar, labeled Triggers, to confirm it was created. You should see checkAndSendEmails listed, set to run daily.
From this point forward, Google's servers check your list every day on their own schedule — the script doesn't need your browser open, your computer turned on, or anything else on your end.
8. The Enhanced Version: Advance Reminders and Quota Safety
The original script sends a reminder exactly on the target date. For plenty of real reminders — a license renewal, a passport expiration — knowing thirty days in advance is far more useful than finding out on the day itself, when it may already be too late to act. The enhanced version below adds an optional daysBefore list to any task, letting you fire off multiple advance warnings for the same event, plus a built-in check against Gmail's daily sending quota so the script logs a clear warning instead of silently failing if you ever approach that limit.
/**
* 1. CONFIGURE YOUR EMAILS HERE
* -------------------------------------------------
* type: "Yearly" (recurring) OR "Exact" (one-time)
* targetDate format for Yearly: "d-mon" (e.g., "5-sep")
* targetDate format for Exact: "d-mon-yyyy" (e.g., "1-sep-2027")
* id: MUST be unique for every "Exact" email so the script remembers it was sent.
* daysBefore: OPTIONAL array of advance-notice days, e.g. [30, 7, 1]
* Leave it out entirely to only send on the exact date, like before.
*/
const EMAIL_TASKS = [
{
id: "task_001",
recipient: "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
subject: "Happy Birthday!",
message: "Wishing you a wonderful birthday today!",
type: "Yearly",
targetDate: "5-sep"
},
{
id: "task_002",
recipient: "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
subject: "Project Launch",
message: "This is the automated reminder for the project launch.",
type: "Exact",
targetDate: "1-sep-2027"
},
{
id: "task_003",
recipient: "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
subject: "Driver's License Renewal Coming Up",
message: "Your driver's license renews soon. This is your advance heads-up.",
type: "Yearly",
targetDate: "15-jun",
daysBefore: [30, 7, 1]
}
];
/**
* 2. MAIN FUNCTION (Runs daily via trigger)
* -------------------------------------------------
*/
function checkAndSendEmails() {
const today = new Date();
const timezone = Session.getScriptTimeZone();
const properties = PropertiesService.getScriptProperties();
const todayYearly = Utilities.formatDate(today, timezone, "d-MMM").toLowerCase();
const todayExact = Utilities.formatDate(today, timezone, "d-MMM-yyyy").toLowerCase();
const remainingQuota = MailApp.getRemainingDailyQuota();
if (remainingQuota <= 0) {
console.error("Daily email quota reached. No emails will be sent today.");
return;
}
EMAIL_TASKS.forEach(task => {
const offsets = (task.daysBefore && task.daysBefore.length) ? task.daysBefore : [0];
offsets.forEach(offsetDays => {
let shouldSend = false;
let storageKey = task.id + "_" + offsetDays;
if (task.type === "Yearly") {
const parts = task.targetDate.toLowerCase().trim().split("-");
const day = parseInt(parts[0], 10);
const monthStr = parts[1];
const currentYear = today.getFullYear();
// Check both this year and next year to correctly handle
// reminders that cross the December/January boundary.
[currentYear, currentYear + 1].forEach(candidateYear => {
const monthIndex = new Date(Date.parse(monthStr + " 1, " + candidateYear)).getMonth();
const targetDateObj = new Date(candidateYear, monthIndex, day);
targetDateObj.setDate(targetDateObj.getDate() - offsetDays);
const formatted = Utilities.formatDate(targetDateObj, timezone, "d-MMM").toLowerCase();
if (formatted === todayYearly) {
shouldSend = true;
}
});
}
else if (task.type === "Exact") {
const parts = task.targetDate.toLowerCase().trim().split("-");
const day = parseInt(parts[0], 10);
const monthStr = parts[1];
const year = parseInt(parts[2], 10);
const monthIndex = new Date(Date.parse(monthStr + " 1, " + year)).getMonth();
const targetDateObj = new Date(year, monthIndex, day);
targetDateObj.setDate(targetDateObj.getDate() - offsetDays);
const formatted = Utilities.formatDate(targetDateObj, timezone, "d-MMM-yyyy").toLowerCase();
if (formatted === todayExact) {
const hasBeenSent = properties.getProperty(storageKey);
if (!hasBeenSent) {
shouldSend = true;
}
}
}
if (shouldSend) {
try {
const prefix = offsetDays > 0 ? "[" + offsetDays + " day" + (offsetDays > 1 ? "s" : "") + " reminder] " : "";
MailApp.sendEmail(task.recipient, prefix + task.subject, task.message);
console.log("Email sent to " + task.recipient + " (offset: " + offsetDays + " days)");
if (task.type === "Exact") {
properties.setProperty(storageKey, "SENT");
}
} catch (error) {
console.error("Failed to send email to " + task.recipient + ": " + error.message);
}
}
});
});
}
/**
* 3. AUTOMATION SETUP
* -------------------------------------------------
* Run this function ONCE manually to set up the daily automation.
*/
function setupDailyAutoSend() {
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
if (trigger.getHandlerFunction() === "checkAndSendEmails") {
ScriptApp.deleteTrigger(trigger);
}
});
ScriptApp.newTrigger("checkAndSendEmails")
.timeBased()
.atHour(8)
.everyDays(1)
.create();
console.log("Automation Active! The script will now check and send emails daily at 8 AM.");
}
/**
* UTILITY: Reset Sent Status (For testing purposes)
* -------------------------------------------------
* Run this if you want to test an "Exact" email again after it has already sent.
*/
function resetAllSentStatuses() {
PropertiesService.getScriptProperties().deleteAllProperties();
console.log("Memory cleared. All 'Exact' emails are ready to be tested/sent again.");
}
Everything about setup, testing, and triggers described above works identically with this enhanced version. The only new thing to understand is the optional daysBefore field — leave it off any task entirely, and that task behaves exactly like the original script, firing only on the exact date.
9. Honest Limits Worth Knowing
- Free Gmail accounts have a daily sending quota through Apps Script — Google Workspace accounts get a considerably higher limit. The quota check built into the enhanced script logs a clear warning rather than failing silently if you ever approach it.
- Time-based triggers run within an hour-long window rather than at the exact minute — the
atHour(8)setting means sometime between 8:00 and 9:00 AM, not precisely on the hour. - If you ever revoke this script's permissions from your Google Account's security settings, the trigger stops working silently until you re-authorize it.
- This sends from your own Gmail address, so recipients will see your email as the sender — it's genuinely you sending it, not an anonymous notification service.
Explore More Awareness & Security Content
Discover more security tips, threat analysis, hacking awareness, and practical guides designed to help you stay safe online.
Visit Awareness & Security →