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
Set up a quarterly NPS survey, then in 3 months compare detractor vs promoter churn.
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" }
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)
Now do the NPS-vs-churn analysis.
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 }
]
}
Strong signal:
| Bucket | n | 90-day cancel rate |
|---|---|---|
| detractor | 82 | 34.1% |
| passive | 142 | 12.7% |
| promoter | 188 | 3.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:
- Every NPS ≤ 6 response should trigger a CS workflow (call, save offer).
- 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.”