Does NPS predict cancellation?

Compare 90-day cancellation rates between NPS detractors and promoters. Get the real number for your business.

Published

difficulty advanced · time to value 1 day to set up, 3 months for signal · tools used 4

Just say this

Run a survey collecting NPS quarterly. After 3 months, compare 90-day cancellation rate of detractors (NPS <= 6) vs promoters.

Why this matters

NPS gets dismissed as vanity. The exec deck shows “NPS: 47” and nobody knows whether to celebrate or panic, because nobody has the comparison: “do our detractors actually churn?”

The answer matters because it dictates how you use NPS. If detractors are 4x more likely to cancel within 90 days, NPS is a leading indicator and you should weight your CS workflow around it. If detractors and promoters churn at the same rate, NPS is noise and you should stop asking.

Almost no SaaS company runs this analysis. The survey lives in Wootric/Delighted; the churn data lives in Stripe; nobody joins them. The agent does it in one prompt — once the data has been collecting for 90 days.

What you get

  • A cohort comparison: cancellation rate of NPS detractors vs passives vs promoters
  • Statistical significance check (or a sample-size warning)
  • A recommendation: keep running NPS or kill it

Walk through it

You

Set up a quarterly NPS survey, then in 3 months compare detractor vs promoter churn.

Agent

Two-phase plan. Phase 1 today: create the survey. Phase 2 in 90 days: run the analysis.

agentry_create_survey {
  project_id: "default",
  name: "nps_quarterly_2026q2",
  questions: [
    {
      type: "rating",
      question: "On a scale of 0-10, how likely are you to recommend us to a colleague?",
      scale: 11,
      min_label: "Not at all likely",
      max_label: "Extremely likely",
      key: "nps_score"
    },
    {
      type: "long_text",
      question: "What's the main reason for your score?",
      key: "nps_reason",
      optional: true
    }
  ],
  conditions: {
    show_to_users_with_event: "signup_completed",
    where_event_was_at_least_days_ago: 30,
    show_once_per_user_quarterly: true
  }
}
{ "survey_id": "srv_nps_q2", "status": "active" }
Agent

Survey live. It’ll appear to users who signed up >30 days ago, once per quarter. Expect ~15-30% response rate over 4 weeks. Come back after May 31 for the analysis.


(90 days later)

You

Now do the NPS-vs-churn analysis.

Agent

Let me pull responses and build the three cohorts.

agentry_survey_responses {
  project_id: "default",
  survey_id: "srv_nps_q2",
  include_distinct_id: true
}
{
  "responses": [
    { "distinct_id": "[email protected]",   "nps_score": 9, "responded_at": 1746000000 },
    { "distinct_id": "[email protected]",    "nps_score": 5, "responded_at": 1746100000 },
    { "distinct_id": "[email protected]","nps_score": 3, "responded_at": 1746200000 }
  ],
  "summary": { "total_responses": 412, "by_score_bucket": { "promoters": 188, "passives": 142, "detractors": 82 } }
}
agentry_analytics_query {
  project_id: "default",
  hogql: "WITH responses AS (SELECT distinct_id, nps_score, responded_at, CASE WHEN nps_score >= 9 THEN 'promoter' WHEN nps_score >= 7 THEN 'passive' ELSE 'detractor' END AS bucket FROM surveys_view WHERE survey_id = 'srv_nps_q2'), canceled AS (SELECT DISTINCT distinct_id, min(timestamp) AS cancel_ts FROM events WHERE event = 'subscription_canceled' GROUP BY distinct_id) SELECT r.bucket AS bucket, count(DISTINCT r.distinct_id) AS n, countIf(DISTINCT r.distinct_id, c.cancel_ts > r.responded_at AND c.cancel_ts < r.responded_at + INTERVAL 90 DAY) AS canceled_90d, round(countIf(DISTINCT r.distinct_id, c.cancel_ts > r.responded_at AND c.cancel_ts < r.responded_at + INTERVAL 90 DAY) * 100.0 / count(DISTINCT r.distinct_id), 1) AS cancel_rate_pct FROM responses r LEFT JOIN canceled c ON r.distinct_id = c.distinct_id GROUP BY bucket ORDER BY bucket"
}
{
  "rows": [
    { "bucket": "detractor", "n": 82,  "canceled_90d": 28, "cancel_rate_pct": 34.1 },
    { "bucket": "passive",   "n": 142, "canceled_90d": 18, "cancel_rate_pct": 12.7 },
    { "bucket": "promoter",  "n": 188, "canceled_90d": 6,  "cancel_rate_pct": 3.2 }
  ]
}
Agent

Strong signal:

Bucketn90-day cancel rate
detractor8234.1%
passive14212.7%
promoter1883.2%

Detractors are 10.7x more likely to cancel than promoters within 90 days. NPS is a legitimate leading indicator for your business. Two operational implications:

  1. Every NPS ≤ 6 response should trigger a CS workflow (call, save offer).
  2. The quarterly NPS deck deserves a “detractor risk” column — number times 34% is the projected 90-day churn.

The output

NPS → 90d Cancellation (n = 412 responses)

BUCKET       N    CANCEL%   LIFT vs PROMOTER
detractor   82    34.1%    10.7x
passive    142    12.7%     4.0x
promoter   188     3.2%     1.0x (baseline)

INTERPRETATION:
  Strong predictive signal. Detractors are 10x more likely to cancel
  than promoters within 90 days. NPS is NOT vanity for this business.

OPERATIONAL ACTIONS:
  1. Wire NPS detractor → CS Save workflow (immediate call within 24h)
  2. Track detractor count weekly as a leading churn indicator
  3. Quarterly: project (detractor_count × 34%) as expected lost-revenue

CAVEATS:
  - n=82 detractors is modest. Re-run after next quarter for tighter bands.
  - "Self-selected respondents" bias — non-respondents may behave differently.
  - Cancellation != revenue loss yet; some are free-tier or seat-reduction.

Setting it up

Agentry has a native survey primitive (see agentry_create_survey in the conversation above). It renders an in-product widget to users matching your conditions, stores responses, and exposes them via agentry_survey_responses.

If you’d rather use PostHog surveys (already running): they get stored as analytics events; the join logic is identical, just swap surveys_view for the right event filter:

// PostHog stores survey responses as events with event name `survey sent`
// and properties.$survey_id / properties.$survey_response. Adjust the
// HogQL CTE accordingly:

WITH responses AS (
  SELECT distinct_id,
         toInt(properties.$survey_response) AS nps_score,
         timestamp AS responded_at,
         CASE WHEN nps_score >= 9 THEN 'promoter'
              WHEN nps_score >= 7 THEN 'passive'
              ELSE 'detractor' END AS bucket
  FROM events
  WHERE event = 'survey sent'
    AND properties.$survey_id = '<your-survey-id>'
)

For the cancellation side, you need the Stripe webhook handler that fires subscription_canceled:

// `customer.subscription.deleted` from Stripe
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-stripe/1.0",  // REQUIRED — Cloudflare 403s default UAs
  },
  body: JSON.stringify({
    event: "subscription_canceled",
    distinct_id: stripeCustomerEmail,
    properties: { reason: subscription.cancellation_details?.reason },
  }),
});

Important: distinct_id on the survey response and on the cancellation event must match. Use the user’s email everywhere.

Variations

  • “Do the same analysis but split by plan tier — is NPS a stronger predictor for enterprise customers?”
  • “Look at NPS reason text for detractors — what topics come up most? Build a categorization.”
  • “What % of promoters became detractors within 12 months? Identify the moment the relationship turned.”
  • “Quarterly cron: re-run this analysis on the latest quarter’s responses and post the lift number to leadership Slack.”

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