Step 1 — Pull usage from Postgres (read-only)
Query per-account activity trend over a rolling window (active seats,
key-feature events, login frequency over the last 30 days vs. the prior 30).
Classify:
| Usage signal |
Criteria |
| Stable/growing |
Flat or increasing vs. prior window |
| Declining |
≥20% drop in core activity vs. prior window |
| Dormant |
Near-zero activity for 14+ days on a previously active account |
Step 2 — Pull support friction from Plain (read-only)
Pull thread volume and tone per account over the same window.
| Support signal |
Criteria |
| Normal |
Thread volume in line with the account's history |
| Rising friction |
Thread count up materially vs. baseline, or repeated unresolved threads |
| Escalated |
An open thread flagged urgent/escalated, or explicit cancellation language |
Step 3 — Pull billing state from Stripe (read-only)
Pull payment status and renewal date per account.
| Billing signal |
Criteria |
| Healthy |
Payments current, renewal more than 45 days out |
| At risk |
One missed or failed payment in the last billing cycle |
| Renewal-imminent |
Renewal within 30 days |
Step 4 — Combine into one score per account
No single signal alone is high risk. Weigh combinations:
| Combination |
Risk |
| Declining/dormant usage alone |
Low–medium — watch |
| Declining usage + rising/escalated support |
Medium–high |
| Any usage signal + a failed payment |
High |
| Declining/dormant usage + renewal-imminent |
High |
| Rising/escalated support + renewal-imminent |
High |
| Declining usage + rising support + (renewal-imminent or failed payment) |
Highest — surface first |
| Stable usage, normal support, healthy billing |
Not at risk — omit from the list |
Step 5 — Write the reason and the next step
For every account that clears the "watch" bar, write:
- Reason — the specific signals that fired, e.g. "usage down 34% in 30d,
2 escalated threads, renewal in 12 days."
- Next step — one concrete, human-actionable suggestion tied to the
dominant signal:
- Usage decline → a check-in call or re-onboarding nudge.
- Support friction → proactive outreach from the account's CSM.
- Payment risk → a billing follow-up (the agent does not send this itself).
- Renewal-imminent combined with any other signal → prioritize a renewal
conversation now, not at the renewal date.
Step 6 — Rank and post
Sort highest risk first. Post one message to {{alert_channel}}: for each
account, its rank, risk level, the reason, and the next step. Post exactly
once per run — this is a fresh session, so there is no prior list to update or
diff against; the whole list is recomputed and reposted every day.
1---2name: churn-signals3description: Scoring rules for daily churn-risk detection — usage decline, support friction, billing/payment risk, and renewal proximity — plus how to combine them into a ranked at-risk list with a reason and a suggested next step per account.4---56<skill name="churn-signals">78<overview>9Turn four partial views — product usage, support friction, payment health, and10renewal timing — into one ranked at-risk list. A daily cron spawns a fresh11session with read-only access to Postgres, Plain, and Stripe; this skill turns12those raw signals into a score, a reason, and a next step per account. No13signal is scored in isolation: an account with declining usage but nothing14else is very different from one with declining usage, rising support load,15and a renewal next month.16</overview>1718<when-to-load>19- The daily cron fires the churn-risk scan.20- A human asks for the current at-risk list, or why a specific account scored21 the way it did.22</when-to-load>2324<workflow>2526## Step 1 — Pull usage from Postgres (read-only)2728Query per-account activity trend over a rolling window (active seats,29key-feature events, login frequency over the last 30 days vs. the prior 30).30Classify:3132| Usage signal | Criteria |33|---|---|34| Stable/growing | Flat or increasing vs. prior window |35| Declining | ≥20% drop in core activity vs. prior window |36| Dormant | Near-zero activity for 14+ days on a previously active account |3738## Step 2 — Pull support friction from Plain (read-only)3940Pull thread volume and tone per account over the same window.4142| Support signal | Criteria |43|---|---|44| Normal | Thread volume in line with the account's history |45| Rising friction | Thread count up materially vs. baseline, or repeated unresolved threads |46| Escalated | An open thread flagged urgent/escalated, or explicit cancellation language |4748## Step 3 — Pull billing state from Stripe (read-only)4950Pull payment status and renewal date per account.5152| Billing signal | Criteria |53|---|---|54| Healthy | Payments current, renewal more than 45 days out |55| At risk | One missed or failed payment in the last billing cycle |56| Renewal-imminent | Renewal within 30 days |5758## Step 4 — Combine into one score per account5960No single signal alone is high risk. Weigh combinations:6162| Combination | Risk |63|---|---|64| Declining/dormant usage alone | Low–medium — watch |65| Declining usage + rising/escalated support | Medium–high |66| Any usage signal + a failed payment | High |67| Declining/dormant usage + renewal-imminent | High |68| Rising/escalated support + renewal-imminent | High |69| Declining usage + rising support + (renewal-imminent or failed payment) | Highest — surface first |70| Stable usage, normal support, healthy billing | Not at risk — omit from the list |7172## Step 5 — Write the reason and the next step7374For every account that clears the "watch" bar, write:7576- **Reason** — the specific signals that fired, e.g. "usage down 34% in 30d,77 2 escalated threads, renewal in 12 days."78- **Next step** — one concrete, human-actionable suggestion tied to the79 dominant signal:80 - Usage decline → a check-in call or re-onboarding nudge.81 - Support friction → proactive outreach from the account's CSM.82 - Payment risk → a billing follow-up (the agent does not send this itself).83 - Renewal-imminent combined with any other signal → prioritize a renewal84 conversation now, not at the renewal date.8586## Step 6 — Rank and post8788Sort highest risk first. Post one message to {{alert_channel}}: for each89account, its rank, risk level, the reason, and the next step. Post exactly90once per run — this is a fresh session, so there is no prior list to update or91diff against; the whole list is recomputed and reposted every day.9293</workflow>9495<guardrails>96- **Read-only, always.** Postgres and Stripe are read-only connectors, and the97 Plain API key (`PLAIN_API_KEY`) is scoped to read access only. Never write to98 an account, a ticket, or a subscription, even if the credential would99 technically allow it.100- **One output.** The Slack post to {{alert_channel}} is the only thing that101 leaves the sandbox. No emails, no ticket replies, no billing actions.102- **No memory between runs.** Each run is a fresh session; recompute from the103 current state of all three systems rather than assuming anything from the104 prior day's list.105- **Scoped secrets.** Postgres and Stripe access is brokered server-side106 through connectors; the Plain API key is injected as an environment107 variable. No raw credential is ever pasted into chat.108- **People decide, not the agent.** The list flags risk and suggests a next109 step; a human on customer success decides whether and how to act.110</guardrails>111112</skill>