Health score
Design, compute, calibrate, and explain a customer health score.
A health score is a current, explainable summary, not a churn probability. Start with a few independent behaviors that Customer Success can act on.
Define the inputs
Declare every event and trait used by the score:
import { defineConfig } from '@kitesdk/config';
export default defineConfig({
project: 'acme',
events: {
'feature.used': {
properties: { feature: { type: 'string' } },
},
'api.error': {},
},
customerTraits: {
availableFeatures: {
type: 'array',
items: { type: 'string' },
optional: true,
},
seats: { type: 'number', optional: true },
},
});Then combine weighted components:
import { defineHealth } from '@kitesdk/config';
export default defineHealth({
id: 'overall',
name: 'Overall health',
components: {
adoption: {
name: 'Feature adoption',
weight: 0.4,
compute: {
type: 'feature_adoption',
features: ['exports', 'dashboards', 'alerts'],
period: '30d',
respectPlanFeatures: true,
},
},
engagement: {
name: 'Engagement',
weight: 0.4,
compute: {
type: 'event_frequency',
event: 'feature.used',
period: '30d',
target: 20,
minScore: 5,
},
},
stability: {
name: 'Stability',
weight: 0.2,
compute: {
type: 'inverse_event_count',
event: 'api.error',
period: '30d',
penaltyPerEvent: 5,
maxScore: 100,
},
},
},
thresholds: { excellent: 80, good: 60, poor: 30, critical: 0 },
recomputeOn: [
{ event: 'feature.used' },
{ event: 'api.error' },
{ schedule: '0 0 * * *' },
],
});Kite supports one health definition. Component weights must total 1.0. Components are rounded
and clamped to 0–100 before weighting, then the weighted total is rounded and clamped again.
Understand each computation
| Type | Calculation and edge cases |
|---|---|
event_frequency | count / target * 100, capped at 100 and floored by optional minScore. |
inverse_event_count | maxScore - count * penaltyPerEvent; maxScore defaults to 100. |
feature_adoption | Distinct eligible features observed in feature or features properties divided by the eligible set. |
custom | Deterministic numeric formula using event counts, numeric traits, and prior components. |
Feature adoption scans properties across all events in the period; it is not tied to one event
name. When respectPlanFeatures is true and availableFeatures is an array, eligibility is the
intersection with configured features. If the trait is absent, all configured features are
eligible. An empty eligible set scores 0.
Trait updates alone do not necessarily trigger a health recomputation. Include relevant event or schedule triggers, or run a manual recompute after entitlement changes.
Use custom formulas carefully
Components are evaluated by component ID in lexical order. A formula can reference only a component whose ID sorts earlier:
components: {
a_engagement: {
name: 'Engagement',
weight: 0.7,
compute: {
type: 'event_frequency',
event: 'feature.used',
period: '30d',
target: 20,
},
},
b_depth: {
name: 'Depth',
weight: 0.3,
compute: {
type: 'custom',
formula: ({ events, traits, computed }) =>
Math.min(
100,
events.count('team.member_invited', { days: 60 }) * 10 +
(traits.seats ?? 0) +
(computed.a_engagement ?? 0) * 0.25,
),
},
},
}Formulas cannot close over external variables or use nondeterministic globals such as
Date.now() and Math.random().
Recompute triggers
recomputeOn controls normal event-driven and scheduled refreshes. If it is omitted, those
automatic triggers do not update health. Manual recompute always evaluates the score.
Use scheduled recomputation for time-based decay when no new event arrives:
recomputeOn: [
{ event: 'feature.used' },
{ schedule: '0 0 * * *' },
]Thresholds on the overall score and individual components are validated metadata. The engine
does not assign labels from them. Define operational cohorts explicitly with healthScore or
healthComponent conditions.
Calibrate and inspect
- Choose windows matching the product's natural usage frequency.
- Compare component distributions across representative retained and churned cohorts.
- Check new customers and low-frequency plans for unfair default scores.
- Remove redundant components and cap behaviors one customer can inflate.
- Run a dry-run before persisting changed semantics.
- Explain individual results after deployment.
kite validate --strict
kite deploy --env test
kite recompute --all --dry-run --env test
kite health explain account_42 --env test
kite customers inspect account_42 --env test --jsonChanging a score changes current interpretation. Record the model version and rollout decision, and review Backfill and recomputation before rebuilding live state.