Template - Customer health
ScoreChurn risk
How likely a customer is to leave, based on what they wrote.
A score decision that reads a customer message for signs of churn, so customer success can respond before a cancellation arrives.
The decision
How likely is this customer to churn?
Levels, lowest to highest
- 0
Low: Satisfied or neutral. No sign of leaving. - 1
Elevated: Frustrated or disengaged, but no stated intent to leave. - 2
High: States or strongly implies an intent to cancel, not renew, or switch.
Evidence checks (asked in the same request)
- Cancellation intent: The customer says they intend to cancel or not renew.
- Competitor mentioned: The customer mentions a competitor or evaluating alternatives.
- Negative sentiment: The customer expresses frustration or dissatisfaction.
Expected output: A score from 0 to 2, a probability for each level, and a confidence value, plus one probability per evidence check.
Input
The Decisions API reads one input. Combine these fields into it as labeled text so the model sees the facts the decision depends on.
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The customer's message, verbatim. Include the subject line if there is one. |
renewal_date | string | No | Upcoming renewal date, so risk can be read in context. |
Worked examples
Evaluating alternatives before renewal
We're evaluating alternatives because the reporting module is still broken after three tickets. Honestly we're frustrated, and unless this improves we won't renew in March.
A careful reviewer would say: High. An explicit non-renewal warning, an unresolved problem, and a competitor evaluation.
Run this exampleExpanding and asking for more
Loving the new dashboards. We just added 20 more seats for the EMEA team. Is there a way to schedule reports weekly?
A careful reviewer would say: Low. Expansion and positive sentiment. The question is about using the product more.
Run this example
Expected answers are human judgments written for this template, not model output.
How it works
This template reads one message. It is a signal to add to your health model, not a replacement for it: usage data and renewal history say more about churn than any single email.
What it adds is speed. A customer who writes "we're evaluating alternatives" in a support ticket should reach customer success the same day, and this decision can make that happen without anyone reading every ticket.
Human review and thresholds
Recommended review policy
The action here is a notification to a person, so automation risk is low. Send High results to the account owner immediately, and batch Elevated results into a daily digest.
Choosing thresholds
The cost of a false alarm is a customer success manager's attention. If your team is small, alert only on a high probability for the top level. If each account is worth a lot, a lower bar pays for itself.
- Starting automation threshold
- 80%
- Starting don’t-decide threshold
- 50%
Starting points for the playground, not recommendations. The right values depend on your costs and on how the model’s confidence tracks accuracy on your data. Calibrate them.
Limitations
- Many customers who churn never say so. A low score on a message is not evidence of a healthy account.
- Frustration is common in support and usually does not mean churn. Expect the middle level to be noisy.
- This is text classification of intent signals, not a churn forecast. Do not report its scores as probabilities of churn.
Use it in your code
The template definition is plain JSON: the question, the evidence checks, the input fields, and the starting policy. The SDK call below sends the same question and checks to the Decisions API from your server.
{
"$format": "thethinkloop.template/v1",
"slug": "churn-risk",
"title": "Churn risk",
"decides": "How likely a customer is to leave, based on what they wrote.",
"question": {
"name": "decision",
"type": "score",
"instructions": "How likely is this customer to churn?",
"levels": [
{
"label": "Low",
"description": "Satisfied or neutral. No sign of leaving."
},
{
"label": "Elevated",
"description": "Frustrated or disengaged, but no stated intent to leave."
},
{
"label": "High",
"description": "States or strongly implies an intent to cancel, not renew, or switch."
}
]
},
"evidenceChecks": [
{
"name": "factor_1",
"label": "Cancellation intent",
"type": "predicate",
"instructions": "The customer says they intend to cancel or not renew."
},
{
"name": "factor_2",
"label": "Competitor mentioned",
"type": "predicate",
"instructions": "The customer mentions a competitor or evaluating alternatives."
},
{
"name": "factor_3",
"label": "Negative sentiment",
"type": "predicate",
"instructions": "The customer expresses frustration or dissatisfaction."
}
],
"inputs": [
{
"name": "message",
"type": "string",
"required": true,
"description": "The customer's message, verbatim. Include the subject line if there is one."
},
{
"name": "renewal_date",
"type": "string",
"required": false,
"description": "Upcoming renewal date, so risk can be read in context."
}
],
"expectedOutput": "A score from 0 to 2, a probability for each level, and a confidence value, plus one probability per evidence check.",
"examples": [
{
"title": "Evaluating alternatives before renewal",
"input": "We're evaluating alternatives because the reporting module is still broken after three tickets. Honestly we're frustrated, and unless this improves we won't renew in March.",
"expected": "High"
},
{
"title": "Expanding and asking for more",
"input": "Loving the new dashboards. We just added 20 more seats for the EMEA team. Is there a way to schedule reports weekly?",
"expected": "Low"
}
],
"startingPolicy": {
"automateAt": 0.8,
"abstainBelow": 0.5,
"note": "A starting point for exploration, not a recommendation. Calibrate against your own labeled examples."
}
}import OpenAI from "openai"; // openai >= 7.30.0
const client = new OpenAI(); // reads OPENAI_API_KEY on the server
const decision = await client.decisions.create({
model: "gpt-6-luna",
input: "We're evaluating alternatives because the reporting module is still broken after three tickets. Honestly we're frustrated, and unless this improves we won't renew in March.",
questions: [
{
type: "score",
name: "decision",
instructions: "How likely is this customer to churn?",
levels: [
{
label: "Low",
description: "Satisfied or neutral. No sign of leaving."
},
{
label: "Elevated",
description: "Frustrated or disengaged, but no stated intent to leave."
},
{
label: "High",
description: "States or strongly implies an intent to cancel, not renew, or switch."
}
]
},
{
type: "predicate",
name: "factor_1",
instructions: "The customer says they intend to cancel or not renew."
},
{
type: "predicate",
name: "factor_2",
instructions: "The customer mentions a competitor or evaluating alternatives."
},
{
type: "predicate",
name: "factor_3",
instructions: "The customer expresses frustration or dissatisfaction."
}
],
});
// Answers come back in question order. A question can also be refused.
const [main, ...checks] = decision.answers;
if (main.type === "refusal") throw new Error("The model declined to answer.");
if (main.type === "score") {
console.log(main.score, main.confidence, main.probabilities);
}
Download it as churn-risk.json, or browse all developer resources.
Need this decision tuned to your policy?
Related
- Score template
Account health
How healthy a customer account is, from written account notes.
- Predicate template
Support escalation
Whether a conversation should move from a frontline agent to a senior one.
- Choice template
Customer message routing
Which team should own a customer message: sales, customer success, support, or billing.
- Guide
Predicate vs choice vs score
Most decisions can be phrased three ways, and each returns different information. Which type fits which decision, with worked examples and common mistakes.
- Guide
Confidence thresholds
Probability, confidence, accuracy, calibration, and correctness are different things. How to set a review policy from the cost of each kind of mistake.
- Guide
Calibration metrics
Three ways to measure calibration, what each one hides, and which to use for which question, with code that computes them from labeled decisions.