Skip to main content
The Score node in a Decision Flow assigns each candidate offer a numeric score that the downstream Rank node uses to decide what wins. KaireonAI ships three scoring strategies, each suited to a different stage of operational maturity: The strategy is set on the Score node:

How each strategy computes a score

1. priority_weighted — deterministic, no model required

The candidate’s score is a pure product of three operator-set knobs:
  • priority (0–100) lives on the offer. Higher priority → higher score.
  • weight (0–100) lives on the offer. Lets you bias a specific offer up or down without touching priority (useful for short campaigns).
  • fitMultiplier is set upstream by the Qualify node based on soft (fit) rules: a candidate that passed all hard rules but failed a soft fit rule keeps fitMultiplier < 1 and is demoted.
Use it when you have no interaction history yet, you want fully predictable ranking, or you’re A/B-comparing a model against a deterministic baseline. It is also the safe fallback when a model is missing or returns degraded scores.

2. propensity — model-driven, single objective

Returns the predicted probability the customer will convert on this candidate. Resolution is hierarchical — most-specific signal wins:
  1. Per-offer adaptation with ≥ 50 positive/negative interactions → use the learned positiveRate directly.
  2. Per-offer adaptation with 1–49 interactions → blend the learned rate with the strongest available fallback (channel → direction → category → global) using smoothingWeight (default 10).
  3. Per-channel adaptation with ≥ 15 interactions → use the channel posterior.
  4. Per-category adaptation with ≥ 20 interactions → use category warm-start prior.
  5. Per-direction adaptation (inbound / outbound) with ≥ 10 interactions → use the direction prior.
  6. Global adaptation with ≥ 10 interactions → use global prior.
  7. No adaptation data → fall back to the modelKey model (scoreWithModel or ONNX runner).
Final: score = propensityScore × fitMultiplier. Use it when you want the engine to pick the most likely converter and you’ve configured a model (or accumulated enough adaptation data). The downside vs. PRIE is that two equally-likely offers tie regardless of whether one earns 10× more revenue.

3. formula — PRIE composite (multi-objective)

The recommended production strategy. Computes a weighted geometric mean of four 0–1 components:
Weights default to 0.4 / 0.2 / 0.3 / 0.1 and are configurable per Score node OR per Ranking Profile. Each component is clamped to 1e-6 to avoid log(0) while preserving the “any 0 → 0” hard-stop semantic.

Optional exponent terms: uplift and CLV

Two further weights multiply the base PRIE score outside the P+R+I+E sum-to-1 constraint. Each defaults to 0 — leaving them at 0 reproduces the exact legacy four-factor formula. In the direct formula config each ranges 0..2; when supplied through a Ranking Profile’s uplift / clv weight keys they range 0..1. For the theoretical grounding behind the four-factor split and the two optional terms, see PRIE — Design rationale.

A worked example: same candidates, three rankings

Three candidate offers reach the Score node for the same customer: (Assume no adaptation data, channel match boosts R to 0.7 for the channel-targeted Travel Card and stays at 0.5 for the others, and no recent updates.)

Under priority_weighted

Highest-priority offer wins. The model’s belief that Cashback is 3× more likely to convert is ignored, and so is the fact that Travel Card earns 4.5× the margin of No-Annual-Fee.

Under propensity

The likeliest converter wins. Business value and operator priority drop out entirely — you can’t ship “highest-margin among the likely ones” without changing strategy.

Under formula (default weights 0.4 / 0.2 / 0.3 / 0.1)

Impact uses businessValue·0.4 + min(margin/200,1)·0.3 when financial fields are set, and emphasis is priority/100:
PRIE ranks Cashback first (high propensity wins out), Travel second (lower propensity, but big business value and channel match keep it competitive), and No-Annual-Fee last (high priority but weak business value and weak model belief drag it down).

Summary — same inventory, five different winners

Swap only the strategy (and PRIE profile) and the winner moves: The Travel Card never wins under priority_weighted or propensity (it’s not the most-priority or most-likely candidate), but under PRIE with margin-heavy or priority-led weights it climbs to first because the other components compound to overcome its weaker propensity. This is the practical point of the four-factor model: the winner depends on what you choose to value, not on a fixed scoreboard.

Strategy overrides (per-channel, per-category, per-profile)

You can change strategy on a per-candidate basis without writing two flows.

Channel overrides

ScoreNodeConfig.channelOverrides[] lets you pin a different method, modelKey, or formula for candidates whose channelId matches. Example: keep propensity for the in-app channel where the model is mature, but use priority_weighted for direct mail where you have no signal:

Ranking Profile (strategy profile)

strategyProfileId references a Ranking Profile (a.k.a. scoring strategy) whose weight keys map into the formula: conversion → Wp, recency → Wr, margin → Wi, fairness → We, plus the optional exponent terms uplift → upliftWeight and clv → clvWeight. Swapping profiles re-balances the formula without editing the flow:

Strategy overrides (most specific match wins)

strategyOverrides[] lets you pick a different profile per productType, category, or channel. First match wins (in the order productType → category → channel):
A Loan offer reaching the Score node gets rp_aggressive_margin’s weights; everything else gets rp_balanced. Per-candidate-route — same flow, different scoring lens.

Score panel UI — common traps

The Studio’s Score-node panel always renders the PRIE weight fields (P, R, I, E) regardless of the selected method. This is intentional — the weights are stored on the node so swapping back to method: "formula" doesn’t lose them — but it can mislead first-time operators. (Ranking profiles edited under Studio → Scoring Strategies also expose Uplift and CLV sliders alongside Conversion / Margin / Fatigue / Fairness / Recency; both default to 0.) Two specific traps to watch for:
  • “Scoring Strategy = None (use inline weights above)” — this is the default and means there’s no ranking-profile override. The weights you see in the panel ARE the active PRIE weights. Pick a profile from this dropdown to switch — the profile’s weights then drive scoring and the inline values become inert (still stored, still visible, no longer used).
  • “Propensity Model = None (priority-based)” — leaving this unset routes the engine to priority-based scoring even when method is formula or propensity. This is the safety default for fresh tenants with no models; once you have a trained model, point this dropdown at it so the P component carries real signal. If you see scores in the response that match priority/100 exactly, this is the cause.
The most reliable way to confirm what the engine is actually running is to read the flow’s publishedVersions[].configSnapshot.nodes[].config via GET /api/v1/decision-flows — the panel reflects whatever was last saved, but the engine reads the latest published version. See Lifecycle & publication.

Choosing a strategy — decision guide

When in doubt, start with priority_weighted for the first week of traffic, switch to propensity once you have ≥ 50 interactions per offer, and graduate to formula once business stakeholders want to lean on revenue, fairness, or recency in the ranking.

Cold-start, smoothing, and the maturity ramp

Four engine behaviors guard against poor scores when evidence is thin or skewed:
  • Propensity smoothing (propensity and formula): when an offer has any adaptation evidence but below 50 interactions, the learned rate is blended with the strongest available fallback (channel → direction → category → global for propensity; category → global for formula) using smoothingWeight (default 10, tunable per tenant via Settings.propensitySmoothingWeight).
  • Propensity score floor: even at high evidence (evidence ≥ 50), the propensity component is clamped to max(propScore, floor) so an offer with zero positive outcomes cannot score exactly zero. Default 0.05, tunable per tenant via Settings.propensityScoreFloor (clamped to [0, 0.5]). Without this floor, an offer that had been shown 50+ times without a single conversion would score 0, be eliminated by PRIE’s geometric mean (0^Wp = 0) and by the propensity multiplier (0 × fitMult = 0), and never receive another impression — a starvation failure mode that prevents the offer from ever proving itself. Set to 0 if you want classical bandit-style elimination; raise it to 0.10 or 0.15 if you want a stronger exploration tail.
  • Maturity ramp: a new offer whose posterior is still too wide to rank confidently is gated out of some customers’ decisions (probabilistic exposure control, not score-scaling), ramping up as evidence accrues. The default BCB-MR mode keys off the Wilson credible-interval width (Settings.maturityWidthThreshold, default 0.20); the legacy count mode keys off Settings.modelMaturityThreshold (default 100). Applies only to propensity and formula. See How scoring works.
  • Ranking influencers: positive outcomes against an offer’s category nudge sibling-offer scores up; negative outcomes nudge them down. Toggle per tenant via Settings.rankingInfluencersEnabled (default true).
These mechanisms mean two operationally-identical flows configured with different strategies can also produce different scores at different points in the lifecycle of an offer — the same Score node will pick a fresh offer less often than a mature one, even with identical configuration, until the maturity threshold is crossed.

How outcomes update adaptation — positive / negative / neutral classifications

Every OutcomeType row carries a classification field — "positive" | "negative" | "neutral" — and that classification determines what gets incremented when a respond outcome lands. The engine’s hierarchical-propensity branch reads only the positive and negative counters; neutral outcomes don’t move the rate at all (by design — “we showed it, customer didn’t respond” is no signal, not a negative signal). Practical implication: every outcome — positive, negative, OR neutral — increments the per-(model, offer) evidence counter, so the denominator in positiveRate = positives / evidence reflects every observed touch (including “shown but no action”). To produce a meaningful negative-learning signal, use an outcome whose classification = "negative" (not_interested, dismiss, unsubscribe, complaint). Choosing a neutral outcome (deferred, impression) records the impression for cap/frequency purposes but doesn’t move the rate in either direction. Common configuration trap: the platform ships a default set of OutcomeType rows seeded at tenant-creation time. no_action is NOT in that seed — if your client code sends outcome: "no_action" the respond endpoint returns 404 and the outcome is silently discarded (no evidence increment, no counter movement). Either add a custom OutcomeType row with classification neutral for no_action, or use one of the seeded keys (impression, not_presented, expired, deferred) when you mean “shown but no action”. Verified live in T27: switching from no_action to not_interested produced clean convergence to the true 0.6 rate across all 8 algorithms.

Why the floor exists — the starvation failure mode

When an offer accumulates 50+ outcomes that are all negative (e.g. it was shown during testing but never received a convert outcome), the learned positive rate is 0 / N = 0. Without the floor:
  • propensity strategy: candidate.score = 0 × fitMult = 0 → offer drops to last in ranking, Rank top-N drops it, Group never picks it for any placement.
  • formula strategy: Math.pow(0, Wp) = 0 (or, with the existing 1e-6 clamp, 1e-6^Wp — e.g. ≈ 0.004 at the default Wp = 0.4) → score collapses to a near-zero value, same effective elimination.
Once the offer is permanently un-picked, it can never earn a positive outcome that would unlock it — the negative-only evidence becomes self-perpetuating. The floor guarantees a small exploration tail so a starved offer can be re-tested occasionally, and a single positive outcome lifts it out of the floor naturally.

Observing the strategy in decision traces

Every Recommend response with trace: true (or audited via /api/v1/decision-traces) records the active strategy used, the resolved model key, and — for formula — the four component values per candidate. Use this to verify that a strategy override fired as expected. See Decision Traces API.