{
  "id": "tRclcWZzEj8OXIh9",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Churn Risk Monitor - Job Posting Signals + Gmail CSM Alert + Google Sheets Log",
  "tags": [],
  "nodes": [
    {
      "id": "a9b9e3af-a5d4-4f69-9461-9b554563e5f0",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        64
      ],
      "parameters": {
        "width": 544,
        "height": 1148,
        "content": "This workflow watches for an early churn signal, a customer hiring for a competitor product. It checks your customer list every week, searches for matching job postings, and alerts the assigned CSM before a renewal is at risk. It is built for SaaS teams who want a lightweight alternative to paid tech stack tracking tools.\n\n### How it works\n1. **Schedule Trigger** starts the check every Monday at 8 AM.\n2. **Google Sheets** reads your customer list, one row per company.\n3. **JSearch** looks up recent job postings for each company.\n4. The **Code** node scans those postings for competitor tool names and works out days left until renewal.\n5. Rows with a competitor hire inside a 90 day renewal window are marked high risk and pass the IF filter.\n6. High risk rows are logged to Google Sheets and trigger a Gmail alert to the CSM.\n\n### Setup steps\n1. Connect Google Sheets OAuth2, then replace YOUR_CHURN_MONITOR_SHEET_ID in node 2 and node 6.\n2. Sign up free at RapidAPI JSearch and replace YOUR_JSEARCH_API_KEY in node 3.\n3. Connect Gmail OAuth2 for node 7.\n4. In node 4, update the competitors array with your own competitor names.\n5. Activate the workflow. It then runs every Monday at 8 AM.\n\n### Customization\nChange the schedule in node 1 for a different day or time.\nAdjust the 90 day renewal window in node 4 if your team needs more lead time.\nEdit the Gmail message in node 7 to match your brand voice.\nThe free JSearch tier gives 500 calls a month, enough for about 100 customers checked weekly."
      },
      "typeVersion": 1
    },
    {
      "id": "2d4cfefa-1d29-40bd-80ab-087a7c38fa4a",
      "name": "Sheet Schema",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        752,
        896
      ],
      "parameters": {
        "color": 7,
        "width": 1248,
        "height": 216,
        "content": "## Google Sheet setup\nTab 1, name it \"customers\": company_name, company_domain, renewal_date (use YYYY-MM-DD), csm_email, account_tier.\nTab 2, name it \"churn-alerts-log\": Date, Company, Domain, Job Title Found, Competitor, Days to Renewal, CSM Email, Alert Sent At.\nNode 6 writes to churn-alerts-log automatically and the column names must match exactly."
      },
      "typeVersion": 1
    },
    {
      "id": "c99bee5f-c335-4a53-bda7-26e93da9af67",
      "name": "Section - Trigger and Customer List",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        608,
        352
      ],
      "parameters": {
        "color": 7,
        "width": 495,
        "height": 404,
        "content": "## 1. Trigger and customer list\nRuns every Monday at 8 AM and reads all rows from the customers tab in Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "a9d29ba0-01b8-4dc7-8f83-4ae469884bfe",
      "name": "Section - Job Posting Search",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1184,
        272
      ],
      "parameters": {
        "color": 7,
        "width": 294,
        "height": 580,
        "content": "## 2. Job posting search\nSearches JSearch for recent postings that match each company name plus common competitor tool names."
      },
      "typeVersion": 1
    },
    {
      "id": "725e4a77-fdf1-4e4a-bb1a-4affcc30b695",
      "name": "Section - Score Risk and Filter",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1552,
        368
      ],
      "parameters": {
        "color": 7,
        "width": 574,
        "height": 356,
        "content": "## 3. Score risk and filter\nChecks postings for competitor keywords, works out days to renewal, and only lets high risk rows through."
      },
      "typeVersion": 1
    },
    {
      "id": "be934f3d-ac7f-4e80-8173-e0a383de4f14",
      "name": "Section - Log and Alert",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2288,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 356,
        "height": 836,
        "content": "## 4. Log and alert\nSaves every high risk match to the churn-alerts-log tab and emails the assigned CSM with the details."
      },
      "typeVersion": 1
    },
    {
      "id": "dfc4575b-60f7-4e0f-b36f-1b7b1ff08cf5",
      "name": "1. Schedule - Every Monday 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        640,
        528
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "cd3f413a-ed0f-4839-b410-ddd67feaca78",
      "name": "2. Sheets - Read Customer List",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        880,
        528
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "customers"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_CHURN_MONITOR_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "7674d660-ab3c-4479-b1bb-8f92a1a99ec5",
      "name": "3. HTTP - JSearch Job Posting Lookup",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1280,
        528
      ],
      "parameters": {
        "url": "https://jsearch.p.rapidapi.com/search",
        "options": {
          "timeout": 20000
        },
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "={{ $json.company_name + ' Salesforce OR HubSpot OR Zoho OR Pipedrive OR Monday' }}"
            },
            {
              "name": "page",
              "value": "1"
            },
            {
              "name": "num_pages",
              "value": "1"
            },
            {
              "name": "date_posted",
              "value": "month"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "X-RapidAPI-Key",
              "value": "YOUR_JSEARCH_API_KEY"
            },
            {
              "name": "X-RapidAPI-Host",
              "value": "jsearch.p.rapidapi.com"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "71116f65-855a-4600-a28f-06a0119d0c58",
      "name": "4. Code - Detect Competitor Job and Score Risk",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        528
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Detect competitor job postings and calculate renewal risk\n// All context is passed per-item - no index-based reference\n\nconst apiResp   = $json;\nconst sheetRow  = $('2. Sheets - Read Customer List').item.json;\n\n// ---- Pull customer context from Sheets row ----\n// The HTTP node output does not carry the Sheets fields, so we pair back\n// to the matching row with .item, which resolves to the same item index\n// as the one currently being processed.\nconst companyName   = ($json.company_name   || sheetRow.company_name   || '').trim();\nconst companyDomain = ($json.company_domain || sheetRow.company_domain || '').trim();\nconst renewalDate   = ($json.renewal_date   || sheetRow.renewal_date   || '').trim();\nconst csmEmail      = ($json.csm_email      || sheetRow.csm_email      || '').trim();\nconst accountTier   = ($json.account_tier   || sheetRow.account_tier   || 'Standard').trim();\n\n// ---- Competitor keyword list - update this to match your actual competitors ----\nconst competitors = [\n  'Salesforce',\n  'HubSpot',\n  'Zoho CRM',\n  'Zoho',\n  'Pipedrive',\n  'Monday.com',\n  'monday'\n];\n\n// ---- Scan job postings for competitor mentions ----\nconst jobs = apiResp.data || [];\n\nlet competitorDetected = false;\nlet detectedCompetitor = '';\nlet detectedJobTitle   = '';\nlet detectedEmployer   = '';\n\nfor (const job of jobs) {\n  const title       = (job.job_title || '').toLowerCase();\n  const description = (job.job_description || '').toLowerCase().substring(0, 500);\n  const employer    = (job.employer_name || '').toLowerCase();\n\n  // Skip if the job is AT the competitor company (not a customer hiring)\n  const isCompetitorCompany = competitors.some(function(c) {\n    return employer.includes(c.toLowerCase());\n  });\n  if (isCompetitorCompany) continue;\n\n  // Check if job title or description mentions a competitor\n  for (const comp of competitors) {\n    if (title.includes(comp.toLowerCase()) || description.includes(comp.toLowerCase())) {\n      // Also verify this job is likely from our customer's company\n      const matchesDomain = employer.includes(companyName.toLowerCase().split(' ')[0]);\n      if (matchesDomain || jobs.length <= 3) {\n        competitorDetected = true;\n        detectedCompetitor = comp;\n        detectedJobTitle   = job.job_title || '';\n        detectedEmployer   = job.employer_name || '';\n        break;\n      }\n    }\n  }\n  if (competitorDetected) break;\n}\n\n// ---- Calculate days to renewal ----\nconst today = new Date();\ntoday.setHours(0, 0, 0, 0);\n\nlet daysToRenewal = 999;\nlet within90Days  = false;\n\nif (renewalDate) {\n  const renewal = new Date(renewalDate);\n  if (!isNaN(renewal.getTime())) {\n    const diffMs  = renewal - today;\n    daysToRenewal = Math.round(diffMs / (1000 * 60 * 60 * 24));\n    within90Days  = daysToRenewal >= 0 && daysToRenewal <= 90;\n  }\n}\n\n// ---- Risk classification ----\nconst isHighRisk = competitorDetected && within90Days;\n\nconst riskLevel  =\n  isHighRisk                              ? 'HIGH'   :\n  competitorDetected && !within90Days     ? 'MEDIUM' :\n  'LOW';\n\nreturn [{\n  json: {\n    companyName,\n    companyDomain,\n    renewalDate,\n    csmEmail,\n    accountTier,\n    competitorDetected,\n    detectedCompetitor,\n    detectedJobTitle,\n    detectedEmployer,\n    daysToRenewal,\n    within90Days,\n    isHighRisk,\n    riskLevel,\n    jobsScanned: jobs.length,\n    checkedAt:   new Date().toISOString(),\n    dateOnly:    new Date().toISOString().split('T')[0]\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "2b382605-7ff6-4f0c-b336-2821bd73d396",
      "name": "5. IF - Is High Risk? (Competitor Job + 90 Days)",
      "type": "n8n-nodes-base.if",
      "position": [
        1856,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-highrisk",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.isHighRisk }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "354baf40-22b0-40e2-a3d5-8b6ee2d7e284",
      "name": "6. Sheets - Log Churn Alert",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2416,
        336
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $json.dateOnly }}",
            "Domain": "={{ $json.companyDomain }}",
            "Company": "={{ $json.companyName }}",
            "CSM Email": "={{ $json.csmEmail }}",
            "Competitor": "={{ $json.detectedCompetitor }}",
            "Alert Sent At": "={{ $json.checkedAt }}",
            "Days to Renewal": "={{ $json.daysToRenewal }}",
            "Job Title Found": "={{ $json.detectedJobTitle }}"
          },
          "schema": [
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Company",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Company",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Domain",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Domain",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Job Title Found",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Job Title Found",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Competitor",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Competitor",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Days to Renewal",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Days to Renewal",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "CSM Email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "CSM Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Alert Sent At",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Alert Sent At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "churn-alerts-log"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_CHURN_MONITOR_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "70073728-979a-45c9-86ec-6a3b56088434",
      "name": "7. Gmail - Send Churn Alert to CSM",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2432,
        688
      ],
      "parameters": {
        "sendTo": "={{ $json.csmEmail }}",
        "message": "=<div style=\"font-family:Arial,sans-serif;max-width:620px;margin:0 auto;background:#f9fafb;\">\n\n  <div style=\"background:#dc2626;padding:20px 24px;border-radius:8px 8px 0 0;\">\n    <h2 style=\"color:#fff;margin:0;font-size:17px;\">Churn Risk Alert - Immediate Action Needed</h2>\n    <p style=\"color:rgba(255,255,255,0.85);margin:5px 0 0;font-size:12px;\">{{ $json.accountTier }} Account | Detected: {{ $json.dateOnly }}</p>\n  </div>\n\n  <div style=\"background:#fff;padding:20px 24px;border:1px solid #e5e7eb;border-top:none;\">\n\n    <table style=\"width:100%;font-size:13px;margin-bottom:16px;\">\n      <tr><td style=\"color:#6b7280;padding:5px 0;width:140px;\">Company</td><td style=\"color:#111827;font-weight:700;\">{{ $json.companyName }}</td></tr>\n      <tr><td style=\"color:#6b7280;padding:5px 0;\">Domain</td><td style=\"color:#111827;\">{{ $json.companyDomain }}</td></tr>\n      <tr><td style=\"color:#6b7280;padding:5px 0;\">Competitor Tool</td><td style=\"color:#dc2626;font-weight:700;\">{{ $json.detectedCompetitor }}</td></tr>\n      <tr><td style=\"color:#6b7280;padding:5px 0;\">Job Title Spotted</td><td style=\"color:#111827;\">{{ $json.detectedJobTitle }}</td></tr>\n      <tr><td style=\"color:#6b7280;padding:5px 0;\">Renewal Date</td><td style=\"color:#111827;\">{{ $json.renewalDate }}</td></tr>\n      <tr><td style=\"color:#6b7280;padding:5px 0;\">Days to Renewal</td><td style=\"color:#dc2626;font-weight:700;font-size:15px;\">{{ $json.daysToRenewal }} days</td></tr>\n    </table>\n\n    <div style=\"background:#fef2f2;border-left:4px solid #dc2626;padding:13px 15px;border-radius:0 6px 6px 0;margin-bottom:14px;\">\n      <p style=\"margin:0;font-size:13px;color:#7f1d1d;font-weight:600;\">Why this is a risk</p>\n      <p style=\"margin:6px 0 0;font-size:13px;color:#374151;line-height:1.6;\">{{ $json.companyName }} has posted a job opening requiring {{ $json.detectedCompetitor }} skills. When a customer hires for a competitor product, they are actively planning to switch. With only {{ $json.daysToRenewal }} days until renewal, this is an urgent signal.</p>\n    </div>\n\n    <div style=\"background:#fef9c3;border-left:4px solid #ca8a04;padding:13px 15px;border-radius:0 6px 6px 0;\">\n      <p style=\"margin:0;font-size:13px;color:#713f12;font-weight:600;\">Recommended Actions</p>\n      <ul style=\"margin:6px 0 0;padding-left:18px;font-size:13px;color:#374151;line-height:1.9;\">\n        <li>Schedule a call with your main contact this week</li>\n        <li>Understand what pain points are driving this evaluation</li>\n        <li>Offer a product roadmap review or executive sponsor meeting</li>\n        <li>Consider a retention discount or added value for early renewal</li>\n      </ul>\n    </div>\n\n    <p style=\"font-size:11px;color:#9ca3af;margin:14px 0 0;\">Churn Risk Monitor - n8n automation. Signal source: job posting search. This is an early warning - not a confirmed churn event.</p>\n  </div>\n\n</div>",
        "options": {
          "senderName": "Churn Risk Monitor",
          "appendAttribution": false
        },
        "subject": "=URGENT Churn Risk - {{ $json.companyName }} is hiring for {{ $json.detectedCompetitor }}"
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "ff408fa6-bdf8-454a-9148-5208e95bade5",
  "nodeGroups": [],
  "connections": {
    "2. Sheets - Read Customer List": {
      "main": [
        [
          {
            "node": "3. HTTP - JSearch Job Posting Lookup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Schedule - Every Monday 8 AM": {
      "main": [
        [
          {
            "node": "2. Sheets - Read Customer List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. HTTP - JSearch Job Posting Lookup": {
      "main": [
        [
          {
            "node": "4. Code - Detect Competitor Job and Score Risk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Code - Detect Competitor Job and Score Risk": {
      "main": [
        [
          {
            "node": "5. IF - Is High Risk? (Competitor Job + 90 Days)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. IF - Is High Risk? (Competitor Job + 90 Days)": {
      "main": [
        [
          {
            "node": "6. Sheets - Log Churn Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "7. Gmail - Send Churn Alert to CSM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}