Skip to main content

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; 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: 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 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: Tenant-schema isolation applies here exactly as in 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: 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