Reasons for customer churn

Find expressed cancellation intent and inspect the reasons behind it.

Illustrative workflow · Sample data · Coming soon

Unit
One customer, last 7 days of tickets
Ask
Cancel intent, concern, impact. Then: why.
Example result
38 at-risk accounts, each with a reason and a receipt

Pylon · Attio · 2 model stages · 5 questions · updated Sep 21, 2026

The flow

1,284 tickets source snapshot v12
412 Group by customer group no model call
412 Customer signals reducer requests, 3 questions each
38 Keep high-risk filter p(cancel) ≥ 0.65
38 Attach history join notes from Attio
38 Why might they leave? reducer requests, 2 questions each
38 Outputs output rows, each with a receipt

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

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

cancelbinary
100.0%
  • true1.00

probability the proposition holds

concerncategorical
reliability
  • reliability0.98
  • other0.02
  • pricing0.00
  • support0.00
  • missing feature0.00

confidence 0.97, provider reported

impactordinal
1.99
  • Minor inconvenience0.00
  • Work is delayed0.01
  • Critical work is blocked0.99

expected position on a three level scale

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

reasoncategorical
repeated outages
  • repeated outages0.86
  • unresolved escalation0.09
  • competitor0.03
  • cost0.02
  • other0.00

confidence 0.84, provider reported

explicitbinary
97.0%
  • true0.97

stated in the customer's own words

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
1 of 7

What comes out

Customer risk reviewrun 7f3a2c19, 38 rows, sorted by p(cancel)
Customerp(cancel)ConcernImpactLikely reasonExplicit
Northstar1.00reliabilityCritical work is blockedRepeated outagesyes 0.97
Atlas0.91supportCritical work is blockedUnresolved escalationyes 0.93
Halyard Freight0.78pricingWork is delayedCostyes 0.88
Corvid Systems0.72reliabilityWork is delayedRepeated outagesno 0.41
Larkspur Health0.66missing featureWork is delayedCompetitoryes 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

  1. Edit a question. A new version is saved.
  2. Old runs keep the old version.
  3. 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
A deterministic operation. Tickets are grouped on 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
One request per customer. All three questions travel together because each can be answered from the same state, and the answers do not leak into each other. The state is the group’s tickets; the model sees nothing else. The executor freezes the model profile (endpoint, wire model, limits) into the run.

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
A comparison on a stored field. The threshold is a policy you own, written down where anyone can read it. The filter emits a matched collection and an excluded collection; the excluded customers keep their probabilities and appear on the trace map, dashed, in every later column.

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
A join on the customer key against the Attio snapshot. Only the 38 kept customers get history attached, so the expensive context is read only where it will be used. Missing history is an explicit null, not a silent drop.

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?
The second reducer exists for one reason: its questions need data the first request did not have. The state now carries the first stage’s answers, the tickets and the joined notes. Two questions, one request per kept customer. The 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.
relatedRelated pages

Bring your next question.

Join the waitlist to explore your own data with Deduced.