> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# The contact scoring pipeline: daily cadence, batch execution, and the auditable scorer

> How Orbit scores every contact once a day: what the four predictive sub-scores measure and which signals feed them, why a daily cadence beats real-time-per-event scoring, how serial-per-tenant batching keeps the pass predictable, the auditable scoring algorithm, the on-demand pass for freshly imported contacts, and where scores surface downstream.

# The contact scoring pipeline

Orbit scores every contact in every tenant — churn risk, intent,
propensity, and a lifetime-value estimate — and writes the result to a
per-contact row that segments, automations, and dashboards read. The
operational guide for training and activating predictive models is
[CDP predictive models](/guides/cdp-predictive-models); this page is the
concept underneath it: **why the pipeline runs daily, how it batches,
what the scorer is, and where a score lands.** Read it once and "where
does this number come from?" stops being a mystery.

## Section 1 — What the pipeline scores, and what feeds it

Each pass derives a fixed signal set per contact from the tenant's own
transactional tables — the message history, the engagement profile
(`contact_engagement_profiles`), and agent-conversation outcomes — then
blends those signals into four sub-scores:

| Sub-score            | Range | What it answers                                                                 |
| -------------------- | ----- | ------------------------------------------------------------------------------- |
| `churn_risk`         | 0–1   | How likely is this contact to disengage?                                        |
| `intent_score`       | 0–1   | How engaged are they right now — recent inbound replies, recent clicks?         |
| `propensity_score`   | 0–1   | How engaged are they on average — reply ratio, sample depth, channel breadth?   |
| `ltv_estimate_cents` | cents | A value estimate from lifetime message volume and your revenue-per-message rate |

A rule-based `segment_label` — one of `champion`, `high_value_engaged`,
`engaged`, `new`, `passive`, `at_risk`, `dormant`, `lost` — is derived
from the score triad and rides on the same row. That label is what the
dashboard Health badge shows, and it always resolves to a value, even
when signals are thin.

The four built-in predictive models the
[CDP predictive models guide](/guides/cdp-predictive-models) trains and
activates — churn propensity, conversion intent, lifetime value, and
engagement fatigue — answer operator-facing "who should I target?"
questions on demand. The pipeline on this page is the always-on twin:
it keeps a per-contact score row current for **every** contact, so
segments and dashboards can filter on scores without running a model
first. The signals overlap (message counts, recency, engagement
velocity, channel breadth); the pipeline's blend is deterministic and
needs no training population.

## Section 2 — Why daily, and how the batch runs

Predictive scores feed **slow decisions**: campaign targeting,
dashboard segments, lifecycle automations. None of those need a score
that is milliseconds fresh — yesterday's score targets today's campaign
just as well. Orbit therefore scores once a day, per tenant, and made
that choice deliberately:

* **Real-time-per-event scoring was rejected.** Scoring on every event
  turns one write into a fan of reads across messages, engagement
  profiles, and conversations per event — read amplification that
  grows with traffic and buys nothing a daily score doesn't already
  give a campaign.
* **A daily pass yields a trend.** Recomputing on a fixed cadence makes
  day-over-day movement comparable — the same cut of signals, one day
  apart — instead of a jittery per-event number.
* **Load stays predictable.** Tenants are processed **serially, one at
  a time**, so a 50+-tenant fleet never pummels the shared Postgres
  connection pool. Inside one tenant, the walk parallelises through
  **500-row batches**, and the whole pass is an idempotent upsert —
  rerunning it writes the same rows, so a retry is never harmful.

Per-tenant failures are isolated: one broken tenant schema is counted
and logged, and the other tenants in the pass still score. The daily
tick is also the ultimate backstop — anything the on-demand pass
(Section 4) misses, the next daily run repairs.

## Section 3 — The auditable scorer, and where scores land

The scoring algorithm is a deterministic, weighted blend — not a trained
black box. Each sub-score is a fixed-weight combination of normalized
signals, and **every input signal is stamped into the row's `signals`
JSONB column**. Any "why is this contact flagged at-risk?" question has
a concrete answer you can read off the row: recency, reply counts,
velocity, sample depth. There is no "trust the model" hand-wave — the
score is auditable by construction.

Results persist in the tenant schema's `contact_scores` table, one row
per contact:

| Column                                             | Holds                                                             |
| -------------------------------------------------- | ----------------------------------------------------------------- |
| `churn_risk` / `intent_score` / `propensity_score` | The three 0–1 sub-scores                                          |
| `ltv_estimate_cents`                               | The value estimate in cents                                       |
| `segment_label`                                    | The rule-derived bucket drives the Health badge and score filters |
| `signals`                                          | The exact input values the blend consumed (audit trail)           |
| `computed_at`                                      | When the current row was written                                  |

Tenant-schema isolation applies here exactly as in
[Tenant isolation](/concepts/tenant-isolation): scores live in
`tenant_<id>.contact_scores`, never in a shared table, and one tenant's
scoring cannot read another's rows.

## Section 4 — The on-demand pass for fresh contacts

A daily cadence leaves one visible gap: a contact imported two minutes
ago has no score row until the next daily tick, so its Health badge
reads **Pending** for up to 24 hours. To close that gap, Orbit fires a
per-tenant **on-demand scoring job** the moment an import completes or
a bulk contact create succeeds. That job runs the same scorer, scoped
to just the new contact ids, and idempotently upserts the same
`contact_scores` rows — so a same-contact retry collapses to one write
and re-scoring later is harmless.

The daily pass and the on-demand pass are additive, not alternatives:
the daily sweep keeps every contact current and converges anything the
scoped jobs skipped; the on-demand jobs remove first-touch latency for
the contacts you just brought in.

## Section 5 — Where scores surface

Score rows are inputs; the surfaces that read them are the payoff:

| Surface               | What it reads                                                                                  |
| --------------------- | ---------------------------------------------------------------------------------------------- |
| Segments              | Score and `segment_label` filters (`churn_risk ≥ 0.7`, label `at_risk`) for campaign targeting |
| Lifecycle automations | Journeys triggered on label or score movement                                                  |
| Propensity segments   | The CLV tier distribution and threshold-gated value audiences on `ltv_estimate_cents`          |
| Churn risk views      | The at-risk ranking and retention segments driven by `churn_risk`                              |
| Contact Health badges | The per-contact `segment_label` on the contact profile                                         |

Because all of these read the same `contact_scores` row, they never
disagree about a contact's current standing — the daily pass is the
single writer, and everything downstream is a reader.

## Cross-references

* [CDP predictive models](/guides/cdp-predictive-models) — the
  operational guide: catalog, train, tier, activate.
* [The CDP event model](/concepts/cdp-event-model) — the upstream event
  stream the engagement signals derive from.
* [How CDP segments recompute](/concepts/cdp-segment-recompute-model) —
  how score-driven segment membership converges downstream.
* [Tenant isolation](/concepts/tenant-isolation) — why
  `tenant_<id>.contact_scores` is per-tenant.
