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:
| Strategy | Uses ML? | Best for |
|---|---|---|
priority_weighted | No | Day-1 deployments with no interaction history; pure configuration-driven ranking. |
propensity | Yes | Once you have learned models or adaptation data — picks the offer most likely to convert. |
formula (PRIE) | Yes (component) | Production decisioning that balances likelihood, contextual fit, business value, and operator emphasis. |
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 learned rate with category/global fallback using
smoothingWeight(default 10). - Per-category adaptation with ≥ 20 interactions → use category warm-start 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:
| Component | What it captures | Source |
|---|---|---|
| P — Propensity | Predicted positive rate | Same hierarchical resolution as the propensity strategy (adaptation → model → 0.5 fallback). |
| R — Relevance | Contextual fit at request time | Channel match (+0.2 if the candidate’s creative matches the request channel) and recency boost (offers updated in the last 7 days). |
| I — Impact | Per-offer business value | businessValue (40%) + clipped margin (30%) + clipped revenue (30%) when financial fields are set, otherwise pure businessValue. |
| E — Emphasis | Operator priority | priority / 100. The same dial as priority_weighted but exponentiated by emphasisWeight. |
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.
| Weight | Effect when > 0 |
|---|---|
upliftWeight (Wu) | score ×= upliftMultiplier^Wu. Persuadable offers (positive CATE τ) climb the ranking; sleeping-dog offers (negative τ) sink. The engine stamps upliftTau and upliftMultiplier on each candidate’s trace. See Uplift modeling. |
clvWeight (Wclv) | score ×= impact^(Wclv × clvNorm), where clvNorm = clvScore / 100 from the customer’s CLV row. Because a per-customer constant scales every candidate equally in the geometric mean and can’t reorder them, the CLV weight rides the per-offer impact factor — high-CLV customers see high-impact offers ranked higher. No CLV row → term skipped (never penalizes). The engine stamps clvNorm and clvImpactExponent on the trace; the CLV lookup is cached 300 s. |
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:| Offer | priority | weight | businessValue | margin | model propensity | fitMultiplier |
|---|---|---|---|---|---|---|
| Travel Card 1.5x | 80 | 100 | 90 | 180 | 0.30 | 1.00 |
| Cashback Card 2% | 50 | 100 | 60 | 120 | 0.65 | 1.00 |
| No-Annual-Fee Card | 90 | 100 | 40 | 40 | 0.20 | 1.00 |
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:| Strategy | PRIE weights | Travel | Cashback | No-Fee | Winner |
|---|---|---|---|---|---|
priority_weighted | — | 0.800 | 0.500 | 0.900 | No-Fee |
propensity | — | 0.300 | 0.650 | 0.200 | Cashback |
formula — default | 0.40 / 0.20 / 0.30 / 0.10 | 0.490 | 0.527 | 0.287 | Cashback |
formula — aggressive-margin | 0.15 / 0.10 / 0.70 / 0.05 | 0.577 | 0.460 | 0.253 | Travel |
formula — priority-led | 0.10 / 0.10 / 0.10 / 0.70 | 0.699 | 0.504 | 0.634 | Travel |
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
| Question | Strategy |
|---|---|
| Just launching, no model, no history? | priority_weighted |
| Have a trained model OR accumulated adaptation data? | propensity |
| Need to balance likelihood with revenue or operator priority? | formula |
| Different channels at different maturity? | One base strategy + channelOverrides |
| Different categories want different tradeoffs? | formula + strategyOverrides by category |
| Tuning aggressiveness without touching flows? | formula + swap strategyProfileId |
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 category or global fallback 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 is intentionally throttled — early scores are scaled down based on how few interactions the model has seen. Threshold tunable per tenant via
Settings.modelMaturityThreshold(default 100). Applies only topropensityandformula. - 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).
| Common outcome | Default classification | Effect on adaptation |
|---|---|---|
convert | positive | positive++ — positiveRate rises |
accept | positive | positive++ — positiveRate rises |
not_interested | negative | negative++ — positiveRate falls |
reject | negative | negative++ — positiveRate falls |
no_action | neutral | No counter incremented — rate unchanged |
impression | neutral | impressions++ only (for caps); no rate impact |
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(or1e-6^Wp ≈ 0.001with the existing 1e-6 clamp) → 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.