Use cases / Customer analytics
Reasons for customer churn
Who is about to cancel, and why.
- Unit
- One customer, last 7 days of tickets
- Ask
- Cancel intent, concern, impact. Then: why.
- Get
- 38 at-risk accounts, each with a reason and a receipt
Pylon · Attio · 2 reducers · 5 questions · updated Sep 21, 2026
The flow
Fewer rows at every deterministic step. Deeper questions at every reducer. 412 requests, 1,236 judgments. Asked one at a time: 2,472 requests.
Follow one record
- tickets
- Group by customer
- Customer signals
- Keep high-risk
- Attach history
- Why might they leave?
- Outputs
Two tickets in the last seven days
snapshot v12, records T-1042 and T-1057
- T-1042The sync failed again. Our team cannot submit reports. If this keeps happening we have a problem.
- T-1057Four outages this month. Fix this or we will not renew.
Grouped on customer_id
deterministic, no model call
- key
- Northstar
- members
- T-1042, T-1057
- segment
- Enterprise
- state
- 2 messages, 41 words
Request 1 of 412
model jev-1.13.0, attempt 1, succeeded
- true1.00
probability the proposition holds
- reliability0.98
- other0.02
- pricing0.00
- support0.00
- missing feature0.00
confidence 0.97, provider reported
- Minor inconvenience0.00
- Work is delayed0.01
- Critical work is blocked0.99
expected position on a three level scale
{
"model": "jev-1.13.0",
"state": {
"customer_id": "Northstar",
"segment": "Enterprise",
"tickets": [
{ "id": "T-1042", "opened": "2026-09-15",
"text": "The sync failed again. Our team cannot submit reports. If this keeps happening we have a problem." },
{ "id": "T-1057", "opened": "2026-09-18",
"text": "Four outages this month. Fix this or we will not renew." }
]
},
"questions": {
"cancel": { "type": "noul",
"instructions": "Does the customer in `tickets` express an intention or a threat to cancel or not renew?" },
"concern": { "type": "choice",
"instructions": "What is the primary concern raised in `tickets`?",
"criteria": { "reliability": "Outages, broken features or failures",
"pricing": "Cost, price or unexpected charges",
"support": "Slow or unhelpful support",
"missing_feature": "A capability the product lacks",
"other": "Another concern" } },
"impact": { "type": "score",
"instructions": "How severe is the impact on the customer's work described in `tickets`?",
"criteria": ["Minor inconvenience", "Work is delayed", "Critical work is blocked"] }
}
}
{
"model": "jev-1.13.0",
"answers": {
"cancel": { "probability": 1.00 },
"concern": { "choice": "reliability",
"probabilities": { "reliability": 0.98, "other": 0.02, "pricing": 0.00,
"support": 0.00, "missing_feature": 0.00 },
"confidence": 0.97 },
"impact": { "expected": 1.99, "probabilities": [0.00, 0.01, 0.99], "confidence": 1.00 }
}
}
1.00 is at or above 0.65, kept
deterministic; the probability stays in the row
- field
- answers.cancel.probability
- comparison
- gte 0.65
- value
- 1.00
- result
- matched collection
Four notes joined from Attio
left key input.key, right key customer_id
- 2026-07-14Sync incident reopened, third time this quarter.
- 2026-08-02Escalation call. Asked for service credits.
- 2026-08-29CS check-in. Mentioned they are evaluating alternatives.
- 2026-09-11Renewal date confirmed for November.
Request 1 of 38
model jev-1.13.0, attempt 1, succeeded
- repeated outages0.86
- unresolved escalation0.09
- competitor0.03
- cost0.02
- other0.00
confidence 0.84, provider reported
- true0.97
stated in the customer's own words
{
"model": "jev-1.13.0",
"state": {
"customer_id": "Northstar",
"signals": { "cancel": 1.00, "concern": "reliability", "impact": 1.99 },
"tickets": [
"The sync failed again. Our team cannot submit reports. If this keeps happening we have a problem.",
"Four outages this month. Fix this or we will not renew."
],
"history": [
{ "date": "2026-07-14", "note": "Sync incident reopened, third time this quarter." },
{ "date": "2026-08-02", "note": "Escalation call. Asked for service credits." },
{ "date": "2026-08-29", "note": "CS check-in. Mentioned they are evaluating alternatives." },
{ "date": "2026-09-11", "note": "Renewal date confirmed for November." }
]
},
"questions": {
"reason": { "type": "choice",
"instructions": "Considering `tickets` and `history`, what is the most likely reason this customer would leave?",
"criteria": { "repeated_outages": "Repeated product failures or outages",
"unresolved_escalation": "An escalation that was not resolved",
"cost": "Price or unexpected charges",
"competitor": "Evaluating or moving to an alternative",
"other": "Another reason" } },
"explicit": { "type": "noul",
"instructions": "Is that reason stated explicitly in the customer's own words in `tickets` or `history`, rather than inferred?" }
}
}
{
"model": "jev-1.13.0",
"answers": {
"reason": { "choice": "repeated_outages",
"probabilities": { "repeated_outages": 0.86, "unresolved_escalation": 0.09,
"competitor": 0.03, "cost": 0.02, "other": 0.00 },
"confidence": 0.84 },
"explicit": { "probability": 0.97 }
}
}
One row, six columns, one receipt
every value links back to the node that produced it
- customer
- Northstar
- p(cancel)
- 1.00
- concern
- reliability (0.98)
- impact
- Critical work is blocked (1.99)
- likely reason
- Repeated outages (0.86)
- explicit
- yes (0.97)
- trace
- 7 nodes, 2 attempts, 2 raw requests, 2 raw responses
- tickets
- Group by customer
- Customer signals
- Keep high-risk
- Skipped stages
One ticket in the last seven days
snapshot v12, record T-1063
- T-1063Please add CSV exports. The workaround is fine for now.
Grouped on customer_id
deterministic, no model call
- key
- Meridian
- members
- T-1063
- segment
- Mid-market
Request 118 of 412
model jev-1.13.0, attempt 1, succeeded
- true0.12
probability the proposition holds
- missing feature0.81
- other0.11
- pricing0.05
- reliability0.02
- support0.01
confidence 0.78, provider reported
- Minor inconvenience0.90
- Work is delayed0.10
- Critical work is blocked0.00
expected position on a three level scale
{
"model": "jev-1.13.0",
"answers": {
"cancel": { "probability": 0.12 },
"concern": { "choice": "missing_feature",
"probabilities": { "missing_feature": 0.81, "other": 0.11, "pricing": 0.05,
"reliability": 0.02, "support": 0.01 },
"confidence": 0.78 },
"impact": { "expected": 1.10, "probabilities": [0.90, 0.10, 0.00], "confidence": 0.90 }
}
}
0.12 is below 0.65, excluded
the row moves to the excluded collection with its probability intact
- field
- answers.cancel.probability
- comparison
- gte 0.65
- value
- 0.12
- result
- excluded collection, still on the map
Skipped, and recorded as skipped
Attach history and Why might they leave? never ran for Meridian
No request was sent for Meridian in the second reducer, and no history was read. The trace map still shows the customer in every later column, dashed, so the expected units always add up: 412 in, 38 evaluated twice, 374 excluded, 0 unaccounted for. If you move the threshold to 0.10 next month, Meridian is one replay away, and this run does not change.
What comes out
| Customer | p(cancel) | Concern | Impact | Likely reason | Explicit |
|---|---|---|---|---|---|
| Northstar | 1.00 | reliability | Critical work is blocked | Repeated outages | yes 0.97 |
| Atlas | 0.91 | support | Critical work is blocked | Unresolved escalation | yes 0.93 |
| Halyard Freight | 0.78 | pricing | Work is delayed | Cost | yes 0.88 |
| Corvid Systems | 0.72 | reliability | Work is delayed | Repeated outages | no 0.41 |
| Larkspur Health | 0.66 | missing feature | Work is delayed | Competitor | yes 0.79 |
Synthetic rows in the app's Outputs layout. Northstar's first-stage numbers are the ones the app recorded on the example data; the rest are illustrative. Export is CSV or JSON with the trace IDs attached.
Details, if you want them
whyWhy this shape
Why this shape
- Unit: the customer, not the ticket. Two angry tickets are one relationship.
- One request, three questions. 1,284 tickets, 412 requests. One question at a time: 1,236.
- Filter before you read more. History is read for 38 customers, not 412.
- Distributions, not labels. Close calls look like close calls.
- The receipt. Why is Northstar on the list? Two clicks, not a re-run.
When the questions change
- Edit a question. A new version is saved.
- Old runs keep the old version.
- Replay to apply the new wording to old data. New run, parent kept.
questionsThe 5 questions, verbatim
- Customer signals
cancelbinary, noul
Does the customer in `tickets` express an intention or a threat to cancel or not renew?
- Customer signals
concerncategorical, choice
What is the primary concern raised in `tickets`?
reliabilitypricingsupportmissing featureother - Customer signals
impactordinal, score
How severe is the impact on the customer’s work described in `tickets`?
Minor inconvenienceWork is delayedCritical work is blocked - Why might they leave?
reasoncategorical, choice
Considering `tickets` and `history`, what is the most likely reason this customer would leave?
repeated outagesunresolved escalationcostcompetitorother - Why might they leave?
explicitbinary, noul
Is that reason stated explicitly in the customer’s own words in `tickets` or `history`, rather than inferred?
buildHow each stage is built, as JSON
1 group Group by customer
customer_id; each group is one unit for the next reducer. Choosing the unit is the design decision here: per ticket would score messages, per customer scores the relationship. Grouping keeps the members, so the trace can show every ticket behind a score.stage definition
{
"id": "customers",
"input": "source:tickets",
"key": [
"customer_id"
],
"kind": "group",
"name": "Group by customer"
}2 reducer Customer signals
stage definition
{
"id": "signals",
"input": "customers",
"kind": "evaluate",
"model": "jev",
"name": "Customer signals",
"questions": {
"cancel": {
"instructions": "Does the customer in `tickets` express an intention or a threat to cancel or not renew?",
"type": "noul"
},
"concern": {
"criteria": {
"missing_feature": "A capability the product lacks",
"other": "Another concern",
"pricing": "Cost, price or unexpected charges",
"reliability": "Outages, broken features or failures",
"support": "Slow or unhelpful support"
},
"instructions": "What is the primary concern raised in `tickets`?",
"type": "choice"
},
"impact": {
"criteria": [
"Minor inconvenience",
"Work is delayed",
"Critical work is blocked"
],
"instructions": "How severe is the impact on the customer's work described in `tickets`?",
"type": "score"
}
},
"state_path": []
}3 filter Keep high-risk customers
stage definition
{
"comparison": "gte",
"field": [
"answers",
"cancel",
"probability"
],
"id": "at_risk",
"input": "signals",
"kind": "filter",
"name": "Keep high-risk customers",
"value": 0.65
}4 join Attach customer history
stage definition
{
"id": "context",
"input": "at_risk",
"kind": "join",
"left_key": [
"input",
"key"
],
"name": "Attach customer history",
"right": "source:history",
"right_key": [
"customer_id"
]
}5 reducer Why might they leave?
explicit question separates what the customer said from what the model inferred.stage definition
{
"id": "reasons",
"input": "context",
"kind": "evaluate",
"model": "jev",
"name": "Why might they leave?",
"questions": {
"explicit": {
"instructions": "Is that reason stated explicitly in the customer's own words in `tickets` or `history`, rather than inferred?",
"type": "noul"
},
"reason": {
"criteria": {
"competitor": "Evaluating or moving to an alternative",
"cost": "Price or unexpected charges",
"other": "Another reason",
"repeated_outages": "Repeated product failures or outages",
"unresolved_escalation": "An escalation that was not resolved"
},
"instructions": "Considering `tickets` and `history`, what is the most likely reason this customer would leave?",
"type": "choice"
}
},
"state_path": []
}6 definition The whole workflow, as saved
Saving publishes an immutable version. A run freezes this definition, the snapshot IDs and each reducer’s model profile, so the trace can always show exactly what ran. Edits create a new version; old runs keep theirs.
jevreduce.local.v1
{
"name": "Reasons for customer churn",
"schema_version": "jevreduce.local.v1",
"sources": {
"history": "snap_attio_notes_v3",
"tickets": "snap_pylon_tickets_v12"
},
"stages": [
{
"id": "customers",
"input": "source:tickets",
"key": [
"customer_id"
],
"kind": "group",
"name": "Group by customer"
},
{
"id": "signals",
"input": "customers",
"kind": "evaluate",
"model": "jev",
"name": "Customer signals",
"questions": {
"cancel": {
"instructions": "Does the customer in `tickets` express an intention or a threat to cancel or not renew?",
"type": "noul"
},
"concern": {
"criteria": {
"missing_feature": "A capability the product lacks",
"other": "Another concern",
"pricing": "Cost, price or unexpected charges",
"reliability": "Outages, broken features or failures",
"support": "Slow or unhelpful support"
},
"instructions": "What is the primary concern raised in `tickets`?",
"type": "choice"
},
"impact": {
"criteria": [
"Minor inconvenience",
"Work is delayed",
"Critical work is blocked"
],
"instructions": "How severe is the impact on the customer's work described in `tickets`?",
"type": "score"
}
},
"state_path": []
},
{
"comparison": "gte",
"field": [
"answers",
"cancel",
"probability"
],
"id": "at_risk",
"input": "signals",
"kind": "filter",
"name": "Keep high-risk customers",
"value": 0.65
},
{
"id": "context",
"input": "at_risk",
"kind": "join",
"left_key": [
"input",
"key"
],
"name": "Attach customer history",
"right": "source:history",
"right_key": [
"customer_id"
]
},
{
"id": "reasons",
"input": "context",
"kind": "evaluate",
"model": "jev",
"name": "Why might they leave?",
"questions": {
"explicit": {
"instructions": "Is that reason stated explicitly in the customer's own words in `tickets` or `history`, rather than inferred?",
"type": "noul"
},
"reason": {
"criteria": {
"competitor": "Evaluating or moving to an alternative",
"cost": "Price or unexpected charges",
"other": "Another reason",
"repeated_outages": "Repeated product failures or outages",
"unresolved_escalation": "An escalation that was not resolved"
},
"instructions": "Considering `tickets` and `history`, what is the most likely reason this customer would leave?",
"type": "choice"
}
},
"state_path": []
}
]
}limitsWhat it will not do
- Text only. Transcribe calls and extract documents first.
- Silent customers are not scored. Report how many had no tickets.
- 0.65 is your policy. Check recall on a labelled sample.
- No summary email. Jev does not write prose.
- No deduplication. Two identical tickets stay two.