# How we flag at-risk accounts

A daily cron scores accounts on leading churn signals across product usage, support, and billing, then posts a ranked at-risk list to Slack with a reason and a suggested next step for each.

Canonical page: https://kortix.com/use-cases/churn-risk

Churn is usually visible before it happens. Usage tapers off, support threads get
more frequent and more frustrated, a payment fails, a renewal approaches. The
signals are there, but they sit in different systems, and no one is watching all
of them at once. By the time an account cancels, the warning signs had been
accumulating for weeks.

We run a churn-risk agent on Kortix that reads those signals every day and posts a
ranked at-risk list to our customer-success Slack channel. It only reads customer
data; the single output is the Slack post. This is how we watch our own accounts.

- **Team:** Kortix
- **Runs on:** Daily cron
- **Connected systems:** Postgres · Plain · Stripe · Slack
- **Mode:** Read-only · one Slack post per day

## The problem

The signals that predict churn live in separate systems: product usage in
Postgres, support friction in Plain, payment health in Stripe, renewal dates in
billing. Each one is a partial view. An account with declining usage might be
fine; an account with declining usage, a rising support load, and a renewal next
month is not.

The common approaches don't combine them. A usage dashboard shows one signal and
leaves the reader to correlate the rest. A health score baked into one tool only
sees that tool's data. Manual account reviews are thorough but happen quarterly,
long after the signals first appeared, and depend on someone remembering to look.

## What we built

On Kortix, a daily cron triggers an agent. It spawns an isolated session (a cloud
sandbox) with read-only access to Postgres, Plain, and Stripe, scores every
account on leading churn signals — declining usage, rising support friction,
missed or failed payments, an upcoming renewal — and posts a ranked at-risk list
to the customer-success Slack channel, with the reason for each account and a
suggested next step. It writes nothing back to customer systems.

## How it works

### Run on a daily cron

A **cron trigger** fires the agent once a day. Each firing spawns a fresh
**session** in its own sandbox. One day maps to one run on one disposable machine,
so the score is recomputed from the current state every time and nothing carries
over.

### Give the agent the scoring rules

How we weigh the signals lives as **skills** and **memory** that travel with the
agent: what counts as a usage decline, which support patterns matter, how a failed
payment and an upcoming renewal combine, and what a good next step looks like for
each kind of risk. When we learn which signals actually preceded a churn, we write
it down and the scoring improves.

### Connect the signal sources read-only

Through scoped **connectors**, brokered server-side so no raw token reaches the
model, the agent reads:

- **Product usage from Postgres** — activity trends per account, to catch a decline
before it bottoms out.
- **Support signals from Plain** — thread volume and tone, to catch rising
friction.
- **Billing from Stripe** — missed or failed payments and the upcoming renewal
date.
- **Posts to Slack** — the ranked at-risk list, with a reason and a suggested next
step per account.

### Set the guardrails

The agent is **read-only** across every customer system. It has no write access to
Postgres, Plain, or Stripe, so it cannot change an account, a ticket, or a
subscription. Its only output is the Slack post. Credentials are encrypted in the
Secrets Manager and injected at runtime, scoped to the agents you grant them to or written to
logs.

### Post the ranked list

With that in place, each morning brings one Slack post: accounts ranked by risk,
each with the signals that put it there — the usage drop, the support thread, the
failed payment, the renewal date — and a suggested next step. The customer-success
team reads it and decides what to do. Nothing is written back automatically.

> **The pattern**
> A daily **cron** spawns a session with read-only **connectors** into Postgres,
> Plain, and Stripe. The scoring lives as **skills** and **memory**. The agent
> reads everything and writes nothing but the Slack post.

## Guardrails

The agent reads across every customer system, so its access is scoped and
one-directional:

- **Isolation.** Every run happens in its own isolated sandbox. The session is granted access only to the systems it's scoped to, and only the Slack post is written back out.
- **Scoped secrets.** The Postgres, Plain, and Stripe credentials are encrypted in
  the Secrets Manager and injected into the sandbox at runtime, scoped to the agents you grant them to.
- **Read-only.** The connectors into customer data are read-only. The agent cannot
  change an account, a ticket, or a subscription; it can only report.
- **Everything is code.** The agent's scoring rules, skills, and per-system
  permissions are files in the repo, versioned and changed through a reviewed
  **change request** rather than a dashboard setting.

## The outcome

- **Every day:** Accounts rescored on the current state of the data
- **Read-only:** Nothing written back to any customer system
- **4 signals:** Usage, support, billing, and renewal in one score

Churn signals that used to sit in four separate systems now arrive as one ranked
list in the channel where the team already works, each account carrying the reason
it surfaced and a suggested next step. The agent only reads; the people decide
what to do about each account.
