AutomationFlowsEmail & Gmail › Send Due-date Reminders From Google Sheets with Gmail and Slack

Send Due-date Reminders From Google Sheets with Gmail and Slack

ByJulian Abt @automationsmanufaktur on n8n.io

This workflow runs daily, reads deadline rows from Google Sheets, and sends due-soon, due-today, or overdue reminder emails via Gmail, escalating overdue items to Slack and updating the sheet to prevent duplicate reminders. Runs every day at 07:00 on a schedule. Reads all rows…

Cron / scheduled trigger★★★★☆ complexity12 nodesGoogle SheetsGmailSlack
Email & Gmail Trigger: Cron / scheduled Nodes: 12 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #16312 — we link there as the canonical source.

This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "Due-Date Reminder Engine from Google Sheets (no AI)",
  "nodes": [
    {
      "id": "sticky-05-overview",
      "name": "Overview & Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -780,
        -40
      ],
      "parameters": {
        "color": 3,
        "width": 560,
        "height": 620,
        "content": "## \u23f0 Due-Date Reminder Engine (Google Sheets)\n\nA universal, AI-free deadline watcher: reads dated rows from a sheet, works out what's due soon / today / overdue, emails reminders and escalates overdue items to Slack.\n\n### \ud83d\udc64 Who's it for\nAnyone tracking deadlines in a spreadsheet \u2014 invoices, renewals, contracts, tasks, follow-ups \u2014 who wants automatic reminders.\n\n### \u2699\ufe0f How it works\n1. **Schedule** reads the sheet once a day.\n2. A Code node classifies each open row with timezone-safe calendar-day math (Luxon) and skips rows already notified today.\n3. **Gmail** emails every reminder (tone matches urgency).\n4. **Is Overdue?** escalates only overdue items to **Slack**.\n5. **Mark Notified** stamps `NotifiedOn` so nothing fires twice in a day.\n\n### \ud83d\udd27 Set up (~5 min)\n- Add **Google Sheets**, **Gmail OAuth2** and **Slack** credentials.\n- Sheet columns: `Title, DueDate (YYYY-MM-DD), Email, Status, NotifiedOn`.\n- Tune `REMINDER_WINDOW_DAYS` (default 3) in **Compute Due Reminders**."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-05-s1",
      "name": "Section 1 \u00b7 Read",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -170,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 440,
        "content": "### 1. Read\nDaily trigger \u2192 reads every row of the sheet, keeping the hidden `row_number` for the write-back later."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-05-s2",
      "name": "Section 2 \u00b7 Compute & Send",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        430,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 540,
        "height": 440,
        "content": "### 2. Compute & Send\nClassifies each open item as overdue / due today / due soon (skipping rows already notified today), then emails an HTML reminder whose tone matches the urgency."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-05-s3",
      "name": "Section 3 \u00b7 Escalate Overdue",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1070,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 440,
        "content": "### 3. Escalate Overdue\n**Is Overdue?** lets only overdue items through to **Slack** for team escalation. Everything else stops after the email."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-05-s4",
      "name": "Section 4 \u00b7 Mark Done",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1670,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 200,
        "height": 440,
        "content": "### 4. Mark Done\nStamps `NotifiedOn` = today on every reminded row, so the same item never re-fires on the same day."
      },
      "typeVersion": 1
    },
    {
      "id": "trigger-daily",
      "name": "Every Day at 07:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -120,
        160
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 7,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "sheets-read",
      "name": "Read Items",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        180,
        160
      ],
      "parameters": {
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "detectAutomatically"
            }
          }
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "Sheet1",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/REPLACE_WITH_YOUR_SHEET_ID/edit"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "code-compute",
      "name": "Compute Due Reminders",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        160
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 Due-Date Reminder Engine \u2014 classification & dedup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Pure core JS. Uses Luxon (DateTime) which n8n injects globally.\n// WHY Luxon: whole-calendar-day math in the workflow timezone avoids\n// UTC off-by-one bugs that plain `new Date()` subtraction produces.\n\n// ===== EDITABLE CONSTANTS ==========================================\nconst REMINDER_WINDOW_DAYS = 3;   // days ahead that count as \"due soon\"\nconst DONE_STATUS = 'Done';       // Status value that means \"ignore\"\nconst DATE_FMT = 'yyyy-MM-dd';    // sheet date format (YYYY-MM-DD)\n// ===================================================================\n\n// \"today\" as a date-only anchor in the workflow's timezone.\n// WHY startOf('day'): strips the clock time so diffs are full days.\nconst today = DateTime.now().startOf('day'); // DateTime.now() already honours the workflow timezone\nconst todayStr = today.toFormat(DATE_FMT);\n\nconst out = [];\n\nfor (const item of $input.all()) {\n  const row = item.json;\n\n  // Skip blank spacer rows that have no due date at all.\n  const rawDue = (row.DueDate ?? '').toString().trim();\n  if (!rawDue) continue;\n\n  // Skip anything already marked finished.\n  const status = (row.Status ?? '').toString().trim();\n  if (status === DONE_STATUS) continue;\n\n  // \u2500\u2500 DEDUP GUARD \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  // If this row was already reminded today, do nothing for it now.\n  const notifiedOn = (row.NotifiedOn ?? '').toString().trim();\n  if (notifiedOn === todayStr) continue;\n\n  // Parse the due date as a date-only value in the same timezone.\n  const due = DateTime.fromFormat(rawDue, DATE_FMT, { zone: today.zoneName }).startOf('day');\n  if (!due.isValid) continue; // WHY: bad date strings shouldn't crash the run\n\n  // Whole-day difference: negative = past, 0 = today, positive = future.\n  const daysUntilDue = Math.round(due.diff(today, 'days').days);\n\n  // Classify; rows outside the window are left alone.\n  let reminderType = null;\n  if (daysUntilDue < 0) reminderType = 'overdue';\n  else if (daysUntilDue === 0) reminderType = 'due_today';\n  else if (daysUntilDue <= REMINDER_WINDOW_DAYS) reminderType = 'due_soon';\n  if (!reminderType) continue;\n\n  // Human-readable line reused by both email and Slack.\n  const overdueDays = Math.abs(daysUntilDue);\n  const phrase =\n    reminderType === 'overdue'\n      ? `is OVERDUE by ${overdueDays} day${overdueDays === 1 ? '' : 's'}`\n      : reminderType === 'due_today'\n      ? 'is due TODAY'\n      : `is due in ${daysUntilDue} day${daysUntilDue === 1 ? '' : 's'}`;\n\n  const title = (row.Title ?? 'Untitled item').toString().trim();\n  const message = `\"${title}\" ${phrase} (due ${rawDue}).`;\n\n  out.push({\n    json: {\n      Title: title,\n      Email: (row.Email ?? '').toString().trim(),\n      DueDate: rawDue,\n      daysUntilDue,\n      reminderType,\n      isOverdue: reminderType === 'overdue',\n      row_number: row.row_number, // WHY: needed to update the exact row later\n      todayStr,\n      message,\n    },\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "gmail-send",
      "name": "Send Reminder Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        820,
        160
      ],
      "parameters": {
        "sendTo": "={{ $json.Email }}",
        "message": "=<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:15px;color:#1a1a1a;line-height:1.6\">\n  <p>Hi,</p>\n  <p>This is an automated reminder about a tracked deadline:</p>\n  <table style=\"border-collapse:collapse;margin:12px 0\">\n    <tr><td style=\"padding:4px 12px 4px 0;color:#666\">Item</td><td style=\"padding:4px 0\"><strong>{{ $json.Title }}</strong></td></tr>\n    <tr><td style=\"padding:4px 12px 4px 0;color:#666\">Due date</td><td style=\"padding:4px 0\">{{ $json.DueDate }}</td></tr>\n    <tr><td style=\"padding:4px 12px 4px 0;color:#666\">Status</td><td style=\"padding:4px 0\">{{ $json.reminderType === 'overdue' ? '<span style=\\\"color:#c0392b;font-weight:bold\\\">OVERDUE by ' + (-1 * $json.daysUntilDue) + ' day(s)</span>' : ($json.reminderType === 'due_today' ? '<span style=\\\"color:#e67e22;font-weight:bold\\\">Due today</span>' : 'Due in ' + $json.daysUntilDue + ' day(s)') }}</td></tr>\n  </table>\n  <p style=\"color:#444\">{{ $json.message }}</p>\n  <p style=\"font-size:12px;color:#999;margin-top:24px\">Sent automatically by the Due-Date Reminder Engine. You receive at most one reminder per item per day.</p>\n</div>",
        "options": {
          "appendAttribution": false
        },
        "subject": "={{ $json.reminderType === 'overdue' ? '\u26a0\ufe0f OVERDUE: ' : ($json.reminderType === 'due_today' ? '\u23f0 Due today: ' : '\ud83d\udd14 Reminder: ') }}{{ $json.Title }}",
        "resource": "message",
        "emailType": "html",
        "operation": "send"
      },
      "typeVersion": 2.2
    },
    {
      "id": "if-overdue",
      "name": "Is Overdue?",
      "type": "n8n-nodes-base.if",
      "position": [
        1120,
        160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-overdue",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.isOverdue }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "slack-escalate",
      "name": "Escalate Overdue",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        1420,
        60
      ],
      "parameters": {
        "text": "=:rotating_light: *Overdue item needs attention*\\n>{{ $json.message }}\\n\u2022 Owner: {{ $json.Email }}\\n\u2022 Due: {{ $json.DueDate }}  (overdue by {{ -1 * $json.daysUntilDue }} day(s))",
        "select": "channel",
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#deadlines",
          "cachedResultName": "#deadlines"
        },
        "operation": "post",
        "messageType": "text",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "typeVersion": 2.4
    },
    {
      "id": "sheets-mark",
      "name": "Mark Notified",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        1720,
        160
      ],
      "parameters": {
        "columns": {
          "value": {
            "NotifiedOn": "={{ $json.todayStr }}",
            "row_number": "={{ $json.row_number }}"
          },
          "schema": [
            {
              "id": "row_number",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "NotifiedOn",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "NotifiedOn",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "resource": "sheet",
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "Sheet1",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/REPLACE_WITH_YOUR_SHEET_ID/edit"
        }
      },
      "typeVersion": 4.7
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Read Items": {
      "main": [
        [
          {
            "node": "Compute Due Reminders",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Overdue?": {
      "main": [
        [
          {
            "node": "Escalate Overdue",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Notified",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Escalate Overdue": {
      "main": [
        [
          {
            "node": "Mark Notified",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Day at 07:00": {
      "main": [
        [
          {
            "node": "Read Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reminder Email": {
      "main": [
        [
          {
            "node": "Is Overdue?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Due Reminders": {
      "main": [
        [
          {
            "node": "Send Reminder Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs daily, reads deadline rows from Google Sheets, and sends due-soon, due-today, or overdue reminder emails via Gmail, escalating overdue items to Slack and updating the sheet to prevent duplicate reminders. Runs every day at 07:00 on a schedule. Reads all rows…

Source: https://n8n.io/workflows/16312/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

Automatically identify clients who haven’t been contacted in 14+ days and re-engage them with personalized Gmail follow-up emails, Google Sheets tracking, and Slack notifications for account managers.

High Level, Gmail, Google Sheets +2
Email & Gmail

Streamline client retention and contract renewals by automatically identifying expiring accounts, sending personalized reminder emails, and notifying account managers through Slack. This workflow ensu

High Level, Gmail, Slack +1
Email & Gmail

This powerful n8n workflow automatically processes, categorizes, and organizes your Gmail inbox using customizable rules stored in Google Sheets. Say goodbye to manual email sorting and hello to a per

Gmail, Google Sheets, Slack
Email & Gmail

This workflow runs daily to pull cloud spend from a billing API, compare it to a Google Sheets rolling baseline, and alert on cost spikes by creating a Jira incident, posting to Slack, emailing Financ

HTTP Request, Google Sheets, Jira +2
Email & Gmail

This workflow automatically monitors solar energy production every 2 hours by fetching data from the Energidataservice API. If the energy output falls below a predefined threshold, it instantly notifi

HTTP Request, Gmail, Google Sheets +1