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).fitMultiplieris set upstream by the Qualify node based on soft (fit) rules: a candidate that passed all hard rules but failed a soft fit rule keepsfitMultiplier < 1and is demoted.
2. propensity — model-driven, single objective
Returns the predicted probability the customer will convert on this candidate. Resolution is hierarchical — most-specific signal wins:
- Per-offer adaptation with ≥ 50 positive/negative interactions → use the learned
positiveRatedirectly. - Per-offer adaptation with 1–49 interactions → blend the learned rate with the strongest available fallback (channel → direction → category → global) using
smoothingWeight(default 10). - Per-channel adaptation with ≥ 15 interactions → use the channel posterior.
- Per-category adaptation with ≥ 20 interactions → use category warm-start prior.
- Per-direction adaptation (
inbound/outbound) with ≥ 10 interactions → use the direction prior. - Global adaptation with ≥ 10 interactions → use global prior.
- No adaptation data → fall back to the
modelKeymodel (scoreWithModelor ONNX runner).
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 to0 — 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
Under propensity
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:
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):
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 selectedmethod. 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
methodisformulaorpropensity. 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 matchpriority/100exactly, this is the cause.
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 (
propensityandformula): 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 forpropensity; category → global forformula) usingsmoothingWeight(default 10, tunable per tenant viaSettings.propensitySmoothingWeight). - Propensity score floor: even at high evidence (
evidence ≥ 50), the propensity component is clamped tomax(propScore, floor)so an offer with zero positive outcomes cannot score exactly zero. Default0.05, tunable per tenant viaSettings.propensityScoreFloor(clamped to[0, 0.5]). Without this floor, an offer that had been shown 50+ times without a single conversion would score0, 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 to0if you want classical bandit-style elimination; raise it to0.10or0.15if 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 offSettings.modelMaturityThreshold(default 100). Applies only topropensityandformula. 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).
How outcomes update adaptation — positive / negative / neutral classifications
EveryOutcomeType 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 aconvert outcome), the learned positive rate is 0 / N = 0. Without the floor:
propensitystrategy:candidate.score = 0 × fitMult = 0→ offer drops to last in ranking, Rank top-N drops it, Group never picks it for any placement.formulastrategy:Math.pow(0, Wp) = 0(or, with the existing 1e-6 clamp,1e-6^Wp— e.g.≈ 0.004at the defaultWp = 0.4) → score collapses to a near-zero value, same effective elimination.
Observing the strategy in decision traces
Every Recommend response withtrace: 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.