{
  "name": "Send certification expiry alerts for security agents with Gmail and Google Sheets",
  "nodes": [
    {
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -580,
        -147
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 1400,
        "content": "## Send Certification Expiry Alerts for Security Agents\n\nEvery Monday morning, this workflow scans your Google Sheets agent roster for APS, SSIAP, or SST certifications expiring within the next 30 days, and sends a consolidated alert email to the manager \u2014 no manual spreadsheet checks needed.\n\n### How it works\n\n1. **Every Monday 08:00** fires the workflow on a weekly schedule.\n2. **Configure** holds all user-facing settings (sheet URL, expiry threshold, manager email).\n3. **Read Agent Roster** fetches all rows from your agent database.\n4. **Calculate Days Until Expiry** computes how many days remain before each certification expires.\n5. **Filter Expiring Soon** keeps only agents with a certification expiring within the threshold.\n6. **Build Alert Line** formats each agent's details into an HTML table row.\n7. **Aggregate Alert Rows** combines all expiring agents into a single list.\n8. **Send Alert Email** delivers one consolidated email to the manager with the full list.\n\n### Setup\n\n1. Connect your **Google Sheets OAuth2** credential.\n2. Connect your **Gmail OAuth2** credential.\n3. In the **Configure** node, update `sheetUrl`, `managerEmail`, and `expiryThreshold`.\n4. Ensure your sheet has these columns: Agent_Name, Cert_Type, Expiry_Date.\n5. Activate the workflow.\n\n### Customization\n\n- Change the alert threshold in the **Configure** node (default: 30 days).\n- Filter by certification type by adding a condition in **Filter Expiring Soon**.\n- Send individual emails per agent by removing the Aggregate node."
      },
      "typeVersion": 1
    },
    {
      "name": "Trigger & Configure",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -60,
        -147
      ],
      "parameters": {
        "color": 1,
        "width": 720,
        "height": 367,
        "content": "## Trigger & Configure\n\nFires every Monday at 08:00. Configure holds the sheet URL, manager email, and expiry threshold (default: 30 days)."
      },
      "typeVersion": 1
    },
    {
      "name": "Read & Calculate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        740,
        -147
      ],
      "parameters": {
        "color": 1,
        "width": 720,
        "height": 367,
        "content": "## Read & Calculate\n\nFetches all agents from the roster. Computes the number of days remaining before each certification expires."
      },
      "typeVersion": 1
    },
    {
      "name": "Filter & Format",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1540,
        -147
      ],
      "parameters": {
        "color": 1,
        "width": 720,
        "height": 367,
        "content": "## Filter & Format\n\nKeeps only agents whose certification expires within the configured threshold. Formats each entry as an HTML table row."
      },
      "typeVersion": 1
    },
    {
      "name": "Aggregate & Alert",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2340,
        -128
      ],
      "parameters": {
        "color": 1,
        "width": 720,
        "height": 348,
        "content": "## Aggregate & Alert\n\nCombines all expiring agent rows into a single list. Sends one consolidated alert email to the manager."
      },
      "typeVersion": 1
    },
    {
      "name": "Every Monday 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "name": "Configure",
      "type": "n8n-nodes-base.set",
      "position": [
        400,
        0
      ],
      "parameters": {
        "mode": "manual",
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-sheet",
              "name": "sheetUrl",
              "type": "string",
              "value": "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID_HERE/edit"
            },
            {
              "id": "cfg-email",
              "name": "managerEmail",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "cfg-threshold",
              "name": "expiryThreshold",
              "type": "number",
              "value": 30
            }
          ]
        },
        "includeOtherFields": false
      },
      "typeVersion": 3.4
    },
    {
      "name": "Read Agent Roster",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        800,
        0
      ],
      "parameters": {
        "options": {},
        "resource": "sheet",
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": 0
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "={{ $('Configure').first().json.sheetUrl }}"
        }
      },
      "typeVersion": 4.7
    },
    {
      "name": "Calculate Days Until Expiry",
      "type": "n8n-nodes-base.set",
      "position": [
        1200,
        0
      ],
      "parameters": {
        "mode": "manual",
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "assign-days",
              "name": "daysUntilExpiry",
              "type": "number",
              "value": "={{ Math.floor(DateTime.fromFormat($json.Expiry_Date, 'dd/MM/yyyy').diff($now, 'days').days) }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "name": "Filter Expiring Soon",
      "type": "n8n-nodes-base.filter",
      "position": [
        1600,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-lte",
              "operator": {
                "type": "number",
                "operation": "lte"
              },
              "leftValue": "={{ $json.daysUntilExpiry }}",
              "rightValue": "={{ $('Configure').first().json.expiryThreshold }}"
            },
            {
              "id": "cond-gte",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.daysUntilExpiry }}",
              "rightValue": "0"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "name": "Build Alert Line",
      "type": "n8n-nodes-base.set",
      "position": [
        2000,
        0
      ],
      "parameters": {
        "mode": "manual",
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "assign-row",
              "name": "alertRow",
              "type": "string",
              "value": "={{ \"<tr><td>\" + $json.Agent_Name + \"</td><td>\" + $json.Cert_Type + \"</td><td>\" + $json.Expiry_Date + \"</td><td>\" + $json.daysUntilExpiry + \" days</td></tr>\" }}"
            }
          ]
        },
        "includeOtherFields": false
      },
      "typeVersion": 3.4
    },
    {
      "name": "Aggregate Alert Rows",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2400,
        0
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "agents"
      },
      "typeVersion": 1
    },
    {
      "name": "Send Alert Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2800,
        0
      ],
      "parameters": {
        "sendTo": "={{ $('Configure').first().json.managerEmail }}",
        "message": "={{ \"<html><body style=\\\"font-family:Arial,sans-serif\\\"><p>The following agents have certifications expiring within \" + $('Configure').first().json.expiryThreshold + \" days:</p><table border=\\\"1\\\" cellpadding=\\\"8\\\"><tr><th>Agent</th><th>Certification</th><th>Expiry date</th><th>Days remaining</th></tr>\" + $json.agents.map(a => a.alertRow).join('') + \"</table><p>Please arrange renewal as soon as possible.</p></body></html>\" }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "={{ \"Certification expiry alert \u2014 \" + $json.agents.length + \" agent(s) require renewal\" }}",
        "resource": "message",
        "emailType": "html",
        "operation": "send"
      },
      "typeVersion": 2.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Configure": {
      "main": [
        [
          {
            "node": "Read Agent Roster",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Alert Line": {
      "main": [
        [
          {
            "node": "Aggregate Alert Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Agent Roster": {
      "main": [
        [
          {
            "node": "Calculate Days Until Expiry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Monday 08:00": {
      "main": [
        [
          {
            "node": "Configure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Alert Rows": {
      "main": [
        [
          {
            "node": "Send Alert Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Expiring Soon": {
      "main": [
        [
          {
            "node": "Build Alert Line",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Days Until Expiry": {
      "main": [
        [
          {
            "node": "Filter Expiring Soon",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "description": "Every Monday, scans a Google Sheets agent roster for APS, SSIAP, or SST certifications expiring within 30 days, and sends a consolidated alert email to the manager listing agents who need renewal."
}