Route structure
A YAML route file defines one or more routes. Each route has an identifier, a source endpoint, and an ordered list of processing steps.
Minimal route
The smallest useful route reads from a source and writes to a destination.
routes:
- id: "hello-timer"
from: "timer:tick?period=2000&repeatCount=3"
steps:
- log: "Hello from config-loaded route!"
- to: "log:info"
The file opens with a routes list. Each list entry is one route object. The
route above, hello-timer, reads from a timer endpoint, logs a greeting, and
forwards the exchange to the log:info endpoint.
Route fields
Each route object accepts these top-level fields. Only id and from are
required. The error_handler and circuit_breaker objects are documented
below. The security_policy object is documented in
Authorization.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | yes | — | Unique route identifier |
from | string | yes | — | Source endpoint URI |
steps | list | no | [] | Ordered step verbs |
auto_startup | bool | no | true | Start the route when the context starts |
startup_order | integer | no | 1000 | Ascending start order; shutdown reverses it |
sequential | bool | no | false | Process exchanges one at a time |
concurrent | integer | no | — | Maximum concurrent exchanges |
error_handler | object | no | — | Per-route error handler |
circuit_breaker | object | no | — | Route-level circuit breaker with optional fallback sub-pipeline |
security_policy | object | no | — | Route-level authorization |
on_complete | string | no | — | Producer URI for the success hook |
on_failure | string | no | — | Producer URI for the failure hook |
auto_startup: false registers the route but does not start its consumer. Start
it later through the route controller or control bus. concurrent caps how many
exchanges the pipeline processes in parallel; omit it to let the runtime decide.
on_complete and on_failure fire when an exchange exits the pipeline, on
success or on error respectively.
Error handling
Set error_handler to retry failed exchanges and send them to a dead letter
channel when retries run out. The handler holds a redelivery policy and optional
per-exception clauses. The full field set lives on the
step verbs reference.
Circuit breaker
Set circuit_breaker to protect a route from a failing downstream service. The
breaker opens after failure_threshold consecutive failures. While open, it
rejects exchanges for open_duration_ms. See Circuit
breaker for the breaker states.
The optional fallback list holds a sub-pipeline. The breaker runs the
sub-pipeline instead of rejecting the exchange while the circuit is open. An
absent or empty fallback keeps the existing behavior: the breaker returns
CircuitOpen.
circuit_breaker:
failure_threshold: 1
open_duration_ms: 60000
fallback:
- cache_peek_stale:
key: "user-profile-42"
The fallback runs on routes with and without an error_handler. A fallback
step that stops cleanly (for example, a cache_peek_stale MISS with the
default on_miss: stop) surfaces Ok(exchange) with the exchange state
intact. No CircuitOpen escapes.
A failing fallback step follows the route's error handling. A route with an
error_handler routes the failure through the handler. A route without one
surfaces the raw error to the caller.
The Cache page shows the stale-on-error composition with
cache_peek_stale.
Half-open fallback asymmetry
During the half-open probe-in-flight window, the fallback behavior differs by route shape. A route with an error_handler compiles the breaker into a CircuitBreakerGate. The gate serves the fallback to concurrent callers while the probe runs. A route without an error_handler compiles the breaker into a Tower CircuitBreakerService. That service rejects concurrent callers with CircuitOpen while the probe runs, even when a fallback is configured.
Both behaviors are sound. The gate keeps a single probe in flight and serves stale fallback data. The service keeps a single probe in flight and rejects. The asymmetry is intentional. See Circuit breaker for the breaker states.
Security policy
Set security_policy to authorize exchanges before the steps run. The object
takes one of five forms: roles, scopes, ref, wasm, or permission.
The full policy model lives in Authorization.
The optional credential_sources list names where the route reads its
credential. When absent, the default is [authorization_header]: the route
reads the Authorization header only. Each entry names one source:
| Form | Meaning |
|---|---|
authorization_header | Bearer token in the Authorization header |
query_param: { param: <name> } | Token in a query parameter |
cookie: { name: <name> } | Token in a cookie |
header: { name: <name> } | API key in a named custom header |
routes:
- id: tile-service
from: "http://0.0.0.0:8090/tiles"
security_policy:
roles: ["tile-user"]
credential_sources:
- cookie: { name: session }
- query_param: { param: token }
- header: { name: X-Api-Key }
- authorization_header
steps:
- set_header:
key: CamelHttpResponseCode
value: 200
- set_header:
key: Content-Type
value: "application/json"
- set_body: '{"layer":"streets","status":"authenticated"}'
Extraction runs in the declared order. The first source that supplies a
credential wins. credential_sources is valid only with the roles or
scopes form. Load-time validation rejects malformed credential_sources
entries: an empty list, an empty parameter or cookie name, and a header name
that is not a valid RFC 9110 token. See Authorization
for the extraction semantics and
ADR-0059.
The optional provider string names the configured authenticator for the
route. The name must match one of the configured providers: keycloak,
oidc, or native. When more than one provider is configured, provider
is required. A route without it fails to load, and the error names the
available providers. An unknown provider name also fails the load. Like
credential_sources, provider is valid only with the roles or scopes
form.
REST block policy
A rest: block accepts the same security_policy object, declared once
on the block. Lowering copies it onto every route the block produces, so
every operation enforces the same policy before its handler runs. The
object takes the same five forms and passes the same load-time validation
as the route-level key, including credential_sources, provider, and
audiences.
rest:
- host: 0.0.0.0
port: 9090
path: /api/users
security_policy:
roles: ["user"]
provider: "native-demo"
operations:
- method: GET
operation_id: listUsers
to: direct:listUsers
produces: application/json
- method: POST
operation_id: createUser
consumes: application/json
produces: application/json
success_status: 201
to: direct:createUser
- method: PUT
path: /{id}
operation_id: updateUser
consumes: application/json
produces: application/json
to: direct:updateUser
- method: DELETE
path: /{id}
operation_id: deleteUser
to: direct:deleteUser
success_status: 204
The source lives at
examples/rest-crud/routes/secured.yaml.
Run the runnable variant with cargo run -p rest-crud --bin secured: a
request without a credential gets 401 before any handler runs, and a
token whose principal holds the required role is granted. A block without
security_policy lowers to public routes.
List-form variant
The hot-reload subsystem consumes a flatter form. When a file holds only routes
and omits the top-level routes wrapper, the parser reads the file as a bare
list of route objects:
- from: timer:hot-reload?period=1000
route_id: hot-reload-route
steps:
- to: log:info?showHeaders=true
Each entry uses route_id instead of id and carries from and steps. This
list form maps to the canonical route contract the runtime exchanges over the
control bus, not the full authoring model.
Next
- Step verbs: every verb and its fields
Reference: DSL crate