YAML routes for coding agents
How an AI coding assistant should write Cameleer YAML routes: start from a tested skeleton, follow a short list of rules, look up component options instead of guessing, and run
cameleer validateuntil the file is clean before running it.
This page is written for coding agents (Claude Code, Codex, Copilot, Cursor and similar) and for
the people who review their work. It is deliberately strict: it lists one way to do each thing,
the way that is known to work. The complete syntax is in the YAML DSL reference.
Every skeleton below runs in the test suite (crates/cameleer-main/tests/docs_yaml.rs).
The checklist
Do these in order for every route file you write or change:
- Name the file
<purpose>.camel.yaml. - Start from the closest skeleton on this page. Do not start from memory of the Java DSL or Spring XML.
- For every endpoint, open
docs/components/<scheme>.md(or runcameleer catalog components) and use only options listed there. Never invent an option. - Put hosts, ports, credentials and tokens in placeholders:
{{env:NAME:default}}for environment variables,{{name:default}}for properties. No secret ever goes in the file. - Give every route an
id(kebab-case, unique across files). - Validate:
cameleer validate <file> --format json. Fix every finding with severityerror, read everywarning, and repeat until"valid": true. - Run it:
cameleer run <file>(add--max-seconds 10for a smoke test), or in Docker:docker run --rm -v "$PWD:/routes:ro" pmsudhi/cameleer run /routes/<file>. - If you changed Cameleer itself rather than a route, run
cargo test --workspace.
Rules that prevent the usual mistakes
Structure
- The file is a YAML list: every top-level item starts with
-and has exactly one key (route,rest,restConfiguration,routeConfiguration,routeTemplate,templatedRoute,errorHandler,beans). - A route is
- route:withidandfrom;fromhasuriandsteps;stepsis indented underfrom, aligned withuri. - Each step is one list item with exactly one key, the step name. All options of a step go
under that key. A step that contains steps (
filter,split,choice/when,doTry,aggregate,loop, ...) has its ownstepslist under its own keys. choicetakeswhen:(a list, each item with one expression key andsteps) andotherwise:(a mapping withsteps).- Use Camel 4 names exactly:
setBody,setHeader,toD,wireTap,doTry/doCatch/doFinally,onException,aggregationStrategy,completionSize. They are camelCase.
Expressions
- Write expressions in the short form:
simple: "...",constant: ...,header: name,jsonpath: "$.x". One language key per step. - Always quote Simple expressions with double quotes. Use single quotes for string literals
inside Simple:
simple: "${header.type} == 'gold'". - A JSON body built with Simple goes in single quotes:
simple: '{"id": "${header.id}"}'. ${...}is evaluated only in expressions,logmessages andtoDURIs. In atoURI it is literal text: usetoDwhen the URI depends on the message.- Header values from HTTP requests, files and queues are text: compare numbers with
${header.amount} > 100(Simple converts) rather than string equality.
Endpoints
- Prefer
uri:plusparameters:over long inline query strings: values are URL-encoded for you and option names are easy to check. - Quote any value that starts with
{{,{,*,&,!,%or@, or that contains:or#. - Use
direct:to connect your own routes;seda:for an in-process queue. Sending from a request/reply route (REST, HTTP) toseda:with the default pattern waits for the consumer; addpattern: InOnlyto hand the message off and reply at once. - Use booleans
true/falseonly.yes,no,on,offare strings in YAML 1.2 and are rejected or ignored.
Behaviour
- Error handling goes in a
routeConfiguration(shared) or anonExceptionat the start of a route's steps. Usehandled: {constant: true}when the error should not reach the caller. - After
split, the message is the original one unless you setaggregationStrategy(groupedBodycollects the parts' bodies in a list). - Durations: plain numbers are milliseconds;
5s,250ms,PT1Malso work. - Do not use
beans:for Java classes: thecameleerbinary cannot create them. Use built-in aggregation strategies by name (groupedBody,useLatest,string) and Simple, JSONPath or jq instead of custom code.
Look up a component's options
docs/components/<scheme>.mdhas the URI format, every supported option with its default, the message headers, and a tested example. For exampledocs/components/kafka.md,docs/components/http.md,docs/components/aws2-s3.md.cameleer catalog componentslists every scheme in your build;cameleer catalog languageslists the expression languages;cameleer catalog kameletslists the Kamelets.- Options and headers have the same names as in Apache Camel. When a page and the Camel docs disagree, the page (and the validator) describe what Cameleer does.
- An option that is not supported fails validation with code
endpoint. Do not "fix" it by removing random options: look the option up.
Secrets and configuration
{{env:KAFKA_BROKERS:localhost:9092}} environment variable with a default
{{env:SLACK_TOKEN}} environment variable, required
{{orders.topic:orders}} property from application.properties, --prop or CAMEL_* env vars
{{aws:db-password}} secret from AWS Secrets Manager (also azure:, gcp:)A required placeholder (no default) that is not set fails at startup; cameleer validate
reports it as unresolved-placeholder (a warning, because it may be set where the route runs).
Pass test values with --prop key=value or --properties application.properties.
Skeletons
Copy the closest skeleton, rename ids and endpoints, then validate.
Consume, transform, deliver
Read JSON files from a folder, add a field, write them to another folder. The same shape works for Kafka, AMQP, SQS or MQTT: change the two URIs and their options.
- route:
id: enrich-orders
from:
uri: "file:{{env:INBOX_DIR:inbox}}"
parameters:
include: ".*\\.json"
move: .done
steps:
- unmarshal:
json: {}
- setHeader:
name: orderId
simple: "${body[id]}"
- setBody:
simple: '{"id": "${header.orderId}", "status": "received"}'
- log: "order ${header.orderId} received"
- to:
uri: "file:{{env:OUTBOX_DIR:outbox}}"
parameters:
fileName: "${header.orderId}.json"REST API
Two operations backed by direct: routes. Path parameters become headers; the response code
is the CamelHttpResponseCode header.
- restConfiguration:
contextPath: /api
- rest:
path: /devices
get:
- path: /{id}
id: get-device
to: direct:get-device
post:
- id: register-device
to: direct:register-device
- route:
id: get-device-route
from:
uri: direct:get-device
steps:
- setHeader:
name: Content-Type
constant: application/json
- setBody:
simple: '{"id": "${header.id}", "online": true}'
- route:
id: register-device-route
from:
uri: direct:register-device
steps:
- setHeader:
name: CamelHttpResponseCode
constant: 201
- setBody:
simple: "registered ${body}"Scheduled job
Call an HTTP API on a schedule and deliver the answer. timer takes a period in milliseconds;
use cron or quartz for calendar schedules.
- route:
id: poll-status
from:
uri: timer:poll-status
parameters:
period: "{{status.period:60000}}"
steps:
- setHeader:
name: CamelHttpMethod
constant: GET
- to: "{{env:STATUS_URL:http://localhost:8080/health}}"
- convertBodyTo: String
- log: "status: ${body}"
- to: "{{env:STATUS_SINK:log:status}}"Error handling
Retry, then send what still fails to a dead letter endpoint; turn bad input into a clear reply instead.
- routeConfiguration:
errorHandler:
deadLetterChannel:
deadLetterUri: "{{env:DEAD_LETTER_URI:log:dead-letters}}"
useOriginalMessage: true
redeliveryPolicy:
maximumRedeliveries: 3
redeliveryDelay: 1s
useExponentialBackOff: true
onException:
- onException:
exception:
- java.lang.IllegalArgumentException
handled:
constant: true
steps:
- setBody:
simple: "invalid input: ${exception.message}"
- route:
id: process-payment
from:
uri: direct:payment
steps:
- filter:
simple: "${body} == ''"
steps:
- throwException:
exceptionType: java.lang.IllegalArgumentException
message: empty payment
- to: direct:gateway
- route:
id: gateway
from:
uri: direct:gateway
steps:
- setBody:
simple: "charged ${body}"The validate loop
cameleer validate checks route files without starting routes or
connecting to anything. It is safe to run on any file, any number of times.
cameleer validate orders.camel.yaml --format json
cameleer validate routes/ --format json --strict --properties application.properties
docker run --rm -v "$PWD:/routes:ro" pmsudhi/cameleer validate /routesThe loop:
- Write the file.
- Run
cameleer validate <file> --format json. - If
validisfalse, fix each finding, usingline,column,messageandhint. Fix the firstyaml-syntaxerror first: later findings may come from it. - Repeat from step 2 until
validistrue(exit code 0). With--strict, warnings count as errors, which is a good default for generated files. - Run the route.
The finding codes:
| code | severity | meaning | typical fix |
|---|---|---|---|
yaml-syntax | error | the file is not valid YAML | fix indentation, quote the value |
dsl-structure | error | the YAML is not a valid route: unknown element, step or key, a missing required key, a wrong type; the hint suggests the name you probably meant | rename or move the key as the hint says |
endpoint | error | an unknown scheme or an option the component does not support (checked the same way Apache Camel does) | look up docs/components/<scheme>.md |
expression | error | an expression does not compile | fix the Simple/JSONPath/jq syntax |
unresolved-placeholder | warning | a {{...}} placeholder has no value and no default | add a default, or pass --prop/--properties |
dangling-endpoint | warning | to: direct:x but no route consumes from: direct:x in the validated files | add the route, or validate all files together |
duplicate-route-id | error | two routes have the same id | rename one |
Exit codes: 0 valid, 1 errors (or warnings with --strict), 2 bad usage. The full
reference, with the text and JSON output formats, is on the validate page.
Worked example
The request: "Add an endpoint POST /orders that takes a JSON order. Reject an order with
no id with HTTP 400 and a JSON error. Put valid orders on a queue and answer 202 at once. A
worker with four consumers writes each order to the folder in ORDERS_DIR as
order-<id>.json."
First draft. The agent starts from the REST skeleton and writes the route. One line of the draft:
- to: "seda:orders?concurrentConsumer=4"Validate.
$ cameleer validate orders.camel.yaml
orders.camel.yaml:33:15: error[endpoint]: seda:orders?concurrentConsumer=4: unknown option 'concurrentConsumer' (did you mean 'concurrentConsumers'?)
1 file, 1 error, 0 warningsTwo things are wrong here, and the validator found one of them: the option is
concurrentConsumers, and it belongs on the consumer (from), not on the producer. Looking up
docs/components/seda.md shows both. The agent also notices that a
REST route sending to seda: waits for the worker unless the send is InOnly.
The fixed route, which validates cleanly and runs:
- rest:
path: /orders
post:
- id: create-order
consumes: application/json
to: direct:create-order
- route:
id: create-order-route
from:
uri: direct:create-order
steps:
- unmarshal:
json: {}
- setHeader:
name: Content-Type
constant: application/json
- choice:
when:
- simple: "${body[id]} == null"
steps:
- setHeader:
name: CamelHttpResponseCode
constant: 400
- setBody:
constant: '{"error": "order id is required"}'
otherwise:
steps:
- to:
uri: seda:orders
pattern: InOnly
- setHeader:
name: CamelHttpResponseCode
constant: 202
- setBody:
simple: '{"accepted": "${body[id]}"}'
- route:
id: order-worker
from:
uri: seda:orders
parameters:
concurrentConsumers: 4
steps:
- setHeader:
name: CamelFileName
simple: "order-${body[id]}.json"
- marshal:
json: {}
- to: "file:{{env:ORDERS_DIR:orders}}"$ cameleer validate orders.camel.yaml --format json
{"valid": true, "files": [{"path": "orders.camel.yaml", "valid": true, "routes": 3, "findings": []}], "summary": {"files": 1, "errors": 0, "warnings": 0}}
$ ORDERS_DIR=/tmp/orders cameleer run orders.camel.yamlcurl -X POST localhost:8080/orders -d '{"id": "A-1", "total": 30}' answers
{"accepted": "A-1"} with status 202 and writes /tmp/orders/order-A-1.json; a body without
id gets status 400 and {"error": "order id is required"}.
Common questions
Which file should an agent read first?
AGENTS.md at the repository root, then this page, then the YAML DSL reference
for anything a skeleton does not cover, and docs/components/<scheme>.md for each endpoint.
How does an agent know an option exists?
It looks it up in docs/components/<scheme>.md and lets cameleer validate confirm it. The
validator resolves endpoints the same way the runtime does, so an option it accepts works.
Can the validator run without the route's servers and credentials?
Yes. It never starts routes or connects to brokers, databases or APIs, so it runs anywhere, including CI and sandboxes without network access. Placeholders without a value are reported as warnings, not errors.
What if the validator accepts a route that then fails at runtime?
The validator checks structure, endpoints and expressions, not reachability or data. Run the
route with --max-seconds against test inputs, and if the failure looks like a Cameleer bug,
report it (see below).
Related
- YAML DSL reference,
cameleer validate - Getting started, EIP guide, error handling
- Cameleer for AI agents: routes that call LLMs and tools
- Components
Found a problem?
If something on this page doesn't match what Cameleer does, or you hit a bug while building
with it, please help fix it, whether you are a person or a coding agent:
open an issue that describes what you saw,
then fork the repository, fix it, add or update a test, run cargo test --workspace, and open
a pull request. CONTRIBUTING.md
explains the ground rules.