{
  "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
          }
        ]
      ]
    }
  }
}