Producfy

Remote Ops

Send One Daily Task Reminder from Google Sheets Without Repeat Emails

How can I send one daily email with tomorrow's unfinished Google Sheets tasks without sending repeat reminders?

Use Google Sheets with a locked Apps Script that combines tomorrow's unfinished tasks into one email and saves a sent marker for the recipient and due date. A daily trigger schedules the reminder, while the lock and marker suppress ordinary overlapping runs and reruns without guaranteeing exactly-once delivery.

What you need

  • Google Sheets spreadsheet with a Tasks tab and Task, Due, Status columns
  • Actual date values in column B and United States locale for the sample
  • Apps Script project with SHEET_ID and REMINDER_TO Script Properties
  • Spreadsheet access and authorization for spreadsheet and email permissions

How this script runs

  1. A daily trigger starts the script
  2. It takes a lock so runs can't overlap
  3. It checks the sent marker for tomorrow's date
  4. It collects unfinished tasks due tomorrow
  5. It sends one combined email
  6. It saves the sent marker and clears older ones

How should the Google Sheets task table be structured?

The Google Sheets reminder requires a Tasks tab with Task, Due, and Status headers in columns A through C.

Task Due Status
Draft remote onboarding checklist 7/15/2026 Open
Confirm team coverage 7/15/2026 In progress
Archive old meeting notes 7/15/2026 Done
Review support handoff 7/14/2026 Open
Prepare August agenda 7/16/2026 Open

Set the spreadsheet locale to United States before entering this sample. Column B must contain actual dates, not date-looking text.

  • Done, ignoring case and surrounding spaces, means finished.
  • Every other status, including blank, means unfinished.
  • Rows missing a task name or due date are ignored.

The reminder goes to one configured recipient, not individual task owners. “Tomorrow” means the next calendar date in the spreadsheet’s time zone, including weekends.

How do you install the Google Sheets reminder script?

Install the reminder as an Apps Script project with spreadsheet and recipient settings stored in Script Properties.

  1. Prepare the spreadsheet. Create Tasks with the table above. Under File > Settings, choose the locale and time zone.
  2. Open Apps Script. Choose Extensions > Apps Script and replace the starter code with the script below.
  3. Add Script Properties. Under Project Settings, add SHEET_ID, using the spreadsheet address’s value between /d/ and /edit, and REMINDER_TO, containing one plain email address. No API key is needed. Keep configuration and future secrets out of source code; project editors can access properties, so restrict editor access.
  4. Match time zones. Set the Apps Script project time zone to match the spreadsheet. Spreadsheet time controls date comparisons; project time controls the trigger schedule.
  5. Save and authorize. Select sendTomorrowTaskReminder, click Run, and approve spreadsheet and email permissions. Manual execution sends a real email when tasks qualify, so configure an intended recipient.
function sendTomorrowTaskReminder() {
  const lock = LockService.getScriptLock();

  if (!lock.tryLock(30000)) {
    console.log("Skipped: another reminder run holds the lock.");
    return;
  }

  try {
    const properties = PropertiesService.getScriptProperties();
    const sheetId = (properties.getProperty("SHEET_ID") || "").trim();
    const recipient = (properties.getProperty("REMINDER_TO") || "")
      .trim()
      .toLowerCase();

    if (!sheetId) {
      throw new Error("Set SHEET_ID in Script Properties.");
    }

    if (!/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(recipient)) {
      throw new Error("Set REMINDER_TO to one plain email address.");
    }

    const spreadsheet = SpreadsheetApp.openById(sheetId);
    const sheet = spreadsheet.getSheetByName("Tasks");

    if (!sheet) {
      throw new Error('Create a sheet named "Tasks".');
    }

    const timeZone = spreadsheet.getSpreadsheetTimeZone();
    const today = Utilities.formatDate(
      new Date(),
      timeZone,
      "yyyy-MM-dd"
    );
    const dueDate = nextCalendarDate_(today);
    const sentKey = "sent:" + recipient + ":" + dueDate;

    if (properties.getProperty(sentKey) !== null) {
      console.log("Skipped: reminder already marked sent for " + dueDate);
      return;
    }

    const rows = sheet.getDataRange().getValues();
    const expectedHeaders = ["Task", "Due", "Status"];
    const validHeaders = expectedHeaders.every(
      (header, index) => String(rows[0][index] || "").trim() === header
    );

    if (!validHeaders) {
      throw new Error("Use Task, Due, Status as headers in A1:C1.");
    }

    const tasks = [];

    for (let index = 1; index < rows.length; index++) {
      const task = String(rows[index][0] ?? "").trim();
      const due = rows[index][1];
      const status = String(rows[index][2] ?? "").trim();

      if (!task || due === "" || due == null) {
        continue;
      }

      if (status.toLowerCase() === "done") {
        continue;
      }

      if (!(due instanceof Date) || Number.isNaN(due.getTime())) {
        throw new Error(
          "Row " + (index + 1) +
          ": Due must be a real Google Sheets date."
        );
      }

      const rowDate = Utilities.formatDate(due, timeZone, "yyyy-MM-dd");

      if (rowDate === dueDate) {
        tasks.push("- " + task + " [" + (status || "Unspecified") + "]");
      }
    }

    if (tasks.length === 0) {
      console.log("No unfinished tasks due " + dueDate);
      return;
    }

    const subject =
      "Task reminder for " + dueDate +
      " (" + tasks.length + " unfinished)";
    const body = [
      "Unfinished tasks due " + dueDate + ":",
      "",
      ...tasks,
      "",
      "Source sheet: Tasks"
    ].join("\n");

    MailApp.sendEmail({
      to: recipient,
      subject: subject,
      body: body
    });

    properties.setProperty(sentKey, new Date().toISOString());
    // Remove markers for past dates so they don't pile up in Script Properties.
    properties.getKeys()
      .filter(key => key.startsWith("sent:") && key.slice(-10) < today)
      .forEach(key => properties.deleteProperty(key));
    console.log("Sent reminder for " + dueDate);
  } finally {
    lock.releaseLock();
  }
}

function nextCalendarDate_(dateKey) {
  const [year, month, day] = dateKey.split("-").map(Number);
  const nextDate = new Date(Date.UTC(year, month - 1, day + 1));
  return Utilities.formatDate(nextDate, "UTC", "yyyy-MM-dd");
}

The helper advances a calendar date rather than adding 24 hours, avoiding daylight saving shifts. Before sending, the script validates every unfinished row with a task and due value; a text date anywhere in those rows stops the send.

How do you schedule one daily task reminder?

Schedule sendTomorrowTaskReminder with one daily time-driven trigger after normal task updates.

  1. Create the trigger. In Apps Script, open Triggers > Add Trigger. Select sendTomorrowTaskReminder, Time-driven, Day timer, and an afternoon window.
  2. Remove duplicate triggers. Keep one intended daily trigger; redundant triggers create unnecessary executions despite duplicate protection.
  3. Set the cutoff. Choose a window after task updates and away from midnight. Triggers run within a window, not at a guaranteed minute.

The trigger uses its creator’s authorization, and that account must retain spreadsheet access. Execution after midnight targets a different tomorrow.

What email does the sample produce, and what happens on reruns?

On July 14, 2026, in the spreadsheet’s time zone, the sample reminder includes two unfinished tasks due July 15, provided no matching sent marker exists.

Subject:

Task reminder for 2026-07-15 (2 unfinished)

Plain-text body:

Unfinished tasks due 2026-07-15:

- Draft remote onboarding checklist [Open]
- Confirm team coverage [In progress]

Source sheet: Tasks

The Done task and tasks due July 14 or July 16 are excluded.

Execution condition Result
Qualifying tasks, no marker Send one digest; save timestamp marker
Matching recipient-and-date marker Skip, even if tasks changed
No qualifying tasks No email or marker; later runs may send

The marker uses the trimmed, lowercased recipient address and target due date. The email is a daily snapshot, not an updating notification.

Why does sendTomorrowTaskReminder send a date-specific digest instead of separate task emails?

sendTomorrowTaskReminder collects unfinished tasks matching tomorrow’s calendar date into one list, then submits that list as a single email.

First, getScriptLock and tryLock serialize executions within the project. getScriptProperties loads configuration separately from source code, and the script normalizes and validates the settings before opening Tasks.

Next, Utilities.formatDate derives today’s date in the spreadsheet’s time zone. nextCalendarDate_ splits that date into numbers and uses Date.UTC with an incremented day, choosing calendar arithmetic over adding elapsed hours. A recipient-and-date property then determines whether this digest is already recorded.

getValues reads the table in one batch rather than requesting individual cells. After checking headers, the loop trims task and status text, skips incomplete or finished entries, validates remaining dates, and compares formatted date keys rather than timestamps. Matching tasks become list entries, with Unspecified for blank statuses.

If entries exist, the script builds a count-bearing subject and joins the body lines. MailApp.sendEmail submits one digest rather than emailing inside the loop. Only afterward does the script record the sent marker and remove older markers. Finally, finally releases the lock even when execution returns early.

How do the lock and sent marker prevent repeat emails?

The Apps Script lock serializes executions within one project, while the persistent sent marker suppresses later sends for the same recipient and due date.

An overlapping execution waits up to 30 seconds for the lock. After acquiring it, the execution checks the marker; failure to acquire it causes an exit.

Email submission and marker storage are separate operations. If submission succeeds but execution stops before saving the marker, another run may duplicate the email. Saving the marker first would instead risk suppressing a failed send.

Copied projects have separate locks and properties. Keep only one project active for this workflow.

How do you fix date errors, skipped reminders, and duplicate emails?

Fix Google Sheets reminder failures by correcting date values or statuses, checking the matching sent marker, and disabling independent sending projects.

Due date error

Symptom: Row 3: Due must be a real Google Sheets date. Execution stops without sending.

Exact fix: Remove any leading apostrophe in the reported column B cell, re-enter the date using the spreadsheet locale, and apply Format > Number > Date. Formatting alone cannot convert existing text. Correct all reported rows before rerunning.

Completed task appears

Symptom: A task labeled Completed or Closed appears.

Exact fix: Change the status to Done. Add a column C data-validation dropdown containing working statuses and Done; only Done is recognized as finished.

Changed rows produce no replacement email

Symptom: Logs report the reminder already marked sent.

Exact fix: Normally retain the daily snapshot. For an intentional replacement, delete only the matching sent:recipient:YYYY-MM-DD Script Property in Project Settings, then rerun. Deletion permits another email without retracting the first.

Duplicate reminders arrive

Symptom: Two emails arrive for the same due date.

Exact fix: Disable triggers in separate sending projects, leaving one project active. For failures around sending or marker storage, inspect the execution record and verify the earlier send before retrying. Do not delete markers indiscriminately.

When should you avoid this Google Sheets reminder approach?

Avoid this Google Sheets reminder when individualized owner delivery, very large task lists, or strict duplicate prevention are required.

Owner-specific reminders need a recipient field and separate recipient-level delivery records. Large task lists may produce unwieldy emails.

For critical delivery requirements, choose a durable job system with an auditable delivery ledger and an email service supporting idempotent send requests. Apps Script cannot make email submission and marker storage one atomic transaction.

Related questions

Will tasks added after the daily reminder trigger another email?

The Google Sheets reminder script skips another send once it finds the recipient-and-date sent marker, even if tasks have changed. If no tasks qualified during an earlier run, no marker was written, so a later run can send.

Can this reminder email each task owner separately?

This Apps Script sends one combined reminder to the single address configured in REMINDER_TO. Owner-specific reminders need a recipient field and separate recipient-level delivery records.

Does the reminder include overdue tasks or skip weekends?

The Google Sheets reminder includes only unfinished tasks due on the next calendar date in the spreadsheet's time zone. It excludes overdue tasks and does not skip weekends.

Does the sent marker guarantee that duplicate emails are impossible?

The Apps Script sent marker suppresses ordinary reruns but cannot guarantee exactly-once delivery. If email submission succeeds and execution stops before the marker is saved, a later execution may send another copy.