Part 4 of 7 · Churn predictor series ~5 min read

How an at-risk list reaches the owner

The scorer found this week’s at-risk and churning customers. Now the hand-off Lambda has to figure out who owns each one, trim the list to something a person will actually act on, write a plain reason next to every name, and deliver it on the right channel. Get any of those wrong and the list is worse than nothing: forty names nobody reads, a customer routed to a rep who never met them, a score with no reason that gets waved away. Four small guardrails sit between the score and the list landing.

Key takeaways

  • Owner lookup: per-customer owner beats per-segment default beats fallback to the configured admin.
  • The weekly list is capped (default five per owner) and ranked by score, churning first.
  • Each name gets a plain one-line reason built from the exact points that flagged it.
  • Slack DMs are the default; email is the fallback if no Slack ID is configured.
  • The list only suggests who to contact — the owner decides, and nothing is sent to the customer.

Four guardrails on every list

Four guardrails between the score and the delivered at-risk list A horizontal flow diagram. On the far left, a "Scored customers" box: the scorer produced this week's at-risk and churning customers, each with a score and the points that flagged them. Four guardrail gates sit in a row to the right, each drawn as a vertical bar. Gate 1: Resolve owner — looks up the per-customer owner column first; if blank, falls back to the per-segment owner from the rules doc; if still blank, falls back to the configured admin. Returns a Slack member ID if one is set, otherwise an email address, and groups the candidates by owner. Gate 2: Apply the cap — ranks each owner's candidates by score, churning first, and keeps only the top few (default five); the rest wait for a future week so the list stays short enough to act on. Gate 3: Skip recent contacts — drops any candidate the owner already reached out to inside the pause window, read from the cp-state table, so a name isn't pushed again mid-conversation. Gate 4: Compose the reason — for each surviving name, writes a one-line plain reason from the scorer's points, fetches the list template from the voice doc, and attaches three outcome buttons: Reached out, Won back, Lost. After all four gates pass, the list ships via the Slack DM for the resolved Slack ID, or via SES outbound for the resolved email. A note at the bottom: the list only suggests who to contact — the owner decides, and nothing is sent to the customer. Scored at-risk and churning, with points Gate 1 Resolve owner customer owner? segment default? admin fallback? prefer Slack ID, fall back to email Gate 2 Apply the cap rank by score, churning first keep top five, rest wait for a later week Gate 3 Skip recent contacted in pause window? if yes, drop for now (read cp-state) Gate 4 Compose the reason voice template + plain reason + value, days, three outcome buttons Deliver — Slack DM (preferred) or SES outbound email (fallback) incoming webhook for Slack · SES SendRawEmail for inbox every list logged to DDB cp-state — next week won’t repeat a name The list only suggests who to contact — the owner decides, and nothing is sent to the customer.
Fig 4. Four guardrails between the score and the delivered list. Resolve the owner. Apply the cap. Skip recent contacts. Compose a plain reason. Then ship via Slack or email and log the list so next week doesn’t repeat a name.

Gate 1: resolve the owner

Three places the hand-off Lambda looks for the owner of a customer, in order. First, the list sheet’s per-customer owner_email column — if a row has a specific account manager assigned, that person owns it regardless of segment. Second, the per-segment default in the rules doc (“all small-plan customers default to the success team”). Third, the configured admin fallback — the person who set up the predictor and gets every unowned name. The fallback should never fire in steady state; if it does, the monthly summary names every customer that hit the fallback so the rules doc can be updated.

Once the hand-off knows which person owns a name, it looks up their delivery preference. The voice doc maps each owner to a Slack member ID if one is set, otherwise to an email address. Slack is preferred because the list is a private DM with buttons the owner can tap as they work through it. Email is the fallback so nobody is left out.

Gate 2: apply the cap

This is the gate that makes the whole system usable. A scorer let loose on a few thousand customers will happily find forty at-risk names in a bad week — and a list of forty is a list an owner glances at and closes. Gate 2 ranks each owner’s candidates by score, churning customers first, and keeps only the top few. The default cap is five, set in the rules doc. The customers who didn’t make the cut aren’t lost — their score is still recorded, and they’ll surface in a future week if they stay at-risk and the higher-scoring names get handled.

Five is a deliberate number. It’s short enough that an owner can actually call all five in a week, which means the list is a to-do, not a wall. A churn predictor that surfaces everything surfaces nothing.

Gate 3: skip recent contacts

When an owner reaches out to a customer, they don’t want that name back on the list next Monday while the conversation is still going. Gate 3 reads the cp-state table for each candidate’s last_contact date and drops anyone the owner contacted inside the pause window (default two weeks, set in the rules doc). The customer is still being scored every week; they’re just held off the list until the pause ends, at which point — if their signals haven’t improved — they come back.

The pause exists because nagging an owner about a customer they’re already working is the fastest way to get the whole list ignored. The right cadence is “here are new names, and the ones you haven’t had a chance to act on,” not “here is the same list as last week.”

Gate 4: compose a plain reason, then ship

The voice doc has a short list template and a reason template. For each surviving name, the hand-off turns the scorer’s points into one plain sentence: “no order in 47 days, down from weekly; two sour support tickets last month.” This is the one place a model earns its keep on the hand-off — a small Bedrock Haiku 4.5 call turns the raw points into a readable line, grounded strictly in those points so it can’t invent a reason that isn’t in the data. The Lambda fills the template, attaches three buttons — Reached out, Won back, Lost — and ships the list via the Slack DM. The webhook and bot token live in Secrets Manager.

For email fallback, the same list is wrapped in a small HTML email with the same reasons and links that, when clicked, hit a Function URL that records the outcome — the email equivalent of the Slack buttons.

The reason is the heart of the whole design. A score on its own (“Greenfield Cafe: 72”) tells the owner nothing they can act on. The reason (“no order in 34 days, down from weekly; one sour ticket”) tells them exactly what to ask about when they call. The list isn’t “the computer says these people are leaving.” It’s “here’s what changed for each of them — you decide.”

Every list — Slack or email — writes a row to cp-state in DynamoDB. Next week’s run reads it and knows which names were already surfaced and to whom.

Why the guardrails exist

None of these gates are exotic. They’re the kind of small care a thoughtful manager would take if they were building the list by hand — check who actually owns each account, keep it short enough to action, don’t re-list someone you’re mid-conversation with, and say why next to every name. Putting them in code as four small sequential gates makes them part of the design, not something you’re trusting a busy week to remember. And keeping the act of reaching out with a human is the most important guardrail of all: the system points, a person decides.

Next post: how a win-back gets tracked once an owner has acted — how the predictor records the contact, the save, or the loss, and keeps the trail straight.

All posts