# Routing Strategies (/docs/strategies)



A **strategy** is a pluggable algorithm that answers one question: given a set
of eligible candidates for a unit of work, &#x2A;which one wins?* It is the
decision-making core of Ductor's [routing pipeline](/docs/concepts/routing-pipeline) —
the swappable part that turns "these three candidates all qualify" into "route
to candidate B, and here's why." A strategy produces a
<TechnicalName public="Route" api="Decision" />, and its explanation is the seed
of the work's eventual <Term name="Receipt" />.

Most strategies rank candidates by weight, load, score, or learned value. A
few — `shark_tank` (broadcast-and-claim) and the capacity marketplace — clear
work through a *market*: candidates effectively **bid**, and the winner is chosen
by market rules rather than a fixed score. Those are the clearing-native
strategies. Priced market clearing (auctions, ping/post) runs today for lead
distribution. **Agent RFQ** — humans and agents bidding on work — extends the
same machinery: typed bid terms, buyer-side clearing, authenticated submission,
a published task spec, disclosure-scoped discovery, and an autonomous agent
bidder all ship as opt-in surfaces (`agent_rfq.enabled`, and
`agent_rfq.bidder_enabled` separately gating agent spending). Executing the won
work — turning an award into a run — is on the roadmap.

Everything a strategy needs is handed to it in a single request, and everything
it decides comes back in a single response. Strategies never touch the database,
Redis, or the network directly — the pipeline loads the facts, calls the
strategy, and commits the result. That separation is what lets you swap a naive
round-robin for a revenue-maximizing scorer without changing anything else.

<Callout title="Where strategies run" type="info">
  Strategies execute in the **Select** stage of the routing pipeline, after
  candidates have been validated, enriched, and filtered. See
  [Routing pipeline](/docs/concepts/routing-pipeline) for the full
  Validate → Enrich → Filter → Select → Assign flow.
</Callout>

## The Strategy interface [#the-strategy-interface]

Every strategy implements two methods, defined in `pkg/strategy/interface.go`:

```go
type Strategy interface {
    // Info returns metadata: name, versions, capabilities. Called FIRST,
    // during loading, for compatibility checking — before any Select call.
    Info() Info

    // Select chooses one candidate from req.Candidates (guaranteed non-empty).
    // MUST be safe for concurrent calls from many goroutines.
    Select(ctx context.Context, req *SelectRequest) (*SelectResponse, error)
}
```

Implementations must be:

* **Thread-safe** — `Select` may be called concurrently from many goroutines.
  Any internal state (counters, cursors) must be synchronized.
* **Stateless, or backed by thread-safe state** — a round-robin counter is fine
  if it uses atomics or a shared cache; a plain map is not.
* **Idempotent for identical inputs** — the same request should produce the same
  decision. (Randomized and bandit strategies are the deliberate exception:
  their non-determinism is part of the contract.)

### The hot path: Info first, then Select [#the-hot-path-info-first-then-select]

Loading a strategy is a two-phase handshake:

1. **`Info()` is called once, at load time.** It returns the strategy's `Name`,
   `InterfaceVersion`, `PluginVersion`, optional `MinRouterVersion`, and its
   declared `Capabilities`. Ductor checks interface-version compatibility here
   and refuses to load anything it can't speak to. `Info()` never runs on the
   hot path.
2. **`Select()` is called on every routing decision.** This is the hot path: it
   receives a `*SelectRequest`, returns a `*SelectResponse`, and must be fast
   and concurrency-safe. `req.Candidates` is guaranteed non-empty — the pipeline
   returns an error before calling a strategy if filtering left nothing.

### What a strategy receives and returns [#what-a-strategy-receives-and-returns]

The `SelectRequest` (in `pkg/strategy/types.go`) carries everything the strategy
is allowed to see:

| Field        | Type                    | Purpose                                                    |
| ------------ | ----------------------- | ---------------------------------------------------------- |
| `Routable`   | `*Routable`             | The item being routed — attributes, location, priority.    |
| `Candidates` | `[]Candidate`           | The eligible recipients, pre-filtered. Non-empty.          |
| `PoolID`     | `string`                | The pool being routed to.                                  |
| `Options`    | `DynamicMap`            | Strategy-specific configuration for this call.             |
| `Context`    | `*SelectContext`        | Trace ID, tenant ID, routing depth, `DryRun` flag.         |
| `Features`   | `*FeatureSnapshot`      | Typed snapshot of outcome/quality/capacity/SLA facts.      |
| `State`      | `*StrategyStateReadSet` | Frozen state snapshot for stateful strategies (read-only). |

Each `Candidate` exposes `ID`, `Weight`, `CurrentLoad`, `MaxConcurrent`,
`Location`, `Attributes`, `Tags`, a pre-computed rule `Score`, and
`LastAssignedAt`. A strategy reads whichever of these it needs.

The `SelectResponse` carries the decision back:

| Field                  | Type                 | Purpose                                                                           |
| ---------------------- | -------------------- | --------------------------------------------------------------------------------- |
| `Selected`             | `*Candidate`         | The winner. Must be non-nil on success.                                           |
| `Reason`               | `string`             | Short code, e.g. `highest_weight`, `least_loaded`, `nearest_geo`.                 |
| `Metadata`             | `DynamicMap`         | Scores, distances, probabilities — anything worth surfacing.                      |
| `Explain`              | `*SelectExplanation` | Detailed per-candidate reasoning (see [ExplainStrategy](#capability-interfaces)). |
| `Slate`                | `*RankedSlate`       | An ordered slate when the strategy ranks alternates.                              |
| `StateMutationIntents` | `[]…`                | Deterministic state changes to apply *after* the decision commits.                |

<Callout title="State is read at Select, written after commit" type="warn">
  Stateful strategies never mutate durable state inside `Select`. They read a
  frozen `State` snapshot and return `StateMutationIntents` describing what should
  change once the decision is durably accepted. This keeps selection replayable
  and free of side effects. See [Strategy state plane](/docs/strategies/contracts#strategy-state-plane).
</Callout>

## The registry [#the-registry]

Strategies are discovered and instantiated through the `Registry`
(`pkg/strategy/registry.go`). It maps a name to a **factory**, creates instances
lazily, and resolves aliases.

```go
reg := strategy.NewRegistry()
builtin.RegisterBuiltins(reg)              // register the built-in pack
reg.RegisterAlias("rr", "smooth_weighted_round_robin")

s, err := reg.Get("smooth_weighted_round_robin") // lazy, lock-free hot path
```

Key properties:

* **Lazy, exactly-once instantiation.** Factories run inside a `sync.Once` on
  first `Get`; after that, lookups are a lock-free `sync.Map` load. A factory can
  safely call back into the registry to resolve a dependency (for example,
  `fair_catchup` resolving its base strategy) without deadlocking.
* **Aliases.** `RegisterAlias(alias, target)` points a second name at an existing
  factory. Alias chains resolve transitively with cycle protection. Every
  strategy's documented aliases below are real registry aliases.
* **Configured instances.** `Configure(instanceName, factoryName, config)` and
  `GetWithConfig(name, config)` produce named or per-config instances so the same
  algorithm can be bound to different parameters in different pools.

A pool names the strategy it wants; the routing pipeline calls `reg.Get(name)`
and runs `Select`. Swapping a pool's strategy is a config change, not a
redeploy.

### The built-in registry [#the-built-in-registry]

Ductor includes the following built-in strategy names. Aliases resolve to the
same strategy factory.

| Name                          | Aliases                               | What it does                               |
| ----------------------------- | ------------------------------------- | ------------------------------------------ |
| `smooth_weighted_round_robin` | `round_robin`, `weighted_round_robin` | Fair weighted distribution (SWRR).         |
| `consistent_hash`             | —                                     | Route the same key to the same recipient.  |
| `priority_score`              | —                                     | Weight + load + availability blend.        |
| `least_loaded`                | —                                     | Lowest current load wins.                  |
| `random_weighted`             | —                                     | Weighted random pick.                      |
| `power_of_two_choices`        | `p2c`                                 | Sample two, take the less-loaded.          |
| `failover`                    | `active_passive`                      | Tiered active-passive HA.                  |
| `fair_catchup`                | `catch_up`                            | Fair distribution with catch-up debt.      |
| `shark_tank`                  | `broadcast_claim`                     | Broadcast, then first to claim wins.       |
| `multi_objective_score`       | —                                     | Tunable multi-signal scorer.               |
| `sla_deadline`                | —                                     | Deadline- and catchall-aware SLA fit.      |
| `yield_optimized`             | —                                     | Maximize expected commercial yield.        |
| `portfolio_balance`           | —                                     | Cap any one recipient's recent share.      |
| `availability_first`          | —                                     | Prefer candidates with fresh free time.    |
| `soonest_available`           | —                                     | Earliest qualifying free slot.             |
| `coverage_balancer`           | —                                     | Balance free minutes and busy load.        |
| `connector_quota_aware`       | —                                     | Filter/score by connector quota readiness. |
| `reliability_weighted`        | —                                     | Score by reliability-readiness facts.      |
| `entity_sticky_assignment`    | —                                     | Prefer an entity graph's sticky recipient. |
| `capacity_weighted_allocator` | —                                     | Batch plan with capacity deductions.       |
| `portfolio_quota_allocator`   | —                                     | Batch plan under portfolio quotas.         |

Two contrib modules add more names when installed and enabled:

* **`predicted`** (`modules/strategies/predicted/register.go`) — `predicted_value`
  plus the bandits `thompson_sampling`, `linucb`, and `bandit`. See
  [Learning](/docs/strategies/learning).
* **`geographic`** (`modules/strategies/geographic`) — `geo_nearest` and
  `geo_weighted`. See [Geo](/docs/strategies/geo).

<Callout title="Every name must resolve to a contract" type="warn">
  A hard rule of the routing surface: no strategy name, alias, recipe reference, or
  MCP/API surface may exist unless it resolves to a **strategy contract**, and every
  new strategy must ship its **descriptor and contract in the same change**.
  Descriptors render the authoring UI; contracts tell headless tools and validators
  what a strategy can safely consume and produce. See
  [Contracts](/docs/strategies/contracts).
</Callout>

## Capability interfaces [#capability-interfaces]

The base `Strategy` interface is deliberately small. A strategy that can do more
implements an **optional capability interface** and advertises it in
`Info().Capabilities`. The pipeline type-asserts for the interface before using
it, so a strategy only pays for what it supports.

| Interface             | Capability constant                            | Method it adds                                                        |
| --------------------- | ---------------------------------------------- | --------------------------------------------------------------------- |
| `BatchStrategy`       | `CapabilityBatchSelect` (`batch_select`)       | `SelectBatch` — score many routables in one call.                     |
| `AllocationStrategy`  | `CapabilityAllocationPlan` (`allocation_plan`) | `Allocate` → a durable `AllocationPlan` with hard global constraints. |
| `ExplainStrategy`     | `CapabilityExplainDecision` (`explain`)        | `SelectWithExplanation` — populate `Explain` with reasoning.          |
| `HealthCheckStrategy` | `CapabilityHealthCheck` (`health_check`)       | `HealthCheck` — report readiness before use.                          |
| `RankedStrategy`      | `CapabilityRankedSelect` (`ranked_select`)     | `SelectRanked` → populate `Slate` with a ranked / multi-winner list.  |

The remaining capabilities are pure metadata (no method): `CapabilityWeighted`
(honors candidate weights), `CapabilityStateful` (keeps state across selections),
`CapabilityGeoAware` (uses location), `CapabilityMetrics` (exposes Prometheus
metrics), `CapabilityMultiWinner` (marks several winners in one slate), and
`CapabilityPipelineStage` (opts a strategy into bounded composite / pipeline
execution). The complete set — eleven constants — lives in
`pkg/strategy/version.go`; `IsValidCapability` and `AllCapabilities` enumerate
them.

<Callout title="Strategies read facts only from Features" type="warn">
  A strategy may read only what arrives on the `SelectRequest` — chiefly
  `req.Features`, the typed [feature snapshot](/docs/strategies/contracts#strategy-feature-snapshot).
  Strategies must **never** reach into outcome, quality, or connector repositories
  directly. The pipeline loads the facts once, freezes them into the snapshot, and
  hands them over; that is what keeps every decision replayable, shadowable, and
  auditable.
</Callout>

## Params, modes, and aliases [#params-modes-and-aliases]

Throughout these pages each strategy is documented with three consistent
attributes:

* **Params** — typed, tunable knobs. Each has a `kind` (`slider`, `number`,
  `boolean`, `string`, `enum`), a `default`, and for numeric kinds a
  `min`/`max`/`step`. Params are validated against their spec before they reach
  a pool, so dead knobs never get persisted (`pkg/strategy/validate_params.go`).
* **Modes** — the shape of selection a strategy operates in: `weighted`,
  `scoring`, `single`, `ranked`, `multi_winner`, or `broadcast`. A strategy may
  support more than one.
* **Aliases** — alternative registry names that resolve to the same strategy.

## Choosing a strategy [#choosing-a-strategy]

Start from the problem, not the algorithm:

| If you need to…                               | Reach for                                             | Category                                    |
| --------------------------------------------- | ----------------------------------------------------- | ------------------------------------------- |
| Spread load fairly and cheaply                | `smooth_weighted_round_robin`, `power_of_two_choices` | [Balance](/docs/strategies/balance)         |
| Send identical keys to the same recipient     | `consistent_hash`                                     | [Balance](/docs/strategies/balance)         |
| Fail over from a primary to a backup          | `failover`                                            | [Balance](/docs/strategies/balance)         |
| Blend several business signals into one score | `multi_objective_score`                               | [Scoring](/docs/strategies/scoring)         |
| Beat an SLA / deadline                        | `sla_deadline`                                        | [Scoring](/docs/strategies/scoring)         |
| Maximize revenue per route                    | `yield_optimized`                                     | [Scoring](/docs/strategies/scoring)         |
| Cap any one recipient's share                 | `portfolio_balance`                                   | [Scoring](/docs/strategies/scoring)         |
| Learn who wins over time                      | `thompson_sampling`, `linucb`                         | [Learning](/docs/strategies/learning)       |
| Route by geographic proximity                 | `geo_nearest`, `geo_weighted`                         | [Geo](/docs/strategies/geo)                 |
| Let recipients race to claim work             | `shark_tank`                                          | [Markets](/docs/strategies/markets)         |
| Produce ordered fallbacks                     | ranked slates                                         | [Ranked](/docs/strategies/ranked)           |
| Optimize a whole batch at once                | allocation planners                                   | [Allocation](/docs/strategies/allocation)   |
| Match on skills, licenses, territory          | eligibility profiles                                  | [Eligibility](/docs/strategies/eligibility) |
| Drain traffic away from failing targets       | reliability strategies                                | [Reliability](/docs/strategies/reliability) |
| Route work to humans or AI agents             | work assignment                                       | [Work](/docs/strategies/work)               |
| Compose stages into one primitive             | strategy pipelines                                    | [Pipelines](/docs/strategies/pipelines)     |
| Version, replay, and govern behavior          | contracts & governance                                | [Contracts](/docs/strategies/contracts)     |

## Browse by category [#browse-by-category]

<Cards>
  <Card title="Balance" href="/docs/strategies/balance">
    Load spreading, stickiness, and failover — the seven workhorse distribution
    strategies.
  </Card>

  <Card title="Scoring" href="/docs/strategies/scoring">
    Score candidates on weight, SLA, yield, portfolio share, or predicted value.
  </Card>

  <Card title="Learning" href="/docs/strategies/learning">
    Multi-armed and contextual bandits that learn winners from outcomes.
  </Card>

  <Card title="Geo" href="/docs/strategies/geo">
    Distance-based and proximity-weighted routing.
  </Card>

  <Card title="Markets" href="/docs/strategies/markets">
    Broadcast claims and durable market settlement records.
  </Card>

  <Card title="Yield & Quality" href="/docs/strategies/yield-and-quality">
    Analytics, forecasts, heatmaps, and quality signals consumed by scoring strategies.
  </Card>

  <Card title="Capacity Marketplace" href="/docs/strategies/capacity">
    Listings, orders, trades, and pricing for spare recipient capacity.
  </Card>

  <Card title="Ranked" href="/docs/strategies/ranked">
    Ordered slates: selected, alternates, eliminated, pending.
  </Card>

  <Card title="Allocation" href="/docs/strategies/allocation">
    Batch-level assignment plans optimized against global constraints.
  </Card>

  <Card title="Eligibility" href="/docs/strategies/eligibility">
    Trait, skill, license, and territory matching.
  </Card>

  <Card title="Reliability" href="/docs/strategies/reliability">
    Readiness signals and reliability-weighted scoring.
  </Card>

  <Card title="Work" href="/docs/strategies/work">
    Assign work to humans, teams, queues, or AI agents.
  </Card>

  <Card title="Pipelines" href="/docs/strategies/pipelines">
    Compose filter → select → fallback into one reusable primitive.
  </Card>

  <Card title="Contracts" href="/docs/strategies/contracts">
    Versioned contracts, recipes, feature snapshots, and the state plane.
  </Card>

  <Card title="Governance" href="/docs/strategies/governance">
    Experiments, shadow campaigns, tuning proposals, certification, custom runtimes.
  </Card>

  <Card title="Write a custom strategy" href="/docs/strategies/writing-a-custom-strategy">
    Implement the interface, register it, and certify it for production.
  </Card>
</Cards>
