This workflow corresponds to n8n.io template #15496 — we link there as the canonical source.
This workflow follows the Error Trigger → 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 →
{
"id": "14M43oMMxpMzlaSF",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "RenewalFlow Intelligence",
"tags": [],
"nodes": [
{
"id": "5e2e4a16-b291-41dd-846d-754fd7b1fcfc",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
544,
368
],
"parameters": {
"color": 7,
"width": 704,
"height": 240,
"content": "### \ud83d\udea8 ERROR MONITORING\nIf credentials expire or Slack rate limits hit, this section fires an immediate administrative \nalert to prevent audit gaps."
},
"typeVersion": 1
},
{
"id": "924b3593-c05c-4698-a6f9-12c34a47e26a",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1728,
-160
],
"parameters": {
"color": 7,
"width": 272,
"height": 496,
"content": "### 3. STATUS SYNC\nLogs the alert back to the spreadsheet to ensure no duplicate messages are sent for the same renewal stage."
},
"typeVersion": 1
},
{
"id": "40d0d02c-7c46-42dc-86a9-b85543325f9c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1280,
-160
],
"parameters": {
"color": 7,
"width": 416,
"height": 496,
"content": "### 2. NOTIFICATION LAYER\n\nRoutes the processed data. \nIf an alert is due, it sends a formatted Slack message with personalized UTM links and manual research steps."
},
"typeVersion": 1
},
{
"id": "2698d004-da32-4d6e-918f-bc065750be37",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
544,
-160
],
"parameters": {
"color": 7,
"width": 704,
"height": 496,
"content": "### 1. DATA & LOGIC ENGINE\n The workflow wakes up daily, pulls your contract\ndatabase from Google Sheets, and runs the \nproprietary validation & alert logic."
},
"typeVersion": 1
},
{
"id": "2777381b-db5a-430c-9843-0781e8ee2958",
"name": "README: Production Setup",
"type": "n8n-nodes-base.stickyNote",
"position": [
-32,
-160
],
"parameters": {
"width": 544,
"height": 796,
"content": "# RenewalFlow Intelligence\n\n**HOW IT WORKS:**\n\nRuns daily at 9 AM, sends 4 alerts per contract:\n\u2022 Day 45: Research (\ud83d\udfe1 REMINDER)\n\u2022 Day 30: Quotes (\ud83d\udfe0 ATTENTION)\n\u2022 Day 14: Decision (\ud83d\udd34 URGENT)\n\u2022 Day 7: Action (\ud83d\udea8 FINAL NOTICE)\n\n**CATCH-UP LOGIC:**\nIf workflow misses runs (weekend/outage),\nalerts fire within 5-day window of each stage.\n\n## SETUP CHECKLIST\n\n**BEFORE FIRST RUN:**\n\n- [ ] **Google Sheets:**\n \u2022 Get Spreadsheet ID\n \u2022 Replace YOUR_SPREADSHEET_ID in Load Contracts node\n \u2022 Set up OAuth2 credentials in n8n\n \u2022 Share sheet with n8n service account\n\n- [ ] **Sheet Structure (Columns A-F):**\n A: Contract Name (text)\n B: Vendor Name (text)\n C: Renewal Date (YYYY-MM-DD format)\n D: Current Annual Cost (number, no \u20ac symbol)\n E: Vendor Pricing URL (https://...)\n F: Status (Active, Renewed, or Cancelled)\n\n- [ ] **Slack:**\n \u2022 Create #contract-renewals channel\n \u2022 Required scopes: chat:write, chat:write.public\n \u2022 Add bot to #contract-renewals channel\n \u2022 Configure Slack credentials in all Slack nodes\n\n"
},
"typeVersion": 1
},
{
"id": "07ab785a-57b7-47e6-9ab7-7c39067c66aa",
"name": "Slack: Error Alert",
"type": "n8n-nodes-base.slack",
"position": [
864,
448
],
"parameters": {
"text": "\ud83d\udea8 *WORKFLOW ERROR: Contract Renewal Monitor*\n\n*Error Message:*\n{{ $json.error.message }}\n\n*Failed Node:*\n{{ $json.error.node.name }}\n\n*Timestamp:*\n{{ $now.toISO() }}\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n*Common Fixes:*\n\u2022 Google Sheets credentials expired \u2192 Reconnect OAuth\n\u2022 Slack bot removed from channel \u2192 Re-add bot to #contract-renewals\n\u2022 Workflow deactivated \u2192 Toggle 'Active' back on\n\u2022 Invalid date in sheet \u2192 Check YYYY-MM-DD format\n\n*Check Execution Logs:*\nWorkflow Executions \u2192 Latest Failed Run \u2192 View Details\n\n_Your contract monitoring is currently down. Fix immediately._",
"otherOptions": {}
},
"typeVersion": 2.2
},
{
"id": "26eab4af-a1ab-42f2-8f28-1622cda78570",
"name": "Error Trigger",
"type": "n8n-nodes-base.errorTrigger",
"position": [
640,
448
],
"parameters": {},
"typeVersion": 1
},
{
"id": "b2a53ed0-e735-4997-b671-450237dcd3d5",
"name": "Google Sheets: Update Status",
"type": "n8n-nodes-base.googleSheets",
"position": [
1760,
-32
],
"parameters": {
"columns": {
"value": {
"Status": "={{ $json.status }}, {{ $json._alertStage }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Google Sheets: Load Contracts').params.documentId.value }}"
}
},
"typeVersion": 4.5
},
{
"id": "ea9a09ae-ccd0-47be-9a2f-bfc783bf6dcf",
"name": "Slack: Daily Summary",
"type": "n8n-nodes-base.slack",
"position": [
1536,
160
],
"parameters": {
"text": "={{ $json.summaryMsg }}",
"otherOptions": {}
},
"typeVersion": 2.2
},
{
"id": "5a157762-b92f-44bb-8015-91694809db57",
"name": "Slack: Send Alert",
"type": "n8n-nodes-base.slack",
"position": [
1536,
-32
],
"parameters": {
"text": "*{{ $json.urgency }} RENEWAL ALERT: {{ $json.contractName }}*\n\n\u26a1 *SKIP THE RESEARCH?*\nThis \u20ac{{ $json.currentCost.toLocaleString('en-US') }}/year contract could save you \u20ac{{ $json.potentialSavingsMin.toLocaleString('en-US') }}-\u20ac{{ $json.potentialSavingsMax.toLocaleString('en-US') }} with better negotiation.\n\nUpgrade to Premium for AI-powered price scraping and GPT-4o negotiation brief.\n\ud83d\udc49 <https://lemonsqueezy.com/checkout?utm_source=slack&utm_medium=alert&utm_campaign={{ $json.contractSlug }}&utm_content={{ $json._alertStage }}|*Unlock Analysis - \u20ac99*>\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n*Contract Details:*\n\u2022 *Vendor:* {{ $json.vendorName }}\n\u2022 *Renewal Date:* {{ $json.renewalDate }}\n\u2022 *Days Remaining:* {{ $json.daysUntilRenewal }}\n\u2022 *Annual Cost:* \u20ac{{ $json.currentCost.toLocaleString('en-US') }}\n\n*\ud83d\udccb MANUAL STEPS ({{ $json._alertStage }}):*\n{{ $json._alertMessage }}\n1. Visit: {{ $json.pricingUrl }}\n2. Search: \"{{ $json.vendorName }} alternatives 2026\"\n3. Email account manager for renewal quote.\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n_FREE tier - manual research required_",
"otherOptions": {}
},
"typeVersion": 2.2
},
{
"id": "f8cd0a7d-601f-428a-9087-f0282bae6af5",
"name": "IF: Is Alert?",
"type": "n8n-nodes-base.if",
"position": [
1312,
64
],
"parameters": {
"options": {},
"conditions": {
"boolean": [
{
"value1": "={{ $json._isAlert }}",
"value2": true
}
],
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and"
}
},
"typeVersion": 2
},
{
"id": "436c4d37-5745-419d-9cad-79d8ad015f9d",
"name": "Brain: Validate & Route",
"type": "n8n-nodes-base.code",
"position": [
1088,
64
],
"parameters": {
"jsCode": "const alertStages = [\n { days: 45, stage: 'alerted_45d', urgency: '\ud83d\udfe1 REMINDER', msg: '\ud83d\udcc5 Research Phase: Look for alternatives.' },\n { days: 30, stage: 'alerted_30d', urgency: '\ud83d\udfe0 ATTENTION', msg: '\ud83d\udcb0 Quote Phase: Get competitor pricing.' },\n { days: 14, stage: 'alerted_14d', urgency: '\ud83d\udd34 URGENT', msg: '\ud83c\udfaf Decision Phase: Finalize negotiation.' },\n { days: 7, stage: 'alerted_7d', urgency: '\ud83d\udea8 FINAL NOTICE', msg: '\u26a0\ufe0f Action Phase: Confirm or Cancel.' }\n];\n\nconst now = new Date();\nconst results = [];\nconst processed = new Set();\n\nfor (const item of items) {\n const data = item.json;\n const status = data['Status'] || 'Active';\n const rawDate = data['Renewal Date'];\n\n // Skip invalid/completed contracts\n if (!rawDate || status.includes('Cancelled') || status.includes('Renewed')) continue;\n\n // Deduplication\n const contractId = `${data['Contract Name']}-${rawDate}`;\n if (processed.has(contractId)) continue;\n\n // VALIDATION\n const contractName = data['Contract Name']?.trim();\n if (!contractName) continue;\n \n const currentCost = Number(data['Current Annual Cost']);\n if (isNaN(currentCost) || currentCost < 0) continue;\n \n const url = data['Vendor Pricing URL'];\n if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) continue;\n\n // DATE CALCULATION (using standard JS Date)\n const renewalDate = new Date(rawDate);\n const diffInMs = renewalDate - now;\n const daysUntil = Math.ceil(diffInMs / (1000 * 60 * 60 * 24));\n\n // Skip expired or invalid dates\n if (isNaN(daysUntil) || daysUntil < 0) continue;\n\n // SLUG FOR UTM (deterministic fallback using row number)\n let slug = contractName\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .substring(0, 50);\n \n // Fallback: use row number for consistency (not timestamp)\n if (!slug) slug = `contract-row-${data.rowNumber || 'unknown'}`;\n\n // MULTI-STAGE FILTER WITH CATCH-UP WINDOW\n for (const alert of alertStages) {\n // Window: If within alert.days OR slightly past it (catch-up for missed runs)\n const inWindow = daysUntil <= alert.days && daysUntil > (alert.days - 5);\n const notYetAlerted = !status.includes(alert.stage);\n\n if (inWindow && notYetAlerted) {\n processed.add(contractId);\n \n results.push({\n json: {\n contractName: contractName,\n vendorName: data['Vendor Name'] || 'Unknown Vendor',\n renewalDate: rawDate,\n currentCost: currentCost,\n pricingUrl: url,\n status: status,\n rowNumber: data.rowNumber,\n daysUntilRenewal: daysUntil,\n _alertStage: alert.stage,\n _alertMessage: alert.msg,\n urgency: alert.urgency,\n contractSlug: slug,\n _isAlert: true,\n potentialSavingsMin: Math.round(currentCost * 0.15),\n potentialSavingsMax: Math.round(currentCost * 0.25)\n }\n });\n break;\n }\n }\n}\n\n// EMPTY RESULT HANDLER\nif (results.length === 0) {\n return [{ \n json: { \n _isAlert: false, \n summaryMsg: `\u2705 Daily renewal check complete.\\n\\nReviewed ${items.length} contract${items.length !== 1 ? 's' : ''}.\\nNo renewals in alert windows (45d, 30d, 14d, 7d).` \n } \n }];\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "bb64915b-7e7d-496b-a375-55727079a89c",
"name": "Google Sheets: Load Contracts",
"type": "n8n-nodes-base.googleSheets",
"position": [
864,
64
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_SPREADSHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "4f5e6780-c8ea-4fe5-84c3-5a6a7d60254f",
"name": "Schedule: Daily 9AM",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "Runs at 9 AM in your configured timezone",
"position": [
640,
64
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 9 * * *"
}
]
}
},
"typeVersion": 1.2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "9d208650-c7a9-4f7f-b471-9943b3f04c14",
"connections": {
"Error Trigger": {
"main": [
[
{
"node": "Slack: Error Alert",
"type": "main",
"index": 0
}
]
]
},
"IF: Is Alert?": {
"main": [
[
{
"node": "Slack: Send Alert",
"type": "main",
"index": 0
}
],
[
{
"node": "Slack: Daily Summary",
"type": "main",
"index": 0
}
]
]
},
"Slack: Send Alert": {
"main": [
[
{
"node": "Google Sheets: Update Status",
"type": "main",
"index": 0
}
]
]
},
"Schedule: Daily 9AM": {
"main": [
[
{
"node": "Google Sheets: Load Contracts",
"type": "main",
"index": 0
}
]
]
},
"Brain: Validate & Route": {
"main": [
[
{
"node": "IF: Is Alert?",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets: Load Contracts": {
"main": [
[
{
"node": "Brain: Validate & Route",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Stop missing renewal deadlines and overpaying on auto-renewals. This workflow monitors your contract calendar in Google Sheets, sends progressive Slack notifications with manual research checklists, and tracks alert history to prevent duplicate notifications. You manage the…
Source: https://n8n.io/workflows/15496/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Google Sheets CRM Enrichment. Uses googleSheetsTrigger, httpRequest, googleSheets, slack. Event-driven trigger; 15 nodes.
This template is for teams using n8n in production who want immediate visibility into workflow failures. It’s ideal for DevOps teams, automation engineers, and operations teams who need reliable error
Error Logger. Uses errorTrigger, googleSheets, slack, stickyNote. Event-driven trigger; 5 nodes.
Transform your lead list into an AI-powered calling machine. This workflow automates your entire cold calling process using Vapi's conversational AI to initiate calls, qualify leads, capture detailed
Type in Slack. Walk away. Get a professional PDF report and a structured Excel fix sheet delivered to Google Drive and posted back in your Slack thread — fully automated, zero manual work.