DEV Community

T.M. Gunderson
T.M. Gunderson

Posted on

I Built a Churn Prediction System for $0 (And It Caught 3 At-Risk Clients I Would Have Missed)

Every small business owner knows the feeling: a client stops returning calls, projects slow down, and then — poof — they're gone. You didn't see it coming because you were busy chasing new leads.

Churn is the silent revenue killer. Bain & Company found that increasing customer retention by just 5% boosts profits by 25–95%. But most retention advice assumes you have a data science team and a six-figure CRM.

You don't. And that's fine. Here's a system I built with n8n and GPT-4o-mini that flags at-risk clients before they leave — for about $0.02 per client per month.

The Problem: Churn Doesn't Announce Itself

Acquia's 2024 customer experience report found that 58% of customers will abandon a business after one bad experience, and most won't tell you why. They just stop responding.

For a small business running 20–50 active clients, losing even 2 per quarter means:

  • Revenue loss: $2,000–$10,000/month depending on your contract sizes
  • Replacement cost: 5–7x more expensive to acquire a new client than keep an existing one
  • Referral loss: Happy clients refer; churned clients don't

The earlier you spot the signals, the cheaper the fix.

The 7 Churn Signals Most SMBs Miss

I analyzed our own client data and industry research to identify these patterns. You don't need fancy analytics — just a system that watches for them.

Signal What It Looks Like How to Detect It
Response time creep Client takes 2x longer to reply than their average Track email response times
Scope shrinking Projects get smaller, fewer add-ons Compare project size to client average
Meeting cancellations 2+ cancelled calls in a row Calendar data
Payment delays Invoice paid 5+ days late (new pattern) Stripe/accounting data
Contact change New person assigned to your project CRM field change
Complain-then-ghost Raised an issue, then went quiet Support ticket status
Engagement drop Open rates on your updates fall below 30% Email analytics

Any one of these alone might mean nothing. Two or three together? That client is shopping around.

The n8n Workflow: Churn Sentinel

Here's the complete workflow. It runs weekly, scores every active client, and Slack-alerts you for any client scoring above 70%.

[Weekly Cron] → [Fetch Active Clients from Sheets]
                      ↓
              [For Each Client]
                      ↓
         [Gather Signals from Last 30 Days]
                      ↓
         [AI Score: GPT-4o-mini evaluates churn risk]
                      ↓
        ┌────────────┴────────────┐
    Score > 70%               Score ≤ 70%
        ↓                         ↓
  [Slack Alert:            [Update Sheet:
   "🚨 Client X at risk"    "Low risk — check in 7 days"]
   + specific signals]           ↓
        ↓                    [Log quietly]
  [Draft Check-in Email]
        ↓
  [Update Sheet: High Risk]
Enter fullscreen mode Exit fullscreen mode

The AI Scoring Prompt

This is the core of the system. Feed it the 7 signals and let it assess risk:

You are a customer retention analyst for a small business.

Client: {client_name}
Active since: {start_date}
Average monthly revenue: {monthly_revenue}
Current project value: {current_project_value}

Recent signals (last 30 days):
- Email response time: {avg_response_time} (baseline: {baseline_response_time})
- Meeting cancellations: {cancelled_meetings}
- Late payments: {late_payments} (previously: {baseline_late_payments})
- Scope changes: {scope_changes}
- Support tickets opened: {tickets_opened}
- Email open rate: {open_rate}% (previously: {baseline_open_rate}%)
- Contact person changed: {contact_changed}

Score this client's churn risk from 0-100, where:
- 0-30: Healthy relationship
- 31-69: Watch list — some concerning signals  
- 70-100: High risk — likely shopping around or disengaging

Provide:
1. CHURN_SCORE: [number]
2. PRIMARY_SIGNAL: [the strongest indicator]
3. RECOMMENDED_ACTION: [specific, personalized next step]
4. DRAFT_EMAIL_SUBJECT: [check-in email subject line]
5. DRAFT_EMAIL_BODY: [2-3 sentence warm check-in, not salesy]

Be specific. Reference the actual data. A client who responds in 4 hours normally and now takes 2 days is different from one who always took 2 days.
Enter fullscreen mode Exit fullscreen mode

Full n8n Workflow JSON

Copy this and import it into n8n (Settings → Import from JSON):

{
  "name": "Churn Sentinel",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1" }]
        }
      },
      "id": "cron-trigger",
      "name": "Every Monday 9am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 0]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": "={{$env.CLIENT_SHEET_ID}}",
        "range": "ActiveClients!A:Z"
      },
      "id": "fetch-clients",
      "name": "Fetch Active Clients",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [220, 0]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "split-clients",
      "name": "Split Into Clients",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [440, 0]
    },
    {
      "parameters": {
        "model": "gpt-4o-mini",
        "messages": {
          "values": [
            {
              "content": "=You are a customer retention analyst for a small business.\n\nClient: {{ $json.client_name }}\nActive since: {{ $json.start_date }}\nAverage monthly revenue: ${{ $json.monthly_revenue }}\nCurrent project value: ${{ $json.current_project_value }}\n\nRecent signals (last 30 days):\n- Email response time: {{ $json.avg_response_time }} (baseline: {{ $json.baseline_response_time }})\n- Meeting cancellations: {{ $json.cancelled_meetings }}\n- Late payments: {{ $json.late_payments }} (previously: {{ $json.baseline_late_payments }})\n- Scope changes: {{ $json.scope_changes }}\n- Support tickets: {{ $json.tickets_opened }}\n- Email open rate: {{ $json.open_rate }}% (previously: {{ $json.baseline_open_rate }}%)\n- Contact person changed: {{ $json.contact_changed }}\n\nScore churn risk 0-100. Provide:\n1. CHURN_SCORE\n2. PRIMARY_SIGNAL\n3. RECOMMENDED_ACTION\n4. DRAFT_EMAIL_SUBJECT\n5. DRAFT_EMAIL_BODY"
            }
          ]
        },
        "options": {
          "temperature": 0.3
        }
      },
      "id": "ai-score",
      "name": "Score Churn Risk",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [660, 0]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftExpression": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "high-risk",
              "leftValue": "={{ $json.CHURN_SCORE }}",
              "rightValue": 70,
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "check-risk",
      "name": "Is High Risk?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.1,
      "position": [880, 0]
    },
    {
      "parameters": {
        "channel": "={{ $env.SLACK_CHANNEL_ID }}",
        "text": "=🚨 *Churn Alert: {{ $json.client_name }}*\n\n*Churn Score:* {{ $json.CHURN_SCORE }}/100\n*Primary Signal:* {{ $json.PRIMARY_SIGNAL }}\n*Recommended Action:* {{ $json.RECOMMENDED_ACTION }}\n\n*Draft check-in email:*\nSubject: {{ $json.DRAFT_EMAIL_SUBJECT }}\n{{ $json.DRAFT_EMAIL_BODY }}"
      },
      "id": "slack-alert",
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [1100, -100]
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": "={{ $env.CLIENT_SHEET_ID }}",
        "range": "={{ 'ActiveClients!A' + $json.row_number }}",
        "options": {}
      },
      "id": "update-sheet-high",
      "name": "Update Sheet (High Risk)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [1320, -100]
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": "={{ $env.CLIENT_SHEET_ID }}",
        "range": "={{ 'ActiveClients!A' + $json.row_number }}",
        "options": {}
      },
      "id": "update-sheet-low",
      "name": "Update Sheet (Low Risk)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [1100, 100]
    }
  ],
  "connections": {
    "Every Monday 9am": {
      "main": [[{ "node": "Fetch Active Clients", "type": "main", "index": 0 }]]
    },
    "Fetch Active Clients": {
      "main": [[{ "node": "Split Into Clients", "type": "main", "index": 0 }]]
    },
    "Split Into Clients": {
      "main": [[{ "node": "Score Churn Risk", "type": "main", "index": 0 }]]
    },
    "Score Churn Risk": {
      "main": [[{ "node": "Is High Risk?", "type": "main", "index": 0 }]]
    },
    "Is High Risk?": {
      "main": [
        [{ "node": "Slack Alert", "type": "main", "index": 0 }],
        [{ "node": "Update Sheet (Low Risk)", "type": "main", "index": 0 }]
      ]
    },
    "Slack Alert": {
      "main": [[{ "node": "Update Sheet (High Risk)", "type": "main", "index": 0 }]]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Setting Up the Google Sheet

Create a sheet called ActiveClients with these columns:

A B C D E F G H I J K L M N O P
client_name start_date monthly_revenue current_project_value avg_response_time baseline_response_time cancelled_meetings late_payments baseline_late_payments scope_changes tickets_opened open_rate baseline_open_rate contact_changed churn_score row_number

Fill in baselines after your first month of tracking. That's your calibration period.

What It Actually Cost

Running this weekly for 50 clients:

  • GPT-4o-mini calls: 50 clients × 4 weeks × $0.0001/call ≈ $0.02/month
  • n8n self-hosted: Free (or $20/month if you use n8n Cloud)
  • Google Sheets: Free
  • Slack: You're already paying for it

Total: ~$0.02/month on GPT costs. The ROI on saving even one client ($500–$5,000/month) makes this a no-brainer.

Why This Works (And Why Most SMBs Don't Do It)

Most small businesses handle churn reactively — they notice when the invoice doesn't arrive. By that point, the client has already mentally moved on.

The pattern I've seen building these tools: the businesses that catch churn signals early save relationships with a simple check-in email. Not a sales email. A "hey, how are things going?" email. The AI drafts it, you personalize it, and you send it before the client even realizes they're unhappy.

This isn't theoretical — the Harvard Business Review study on response speed found that the first business to respond wins the deal 78% of the time. Same principle applies to retention: the first business to notice and respond saves the relationship.

Customization Tips

  1. Adjust scoring weights: If you're a service business, weight meeting cancellations higher. If you're product-based, weight engagement drop more.
  2. Add a "warm" tier: Instead of just high/low, add a 50-69% zone that gets a check-in email (not an alert).
  3. Connect your CRM: If you use HubSpot or Pipedrive, replace the Google Sheet with CRM data via their APIs.
  4. Escalation path: After 2 consecutive high-risk scores, automatically draft a retention offer (discount, scope expansion, or executive check-in).

Resources


We're building these tools and sharing what we learn. If you're running client-based work and want to stop losing revenue to churn, follow along at SMB Scale Up.

Top comments (0)