Health weights
Tune the per-signal weights that drive each account's composite customer-health score, with tenant overrides layered over shipped defaults.
Health weights
Every account gets a 0-100 composite health score and a risk band. That score is
a weighted average of individual signals, and health weights are the knobs that
decide how much each signal counts for your tenant. Weights are managed under
/v1/health/weights; the scoring math lives in
core.customer_health.health_scorer.
Live
Backed by real code and a real table. Defaults live in DEFAULT_WEIGHTS; per-tenant overrides
persist to health_score_weights and are merged in at score time by load_tenant_weights.
How the composite score works
The composite is the weighted average of each signal's sub-score, divided by the total weight — so weights do not need to sum to 1.0, and a missing or zero-weight signal doesn't drag the score down. The result maps to a risk band:
score >= 75 → healthy
50 – 74 → watch
25 – 49 → at_risk
0 – 24 → critical
The signals and their default weights
DEFAULT_WEIGHTS ships these values, chosen so the score is most influenced by
the things a CSM intuitively cares about — are people responding, is there money
in play, are execs engaged:
| Signal | Default weight |
|---|---|
activity_recency | 0.20 |
engagement_frequency | 0.15 |
pipeline_health | 0.15 |
payment_status | 0.15 |
contact_count_active | 0.10 |
executive_engagement | 0.10 |
support_ticket_count | 0.05 |
nps_score | 0.05 |
csat_score | 0.05 |
account_growth_signal | 0.05 |
engagement_velocity_trend | 0.0 |
sentiment_trend | 0.0 |
multi_thread_coverage | 0.0 |
CSAT counts only once someone answers
csat_score averages the 1–5 satisfaction answers an account gave in the last 180 days, directly
or on one of its support cases. With no answers it is left out of the composite entirely — its
weight neither lowers nor raises the score — so turning on a CSAT survey never moves an account
nobody has rated.
Why three signals default to 0.0
engagement_velocity_trend, sentiment_trend, and multi_thread_coverage are the deal +
activity data-layer signals. They default to weight 0.0 so they feed the churn model (via the
feature vector and SHAP explanations) without shifting the human-facing composite score. A tenant
can opt them into the composite by raising their weight override.
Managing weights
| Method & path | Purpose |
|---|---|
GET /v1/health/weights | List the effective per-signal weights (defaults + overrides) |
PATCH /v1/health/weights | Admin: upsert one or more weight overrides |
Overrides are validated: a weight must be >= 0, and unknown signal names are
ignored rather than persisted, so the override set can never drift out of sync
with the signal registry. Every call derives tenant_id from the auth context;
nothing is read from the request body.
Related surfaces
The weights feed the same customer_health module that also exposes the at-risk
feed (GET /v1/health/feed), the band histogram (GET /v1/health/distribution),
save-task review queue, and the churn model endpoints
(POST /v1/health/model/retrain, GET /v1/health/model/info). Weight changes
affect the composite score immediately; the churn model is retrained separately.