csat-risk-detection
Published
Public
in @customer-support

Flag a support conversation that's at risk of a poor CSAT, escalation, or churn, with the signals.

csat-risk-detection

Flag a support conversation that's at risk of a poor satisfaction outcome — a low CSAT score, an escalation, or churn — before the customer fills out the survey. Returns a cheap boolean gate first, then a severity level, the concrete signals, and a rationale, as strict JSON.

Run it across live or recently-closed conversations to surface the threads worth a supervisor's attention: explicit frustration, churn threats, repeated unresolved contacts, long delays.

Entrypoints

EntrypointRendersInputs
systemRisk-detection instructions + level rubric + embedded output schema
userThe delimited conversation block to assesstext

Variables

  • text (string, required) — the conversation to assess. Turns labeled by speaker (Customer: / Agent:) work best.

Output

parseOutput() returns:

{
  "at_risk": true,
  "risk_level": "high",
  "risk_signals": ["explicit churn threat", "third contact about the same issue", "asks for a manager"],
  "reasoning": "Customer states they will cancel if the issue is not resolved and notes this is their third time reaching out.",
  "confidence": 0.9
}
  • at_risk comes first by design — a cheap boolean gate you can route on (e.g. fan out to supervisors) before inspecting the details. It's true for low/medium/high and false only for none; the prompt keeps the two consistent.
  • risk_level is none / low / medium / high, judged on the overall trajectory, not a single emphatic word — and it won't under-call a calm but explicit churn threat.
  • risk_signals are short, concrete, grounded signals — never invented. Empty array when not at risk.
  • reasoning explains the call after the gate.
  • confidence is self-reported and ⚠️ not calibrated — a triage signal, not a probability.

House conventions

  • Decision-first — the at_risk boolean leads as a cheap gate (the pii-detection contains_pii pattern), reasoning follows.
  • Triple-mustache input ({{{text}}}) so special characters in the conversation are never HTML-escaped.
  • Grounded only in the conversation — every signal must actually appear; no invented signals.
  • Empty / non-conversation inputat_risk: false, risk_level: none, empty signals, confidence ≤ 0.3.
  • Strict schemaadditionalProperties: false, closed enum, complete required.

Notes

Verified by local render and a live battle-test (clearly-angry thread → high risk; clearly-happy thread → none; injection resistance). Suggested default model claude-sonnet-4-6 at temperature: 0.

@sufleur/