Routing rules & strategies
Create pools, write CEL routing rules, and pick a selection strategy — via the API or a declarative bundle.
This guide is the routed stage, hands-on: how a Route binds Work to a Worker. Ductor's routing plane decides where a routable lands. It has three moving parts:
- Pools — named destinations that hold recipients.
- Rules — priority-ordered CEL expressions that select a pool.
- Strategies — pluggable algorithms that pick a recipient within the chosen pool.
This guide builds a small topology two ways: imperatively through the API, and declaratively through a routing bundle.
Option A — build it through the API
1. Create pools
curl -s -X POST http://localhost:8080/api/pools \
-H "Authorization: Bearer $DUCTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "name": "Primary Pool", "strategy": "weighted", "status": "STATUS_ACTIVE" }'The strategy field names the selection algorithm the pool uses (see
Strategies). A pool with a single recipient degenerates to
direct dispatch; give a pool at least two recipients for a strategy to have
something to weight.
2. Add recipients
curl -s -X POST http://localhost:8080/api/pools/$POOL_ID/recipients \
-H "Authorization: Bearer $DUCTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"pool_id": "'$POOL_ID'",
"name": "Jane Smith",
"type": "agent",
"status": "STATUS_ACTIVE",
"state": "RECIPIENT_STATE_AVAILABLE",
"weight": 1.0,
"capacity": { "max_concurrent": 10, "daily_limit": 50 },
"timezone": "America/New_York"
}'3. Write routing rules
Rules are CEL expressions evaluated in priority order — lower number wins.
Over the API, the expression field is named cel_expression. It evaluates
against a small, fixed environment — the incoming routable, the candidate
recipient, and now — so you match on routable.attributes.*,
routable.priority, and recipient.attributes.* (there is no tenant variable
in scope). Create a rule that routes EU traffic to a residency pool:
# EU residency rule (high priority)
curl -s -X POST http://localhost:8080/api/pools/$SECONDARY_POOL_ID/rules \
-H "Authorization: Bearer $DUCTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "name": "route-eu-to-secondary", "cel_expression": "routable.attributes.region == '\''EU'\''", "priority": 100, "enabled": true }'Validate an expression before you commit it — POST /api/rules/validate compiles
it against the same environment and returns type/syntax errors without persisting
anything:
curl -s -X POST http://localhost:8080/api/rules/validate \
-H "Authorization: Bearer $DUCTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "cel_expression": "routable.attributes.region == '\''EU'\''" }'Rules are cached and hot-reloaded across pods via Redis pub/sub. Tune the cache
with routing.rule_cache_size (default 1000) and routing.rule_cache_ttl
(default 30s); leave rules.hot_reload on (default true) so rule edits
propagate without a restart. For the full field set, lifecycle verbs, and the
pub/sub invalidation mechanism, see Rules.
Option B — declare the topology as a bundle
For GitOps, declare pools, recipients, rules, and workflow refs in a single YAML bundle and let the reconciler apply it. Point Ductor at the file:
# ductor.yaml
routing:
bundle_file: ./configs/example-routing.yaml
bundle_tenant: tenant-1The bundle itself is strict (unknown keys fail the apply, so typos surface loudly) and dependency-ordered — pools, then recipients, then rules:
pools:
- name: primary
display_name: Primary Pool
categories: [transactional]
strategy: weighted
- name: secondary
display_name: Secondary Pool
categories: [transactional, eu]
strategy: weighted
recipients:
- name: primary-1
pool_name: primary
enabled: true
- name: primary-2
pool_name: primary
enabled: true
rules:
- name: route-eu-to-secondary
expression: "routable.attributes.region == 'EU'"
priority: 100
pool_name: secondary
enabled: true
- name: default-to-primary
expression: "true" # catch-all — must always evaluate true
priority: 999
pool_name: primary
enabled: trueField name differs by surface
The rule's CEL string is named cel_expression over the API and
expression in a bundle — the two surfaces mirror the same
domain/rule field under different YAML/JSON tags. The CEL environment is
identical either way (routable, recipient, now).
On startup the reconciler applies the bundle (creates/updates run pools →
recipients → rules; deletes run child-first). You can also apply, diff, export,
and roll back bundles over the API (POST /api/routing/bundle:apply and
friends).
Bundle values are literal
The bundle parser does not template — no ${SECRETS.x}, anchors, or
!include. Keep secrets out of the bundle and inject them through connector
connections instead.
Pick a strategy
A strategy is the algorithm that selects a recipient within the chosen pool.
Set it per-pool via the pool's strategy field. Built-in strategies live in the
strategy registry; weighted / weighted_random distribute by recipient
weight, respecting each recipient's capacity and availability state.
Strategies implement a small interface — Info() is called first for
compatibility checking, and Select() is the hot path. Optional capability
interfaces (BatchStrategy, ExplainStrategy, HealthCheckStrategy) let a
strategy opt into batch selection, decision explanations, and health probing.
Custom strategies register through the strategy registry and, for remote
strategies, can be wired as strategy deployments
(routing.strategy_deployments.*) with per-call timeouts, circuit breaking, and
a fail-open/closed policy.
The full catalog of built-in strategies — allocation, balance, scoring, ranked, geo, eligibility, and the rest — lives in Strategies; start there when picking the algorithm for a pool. To author routing logic beyond a single CEL predicate — multi-step selection graphs with bindings and env scoping — see the Route DSL v2 in Route authoring.
How a decision flows
A routable moves through the Routing Pipeline,
carried by a mutable RoutingContext:
Rules select the pool during the pipeline; the pool's strategy selects the recipient at the Select stage; the Assign stage reserves capacity and finalizes the decision.
Next steps
Routing pipeline
The stages every routable passes through.
Strategies
The full catalog of built-in selection algorithms.
Rules
The rule object, lifecycle verbs, and hot-reload mechanics.
Route authoring
Route DSL v2 for multi-step selection graphs.
Configuration reference
All routing.* and router.* tuning knobs.