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.
{
"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:
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_robinThe 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:
Stage types and bounds
| Type | Role |
|---|---|
filter | Prune candidates by a predicate; jump forward when it empties the set. |
select | Run a selection strategy over the survivors. At least one is required. |
fallback | The strategy to fall through to when a jump lands here. |
The compiler enforces hard bounds:
- a pipeline must contain at least one
selectstage; - 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:
| Predicate | Keeps candidates that… |
|---|---|
tag:<tag> | carry the tag. |
metadata:<key>=<value> | have the matching metadata value. |
attribute:<key>=<value> | have the matching attribute value. |
has_capacity | have 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_pipelineA 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_pipelinestrategy_pipeline_versionstrategy_pipeline_stage_countstrategy_pipeline_stagestrategy_pipeline_child_strategystrategy_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 stageoptionsmap 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.
Related
Contracts & recipes
Each child strategy is contract-validated against the pipeline_stage execution shape.
Eligibility
The governed qualification layer that runs before a pipeline's filter stages.
Routing pipeline
The outer Validate → Enrich → Filter → Select → Assign flow a pipeline plugs into.
Route authoring
Declaring strategy_pipelines and default_strategy_pipeline on a pool.
Work
The work-assignment bridge — routing human and AI work into the shared strategy substrate, with work.* recipes, a case-management assignment registry, and human_loop workflow steps.
Contracts
The governance layer — strategy contracts, versioned recipes, the contract/recipe API and MCP surface, feature snapshots, and the durable strategy state plane.