Find quiet paying accounts before they churn

List paying accounts with zero events in 14 days. Sort by ARR. Get personalized outreach drafted in one prompt.

Published

difficulty intermediate · time to value 2 minutes · tools used 3

Just say this

List paying accounts with zero events in the last 14 days that haven't canceled. Sort by ARR. Draft me an outreach email per top-5.

Why this matters

Paying customers who stop logging in are 60-90 days from cancellation. The CS-team folklore number is “if a paid account goes 14 days without activity, they’re 4-5x more likely to churn at renewal.” Saving them requires noticing within those 14 days.

Almost no SaaS company does. The list-of-quiet-accounts query is doable in BigQuery if you have a data team, exists buried in Mixpanel under “RFM analysis,” or requires you to install ChartMogul + connect Stripe + pay $200/mo. So nobody does it weekly.

Agentry makes the question one prompt. The agent runs the query, ranks by ARR, looks at the last activity each account had, and drafts a per-account email — no template-y “we miss you” — referencing what they were actually doing.

What you get

  • List of paid accounts with 0 events in last 14 days (or your threshold), excluding canceled
  • Sorted by ARR so CS time goes to highest-impact accounts first
  • A draft email for the top 5, personalized to what they were last doing before going quiet

Walk through it

You

List paying accounts with zero events in the last 14 days that haven’t canceled. Sort by ARR. Draft me an outreach email per top-5.

Agent

Let me query dormant paying accounts first.

agentry_analytics_query {
  project_id: "default",
  hogql: "WITH paid AS (SELECT properties.account_id AS aid, argMax(properties.plan, timestamp) AS plan, argMax(properties.mrr, timestamp) AS mrr FROM events WHERE event IN ('plan_changed','seat_count_set','subscription_started') GROUP BY aid HAVING mrr > 0), canceled AS (SELECT DISTINCT properties.account_id AS aid FROM events WHERE event = 'subscription_canceled'), activity AS (SELECT properties.account_id AS aid, max(timestamp) AS last_event_ts FROM events WHERE event != '$exception' GROUP BY aid) SELECT p.aid AS account, p.plan AS plan, p.mrr AS mrr, a.last_event_ts AS last_event_ts, dateDiff('day', a.last_event_ts, now()) AS days_quiet FROM paid p LEFT JOIN activity a ON p.aid = a.aid WHERE p.aid NOT IN (SELECT aid FROM canceled) AND (a.last_event_ts < now() - INTERVAL 14 DAY OR a.last_event_ts IS NULL) ORDER BY p.mrr DESC LIMIT 20"
}
{
  "rows": [
    { "account": "megacorp",  "plan": "enterprise", "mrr": 5000, "days_quiet": 18 },
    { "account": "globex",    "plan": "scale",      "mrr": 1875, "days_quiet": 22 },
    { "account": "scaleco",   "plan": "scale",      "mrr": 1875, "days_quiet": 15 },
    { "account": "midmarket", "plan": "pro",        "mrr": 750,  "days_quiet": 31 },
    { "account": "smallpaid", "plan": "starter",    "mrr": 250,  "days_quiet": 16 }
  ]
}
Agent

Top dormant account is megacorp — $5,000/mo, quiet for 18 days. Let me look at what they were last doing.

agentry_get_distinct_id_summary {
  project_id: "default",
  account_id: "megacorp",
  windows: [
    { name: "last_30d_active", from_relative: "44d ago", to_relative: "14d ago" }
  ]
}
{
  "windows": {
    "last_30d_active": {
      "total_events": 4200,
      "by_event": {
        "report_generated":  88,
        "api_call":         3200,
        "dashboard_view":    640,
        "team_member_invited": 4
      },
      "top_users": [
        { "distinct_id": "[email protected]", "events": 2840 },
        { "distinct_id": "[email protected]", "events": 1100 }
      ]
    }
  }
}
Agent

Megacorp’s primary user (Sarah) was averaging 200+ events/day on API + reports. Then silence for 18 days. Possibilities: she’s on PTO, the project ended, or she left the company. Draft email:


To: [email protected] Subject: Quick check-in

Hi Sarah,

Noticed your team’s been pretty quiet on Agentry the past couple weeks — usually you and the engineering team are running 200+ API calls a day, and that’s dropped to zero. Wanted to make sure nothing’s blocking you (or that you haven’t been away on a project).

If there’s something we can help unblock — or if you’ve moved to a different tool and want to talk through it — happy to jump on a call this week.

Best, [your CSM]


Want me to do the same for the other four?

The output

Dormant Paying Accounts (>14 days quiet, not canceled, top by ARR)

ACCOUNT     PLAN         MRR     LAST ACTIVITY    PRIMARY USER
megacorp    enterprise   $5,000  18 days ago      [email protected]
globex      scale        $1,875  22 days ago      [email protected]
scaleco     scale        $1,875  15 days ago      multiple
midmarket   pro          $750    31 days ago      one user — gone?
smallpaid   starter      $250    16 days ago      [email protected]

Total ARR dormant: $9,750/mo ($117k/yr at risk)

DRAFT EMAILS:
  → megacorp (sarah@): "noticed your team's been quiet — API was 200+/day"
  → globex (ops@):    "your monthly report cycle hasn't run yet — anything we can help with?"
  → scaleco:           "team dispersed — would a quick 1:1 with each user help?"
  → midmarket:         "no activity for 31d — should we pause your sub temporarily?"
  → smallpaid:         "low-touch nudge — share new feature X"

Setting it up

Same instrumentation as account-health-score: every event needs account_id and you need an MRR-or-plan signal per account. Then the SQL just composes a paid ∩ not_canceled ∩ not_active set.

// Per-user event with account_id
await fetch(`https://api.agentry.sh/v1/analytics/${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: "report_generated",
    distinct_id: user.email,
    properties: { account_id: user.workspace_id, report_type: "monthly" },
  }),
});

// On Stripe cancellation
await fetch(`https://api.agentry.sh/v1/analytics/${PROJECT_ID}/`, {
  method: "POST",
  headers: { /* same */ "User-Agent": "myapp-stripe/1.0" },
  body: JSON.stringify({
    event: "subscription_canceled",
    distinct_id: subscription.metadata.account_id,
    properties: {
      account_id: subscription.metadata.account_id,
      reason: subscription.cancellation_details?.reason,
    },
  }),
});

Why exclude canceled accounts? They’re a different workstream (winback campaigns, not save campaigns) and would dilute the priority list.

Variations

  • “Run the same query for trial accounts — who’s mid-trial and gone silent? Probably abandoning.”
  • “Find accounts that were active last quarter but completely silent this quarter. Slower-burn churn.”
  • “For each dormant account, also pull any support tickets from the last 30 days — context for the outreach.”
  • “Weekly cron Monday 9am: post the previous week’s new-dormant accounts to #cs-pulse.”

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