Predict churn 30 days before it happens

Build a behavioral early-warning system that flags at-risk users while there's still time to save them. No ML team required.

Published

difficulty advanced · time to value 10 minutes · tools used 3

Just say this

Find behaviors that predict a user will churn within 30 days. Compare users who churned last month against users who renewed. Build me a saved cohort of at-risk users I can email.

Why this matters

Saving a churning customer is 5x cheaper than acquiring a new one. But by the time they hit “cancel,” it’s too late — most users have already mentally checked out 2-4 weeks before they actually cancel.

The signals are in your data already:

  • They stopped logging in
  • They stopped using your most-loved feature
  • They invited zero new teammates in the last 30 days
  • They opened only 1 email vs their usual 5

You just need to formalize them. Old way: hire a data scientist, build a churn model, retrain monthly. New way: ask Agentry to diff the behavior of churners vs renewers, and the agent finds the signals empirically.

What you get

  • A ranked list of behavioral signals that predicted churn (with effect size)
  • A saved cohort at_risk_users that updates daily
  • A draft re-engagement email targeted to that cohort
  • Optional: a Routine that emails the at-risk list to you every Monday

Walk through it

You

Find behaviors that predict churn in the next 30 days. Compare users who churned in April against users who renewed in April. Find the top 3 signals.

Agent

Let me build two cohorts and compare their pre-decision behavior.

agentry_create_cohort {
  project_id: "default",
  name: "churned_april_2026",
  filter: { event: "subscription_canceled",
            ts_range: ["2026-04-01", "2026-04-30"] }
}
{ "cohort_id": "co_chur4", "matching_users": 87 }
agentry_create_cohort {
  project_id: "default",
  name: "renewed_april_2026",
  filter: { event: "subscription_renewed",
            ts_range: ["2026-04-01", "2026-04-30"] }
}
{ "cohort_id": "co_ren4", "matching_users": 412 }
agentry_analytics_query {
  project_id: "default",
  hogql: "
    -- For each event, compare frequency in days [-60, -30] (the 'before' window)
    -- between churners and renewers
    SELECT
      event,
      avg(CASE WHEN cohort = 'churned' THEN count_in_window ELSE 0 END) AS churners_avg,
      avg(CASE WHEN cohort = 'renewed' THEN count_in_window ELSE 0 END) AS renewers_avg,
      churners_avg / nullif(renewers_avg, 0) AS ratio
    FROM (
      SELECT
        person.id, event, count() AS count_in_window,
        CASE WHEN person.id IN (SELECT person_id FROM cohort_persons WHERE cohort_id = 'co_chur4') THEN 'churned'
             WHEN person.id IN (SELECT person_id FROM cohort_persons WHERE cohort_id = 'co_ren4') THEN 'renewed' END AS cohort
      FROM events
      WHERE timestamp BETWEEN now() - INTERVAL 60 DAY AND now() - INTERVAL 30 DAY
      GROUP BY person.id, event
    )
    GROUP BY event
    HAVING renewers_avg > 1 AND churners_avg + renewers_avg > 5
    ORDER BY ratio ASC
  "
}
{
  "rows": [
    { "event": "comment_added", "churners_avg": 0.4, "renewers_avg": 8.2, "ratio": 0.049 },
    { "event": "$pageview", "churners_avg": 6.1, "renewers_avg": 38.7, "ratio": 0.158 },
    { "event": "team_member_invited", "churners_avg": 0.02, "renewers_avg": 0.31, "ratio": 0.065 },
    { "event": "first_query_run", "churners_avg": 2.1, "renewers_avg": 14.8, "ratio": 0.142 }
  ]
}
Agent

Strong signals. The 60-30 days before decision:

SignalChurnersRenewersDrop vs renewers
comment_added0.4/mo8.2/mo95% less
team_member_invited0.02/mo0.31/mo94% less
$pageview6.1/mo38.7/mo84% less
first_query_run2.1/mo14.8/mo86% less

Most predictive single signal: collaboration events (comments + invites) drop to near-zero in the 30 days before churn. Suggests churners disengage from the social/team aspects first.

Building the live at_risk_users cohort.

agentry_create_cohort {
  project_id: "default",
  name: "at_risk_users",
  filter: {
    AND: [
      { has_event: "subscription_started", ever: true },
      { event_count_lt: 5, event: "comment_added", window_days: 30 },
      { event_count_lt: 10, event: "$pageview", window_days: 30 },
      { NOT: { event: "subscription_canceled", ever: true } }
    ]
  }
}
{ "cohort_id": "co_atrisk", "matching_users": 31 }
Agent

31 paying users currently match the at-risk pattern. That’s where to focus retention effort this week. Want me to draft a re-engagement email?

The output

🚨 Churn Prediction Model — Built from April 2026 data

Top predictive signals (60-30 days before churn vs renewal):
  1. comment_added drops 95%        (most predictive single signal)
  2. team_member_invited drops 94%
  3. first_query_run drops 86%
  4. $pageview drops 84%

Common pattern: collaboration events go to zero first, content events follow.

LIVE COHORT: `at_risk_users` (31 paying users right now)
  Criteria: paying + <5 comments in last 30d + <10 pageviews in last 30d

Suggested actions:
  📧 Personal re-engagement email from a human (founder, CSM, AE)
  🎯 In-app modal next time they DO log in, asking what's going wrong
  📞 For top-ARR at-risk accounts, a phone call

Re-engagement email draft (queued):
  Subject: paddy, are we still useful?
  Body: "Hey paddy — noticed your team's usage dropped over the past month.
        That's usually one of two things: you've outgrown us, or something's
        in the way. Either way I'd love to hear from you — what changed?"
        — Henrik

Setting it up

Two prereqs:

1. Capture subscription lifecycle events. Agentry has no SDK — these are raw POSTs to /v1/analytics/. The tiny helper below is the kind of wrapper your agent generates at install time (~25 lines, not an SDK):

// Agentry.ts — generated at install time, edit freely
async function sendAnalytics(event: string, distinctId: string, properties: Record<string, unknown>) {
  await fetch(`https://api.agentry.sh/v1/analytics/${process.env.AGENTRY_PROJECT_ID}/`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.AGENTRY_DSN}`,
      "Content-Type": "application/json",
      "User-Agent": "myapp/1.0",  // REQUIRED — Cloudflare 403s default UAs
    },
    body: JSON.stringify({ event, distinct_id: distinctId, properties }),
  });
}

// On Stripe webhook (or your billing provider equivalent)
await sendAnalytics("subscription_started", user.email, { plan, amount_cents });
await sendAnalytics("subscription_renewed", user.email, { plan, amount_cents });
await sendAnalytics("subscription_canceled", user.email, { plan, reason });
await sendAnalytics("subscription_upgraded", user.email, { from_plan, to_plan });

2. Capture engagement events with distinct_id so the cohort logic can split per-user. You probably already do this — see power-users for the setup.

The model improves as you accumulate more churn data. After 6 months, re-run this recipe to refresh the cohort criteria — patterns change as product changes.

Variations

  • “Same analysis but split by plan tier — do free→paid churners and pro→canceled churners have different patterns?”
  • “Find behaviors that predict EXPANSION (upgrade) instead of churn — that’s the cohort to make even happier.”
  • “Look at the 7-day-before-churn window — what was the last thing they did before canceling?”
  • “Set up a Routine: every Monday at 9am, email me the current at_risk_users list with each user’s top 3 last actions.”
  • “Send an in-app survey only to the at_risk_users cohort asking what’s blocking them.” (see churn-survey)

Try this recipe in your own agent.

Paste the prompt above into your agent. It'll set up Agentry against your data, then run the recipe and show you the real output.

Install https://agentry.sh/agentry.md and set it up