Wiki · Substrate piece

Customer health

Leading-indicator churn prediction. AR aging tells you a customer disengaged 60 days ago. Customer health tells you they're about to. Seven signals roll up to a 0-100 score that bands green / yellow / orange / red.

Real · R557 Phase 53
What this is

Predict churn 30-60 days earlier than AR can

AR aging is a lagging indicator. By the time an invoice ages 30 days past due, the customer has already cooled off — placed fewer orders, stopped responding to emails, narrowed their SKU mix. The damage is done; we're just measuring it.

Customer health is the leading-indicator substrate. It watches seven behavioral signals continuously, normalizes each to a 0-1 risk contribution, and rolls them up to a per-customer 0-100 composite score. The score lands in customer_health_scores with a risk_tier band (green/yellow/orange/red), updated nightly with incremental refreshes off the event ledger.

The motivation comes from Mike's high-leverage moves doc — Driscoll is 36.4% of revenue. A single early warning on a top-5 customer pays the entire substrate cost back 100x. Schema lives in migrations/schema/126_customer_health_signals.sql, landed R557 / Phase 53.

The seven signals

What we watch

SignalWhat it measuresTypical weight
order_velocity_declineOrder count last-30 vs trailing-90 baselineHigh
order_recency_gapDays since last order vs typical interval (median of last-12)High
email_response_latencyMean reply time (us → them) trend over last-90Medium
dispute_frequencyDispute / credit-memo count last-30Medium
inbound_sentiment_trendAI-classified email sentiment slopeMedium
order_spread_narrowingDistinct SKU count vs typical mixMedium
call_to_order_ratioInbound calls without orders (when call data exists)Low
reflexion_frictionCount of reflexion entries tagged 'friction' for this customerLow
ar_aging_laggingThe lagging signal — kept for completenessLow (it's confirmation, not prediction)
Why these signals

They're behavioral, not financial. Behavioral data leads financial data by weeks. A customer that's narrowing their order spread is sampling alternatives; a customer that's responding slower has another vendor's email above ours in their inbox. Watch behavior; the dollars follow.

Tier mapping

0-100 score → risk tier

TierScore rangeWhat it meansSuggested action
Green0–25Healthy, engaged customerMonitor, opportunities for upsell
Yellow26–50Some warning signals; watch listDaily digest mention
Orange51–75Multiple signals tripping; intervention warrantedMike personal touch within 2 weeks
Red76–100Imminent churn riskSame-day phone call; escalate to win-back workflow

Lower score = healthier. The tier bands are calibrated against the R567 backtest — a sample of 28 customers who proxy-churned (silent zero-revenue for 90+ days). The substrate would have flagged 21 of those 28 in orange or red tier ≥ 30 days before the AR aging caught it.

Backtest results

R567 — 28 proxy-churned customers

Sample size
28
proxy-churned customers
Detected early
21/28
75% recall
Median lead time
42 days
before AR caught it
False positives
~12%
flagged red but recovered

Top contributing signals across the 21 detections, in order:

The 7 missed cases were customers whose churn was driven by external factors (acquisition, closure, contract loss) that didn't show up in behavioral signals. We can't predict those from our data alone.

Worked example

How a yellow customer moves to orange

Scenario

Magnolia Beef Co (NS customer #3104), trailing-90 weekly orders. On May 1, score was 28 (yellow band). Through May the signals shift: order_velocity_decline rises (4 orders this week vs typical 7), order_recency_gap trips (last order 11 days ago, median interval 5), email_response_latency doubled (4-day mean vs 2-day baseline).

Nightly recompute on May 24 pushes the score to 58 — orange band. The daily digest at 7am May 25 leads with Magnolia. Mike calls the AP contact, learns they're piloting a competitor on chicken. Mike pivots — offers a sample shipment of two assemblies, schedules a follow-up call. Score stays orange for now; if the intervention works, it'll drift back to yellow over 30 days.

Step-by-step recompute

How a score gets built

  1. 01

    Signal compute (per signal)

    Each signal has a compute function in src/lib/customer_health/signals/. They run nightly (full) and incrementally via event subscriptions (partial). Each write to customer_health_signals with normalized value in [0,1].

    Writes customer_health_signals
    Time ~10ms/customer/signal
  2. 02

    Composite score

    Weighted sum over the latest signal values per customer. Weights live in customer_health_weights (configurable). Result: a 0-100 score and the top-3 contributing signals.

    Writes customer_health_scores
  3. 03

    Tier band + trend

    Score is banded into green/yellow/orange/red. Prior score is read; trend_arrow is set (up/down/flat based on delta > 5 points). History row appended to customer_health_score_history.

    Writes customer_health_scores, customer_health_score_history
  4. 04

    Event emission on tier change

    If tier changed (e.g., yellow → orange), emit customer.health_tier_changed to the events ledger. Downstream: digest digester picks it up, AR aging workflow re-prioritizes, sales follow-up may auto-create.

    Emits customer.health_tier_changed
  5. 05

    Daily digest

    The 7am ET digest reads top-N customers by score and any tier changes overnight. Mike's morning email leads with new oranges/reds.

    Reads customer_health_scores
Outcomes

What's different with this substrate

Failure modes

What can go wrong

Cold start (new customer)

New customers don't have enough history for several signals. They start in yellow until 90 days of behavior accumulates. Notes column flags this so Mike doesn't read too much into early scores.

Seasonal signal noise

K-12 customers go quiet over summer — that's expected, not churn. The order_recency_gap signal has a school-calendar-aware baseline; backtest excludes Jun-Aug for school customers.

External churn drivers

7/28 backtest misses were customers we couldn't have predicted from internal data. Adding industry-news ingestion is a future Phase 5x item.

Related

Adjacent substrate + workflows

For developers

Code paths + invariants

ConcernWhere
Schemamigrations/schema/126_customer_health_signals.sql
Signal computessrc/lib/customer_health/signals/
Compositesrc/lib/customer_health/composite.ts
BacktestR567 — eval/customer-health-backtest.yaml
Weightscustomer_health_weights table — configurable
Cronwrangler.toml — nightly + */5 min incremental
Cold-start rule< 90 days history → yellow with note
Tier change eventcustomer.health_tier_changed