Routing Strategies

Pipelines

Compose existing strategies into one bounded, validated filter → select → fallback primitive, declared separately from strategy_options, with a stage-by-stage explain trace.

A strategy pipeline composes existing routing strategies into a single bounded, validated primitive. Instead of bolting an ad-hoc base_strategy and fallback_strategy onto every scorer, you declare the stages once — filter → select → fallback — name the pipeline, and point a pool at it. Each stage is a real, contract-checked strategy, and the pipeline persists a stage-by-stage trace so a decision is explainable end to end.

Pipelines are declared on pool config as strategy_pipelines and default_strategy_pipeline, and can run from a workflow DAG through routing.strategy_pipeline.

Declared separately from strategy_options

A pipeline is not a strategy option. It lives in its own strategy_pipelines map on the pool config and is referenced by default_strategy_pipeline. The two are mutually exclusive with a single default_strategy / default_recipe: pool validation rejects a config that sets both.

Shape

A pipeline has a name, a version, and an ordered list of stages. Each stage declares a type, a unique id, and — for select and fallback — a strategy with optional flat options.

A pool strategy_pipelines entry
{
  "name": "vip-capacity",
  "version": "2026-06-29",
  "stages": [
    {
      "id": "vip_only",
      "type": "filter",
      "condition": "tag:vip",
      "on_no_candidates": "stage:fallback"
    },
    {
      "id": "score",
      "type": "select",
      "strategy": "priority_score",
      "options": { "tie_tolerance": 0.001 }
    },
    {
      "id": "fallback",
      "type": "fallback",
      "strategy": "smooth_weighted_round_robin"
    }
  ]
}

Attached to a pool it looks like this:

pool config
default_strategy_pipeline: vip-capacity
strategy_pipelines:
  vip-capacity:
    name: vip-capacity
    version: "2026-06-29"
    stages:
      - id: vip_only
        type: filter
        condition: "tag:vip"
        on_no_candidates: "stage:fallback"
      - id: score
        type: select
        strategy: priority_score
        options: { tie_tolerance: 0.001 }
      - id: fallback
        type: fallback
        strategy: smooth_weighted_round_robin

The vip-capacity pipeline above runs its stages in order, with the filter's on_no_candidates jump routing an empty VIP set straight to the fallback:

survivors on_no_candidates Candidates vip_onlyfilter: tag:vip scoreselect: priority_score fallbacksmooth_weighted_round_robin Winner

Stage types and bounds

TypeRole
filterPrune candidates by a predicate; jump forward when it empties the set.
selectRun a selection strategy over the survivors. At least one is required.
fallbackThe strategy to fall through to when a jump lands here.

The compiler enforces hard bounds:

  • a pipeline must contain at least one select stage;
  • stage IDs must be unique;
  • jumps must point forward to an existing stage (on_no_candidates: "stage:<id>");
  • the default maximum is 16 stages.

Filter predicates

Filters are deliberately simple and candidate-scoped. The supported conditions are:

PredicateKeeps candidates that…
tag:<tag>carry the tag.
metadata:<key>=<value>have the matching metadata value.
attribute:<key>=<value>have the matching attribute value.
has_capacityhave free capacity.
min_weight:<number>meet a minimum weight.
max_load:<number>are at or below a load ceiling.
min_capacity:<number>have at least this much free capacity.

The same predicates can be supplied as flat stage options instead of a condition string — for example {"tag":"vip"} or {"metadata_key":"region", "metadata_value":"east"}.

Precedence

When several things could pick a strategy, resolution order is:

request `strategy`  >  rule-result `strategy`  >  pool top-level `strategy`
   >  pool default_strategy_pipeline

A route-level or rule-level explicit strategy always means a single strategy and wins. The pool's default_strategy_pipeline is consulted only when no request, rule, or top-level pool strategy override exists.

Decision metadata and explain trace

A successful pipeline selection persists compact metadata keys on the decision — never routable attributes or connector payloads:

  • strategy_pipeline
  • strategy_pipeline_version
  • strategy_pipeline_stage_count
  • strategy_pipeline_stage
  • strategy_pipeline_child_strategy
  • strategy_pipeline_trace

In explain mode the same stage-by-stage trace is returned under strategy_explain.steps: each entry records the stage ID, candidate count, the child strategy name, the selected ID, and any fallback or error transition.

Running a pipeline from a DAG

A workflow DAG can execute the exact same primitive with the routing.strategy_pipeline step. The input accepts pipeline, routable, candidates, and pool_id, and it emits the selected recipient IDs plus the pipeline name, version, final child strategy, and stage trace. See DAG workflow model.

Gotchas

Three sharp edges

  • Nested option values are rejected. Each child strategy receives only its own flat StrategyConfig; a stage options map with a nested object fails validation. This is deliberate — it keeps every child strategy's config the same flat shape it would get standalone.
  • Pipelines never pass through Registry.GetWithConfig. They are compiled at the selector or DAG boundary, so a pipeline spec is never handed to the registry as if it were a single configured strategy.
  • Batch preselection skips pipeline pools. A pool that resolves to a strategy pipeline is skipped during batch preselection; those routables fall through to normal per-routable selection, where the compiled pipeline runs and applies health checks to each child strategy.