# Scoring (/docs/strategies/scoring)



Scoring strategies compute a number for every candidate and route to the top
score. Unlike [balance](/docs/strategies/balance) strategies, they weigh
*multiple* signals — load, capacity, deadline, geography, fairness, margin,
conversion — and most of them are **explainable**: they return a per-dimension
breakdown so you can see exactly why a candidate won.

Five scoring strategies are built in; `predicted_value` is provided by the
optional `predicted` module.

| Strategy                                        | Modes             | Capabilities      | Notes                              |
| ----------------------------------------------- | ----------------- | ----------------- | ---------------------------------- |
| [Priority score](#priority-score)               | scoring           | weighted, explain | Blends weight, load, availability. |
| [Multi-objective score](#multi-objective-score) | scoring, weighted | weighted, explain | The flagship tunable scorer.       |
| [SLA deadline](#sla-deadline)                   | scoring           | explain           | Deadline- and catchall-aware.      |
| [Yield optimized](#yield-optimized)             | scoring, weighted | weighted, explain | Maximizes expected value.          |
| [Portfolio balance](#portfolio-balance)         | weighted, scoring | weighted, explain | Caps recent share.                 |
| [Predicted value](#predicted-value)             | scoring           | explain           | Optional `predicted` module.       |

<Callout title="Fallbacks are first-class" type="info">
  Every scoring strategy takes a `fallback_strategy` (or `base_strategy`) param
  naming another registered strategy — usually `smooth_weighted_round_robin`. It
  is used to break ties and to route when the primary scorer has no confident
  answer. The fallback name is resolved through the [registry](/docs/strategies#the-registry).
</Callout>

## Priority score [#priority-score]

**Modes:** `scoring` · &#x2A;*Capabilities:** weighted, explain

Scores each candidate by a blend of **weight, current load, and availability**,
then routes to the top score. Ties break via smooth weighted round-robin, and it
returns explainable per-candidate scores.

**Why use it.** A step up from plain balance when you want load and availability
to nudge the weighted split but you don't need the full multi-objective knob set.
It's the "weighted, but load-aware and explainable" option.

**How to configure it.** No exposed sliders in the product surface — it derives
its blend internally from `Candidate.Weight`, `CurrentLoad`, and availability
facts. Enable `explain` on the route to capture the per-candidate breakdown.

**Where it fits.** The `Select` stage. Reach for
[multi-objective score](#multi-objective-score) when you need to tune the weights.

## Multi-objective score [#multi-objective-score]

**Modes:** `scoring`, `weighted` · &#x2A;*Capabilities:** weighted, explain

The flagship business-objective strategy. It blends capacity, deadline pressure,
geography (distance), load, fairness, margin, and confidence into **one tunable
composite score**, and returns a per-dimension breakdown for full
explainability. If you only adopt one scorer, this is it.

**Why use it.** When "the best recipient" depends on several competing goals at
once — you want the fast, nearby, high-margin, not-overloaded recipient, and you
want to control the relative pull of each factor. Every dimension has a weight
slider, so you tune the objective without writing code.

**How to configure it.**

<TypeTable
  type="{
  score_weight: { description: &#x22;Weight of the candidate's base score.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  capacity_weight: { description: &#x22;Reward available capacity.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  deadline_weight: { description: &#x22;Reward likelihood of beating the deadline.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  distance_penalty_weight: { description: &#x22;Penalize geographic distance.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;0.5&#x22; },
  load_penalty_weight: { description: &#x22;Penalize current load.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  fairness_weight: { description: &#x22;Reward under-served recipients.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;0.5&#x22; },
  margin_weight: { description: &#x22;Reward margin.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;0.5&#x22; },
  confidence_weight: { description: &#x22;Reward high-confidence facts.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;0.5&#x22; },
  min_confidence: { description: &#x22;Drop candidates below this confidence floor.&#x22;, type: &#x22;slider · 0–1&#x22;, default: &#x22;0&#x22; },
  tie_tolerance: { description: &#x22;Score gap within which candidates count as tied.&#x22;, type: &#x22;slider · 0–1&#x22;, default: &#x22;0.0001&#x22; },
  fallback_strategy: { description: &#x22;Tie-break / no-confidence fallback.&#x22;, type: &#x22;string&#x22;, default: &#x22;smooth_weighted_round_robin&#x22; },
}"
/>

```json
{
  "strategy": "multi_objective_score",
  "options": {
    "capacity_weight": 2,
    "deadline_weight": 3,
    "distance_penalty_weight": 1,
    "min_confidence": 0.2
  }
}
```

**Where it fits.** The `Select` stage. It consumes the typed
[feature snapshot](/docs/strategies/contracts#strategy-feature-snapshot)
(capacity ratio, margin, confidence, and more), which makes its decisions
replayable.

## SLA deadline [#sla-deadline]

**Modes:** `scoring` · &#x2A;*Capabilities:** explain

Deadline-aware routing. Scores recipients by **deadline pressure, SLA quality,
response time, load, and capacity**, preferring those likely to beat the
routable's `ExpiresAt`. A `CatchallAt` threshold sets fall-through behavior when
no candidate can comfortably meet the SLA.

**Why use it.** When time-to-serve is the objective — support tickets that must
be answered within an SLA, leads that must be worked before they go cold, jobs
with a hard deadline. It explicitly prefers recipients whose predicted response
beats the clock.

**How to configure it.**

<TypeTable
  type="{
  sla_weight: { description: &#x22;Weight of SLA-compliance signal.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  load_penalty_weight: { description: &#x22;Penalize load.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  capacity_weight: { description: &#x22;Reward available capacity.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  response_time_weight: { description: &#x22;Reward faster response time.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  min_capacity_percent: { description: &#x22;Exclude candidates below this free-capacity fraction.&#x22;, type: &#x22;slider · 0–1&#x22;, default: &#x22;0&#x22; },
  catchall_mode: { description: &#x22;What to do past the catchall threshold.&#x22;, type: &#x22;enum · best_effort | fallback | fail_closed&#x22;, default: &#x22;best_effort&#x22; },
  fallback_strategy: { description: &#x22;Strategy used in fallback catchall mode.&#x22;, type: &#x22;string&#x22;, default: &#x22;smooth_weighted_round_robin&#x22; },
}"
/>

The `catchall_mode` enum is the key control:

* `best_effort` — still route to the best available candidate even if none beats
  the SLA.
* `fallback` — hand off to `fallback_strategy`.
* `fail_closed` — return no assignment rather than route to a recipient that will
  miss the SLA.

**Where it fits.** The `Select` stage, reading SLA-compliance and response-time
features from the snapshot.

## Yield optimized [#yield-optimized]

**Modes:** `scoring`, `weighted` · &#x2A;*Capabilities:** weighted, explain

The revenue strategy for lead and order routing. It maximizes **expected yield**
— conversion rate × outcome value × quality × margin — while penalizing return
rate and SLA risk, and it gates on a minimum-outcomes confidence floor so it
doesn't over-trust recipients with thin history.

**Why use it.** When routing decisions have a dollar value and you want to send
each opportunity to whoever will extract the most from it. It is the strategy for
marketplaces, lead distribution, and order assignment where conversion and margin
matter more than even load.

**How to configure it.**

<TypeTable
  type="{
  conversion_rate_weight: { description: &#x22;Reward higher conversion.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  outcome_value_weight: { description: &#x22;Reward higher average outcome value.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  quality_weight: { description: &#x22;Reward quality score.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  margin_weight: { description: &#x22;Reward margin.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  return_penalty_weight: { description: &#x22;Penalize return rate.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  sla_risk_weight: { description: &#x22;Penalize SLA risk.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  fairness_floor: { description: &#x22;Guarantee a minimum share to under-served recipients.&#x22;, type: &#x22;slider · 0–1&#x22;, default: &#x22;0&#x22; },
  min_outcomes: { description: &#x22;Confidence gate: ignore recipients with fewer recorded outcomes.&#x22;, type: &#x22;number&#x22;, default: &#x22;0&#x22; },
  fallback_strategy: { description: &#x22;Fallback below the confidence gate.&#x22;, type: &#x22;string&#x22;, default: &#x22;smooth_weighted_round_robin&#x22; },
}"
/>

<Callout title="Cold-start protection" type="warn">
  `min_outcomes` matters: a brand-new recipient with one lucky conversion looks
  infinitely good. Set a floor so yield scoring only trusts recipients with enough
  history, and let the fallback distribute to the rest.
</Callout>

**Where it fits.** The `Select` stage, driven by conversion, outcome-value,
quality, return-rate, and margin features from the snapshot.

## Portfolio balance [#portfolio-balance]

**Modes:** `weighted`, `scoring` · &#x2A;*Capabilities:** weighted, explain

Keeps each recipient's (or metadata group's) **recent assignment share** under a
configured cap, rebalancing toward under-served recipients. Concentration control
for routing.

**Why use it.** When no single recipient — or no single *group* of recipients
(one region, one vendor, one team) — should dominate recent volume, regardless of
how attractive they score. It wraps a base strategy and simply vetoes candidates
that are already over their share.

**How to configure it.**

<TypeTable
  type="{
  window_size: { description: &#x22;How many recent assignments define 'recent share.'&#x22;, type: &#x22;number&#x22;, default: &#x22;100&#x22; },
  max_share: { description: &#x22;Maximum fraction of the window any one recipient/group may hold.&#x22;, type: &#x22;slider · 0–1&#x22;, default: &#x22;0.5&#x22; },
  rebalance_strength: { description: &#x22;How hard to push toward under-served recipients.&#x22;, type: &#x22;slider · 0–10&#x22;, default: &#x22;1&#x22; },
  min_assignments: { description: &#x22;Ignore share caps until this many assignments exist.&#x22;, type: &#x22;number&#x22;, default: &#x22;0&#x22; },
  group_by_metadata: { description: &#x22;Metadata key to group by (cap a group's share, not just a recipient's).&#x22;, type: &#x22;string&#x22; },
  base_strategy: { description: &#x22;Strategy that picks among candidates still under cap.&#x22;, type: &#x22;string&#x22;, default: &#x22;smooth_weighted_round_robin&#x22; },
  empty_window_mode: { description: &#x22;Behavior when the window has no history yet.&#x22;, type: &#x22;enum · base_strategy | fair_weighted | error&#x22;, default: &#x22;base_strategy&#x22; },
}"
/>

**Where it fits.** The `Select` stage. Its batch-scale counterpart is the
[portfolio quota allocator](/docs/strategies/allocation#portfolio-quota-allocator),
which enforces caps across a whole batch instead of per-decision.

## Predicted value [#predicted-value]

**Modes:** `scoring` · &#x2A;*Capabilities:** explain · &#x2A;*Module:** `predicted`

Scores candidates by **predicted outcome value** learned from outcome and quality
history, routing to the highest predicted payoff.

**Why use it.** When you want a learned estimate of what each recipient will
deliver, rather than a hand-tuned blend. It's the supervised sibling of
[yield optimized](#yield-optimized): instead of you weighting the factors, it
predicts the value directly from history.

**How to configure it.** It is an **optional module** and must be registered with
the outcome and quality stores it learns from (`RegisterPredicted` in
`modules/strategies/predicted/register.go`). Without those repositories it is not
available. It has no product-surface sliders — its behavior comes from the
learned model.

<Callout title="Optional module" type="warn">
  `predicted_value` (with [Thompson sampling](/docs/strategies/learning#thompson-sampling)
  and [LinUCB](/docs/strategies/learning#linucb)) ships in the `predicted` module,
  not the built-in pack. It requires the outcome and quality repositories and, for
  the bandits, the strategy state store for durable posteriors.
</Callout>

**Where it fits.** The `Select` stage. See [Learning](/docs/strategies/learning)
for the bandit strategies in the same module.

<Callout title="Behavior change: per-candidate outcome features" type="info">
  Each candidate is now scored on its **own** outcome history. Previously the
  per-recipient outcome stats query ignored the recipient filter and returned the
  pool's highest-total-value recipient, so every candidate received identical
  `conversion_rate`, `avg_outcome_value`, and `total_outcomes` features — the
  predicted score could not tell recipients apart by track record. Those features
  now differ per candidate, so predicted-value rankings will shift toward
  recipients with genuinely stronger histories. The same corrected features feed
  the [learning](/docs/strategies/learning) bandits, which read them as context.
</Callout>

## Related [#related]

<Cards>
  <Card title="Learning strategies" href="/docs/strategies/learning">
    Bandits that explore and learn winners instead of scoring a fixed blend.
  </Card>

  <Card title="Feature snapshot" href="/docs/strategies/contracts#strategy-feature-snapshot">
    The typed facts scoring strategies consume — and replay from.
  </Card>

  <Card title="Governance" href="/docs/strategies/governance">
    Tune these sliders safely with proposals, shadow campaigns, and experiments.
  </Card>
</Cards>
