# Cameleer documentation (full text) > Every page of the Cameleer documentation (https://pmsudhi.github.io/cameleer/docs/) concatenated as plain markdown, for LLMs and coding assistants. Cameleer is Apache Camel rewritten in Rust. --- Source: https://pmsudhi.github.io/cameleer/docs/ # Cameleer documentation > Cameleer is Apache Camel rewritten in Rust; these pages explain how to write, run and deploy its routes, and document every component, language and data format with tested examples. If you already know Apache Camel, the routes, endpoint URIs, EIP names and the YAML DSL are the same. Start with a guide, then look up the components your route uses. ## Where to find what | you want to | go to | |---|---| | write and run your first route | [Guides](https://pmsudhi.github.io/cameleer/docs/guides/index.html) | | send or receive messages with Kafka, HTTP, S3, Slack, SQL, ... | [Components](https://pmsudhi.github.io/cameleer/docs/components/index.html), one page per endpoint scheme, grouped by category | | write expressions and predicates (`${header.x}`, JSONPath, XPath) | [Languages](https://pmsudhi.github.io/cameleer/docs/languages/index.html) | | convert message bodies (JSON, CSV, XML, Avro, ...) | [Data formats](https://pmsudhi.github.io/cameleer/docs/dataformats/index.html) | | run Cameleer in Docker, Kubernetes, Cloud Run or AWS Lambda | [Deployment](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html) | | build a smaller binary or image with only the components you use | [Build features](https://pmsudhi.github.io/cameleer/docs/guides/build-features.html) | | check which Camel features are implemented, with their options | [Feature matrix](https://pmsudhi.github.io/cameleer/docs/FEATURES.html) | | see what people run on it: AI agent tools, HL7 over MLLP, MQTT edge gateways, serverless, file and SFTP pipelines | [Use cases](https://pmsudhi.github.io/cameleer/use-cases/), the [agent cookbook](https://pmsudhi.github.io/cameleer/docs/guides/agents.html) and the [edge cookbook](https://pmsudhi.github.io/cameleer/docs/guides/edge.html) | | compare Cameleer with Apache Camel: throughput, memory, startup | [Apache Camel vs Cameleer](https://pmsudhi.github.io/cameleer/compare/apache-camel-vs-cameleer/) and the [benchmark report](https://pmsudhi.github.io/cameleer/docs/BENCHMARKS.html) | | choose between Apache Camel alternatives | [Apache Camel alternatives](https://pmsudhi.github.io/cameleer/alternatives/apache-camel-alternatives/) | | see what was verified against real servers and clients | [Interop](https://pmsudhi.github.io/cameleer/docs/INTEROP.html) | | see what is planned | [Roadmap](https://pmsudhi.github.io/cameleer/docs/ROADMAP.html) | The project overview, the quick start and the examples are in the [README](https://github.com/pmsudhi/cameleer/blob/main/README.md); the ready-to-run route files are in [examples/](https://github.com/pmsudhi/cameleer/tree/main/examples). ## Connect your coding agent Give Cursor, Claude, Windsurf, VS Code or any MCP-capable assistant Cameleer's real docs, so it writes routes from the documentation instead of guessing. The docs MCP server is `https://gitmcp.io/pmsudhi/cameleer` (it serves this repository's `llms.txt`, docs and README). | tool | add this | |---|---| | Claude Code | `claude mcp add --transport http cameleer-docs https://gitmcp.io/pmsudhi/cameleer` | | Cursor (`~/.cursor/mcp.json`) | `{"mcpServers": {"cameleer-docs": {"url": "https://gitmcp.io/pmsudhi/cameleer"}}}` | | Windsurf (`~/.codeium/windsurf/mcp_config.json`) | `{"mcpServers": {"cameleer-docs": {"serverUrl": "https://gitmcp.io/pmsudhi/cameleer"}}}` | | VS Code (`.vscode/mcp.json`) | `{"servers": {"cameleer-docs": {"type": "http", "url": "https://gitmcp.io/pmsudhi/cameleer"}}}` | | Claude Desktop (`claude_desktop_config.json`) | `{"mcpServers": {"cameleer-docs": {"command": "npx", "args": ["mcp-remote", "https://gitmcp.io/pmsudhi/cameleer"]}}}` | The docs are also indexed by [Context7](https://context7.com) (ask your assistant to "use context7" for Cameleer) and [DeepWiki](https://deepwiki.com/pmsudhi/cameleer). Agents that read files directly can start from [llms.txt](https://pmsudhi.github.io/cameleer/llms.txt) or [llms-full.txt](https://pmsudhi.github.io/cameleer/llms-full.txt). ## How the pages are organized Every page starts with a one-sentence answer under its title, then shows a complete route you can copy. Component pages follow the same layout: when to use it, the URI format, an example in YAML and in the Rust DSL, the options, the message headers, how to run it with Docker, and common questions. Every YAML example is a complete Camel YAML DSL file. Save it as `route.camel.yaml` and run it with the Docker image: ```sh docker run --rm -v "$PWD:/routes:ro" pmsudhi/cameleer run /routes/route.camel.yaml ``` Hosts and credentials in examples are written as `{{env:NAME:default}}` placeholders: set the environment variable (`-e NAME=value` with Docker) or the default is used. ## Common questions ### Are the examples tested? Yes. The test suite parses every YAML example with the real loader and resolves every endpoint URI, which rejects unknown options the same way Apache Camel does. Many examples also run end to end against embedded brokers and mock services. ### Is there a version of these docs for LLMs and coding assistants? Yes. The site publishes `llms.txt` (a summary with links) and `llms-full.txt` (every page as plain markdown), and every HTML page has its markdown source next to it under the same name with `.md`. ### Where do I report a mistake in the docs? Open an issue or a pull request on [GitHub](https://github.com/pmsudhi/cameleer). Each page links to its source file at the bottom. ## Writing documentation - One page per component at `components/.md`; alias schemes (`https` for `http`) go on the page's `**Also:**` line instead of getting their own page. Languages go in `languages/.md`, data formats in `dataformats/.md`, guides in `guides/`. - The first line under the H1 is a blockquote with one plain sentence that answers what the page is about; it becomes the page's search description. - Every ` ```yaml ` block is either a complete Camel route preceded by `` on the line directly above, or something else (a Compose or Kubernetes file) preceded by ``. - A "Common questions" section with `###` question headings becomes FAQ data for search engines. - Links between pages are relative markdown links (`../components/kafka.md`). Check your changes, then build the site: ```sh cargo test -p cameleer-main --test docs_harness python3 scripts/build-docs.py # writes site/docs/, site/sitemap.xml, site/llms*.txt python3 -m http.server -d site 8000 # then open http://localhost:8000/docs/ ``` ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/getting-started.html # Getting started with Cameleer > Install Cameleer (Docker image, `cargo install` or Rust crates), run a first route and a first > REST API, and configure them with properties and environment variables, in about ten minutes. Cameleer is Apache Camel rewritten in Rust: the same routes, endpoint URIs, Enterprise Integration Patterns and Simple language, as one native binary with no JVM. If you know Camel, you already know how to write Cameleer routes. Every route on this page runs in the test suite (`crates/cameleer-main/tests/docs_guides.rs`). ## Install Pick one of three ways. They run the same routes. ### Docker The image `pmsudhi/cameleer` on Docker Hub is a single binary on a distroless base, for `linux/amd64` and `linux/arm64`, about 20 MB to download. Mount your route files and run them: ```sh docker run --rm -p 8080:8080 -v "$PWD:/routes:ro" pmsudhi/cameleer run /routes/hello.camel.yaml ``` It runs as a non-root user, works with `--read-only`, and stops gracefully on `SIGTERM`. The [deployment guide](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html) covers Docker Compose, Kubernetes and Cloud Run. ### The `cameleer` command line With a Rust toolchain installed: ```sh cargo install cameleer-main cameleer run hello.camel.yaml ``` This installs the `cameleer` command with every component. Its subcommands follow Camel JBang (`run`, `init`, `ps`, `stop`, `log`, ...). To build from source: ```sh git clone https://github.com/pmsudhi/cameleer && cd cameleer cargo build --release -p cameleer-main target/release/cameleer run examples/hello-api.camel.yaml ``` ### Rust crates To embed routes in your own Rust program, depend on `cameleer-core` (the engine, EIPs and Rust DSL) and the crates for the components you need: ```toml [dependencies] cameleer-core = "0.4" cameleer-http = "0.4" ``` `cameleer-main` bundles every component and runs YAML routes from Rust; see [Embed routes in a Rust program](#embed-routes-in-a-rust-program) below. ### Every component is included by default The image, `cargo install cameleer-main` and the crates' default features contain every component, language and data format, so any route in these docs runs without extra setup. For a smaller binary (a serverless function, an edge device, a slim image) pick only the families your routes use with cargo features; [Build features](https://pmsudhi.github.io/cameleer/docs/guides/build-features.html) lists them and shows the commands. Each component page names its feature in its header line. ## Your first route A route has one input (`from`) and a list of steps. Save this as `hello.camel.yaml`: ```yaml - route: id: hello from: uri: timer:hello parameters: period: 500 repeatCount: 3 steps: - setBody: simple: "Hello from Cameleer, message ${exchangeProperty.CamelTimerCounter}" - log: "${body}" ``` Run it with `cameleer run hello.camel.yaml` (or the `docker run` line above). The `timer` endpoint fires three times, half a second apart; `setBody` builds a message with the [Simple language](https://pmsudhi.github.io/cameleer/docs/languages/simple.html); `log` prints `Hello from Cameleer, message 1`, then 2 and 3, to standard output. Without `repeatCount` the timer runs until you stop the process with Ctrl+C. ## Your first REST API The REST DSL declares HTTP endpoints and hands each request to a route. Save this as `hello-api.camel.yaml`: ```yaml - rest: path: /hello get: - path: /{name} to: direct:greet - route: id: greet from: uri: direct:greet steps: - setHeader: name: Content-Type constant: application/json - setBody: simple: '{"greeting": "Hello, ${header.name}!"}' ``` ```sh docker run --rm -p 8080:8080 -v "$PWD:/routes:ro" pmsudhi/cameleer run /routes/hello-api.camel.yaml curl localhost:8080/hello/Ada # {"greeting": "Hello, Ada!"} ``` Path parameters such as `{name}` become headers. The server listens on port 8080 by default (`camel.server.port` changes it). For JSON binding, validation and error responses, see [examples/rest-api.camel.yaml](https://github.com/pmsudhi/cameleer/blob/main/examples/rest-api.camel.yaml) and the [REST component](https://pmsudhi.github.io/cameleer/docs/components/rest.html). ## Configuration Routes read configuration with property placeholders, as in Camel: - `{{name}}` and `{{name:default}}` come from `application.properties` (or `application.yaml`), environment variables and `--prop name=value`. - `{{env:NAME}}` and `{{env:NAME:default}}` read an environment variable directly. ```yaml - route: id: configured from: uri: "timer:tick?period={{tick.period:1000}}&repeatCount=1" steps: - setBody: simple: "{{greeting}} from {{env:SITE_NAME:the lab}}" - log: "${body}" ``` With this `application.properties` next to the route: ```properties greeting=Good morning tick.period=200 camel.server.port=9090 ``` ```sh cameleer run configured.camel.yaml --properties application.properties SITE_NAME=plant-7 cameleer run configured.camel.yaml --prop greeting=Hi ``` The first command logs `Good morning from the lab`. Settings are applied in this order, the last one winning: the properties file, `CAMEL_*` environment variables (`CAMEL_SERVER_PORT=9090` sets `camel.server.port`), then `--prop`. Spring Boot and Quarkus names (`server.port`, `spring.profiles.active`) and profiles (`application-dev.properties`) work too. Never put secrets in route files: use `{{env:...}}` or a secrets vault (`{{aws:...}}`, `{{azure:...}}`, `{{gcp:...}}`). ## Embed routes in a Rust program The Rust DSL has the same EIPs as the YAML DSL, with Rust casing: ```rust use cameleer_core::prelude::*; use cameleer_http::rest::RestDslExt; fn main() -> Result<()> { let ctx = CamelContext::new(); cameleer_http::register(&ctx); ctx.properties().set("camel.server.port", "8080"); ctx.add_routes(|b| { b.rest("/api/greet").get("/{name}").to("direct:greet"); b.from("direct:greet") .set_header("Content-Type", constant("application/json")) .set_body(simple(r#"{"greeting": "Hello, ${header.name}!"}"#)); })?; ctx.start()?; loop { std::thread::park(); } } ``` To run YAML route files from your own binary with every component, depend on `cameleer-main` and use `cameleer_main::new_main()`, then `add_route_file("routes/hello.camel.yaml")` and `run()` ([example](https://github.com/pmsudhi/cameleer/blob/main/crates/cameleer-main/examples/embed_yaml.rs)). ## Useful commands | command | what it does | |---|---| | `cameleer run route.camel.yaml` | run one or more route files (all route files of the current folder when none is given) | | `cameleer run --dev route.camel.yaml` | reload route files when they change | | `cameleer init my-route.camel.yaml` | write a starter route | | `cameleer ps`, `cameleer stop --all` | list and stop running integrations | | `cameleer get routes`, `cameleer get health` | inspect a running integration | | `cameleer export --directory my-service` | turn routes into a standalone Rust project | | `cameleer catalog components` | list the components | ## Where to go next - [Concepts](https://pmsudhi.github.io/cameleer/docs/guides/concepts.html): routes, endpoints, exchanges, messages, components and the DSLs. - [Build features](https://pmsudhi.github.io/cameleer/docs/guides/build-features.html): a smaller build with only the components you use. - [Enterprise Integration Patterns](https://pmsudhi.github.io/cameleer/docs/guides/eips.html): every EIP with a YAML and a Rust example. - [Error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html): retries, dead letter channels, `onException`. - [Cameleer for AI agents](https://pmsudhi.github.io/cameleer/docs/guides/agents.html) and [Cameleer at the edge](https://pmsudhi.github.io/cameleer/docs/guides/edge.html): cookbooks. - [Migrating from Apache Camel](https://pmsudhi.github.io/cameleer/docs/guides/migrating-from-apache-camel.html) and the [FAQ](https://pmsudhi.github.io/cameleer/docs/guides/faq.html). - [Examples](https://github.com/pmsudhi/cameleer/tree/main/examples): real routes for Kafka, S3, Slack and OpenAI, each with a `docker run` line. ## Common questions ### Do I need Java or Maven to run Cameleer? No. Cameleer is a native binary with no JVM. You need Rust only to build it from source or to embed it; the Docker image and the installed binary need nothing else. ### Can I run my existing Apache Camel YAML routes? Mostly yes: Camel YAML and XML routes run unchanged when they use components and options Cameleer supports. Java beans and processors must be rewritten in Rust. See [Migrating from Apache Camel](https://pmsudhi.github.io/cameleer/docs/guides/migrating-from-apache-camel.html). ### How do I change the HTTP port? Set `camel.server.port`: `--prop camel.server.port=9090`, the environment variable `CAMEL_SERVER_PORT=9090`, or `camel.server.port=9090` in `application.properties`. ### Where do log messages go? To standard output. Set the level with the `CAMEL_LOG_LEVEL` environment variable (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR` or `OFF`). ## Related - [README](https://github.com/pmsudhi/cameleer/blob/main/README.md), [deployment guide](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html), [feature list](https://pmsudhi.github.io/cameleer/docs/FEATURES.html) - Apache Camel: [Camel JBang](https://camel.apache.org/manual/camel-jbang.html), [YAML DSL](https://camel.apache.org/components/next/others/yaml-dsl.html) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/concepts.html # Cameleer concepts > Cameleer uses Apache Camel's model: a CamelContext runs routes; each route takes exchanges from > an endpoint, passes their messages (body and headers) through processors and EIPs, and sends > them to other endpoints created by components. This page explains each concept, shows how it appears in the YAML, XML and Rust DSLs, and notes where Cameleer differs from Apache Camel. The examples run in the test suite (`crates/cameleer-main/tests/docs_guides.rs`). ## The model in one route ```yaml - route: id: order-intake from: uri: direct:orders steps: - setHeader: name: priority simple: "${body[total] > 1000 ? 'high' : 'normal'}" - setProperty: name: receivedAt simple: "${date:now:yyyy-MM-dd}" - choice: when: - simple: "${header.priority} == 'high'" steps: - to: mock:priority-orders otherwise: steps: - to: mock:orders ``` ```rust b.from("direct:orders") .route_id("order-intake") .set_header("priority", simple("${body[total] > 1000 ? 'high' : 'normal'}")) .set_property("receivedAt", simple("${date:now:yyyy-MM-dd}")) .choice() .when(simple("${header.priority} == 'high'")) .to("mock:priority-orders") .otherwise() .to("mock:orders") .end(); ``` - The **CamelContext** loaded this route and runs it. - `direct:orders` is an **endpoint URI**: the `direct` **component** created the endpoint, and the route's **consumer** takes messages from it. - Each order arrives as an **exchange** holding a **message**. `${body[total]}` reads the message **body**; `setHeader` writes a message **header**; `setProperty` writes an **exchange property**. - `setHeader`, `setProperty` and `choice` are **processors**; `choice` is an **EIP**. - `mock:priority-orders` and `mock:orders` are endpoints too: the route's **producers** send to them. An order with `total` 1500 reaches `mock:priority-orders` with the header `priority: high`. ## CamelContext The CamelContext is the runtime: it holds the components, the registry of beans, the properties, the type converters and the routes, and starts and stops them. Routes start in `startupOrder` and stop in reverse order; stopping waits for in-flight exchanges (45 seconds by default). - **YAML and XML:** `cameleer run` (or `cameleer_main::new_main()`) creates the context, registers every component and loads the route files. - **Rust:** `CamelContext::new()` creates an empty context; register the components you need (`cameleer_http::register(&ctx)`), add routes with `ctx.add_routes(|b| ...)`, then `ctx.start()`. A `CamelContext` is a cheap handle you can clone into threads. ## Routes A route has one input, `from`, and a list of steps. It has an id (generated as `route1`, `route2`... when you do not set one), and can be started, stopped, suspended and resumed at run time (`cameleer cmd stop-route`, the dev console, or `ctx.stop_route(id)`). Options: `autoStartup`, `startupOrder`, `group`, `description`, `routeConfigurationId`, `errorHandler`. Route templates and Kamelets create routes from parameters; see [route templates](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#route-templates). ## Endpoints and URIs An endpoint is addressed by a URI: `scheme:path?option=value&option=value`. ```text kafka:orders?brokers=localhost:9092&groupId=billing file:inbox?include=.*\.csv&move=.done platform-http:/alerts?httpMethodRestrict=POST ``` The scheme picks the component, the path names the resource (a topic, a folder, a URL path), and the options configure it. Options are checked when the endpoint is created: an unknown or misspelled option fails the route at startup, as in Camel. URIs accept property placeholders (`{{env:KAFKA_BROKERS:localhost:9092}}`), and in YAML the options can also go in a `parameters` map. Endpoints are normalized and cached, so two spellings of the same URI share one endpoint. `toD` computes the URI per message from a Simple expression (`toD: "kafka:${header.topic}"`). ## Components A component creates endpoints for one scheme. Cameleer has about 230 components, languages and data formats, all built in; there is nothing to download. They are grouped in crates: `cameleer-core` (direct, seda, timer, file, log, mock, bean, rest...), `cameleer-http`, `cameleer-kafka-mqtt`, `cameleer-cloud` (AWS, Azure, Google Cloud), `cameleer-saas` (OpenAI, Slack, GitHub...) and others. [FEATURES.md](https://pmsudhi.github.io/cameleer/docs/FEATURES.html) lists every component with its supported options; the [component pages](https://pmsudhi.github.io/cameleer/docs/components/index.html) document them one by one. An endpoint can be a **consumer** (the `from` of a route: it receives or polls messages) and a **producer** (a `to`: it sends). Most components are both. ## Exchanges and messages An **exchange** is one message's trip through a route. It holds: | part | what it is | in Simple | |---|---|---| | message body | the payload: text, bytes, a number, a list or a map (JSON-like) | `${body}`, `${body[field]}` | | message headers | name/value pairs that travel with the message; components map them to protocol headers (HTTP, Kafka, MQTT, JMS...) | `${header.name}` | | exchange properties | name/value pairs for the route's own bookkeeping; they are not sent out | `${exchangeProperty.name}` | | variables | named values set with `setVariable` | `${variable.name}` | | exception | the error, when a step failed | `${exception.message}` | | pattern | `InOnly` (fire and forget) or `InOut` (request-reply) | | Rules worth knowing: - Header names are case-insensitive and keep their original spelling. - Headers go out with the message (an `http` call sends them as HTTP headers, `kafka` as record headers), so remove what should not leave: `removeHeaders: {pattern: "Camel*"}`. Properties stay inside. - Cameleer has no Java objects: bodies, headers and properties are dynamic values (null, bool, integer, float, string, bytes, list, map). JSON maps directly onto them, which is why `${body[field]}` works after `unmarshal: json: {}`. - Camel's names carry over: `CamelFileName`, `CamelHttpResponseCode`, `CamelSplitIndex`, `kafka.KEY` and the rest. - Variables are stored as exchange properties named `CamelVariable.` (Camel has a separate variable repository). ## Processors and EIPs Every step of a route is a processor: something that reads and changes the exchange. The built-in ones are the EIPs (`filter`, `choice`, `split`, `aggregate`, `circuitBreaker`...) and the message steps (`setBody`, `setHeader`, `marshal`, `convertBodyTo`...). See [the EIP guide](https://pmsudhi.github.io/cameleer/docs/guides/eips.html) for all of them. Custom logic is a processor written in Rust. In the Rust DSL it is a closure: ```rust b.from("direct:score") .process(|ex: &mut Exchange| { let text = ex.body().to_string(); let score = text.split_whitespace().filter(|w| w.eq_ignore_ascii_case("urgent")).count() as i64; ex.set_header("urgency", score); Ok(()) }) .to("mock:scored"); ``` To use it from YAML or XML, bind it in the registry (`ctx.registry().bind_processor("scorer", ...)`) and call it with `- process: {ref: scorer}`. Returning `Err(CamelError::new("IOException", "..."))` fails the exchange and hands it to the [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html). Camel's Java beans with reflection-based parameter binding do not exist; scripts in Groovy, JavaScript, Python or Java syntax run in Cameleer's own interpreters ([`script`](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html#script)). ## Expressions, predicates and languages An **expression** computes a value from the exchange; a **predicate** is an expression used as a condition. Both are written in a **language**: - [Simple](https://pmsudhi.github.io/cameleer/docs/languages/simple.html): `${header.amount} > 100`, `Hello ${body[name]}`, the default for most steps. - JSONPath (`$.items[0].sku`), jq, XPath and XQuery for structured data. - `constant`, `header`, `exchangeProperty`, `variable` for plain lookups. - Groovy, JavaScript, Python, MVEL, OGNL, SpEL, DataSonnet and Java-like snippets (`cameleer-script`). In YAML the language is the key (`simple: ...`, `jsonpath: ...`); in Rust it is a function (`simple("...")`, `jsonpath("$.items")`, `header("id")`, `constant(3)`). ## Data formats and type conversion A **data format** converts between bytes and structured data: `unmarshal` parses, `marshal` serializes. JSON, CSV, XML, YAML, Avro, Protobuf, CBOR, Base64, GZip, Zip, PGP, HL7 and others are built in. The **type converter** changes a value's type when a step needs it: `convertBodyTo` with Camel's Java type names (`java.lang.String`, `java.lang.Integer`, `byte[]`), or implicitly when Simple compares a text with a number. ## The three DSLs The same route can be written in three ways. All three build the same route model and support the same EIPs. **YAML**, Camel's YAML DSL, the usual choice for route files (`*.camel.yaml`): see the route at the top of this page. **XML**, Camel's XML DSL (``/`` files), converted to the YAML model when loaded: ```xml ${body[total] > 1000 ? 'high' : 'normal'} ${header.priority} == 'high' ``` **Rust**, the equivalent of Camel's Java DSL: `RouteBuilder` methods with snake_case names (`set_header`, `wire_tap`, `to_d`, `loop_`), blocks closed with `.end()`. Use it when routes live in a Rust program, need custom processors, or should be checked by the compiler. The REST DSL (`rest:` in YAML, `b.rest(...)` in Rust) declares HTTP APIs in all three. ## How the concepts map to Apache Camel | Apache Camel | Cameleer | notes | |---|---|---| | `CamelContext` | `CamelContext` | a cloneable handle | | `RouteBuilder.configure()` (Java DSL) | `ctx.add_routes(\|b\| ...)` (Rust DSL) | snake_case method names | | YAML DSL, XML DSL | YAML DSL, XML DSL | the same files | | `Exchange`, `Message` | `Exchange`, `Message` | bodies and headers are dynamic `Value`s, not Java objects | | `Processor` (Java class or lambda) | `Processor` (Rust closure or struct) | | | exception classes | error kinds (`"IOException"`) with a parent hierarchy | `java.io.IOException` in YAML maps to `IOException` | | Java beans, `bean:` with parameter binding | processor beans and named bean methods in the registry | no reflection | | Camel Main, Spring Boot, Quarkus | `cameleer-main` (`application.properties`, profiles, `server.port`) | | | Camel JBang (`cameleer run`) | `cameleer run` / `cameleer run` | the same commands | | Maven dependencies per component | every component built in | | ## Common questions ### What is the difference between a header and an exchange property? Headers belong to the message and are sent out by components (as HTTP headers, Kafka record headers and so on); exchange properties belong to the exchange and never leave the route. Use properties for intermediate values. ### What is an endpoint URI? It is the address of an endpoint: `scheme:path?options`, for example `kafka:orders?brokers=localhost:9092`. The scheme selects the component. ### Do I have to write Rust to use Cameleer? No. YAML or XML route files cover all EIPs and components. Rust is needed only for custom processors and for embedding routes in a Rust program. ### How do routes call each other? With `direct:` (synchronous, in the same thread), `seda:` (asynchronous, through an in-memory queue) or `vm:` (like seda, across contexts in the same process). ## Related - [Getting started](https://pmsudhi.github.io/cameleer/docs/guides/getting-started.html), [EIPs](https://pmsudhi.github.io/cameleer/docs/guides/eips.html), [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html) - [Architecture](https://github.com/pmsudhi/cameleer/blob/main/ARCHITECTURE.md): the internals - Apache Camel: [Architecture](https://camel.apache.org/manual/architecture.html), [Exchange](https://camel.apache.org/manual/exchange.html), [Endpoint](https://camel.apache.org/manual/endpoint.html) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/yaml-dsl.html # YAML DSL reference > The YAML DSL describes Cameleer routes in `*.camel.yaml` files: one input (`from`), a list > of steps (EIPs), endpoints written as URIs, and expressions in Simple or another language, > with the same names as the Apache Camel YAML DSL. This page lists everything the YAML loader (`crates/cameleer-yaml`) accepts: the file layout, every top-level element, the endpoint and expression forms, every step with its keys and defaults, and the mistakes people make most. Every route on this page runs in the test suite (`crates/cameleer-main/tests/docs_yaml.rs`): the test sends messages through it and checks the result, and resolves every endpoint URI the way Apache Camel does, so an unknown option fails the build. If you generate routes with a coding assistant, read [YAML for coding agents](https://pmsudhi.github.io/cameleer/docs/guides/yaml-for-coding-agents.html) too, and check every file with [`cameleer validate`](https://pmsudhi.github.io/cameleer/docs/guides/validate.html). ## A first look ```yaml # hello.camel.yaml: a list of top-level elements - route: id: hello # optional; generated when missing from: uri: timer:hello # the input endpoint parameters: repeatCount: 1 steps: # the steps, in order - setBody: simple: "Hello from ${routeId}" - log: "${body}" - to: mock:greetings ``` Three things to notice: - The file is a YAML **list**. Each item has exactly one key: the element type (`route`, `rest`, `routeConfiguration`, ...). - `steps` belongs to `from` (Camel 4 style). A `steps` list next to `from`, at route level, is accepted too and runs after the steps under `from`. - Every step is a list item with one key: the step name (`setBody`, `log`, `to`, ...). ## Files: naming, finding and loading ### File names | file name | what it holds | |---|---| | `*.camel.yaml` (recommended), any `*.yaml` or `*.yml` | routes and other top-level elements | | `*.kamelet.yaml` | one Kamelet resource (`kind: Kamelet`) | | `*.pipe.yaml` | one Pipe (`kind: Pipe` or the older `KameletBinding`) | | `*.integration.yaml` | one Camel K `Integration` resource (its `spec.flows` are routes) | | `*.xml` | routes in the XML DSL (same steps, same option names) | | `application.properties`, `application.yaml` | configuration, never routes | The `.camel.yaml` suffix is a convention from Camel JBang that makes route files easy to find and lets editors pick the right schema; the loader only looks at the last extension. Use one route per file for small services, or group related routes; there is no limit. `cameleer init orders.camel.yaml` writes a starter route; `cameleer init x.kamelet.yaml`, `x.pipe.yaml` and `x.integration.yaml` write starters for the other kinds. ### Running files ```sh cameleer run orders.camel.yaml # one file cameleer run orders.camel.yaml billing.camel.yaml # several files, one context cameleer run routes/*.camel.yaml # a shell glob cameleer run # every .yaml/.yml/.xml file of the current folder cameleer run --dev orders.camel.yaml # reload when a file changes docker run --rm -v "$PWD:/routes:ro" pmsudhi/cameleer run /routes/orders.camel.yaml ``` - `cameleer run` with no file loads every `.yaml`, `.yml` and `.xml` file of the current folder whose name does not contain `application`. - A directory is not accepted as a file argument: use a shell glob (`routes/*.camel.yaml`) or the include pattern below. - `camel.main.routes-include-pattern` (in `application.properties`, as `--prop`, or the environment variable `CAMEL_MAIN_ROUTES_INCLUDE_PATTERN`) adds files by pattern. It is a comma separated list of Ant-style patterns: `*` matches within a folder, `**` matches any number of folders, and a `file:` or `classpath:` prefix is ignored, so Camel values work: ```sh cameleer run --prop camel.main.routes-include-pattern=routes/**/*.camel.yaml ``` - `--dev` checks the loaded route files about three times a second and, when one changed, removes every route that came from a file and loads all the files again. It also selects the `dev` profile (`application-dev.properties`). A file that fails to load is logged, and its routes stay missing until you fix the file and save it again. - All files are loaded into one context. Route ids must be unique across files; `direct:` endpoints connect routes from different files. - Kamelets in `./kamelets/` (or the folders of `camel.main.kamelet-locations`) are loaded before the route files. ### Inside a file - Comments start with `#` (at the start of a line, or after a space). - Several YAML documents in one file are allowed, separated by `---`; each is a list of top-level elements, loaded in order. - A document may also be a single element written as a mapping (`route: ...` without the leading `-`), but lists are the norm. - Anchors (`&name`), aliases (`*name`) and merge keys (`<<`) work, so a repeated block can be written once. - Errors name the place: `at [0].route.from.steps[1].setBody (line 7, column 13 in orders.camel.yaml)`. ```yaml # first document: the HTTP-facing route - route: id: intake from: uri: direct:intake steps: - to: direct:store --- # second document: the route it calls - route: id: store from: uri: direct:store steps: - setBody: simple: "stored ${body}" - to: mock:stored ``` ## Top-level elements | element | what it does | section | |---|---|---| | `route` | a route: `from` plus steps | [route](#route) | | `from` | a route without the `route:` wrapper | [from](#from-the-short-form) | | `routeConfiguration` | `onException`, `onCompletion`, intercepts and an error handler shared by routes | [routeConfiguration](#routeconfiguration) | | `onException`, `onCompletion`, `intercept`, `interceptFrom`, `interceptSendToEndpoint` | the same, written directly at top level (an anonymous route configuration that applies to every route without `routeConfigurationId`) | [routeConfiguration](#routeconfiguration) | | `errorHandler` | the error handler of every route in the context | [errorHandler](#errorhandler) | | `routeTemplate` (or `routeTemplates`) | a parameterized route | [templates](#routetemplate-and-templatedroute) | | `templatedRoute` (or `templatedRoutes`) | a route created from a template | [templates](#routetemplate-and-templatedroute) | | `rest` | REST endpoints (the REST DSL) | [REST DSL](#rest-and-restconfiguration) | | `restConfiguration` | the HTTP server and binding for `rest` | [REST DSL](#rest-and-restconfiguration) | | `beans` | named beans created by registered bean factories | [beans](#beans) | | `dataFormats` | accepted and stored, but not used: configure data formats on the `marshal`/`unmarshal` step | | A document whose top level is a mapping with `apiVersion` and `kind` is a Kubernetes-style resource instead: `kind: Kamelet`, `kind: Pipe`, `kind: KameletBinding` or `kind: Integration` (see [Kamelets and Pipes](#kamelets-and-pipes)). Any other element name fails with `Unknown top-level element '...'` and the list of supported names. ### route | key | default | description | |---|---|---| | `from` | required | the input endpoint: a URI string, or a mapping with `uri`, `parameters` and `steps` | | `steps` | | more steps, run after the steps under `from` | | `id` (or `routeId`) | `route1`, `route2`, ... | the route id, used by management, logs and `cameleer get routes` | | `description` | | free text | | `group` | | a group name for management | | `autoStartup` | `true` | `false` creates the route stopped; start it with `cameleer cmd start-route --id ID` | | `startupOrder` | | an integer; lower numbers start first and stop last | | `routeConfigurationId` | | the ids of the `routeConfiguration`s to apply, comma separated; `*` patterns match | | `errorHandler` | the context's | an error handler for this route only (same shape as the top-level [errorHandler](#errorhandler)) | | `errorHandlerRef` | | the name of an error handler bound in the registry | | `routePolicy` (or `routePolicyRef`) | | names of `RoutePolicy` beans in the registry, comma separated | | `inputType`, `outputType` | | `{urn, validate}` data type contracts, see [transformDataType](#message-content) | Other keys are kept as route properties and do not change behaviour, so a misspelled option (for example `autostartup`) is silently ignored by the loader. `cameleer validate` reports it. ```yaml - route: id: nightly-report description: Builds the nightly report; started by an operator group: reports autoStartup: false from: uri: direct:report steps: - to: mock:report - route: id: always-on startupOrder: 10 from: uri: direct:ping steps: - setBody: constant: pong ``` ### from: the short form A route can be written without the `route:` wrapper when it needs no route options: ```yaml - from: uri: direct:short steps: - setBody: simple: "short form: ${body}" - to: mock:short ``` ### routeConfiguration A `routeConfiguration` holds error handling and interceptors that several routes share, as in Camel 4. Its keys: | key | description | |---|---| | `id` | the id routes refer to with `routeConfigurationId` | | `errorHandler` | an error handler for the routes it applies to | | `onException` | a list of `- onException:` clauses (see [error handling steps](#error-handling)) | | `onCompletion` | a list of `- onCompletion:` clauses | | `intercept`, `interceptFrom`, `interceptSendToEndpoint` | lists of interceptors | | `precondition`, `description` | accepted and ignored | Which routes get it: - a configuration **with** an `id` applies to the routes that list it in `routeConfigurationId`; - a configuration **without** an `id` applies to every route that has no `routeConfigurationId`; - `onException` and the other clauses written directly at top level count as a configuration without an `id`. It does not matter whether the configuration comes before or after the routes in the file. A `routeConfigurationId` that matches nothing fails at load time. ```yaml - routeConfiguration: id: api-errors onException: - onException: exception: - java.lang.IllegalArgumentException handled: constant: true steps: - setBody: simple: "rejected: ${exception.message}" - to: mock:rejected onCompletion: - onCompletion: onCompleteOnly: true steps: - to: mock:audit - route: id: create-order routeConfigurationId: api-errors from: uri: direct:create-order steps: - filter: simple: "${body} == ''" steps: - throwException: exceptionType: java.lang.IllegalArgumentException message: empty order - setBody: simple: "created ${body}" ``` ### errorHandler The top-level `errorHandler` applies to every route of the context; the `errorHandler` key of a route or a `routeConfiguration` overrides it. It holds exactly one of: | handler | keys | |---|---| | `defaultErrorHandler` | `redeliveryPolicy`, `redeliveryPolicyRef`, `useOriginalMessage`, `useOriginalBody`, `retryWhile`, `onRedeliveryRef`, `onExceptionOccurredRef`, `onPrepareFailureRef` | | `deadLetterChannel` | `deadLetterUri` (required) plus the keys above and `deadLetterHandleNewException` | | `noErrorHandler` | none: failures go straight back to the caller | | `refErrorHandler` | the name of an error handler bound in the registry (or `{ref: name}`) | `redeliveryPolicy` keys: `maximumRedeliveries` (default 0, `-1` = forever), `redeliveryDelay` (default 1000 ms), `useExponentialBackOff`, `backOffMultiplier` (default 2), `maximumRedeliveryDelay`, `useCollisionAvoidance`, `collisionAvoidanceFactor`, `delayPattern` (`0:1000;5:5000`), `retryAttemptedLogLevel`, `retriesExhaustedLogLevel`, `logStackTrace`, `logExhausted`, `logRetryAttempted`, `asyncDelayedRedelivery`. Durations take milliseconds or a unit (`250ms`, `5s`, `PT1M`). Other Camel logging options are accepted and ignored. The [error handling guide](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html) explains the behaviour. ```yaml - errorHandler: deadLetterChannel: deadLetterUri: mock:dead-letters useOriginalMessage: true redeliveryPolicy: maximumRedeliveries: 2 redeliveryDelay: 10ms - route: id: charge from: uri: direct:charge steps: - setBody: simple: "charging ${body}" - throwException: exceptionType: java.io.IOException message: payment gateway down ``` ### routeTemplate and templatedRoute A `routeTemplate` is a route with `{{parameter}}` placeholders; a `templatedRoute` creates a route from it. Templates are read before the other elements, so the order in the file does not matter. | `routeTemplate` key | description | |---|---| | `id` | required: the template id | | `parameters` | a list of `{name, defaultValue, required, description}`; a parameter without `defaultValue` is required unless `required: false` | | `from` (or `route: {from: ...}`) | the route; `{{name}}` and `{{name:default}}` are replaced in every string | | `beans` | template-local beans, created when the template is used | | `description` | free text | | `templatedRoute` key | description | |---|---| | `routeTemplateRef` | required: the template id | | `routeId` | the id of the new route | | `prefixId` | a prefix added to the `id` of every step | | `parameters` | a list of `{name, value}` or a plain mapping `{name: value}` | | `beans` | beans to create for this route | Placeholders that are not template parameters (`{{env:...}}`, application properties) are left for the normal property resolution. A missing required parameter fails at load time: `Route template sensor the following mandatory parameters are missing: room`. ```yaml - routeTemplate: id: sensor parameters: - name: room - name: unit defaultValue: C from: uri: "direct:{{room}}" steps: - setBody: simple: "{{room}}: ${body} {{unit}}" - to: mock:readings - templatedRoute: routeTemplateRef: sensor routeId: kitchen-sensor parameters: - name: room value: kitchen - templatedRoute: routeTemplateRef: sensor routeId: lab-sensor parameters: room: lab unit: K ``` ### rest and restConfiguration The REST DSL declares HTTP operations and hands each request to a route, usually through `direct:`. `restConfiguration` may appear in any file, before or after the `rest` elements: like in Camel it is global, and REST routes are built when the context starts. | `restConfiguration` key | default | description | |---|---|---| | `component` | `platform-http` | the HTTP server component | | `host`, `port` | `camel.server.port` (8080) | where to listen | | `contextPath` | | a prefix for every path, for example `/api` | | `bindingMode` | `off` | `json`, `xml`, `json_xml`, `auto` or `off` | | `scheme` | `http` | `http` or `https` | | `apiContextPath` | | serve the OpenAPI document at this path (turns `apiDocs` on) | | `apiDocs`, `enableCORS`, `skipBindingOnErrorCode` | | booleans | | `corsHeaders`, `apiProperty`, `endpointProperty`, `consumerProperty` | | lists of `{key, value}` | | `rest` key | description | |---|---| | `path` | the base path of the operations | | `get`, `post`, `put`, `delete`, `patch`, `head`, `options` | lists of operations | | `verb` | operations with an explicit `method` | | `consumes`, `produces`, `bindingMode`, `description`, `tag` | defaults for the operations | Each operation takes `path`, `to` (a URI, or `{uri, parameters}`; `toD` is accepted), `id` (the route id), `description`, `consumes`, `produces`, `bindingMode`, `type`, `outType`, `param` (a list of `{name, type: path|query|header|body|formData, required, dataType, defaultValue, description, allowableValues}`), `responseMessage` (a list of `{code, message}`), `apiDocs` and `deprecated`. Path parameters such as `{id}` become headers. Operations route with `to`; they do not take inline steps. ```yaml - restConfiguration: contextPath: /api - rest: path: /orders get: - path: /{id} id: get-order produces: application/json to: direct:get-order post: - to: direct:create-order - route: from: uri: direct:get-order steps: - setBody: simple: '{"id": "${header.id}", "status": "shipped"}' - route: from: uri: direct:create-order steps: - setHeader: name: CamelHttpResponseCode constant: 201 - setBody: simple: "created ${body}" ``` `GET /api/orders/42` returns `{"id": "42", "status": "shipped"}`; `POST /api/orders` returns status 201. See the [REST component](https://pmsudhi.github.io/cameleer/docs/components/rest.html) for binding and OpenAPI. ### beans A `beans` element is a list of `{name, type, properties}`. Rust cannot create Java classes by name, so a bean is created only when a bean factory is registered for its `type` (`cameleer_yaml::register_bean_factory`) by a program that embeds Cameleer. The `cameleer` binary registers no factories: it records the definition and logs `No bean factory registered for bean 'greeter'`. To use processors, aggregation strategies or repositories from YAML, bind them from Rust (`ctx.registry().bind_processor(...)`) and refer to them by name; built-in aggregation strategies work by name without any bean (see [split](#split-aggregate-and-reorder)). ```yaml - beans: - name: greeter type: com.acme.Greeter properties: prefix: Hello - route: from: uri: direct:beans steps: - to: mock:beans ``` ### Kamelets and Pipes A Kamelet is a reusable route snippet with typed properties. Use one as a source (`from: kamelet:name?...`), a sink or an action (`to: kamelet:name?...`, or the `kamelet` step). Built-in Kamelets: `timer-source`, `log-sink`, `log-action`, `insert-header-action` and `set-body-action`; files in `./kamelets/` add more. ```yaml - route: from: uri: kamelet:timer-source parameters: message: Hello from a Kamelet period: 100 repeatCount: 1 steps: - kamelet: name: insert-header-action parameters: name: origin value: kamelet - to: mock:from-kamelet ``` A Kamelet file (`greeting-source.kamelet.yaml`) is a resource, not a list of routes. Its template uses `{{property}}` placeholders, filled from the URI parameters, then `camel.kamelet..` properties, then the declared `default`; `{{?property}}` drops the option when it has no value: ```yaml apiVersion: camel.apache.org/v1 kind: Kamelet metadata: name: greeting-source labels: camel.apache.org/kamelet.type: source spec: definition: required: - message properties: message: type: string period: type: integer default: 1000 template: from: uri: timer:greeting parameters: period: "{{period}}" steps: - setBody: simple: "{{message}}" - to: kamelet:sink ``` A Pipe (`orders.pipe.yaml`) binds a source to a sink, with optional steps in between, and becomes a route whose id is `metadata.name`. Each endpoint is a Kamelet `ref` with `properties` or a plain `uri`; `spec.errorHandler` takes `sink`, `log` or `none`: ```yaml apiVersion: camel.apache.org/v1 kind: Pipe metadata: name: greetings-pipe spec: source: ref: kind: Kamelet apiVersion: camel.apache.org/v1 name: timer-source properties: message: Hello from a Pipe period: 100 repeatCount: 1 steps: - ref: kind: Kamelet apiVersion: camel.apache.org/v1 name: set-body-action properties: value: "piped: ${body}" sink: uri: mock:piped ``` `cameleer bind my-pipe --source timer-source --sink log-sink` writes the Pipe file `my-pipe.yaml` (`--output FILE` picks another name). The [Kamelets section](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#kamelets) of the EIP guide has more. ## Endpoints An endpoint is a URI: `scheme:path?option=value&option=value`. Each component's page lists its options: [docs/components/](https://pmsudhi.github.io/cameleer/docs/components/index.html) (for example [kafka](https://pmsudhi.github.io/cameleer/docs/components/kafka.html)), and `cameleer catalog components` lists every scheme. Unknown options fail when the route starts, exactly like Apache Camel: `There are 1 parameters that couldn't be set on the endpoint ... Unknown parameters=[perod]`. Two equivalent ways to write options: ```yaml - route: from: uri: direct:forms steps: # inline: the whole URI in one string - to: "seda:inline?size=100&blockWhenFull=true" # uri + parameters: easier to read, values are URL-encoded for you - to: uri: seda:mapped parameters: size: 100 blockWhenFull: true - to: mock:forms ``` - With `parameters`, values are URL-encoded (spaces, `&`, `+`, `=` are safe), except values that contain `{{`, `${` or start with `RAW(`, which are passed as they are. A list value is joined with commas. - In an inline URI you encode values yourself; `+` means a space. To pass a value exactly as written (passwords, regular expressions), wrap it in `RAW(...)`: `password=RAW(se+cr&t)`. `RAW{...}` works too when the value contains `)`. - `from` takes the same forms: `from: {uri: ..., parameters: {...}, steps: [...]}`. - Quote a URI when it contains `#` after a space, `: ` (colon and space), or starts with `{`, `*`, `&`, `!`, `%` or `@`; quoting is always safe. ```yaml - route: from: uri: direct:raw steps: - to: "log:audit?marker=RAW(a+b&c)" - to: uri: log:audit-mapped parameters: marker: "a+b&c" - to: mock:raw ``` ### Property placeholders Any string (URIs, step options, expressions) can contain placeholders, resolved when the route is created: | placeholder | value | |---|---| | `{{name}}` | the property `name` from `application.properties`/`application.yaml`, `--prop`, `CAMEL_*` environment variables, or `CAMEL_PROPERTY_NAME`; fails when missing | | `{{name:default}}` | the same, with a default (everything after the first `:`, so `{{broker:localhost:9092}}` works) | | `{{env:NAME}}`, `{{env:NAME:default}}` | the environment variable `NAME` | | `{{sys:NAME}}` | the environment variable `NAME` (there are no JVM system properties) | | `{{aws:...}}`, `{{azure:...}}`, `{{gcp:...}}` | secrets from a cloud vault, see [cloud credentials](https://pmsudhi.github.io/cameleer/docs/guides/cloud-credentials.html) | ```yaml - route: from: uri: "timer:config?period={{poll.period:200}}&repeatCount=1" steps: - setBody: simple: "{{greeting:Hello}} from {{env:SITE_NAME:the lab}}" - to: mock:configured ``` A value that **starts** with `{{` must be quoted (`"{{broker}}"`), otherwise YAML reads it as a flow mapping. Never write secrets in route files: use `{{env:...}}` or a vault. ### Sending to endpoints | step | keys | description | |---|---|---| | `to` | a URI string, or `uri`, `parameters`, `pattern` (`InOnly`/`InOut`) | send the message and continue with the reply | | `toD` | the same plus `ignoreInvalidEndpoint` (default `false`) | a dynamic URI, evaluated with Simple per message | | `inOnly`, `inOut` | like `to` | `to` with a fixed exchange pattern (deprecated in Camel 4, kept for old routes) | | `wireTap` | URI or `uri`/`parameters`, `copy` (default `true`), `body`, `setHeader` (list of `{name, }`), `onPrepare`, `ignoreInvalidEndpoint` | send a copy asynchronously and continue at once | | `enrich` | URI, `uri`/`parameters`, or an expression; `aggregationStrategy`, `aggregateOnException`, `ignoreInvalidEndpoint`, `shareUnitOfWork` | call an endpoint and merge its reply (default: the reply replaces the message) | | `pollEnrich` | like `enrich`, plus `timeout` (ms; default `-1` = wait) | consume one message from an endpoint (file, seda, ...) and merge it | | `poll` | URI or `uri`/`parameters`, `timeout` (default 20000; whole milliseconds only, a unit such as `2s` is rejected) | consume one message and make it the message | | `kamelet` | a name, or `name` and `parameters` | send to `kamelet:name` | | `bean` | `ref` (or `beanType`), `method`, `scope` | call a bean bound in the registry (`bean:` endpoint) | | `process` | a name, or `ref` | run a processor bound in the registry | | `serviceCall` | `name`, `uri`, `component`, `pattern`, `expression`, `staticServiceDiscovery`, `serviceDiscoveryRef`, filters, `randomLoadBalancer`/`roundRobinLoadBalancer` | call a named service through a load balancer | `uri` values with `${...}` need `toD`; `to` treats the URI as fixed text. `pollEnrich` and `poll` consume from an endpoint in the middle of a route, for example a message waiting on a queue or a file in a folder: ```yaml - route: from: uri: direct:with-config steps: - pollEnrich: uri: seda:latest-config timeout: 2000 - setBody: simple: "config is ${body}" - to: mock:configured-call - route: from: uri: direct:next-job steps: - poll: uri: seda:jobs timeout: 2000 - to: mock:job ``` ```yaml - route: from: uri: direct:dispatch steps: - wireTap: mock:audit-copy - enrich: uri: direct:price-lookup - toD: "mock:region-${header.region}" - route: from: uri: direct:price-lookup steps: - setBody: simple: "${body} costs 42" ``` ## Expressions and predicates Steps such as `setBody`, `filter`, `split` and `choice`/`when` take an expression (a value) or a predicate (an expression whose result is read as true or false). Every form below works wherever a step takes one. | form | example | meaning | |---|---|---| | short | `simple: "${body.length()}"` | a language key and the expression text | | long | `simple: {expression: "${header.n}", resultType: int}` | the text plus language options | | wrapped | `expression: {simple: "${body}"}` | the Camel schema's explicit form (also `expression: {constant: x}`) | | plain string | `setBody: "Hello ${body}"` | a Simple expression | | plain number or boolean | `setBody: 42` | a constant | | generic language | `language: {language: groovy, expression: "..."}` | any registered language by name | A step may name only one language key; two (for example `simple` and `constant`) fail with `only one expression language may be specified`. Language options in the long form (for example `resultType`, `trim`, `headerName`, `source`, `suppressExceptions`) are passed to the language. ```yaml - route: from: uri: direct:forms-of-expressions steps: - setHeader: name: short simple: "${body.toUpperCase()}" - setHeader: name: typed simple: expression: "${body.length()}" resultType: java.lang.Integer - setHeader: name: wrapped expression: constant: fixed - setHeader: name: plain expression: "plain ${body}" - setBody: "Hello ${body}" - to: mock:expressions ``` ### Language keys | key | language | notes | |---|---|---| | `simple` | [Simple](https://pmsudhi.github.io/cameleer/docs/languages/simple.html) | the default; `${body}`, `${header.x}`, operators, functions | | `constant` | [Constant](https://pmsudhi.github.io/cameleer/docs/languages/constant.html) | a fixed value | | `header` | [Header](https://pmsudhi.github.io/cameleer/docs/languages/header.html) | the value of a header: `header: orderId` | | `exchangeProperty` | [Exchange property](https://pmsudhi.github.io/cameleer/docs/languages/exchangeProperty.html) | the value of an exchange property | | `variable` | [Variable](https://pmsudhi.github.io/cameleer/docs/languages/variable.html) | the value of a variable | | `jsonpath` | [JSONPath](https://pmsudhi.github.io/cameleer/docs/languages/jsonpath.html) | `jsonpath: "$.items[*].sku"` | | `jq` | [jq](https://pmsudhi.github.io/cameleer/docs/languages/jq.html) | `jq: ".items \| length"` | | `xpath` | [XPath](https://pmsudhi.github.io/cameleer/docs/languages/xpath.html) | XPath 1.0 | | `xquery` | [XQuery](https://pmsudhi.github.io/cameleer/docs/languages/xquery.html) | | | `tokenize` | [Tokenize](https://pmsudhi.github.io/cameleer/docs/languages/tokenize.html) | `tokenize: ","` or `tokenize: {token: ",", group: 10}` | | `method` | [Bean method](https://pmsudhi.github.io/cameleer/docs/languages/bean.html) | a bean in the registry | | `ref` | [Ref](https://pmsudhi.github.io/cameleer/docs/languages/ref.html) | an expression bound in the registry | | `groovy`, `js`/`javaScript`, `python`, `mvel`, `ognl`, `spel`, `datasonnet`, `csimple`, `joor`/`java` | script languages, for example [groovy](https://pmsudhi.github.io/cameleer/docs/languages/groovy.html) and [python](https://pmsudhi.github.io/cameleer/docs/languages/python.html) | interpreters from `cameleer-script` | | `hl7terser` | [HL7 terser](https://pmsudhi.github.io/cameleer/docs/languages/hl7terser.html) | | | `language` | any | `language: {language: , expression: ...}` | `cameleer catalog languages` lists the languages of your build. The loader also accepts the keys `xtokenize` and `wasm`, but no language of those names is built in, so a route that uses them fails when it starts. ```yaml - route: from: uri: direct:languages steps: - setProperty: name: source constant: sensor-7 - setHeader: name: firstSku jsonpath: "$.items[0].sku" - setHeader: name: count jq: ".items | length" - setHeader: name: from exchangeProperty: source - setHeader: name: copy header: firstSku - to: mock:languages ``` ### Predicates A predicate is an expression read as a boolean. Simple predicates use operators: `==`, `!=`, `>`, `>=`, `<`, `<=`, `contains`, `not contains`, `startsWith`, `endsWith`, `regex`, `in`, `range`, `is`, and `&&` / `||` to combine them. Quote string literals inside Simple with single quotes: `${header.type} == 'gold'`. Any language works as a predicate (`jsonpath`, `jq`, `xpath`, ...). The result is read as in Camel: a boolean is its value; a string is false only when it is `false` (any case), so even an empty string is true; a list is true when it is not empty; `null` is false; anything else (numbers, including `0`) is true. ## Steps Every step is a list item with one key. Keys every step accepts: - `id`: names the step (shown by `cameleer get routes` and in statistics; also applies to `when` and `otherwise`). - `disabled: true`: skips the step, as if it were not there. A step with no options can be written as a bare name (`- stop`) or with an empty mapping (`- stop: {}`). Steps that contain other steps (`filter`, `split`, `doTry`, ...) take a `steps` list, indented under the step's own keys. Unknown keys of a step are ignored by the loader; [`cameleer validate`](https://pmsudhi.github.io/cameleer/docs/guides/validate.html) reports them. The tables below give each step's keys and defaults. The EIP guides explain the patterns in depth: [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html), [split and aggregate](https://pmsudhi.github.io/cameleer/docs/guides/eips/split-aggregate.html), [transformation](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html), [flow control](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html), [interceptors, completions and templates](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html), and [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html). ### Message content | step | keys | description | |---|---|---| | `setBody` | an expression | replace the body | | `transform` | an expression | the same as `setBody` (the Message Translator name) | | `setHeader` | `name`, an expression | set one header | | `setHeaders` | `headers`: a list of `{name, }` | set several headers in order | | `setProperty` | `name`, an expression | set an exchange property | | `setVariable` | `name`, an expression | set a variable | | `setVariables` | `variables`: a list of `{name, }` | set several variables | | `removeHeader` | a name, or `name` | remove one header | | `removeHeaders` | a pattern, or `pattern` and `excludePattern` (list or comma separated) | remove matching headers (`*` wildcards or a regex) | | `removeProperty`, `removeProperties` | like the header steps | remove exchange properties | | `removeVariable` | a name, or `name` | remove a variable | | `convertBodyTo` | a type, or `type`, `mandatory` (default `false`) | convert the body: `String`, `Integer`, `Long`, `Double`, `Boolean`, `byte[]`, Java names like `java.lang.String` | | `convertHeaderTo` | `name`, `type`, `mandatory` | convert a header | | `marshal`, `unmarshal` | a data format: `json: {}`, `csv: {delimiter: ";"}`, `custom: {ref: name}`, or a plain name (`- marshal: json`) | object to text/bytes and back; see [data formats](https://pmsudhi.github.io/cameleer/docs/dataformats/index.html) | | `transformDataType` | `toType`, `fromType` | apply a registered data type transformer | | `script` | an expression | run a script; its result is discarded, its changes to the message are kept | | `log` | a Simple message, or `message`, `loggingLevel` (default `INFO`), `logName`, `marker` | log a line | | `setExchangePattern` | `InOnly` or `InOut` (or `pattern`) | change the exchange pattern | | `claimCheck` | `operation` (`Set`, `Get`, `GetAndRemove`, `Push`, `Pop`), `key`, `filter` | store part of the message and get it back later | | `sort` | an expression, `comparator` | sort a list body | ```yaml - route: from: uri: direct:message steps: - setHeaders: headers: - name: customer simple: "${body[customer]}" - name: tmpTrace constant: "abc" - setProperty: name: total simple: "${body[total]}" - setVariable: name: greeting constant: Dear - removeHeaders: pattern: "tmp*" - marshal: json: {} - convertBodyTo: String - log: message: "order JSON: ${body}" loggingLevel: DEBUG logName: orders - setBody: simple: "${variable.greeting} ${header.customer}, total ${exchangeProperty.total}" - to: mock:message ``` ```yaml - route: from: uri: direct:more-message-steps steps: - setVariables: variables: - name: unit constant: kg - name: tmp constant: scratch - removeVariable: tmp - convertHeaderTo: name: weight type: Integer - claimCheck: operation: Push - transform: simple: "${header.weight} ${variable.unit}" - to: mock:weight - claimCheck: operation: Pop - sort: simple: "${body}" - to: mock:sorted ``` ### Routing | step | keys | description | |---|---|---| | `filter` | a predicate, `steps` | run the steps only for matching messages; others continue after the filter | | `choice` | `when` (list of `{, steps}`), `otherwise` (`{steps}` or a list), `precondition` | the first matching `when` runs; otherwise `otherwise` | | `multicast` | `steps`, `parallelProcessing`, `aggregationStrategy`, `stopOnException`, `streaming`, `timeout`, `shareUnitOfWork` | send a copy of the message to each step | | `recipientList` | an expression, `delimiter` (default `,`), `ignoreInvalidEndpoints`, plus the multicast keys | send to the endpoints the expression returns | | `routingSlip` | an expression, `uriDelimiter` (default `,`), `ignoreInvalidEndpoints` | send through the endpoints one after another | | `dynamicRouter` | an expression, `uriDelimiter`, `ignoreInvalidEndpoints` | ask the expression for the next endpoint until it returns nothing | | `loadBalance` | one policy (`roundRobin`, `random`, `topic`, `sticky: {correlationExpression}`, `weighted: {distributionRatio, roundRobin}`, `failover: {exception, maximumFailoverAttempts, roundRobin, sticky}`, `custom: {ref}`), `steps` | send each message to one of the steps (default `roundRobin`) | | `loop` | an expression (a count, or a predicate with `doWhile: true`), `copy` (default `false`), `steps` | repeat the steps; `CamelLoopIndex` holds the index | | `pipeline` | `steps` | run steps in sequence (useful inside `multicast`) | | `step` | `steps`, `id` | group steps under one id for statistics | | `stop` | none | stop routing this message | ```yaml - route: from: uri: direct:tickets steps: - choice: when: - simple: "${header.priority} == 'P1'" steps: - to: mock:page-oncall - simple: "${header.priority} == 'P2'" steps: - to: mock:team-queue otherwise: steps: - to: mock:backlog - filter: simple: "${header.priority} == 'P3'" steps: - stop - to: mock:escalated ``` ```yaml - route: from: uri: direct:fan-out steps: - multicast: aggregationStrategy: groupedBody steps: - pipeline: steps: - setBody: simple: "email:${body}" - pipeline: steps: - setBody: simple: "sms:${body}" - to: mock:fanned-out - loop: constant: 3 steps: - setBody: simple: "attempt ${exchangeProperty.CamelLoopIndex}" - to: mock:loop - loadBalance: roundRobin: {} steps: - to: mock:node-a - to: mock:node-b ``` ```yaml - route: from: uri: direct:notify steps: # endpoints listed in a header, comma separated - recipientList: simple: "${header.channels}" - route: from: uri: direct:photo steps: # the endpoints in turn, each one's output feeding the next - routingSlip: header: slip - route: from: uri: direct:resize steps: - setBody: simple: "${body}-resized" ``` ### Split, aggregate and reorder | step | keys | description | |---|---|---| | `split` | an expression, `steps`, `aggregationStrategy`, `parallelProcessing`, `streaming`, `stopOnException`, `timeout`, `shareUnitOfWork`, `delimiter` (default `,`, `"false"` to keep a string whole) | run the steps once per part; parts get `CamelSplitIndex`, `CamelSplitSize`, `CamelSplitComplete` | | `aggregate` | `correlationExpression` (required), `aggregationStrategy` (required), `completionSize`, `completionTimeout`, `completionInterval`, `completionPredicate`, `completionSizeExpression`, `completionTimeoutExpression`, `completionFromBatchConsumer`, `completionOnNewCorrelationGroup`, `eagerCheckCompletion`, `forceCompletionOnStop`, `discardOnCompletionTimeout`, `ignoreInvalidCorrelationKeys`, `closeCorrelationKeyOnCompletion`, `aggregationRepository`, `steps` | combine related messages into one | | `resequence` | an expression, `batchConfig: {batchSize: 100, batchTimeout: 1000, allowDuplicates, reverse, ignoreInvalidExchanges}` or `streamConfig: {capacity: 1000, timeout: 1000, rejectOld, ignoreInvalidExchanges}`, `steps` | put messages back in order | `aggregationStrategy` takes the name of a strategy bound in the registry, or a built-in one: `groupedBody` (a list of bodies), `groupedExchange`, `useLatest`, `useOriginal`, `string` (concatenate), also written with Camel class names such as `#class:org.apache.camel.processor.aggregate.GroupedBodyAggregationStrategy`. Without a strategy, `split` continues with the original message, as in Camel. ```yaml - route: from: uri: direct:csv-lines steps: - split: tokenize: "\n" aggregationStrategy: groupedBody steps: - setBody: simple: "${body.toUpperCase()}" - to: mock:each-line - to: mock:all-lines - route: from: uri: direct:readings steps: - aggregate: correlationExpression: header: device aggregationStrategy: groupedBody completionSize: 2 completionTimeout: 5s steps: - to: mock:device-batch ``` ```yaml - route: from: uri: direct:out-of-order steps: - resequence: header: seq batchConfig: batchSize: 3 batchTimeout: 200 steps: - to: mock:in-order ``` ### Error handling | step | keys | description | |---|---|---| | `doTry` | `steps`, `doCatch` (list of `{exception: [...], onWhen, steps}`), `doFinally` (`{steps}`) | try/catch/finally inside a route | | `onException` | `exception` (list of kinds), `handled`, `continued`, `onWhen`, `retryWhile`, `redeliveryPolicy`, `redeliveryPolicyRef`, `useOriginalMessage`, `useOriginalBody`, `onRedeliveryRef`, `onExceptionOccurredRef`, `steps` | react to a kind of failure; as a route step it applies to that route, in a `routeConfiguration` to many | | `onCompletion` | `onCompleteOnly`, `onFailureOnly`, `mode` (`AfterConsumer` default, `BeforeConsumer`), `onWhen`, `steps` | run steps when the exchange finishes | | `circuitBreaker` | `steps`, `onFallback` (`{steps}`), `resilience4jConfiguration` (`failureRateThreshold`, `minimumNumberOfCalls`, `slidingWindowSize`, `waitDurationInOpenState` in seconds, `permittedNumberOfCallsInHalfOpenState`, `timeoutEnabled`, `timeoutDuration`), `faultToleranceConfiguration`, `configuration` (a registry name) | stop calling a failing service for a while | | `throwException` | `exceptionType` (required), `message` | fail the exchange with that kind of exception | | `validate` | a predicate | fail with `PredicateValidationException` when it is false | | `rollback` | a message, or `message`, `markRollbackOnly`, `markRollbackOnlyLast` | roll back the current transaction | | `transacted` | a policy name, or `ref`; `steps` | run in a transaction (without `steps`: the rest of the route) | | `policy` | `ref`, `steps` | wrap steps in a registered policy (without `steps`: the rest of the route) | | `saga` | `propagation`, `completionMode`, `timeout`, `compensation`, `completion`, `option` (list of `{key, }`), `sagaService`, `steps` | a long-running transaction with compensation | | `intercept`, `interceptFrom`, `interceptSendToEndpoint` | `onWhen`, `steps`; `interceptFrom` takes `uri` (default `*`); `interceptSendToEndpoint` takes `uri` (required) and `skipSendToOriginalEndpoint` | run steps before every step, every input, or every send to matching endpoints (usually in a `routeConfiguration`) | Exception kinds are Camel's Java class names; the package is ignored, so `java.lang.IllegalArgumentException` and `IllegalArgumentException` are the same. A kind matches its subkinds (`java.lang.Exception` catches everything). `handled` and `continued` take `true`, `false`, `{constant: true}` or a predicate. ```yaml - route: from: uri: direct:fragile steps: - onException: exception: - java.io.IOException redeliveryPolicy: maximumRedeliveries: 1 redeliveryDelay: 0 handled: constant: true steps: - setBody: constant: "service unavailable, queued" - to: mock:retry-later - doTry: steps: - validate: simple: "${body} != 'bad'" doCatch: - exception: - org.apache.camel.support.processor.validation.PredicateValidationException steps: - setBody: constant: "replaced a bad message" doFinally: steps: - to: mock:checked - circuitBreaker: steps: - filter: simple: "${body} == 'boom'" steps: - throwException: exceptionType: java.lang.IllegalStateException message: downstream failed onFallback: steps: - setBody: constant: "fallback answer" - filter: simple: "${body} == 'offline'" steps: - throwException: exceptionType: java.io.IOException message: disk offline - to: mock:fragile-done ``` Interceptors and completions are usually written once, at top level or in a `routeConfiguration`, and apply to every route without `routeConfigurationId`: ```yaml - interceptSendToEndpoint: uri: "mock:legacy*" skipSendToOriginalEndpoint: true steps: - setBody: simple: "redirected ${body}" - to: mock:modern - onCompletion: onFailureOnly: true steps: - to: mock:failures - route: from: uri: direct:intercepted steps: - to: mock:legacy-system - filter: simple: "${body} contains 'fail'" steps: - throwException: exceptionType: java.lang.IllegalStateException message: failed on purpose ``` ### Flow control | step | keys | description | |---|---|---| | `throttle` | a constant expression or `maximumRequests`, `timePeriodMillis` (default 1000), `correlationExpression`, `rejectExecution`, `asyncDelayed` | at most N messages per period | | `delay` | an expression (milliseconds), `asyncDelayed` (default `true`) | wait before continuing | | `sample` | a period, or `samplePeriod` (default 1 s) or `messageFrequency` | let one message through per period or per N messages | | `idempotentConsumer` | an expression (the message id), `steps`, `idempotentRepository` (default: in memory, 1000 ids), `eager` (`true`), `skipDuplicate` (`true`), `removeOnFailure` (`true`), `completionEager` (`false`) | process each id once | | `threads` | `poolSize`, `maxPoolSize`, `keepAliveTime` (+ `timeUnit`, default seconds), `maxQueueSize`, `allowCoreThreadTimeOut`, `rejectedPolicy`, `callerRunsWhenRejected`, `threadName`, `executorService`, `steps` | continue on a thread pool (without `steps`: the rest of the list) | ```yaml - route: from: uri: direct:events steps: - idempotentConsumer: header: eventId steps: - throttle: constant: 100 timePeriodMillis: 1000 - delay: constant: 10 - to: mock:unique-events ``` ## Values: booleans, numbers and durations - Booleans are `true` and `false` (any case). YAML 1.2 is used, so `yes`, `no`, `on` and `off` are strings: a boolean option set to `yes` fails with `'yes' is not a boolean`, and `disabled: yes` does not disable the step. Placeholders work: `autoStartup: "{{orders.enabled:true}}"`. - Numbers can be written plain (`completionSize: 10`) or quoted (`"10"`). - Durations (`completionTimeout`, `redeliveryDelay`, `timePeriodMillis`, `timeout`, ...) are milliseconds when plain (`5000`), or take a unit: `250ms`, `5s`, `1m`, `PT1M`. The exception is the `timeout` of `poll`, which takes whole milliseconds only. `waitDurationInOpenState` of `resilience4jConfiguration` and `keepAliveTime` of `threads` are in seconds, as in Camel. - Endpoint option values are text: `period: 1000` and `period: "1000"` are the same. ## Ids and descriptions - Route ids: `id` on the route; a missing id becomes `route1`, `route2`, ... Ids must be unique in the context, across all files. - Step ids: `id` on any step, `when` or `otherwise`. With `prefixId`, a `templatedRoute` adds a prefix to each. - `description` on a route is kept for management; on a step it is ignored. ## Common mistakes **`steps` at the wrong indentation.** `steps` belongs under `from` (at the same level as `uri`) or under `route`; nested `steps` belong under their own step, at the level of its other keys. Indentation decides which mapping a key belongs to: ```text - route: from: uri: direct:a steps: # right: under from, aligned with uri - to: mock:b - route: from: uri: direct:c steps: # wrong: a second key of the list item, rejected with - to: mock:d # "Each top-level element must have exactly one key" - route: from: uri: direct:e steps: - filter: simple: "${body} == 'x'" steps: # wrong: a second key of the step, rejected with - to: mock:f # "A step must have exactly one key" ``` **Two keys in one step.** Each list item is one step. Writing an option at the step level fails with `A step must have exactly one key`: ```text - to: mock:a pattern: InOut # wrong: belongs under `to` - to: uri: mock:a pattern: InOut # right ``` **Unquoted special characters.** These need quotes: | value | why | write | |---|---|---| | `log: Status: ${body}` | `: ` starts a mapping | `log: "Status: ${body}"` | | `simple: Order #${body}` | ` #` starts a comment | `simple: "Order #${body}"` | | `simple: {"id": 1}` | `{` starts a flow mapping | `simple: '{"id": 1}'` | | `uri: {{broker}}` | `{{` starts a flow mapping | `uri: "{{broker}}"` | | `constant: 007` | a number (7) | `constant: "007"` | | `constant: true` | a boolean, not text | `constant: "true"` | Single quotes keep backslashes and `${...}` literally (`'a\nb'` is four characters); double quotes process escapes (`"a\nb"` has a line break). Use double quotes for `tokenize: "\n"`. **`${...}` in a `to` URI.** `to` sends to the URI as written; use `toD` for a URI built from the message. **Booleans as words.** `yes`, `on` and `off` are not booleans (see [values](#values-booleans-numbers-and-durations)). **`:` in URIs.** Plain URIs like `timer:tick?period=1000` need no quotes: a colon only matters when a space follows it. Quote a URI that contains `: ` or `#`. **Expecting `split` to return the parts.** Without `aggregationStrategy`, the message after a `split` is the original one. **Two expression languages in one step.** `setBody: {simple: ..., constant: ...}` fails; pick one. ## Differences from Apache Camel - Same element names, step names and option names as the Camel 4 YAML DSL; old Camel 3 shapes that the loader still reads: `expression: "text"` (Simple), `routeTemplates`/`templatedRoutes`, `inOnly`/`inOut`. - Extra short forms: `setBody: "text"` (a Simple expression), `tokenize: ","`, steps as bare names (`- stop`), and a route mapping with `from` but without the `route:` wrapper. - Beans of Java classes are not instantiated (see [beans](#beans)); processors and strategies are bound from Rust. - `throttle` needs a constant maximum; `interceptSendToEndpoint` `afterUri` is ignored with a warning; `dataFormats` at top level is stored but not used; unknown step keys are ignored (Camel's schema rejects them); `cameleer validate` reports them. - The loader uses YAML 1.2 (like Camel's snakeyaml-engine), so `yes`/`no` are strings. ## Common questions ### How do I write a Camel route in YAML? Create a file ending in `.camel.yaml` with a list containing one `- route:`, give it a `from` with a `uri` and a `steps` list, and run `cameleer run file.camel.yaml`. Start from the [first look](#a-first-look) example. ### Can I run my Apache Camel YAML routes unchanged? Yes when they use components and options Cameleer supports: the element, step and option names are the same. Java beans and processors must be rewritten in Rust. `cameleer validate` tells you what does not resolve; see [migrating from Apache Camel](https://pmsudhi.github.io/cameleer/docs/guides/migrating-from-apache-camel.html). ### How do I load all route files of a folder? `cameleer run folder/*.camel.yaml`, or set `camel.main.routes-include-pattern=folder/**/*.camel.yaml`. With no arguments, `cameleer run` loads the route files of the current folder. ### How do I share error handling between routes? Put `onException` clauses and an `errorHandler` in a `routeConfiguration`. Without an `id` it applies to every route; with an `id`, list it in each route's `routeConfigurationId`. ### Is there a JSON Schema for editors? Yes: `cameleer schema > cameleer.schema.json` prints a JSON Schema of the YAML DSL that editors (VS Code with the YAML extension, IntelliJ) use for completion and checks. Setup steps are in [editor support](https://pmsudhi.github.io/cameleer/docs/guides/validate.html#editor-support); `cameleer validate` covers what a schema cannot, such as endpoint options and expressions. ## Related - [YAML for coding agents](https://pmsudhi.github.io/cameleer/docs/guides/yaml-for-coding-agents.html) and [`cameleer validate`](https://pmsudhi.github.io/cameleer/docs/guides/validate.html) - [Getting started](https://pmsudhi.github.io/cameleer/docs/guides/getting-started.html), [concepts](https://pmsudhi.github.io/cameleer/docs/guides/concepts.html), [EIP guide](https://pmsudhi.github.io/cameleer/docs/guides/eips.html), [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html) - [Components](https://pmsudhi.github.io/cameleer/docs/components/index.html), [languages](https://pmsudhi.github.io/cameleer/docs/languages/index.html), [data formats](https://pmsudhi.github.io/cameleer/docs/dataformats/index.html) - Apache Camel: [YAML DSL](https://camel.apache.org/components/next/others/yaml-dsl.html) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/eips.html # Enterprise Integration Patterns in Cameleer > Cameleer implements Apache Camel's Enterprise Integration Patterns (EIPs) with the same names, > options and semantics; this page lists every one and links to a tested YAML example and its > Rust DSL equivalent. An EIP is a named solution to a messaging problem: filter, route, split, aggregate, retry. In a route each EIP is a step. The YAML names are Camel's (`split`, `aggregate`, `circuitBreaker`); the Rust DSL uses snake_case (`split`, `aggregate`, `circuit_breaker`) and closes blocks with `.end()`. Every example on the linked pages runs twice in the test suite, once as YAML and once as Rust, against the same assertions (`crates/cameleer-main/tests/docs_guides.rs`). ## Routing | EIP | YAML | Rust DSL | page | |---|---|---|---| | Pipeline, To, Dynamic To, Step | `to`, `toD`, `step` | `to`, `to_d`, `step` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#pipeline-to-tod-and-step) | | Message Filter | `filter` | `filter` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#message-filter) | | Content-Based Router | `choice` / `when` / `otherwise` | `choice().when().otherwise().end()` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#content-based-router-choice) | | Recipient List | `recipientList` | `recipient_list` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#recipient-list) | | Routing Slip | `routingSlip` | `routing_slip` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#routing-slip) | | Dynamic Router | `dynamicRouter` | `dynamic_router` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#dynamic-router) | | Multicast | `multicast` | `multicast` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#multicast) | | Wire Tap | `wireTap` | `wire_tap` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#wire-tap) | | Load Balancer (round robin, random, sticky, weighted, topic, failover, custom) | `loadBalance` | `load_balance` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#load-balancer) | | Loop | `loop` | `loop_` | [routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#loop) | ## Splitting, aggregating and ordering | EIP | YAML | Rust DSL | page | |---|---|---|---| | Splitter | `split` | `split` | [split and aggregate](https://pmsudhi.github.io/cameleer/docs/guides/eips/split-aggregate.html#splitter) | | Composed Message Processor (split + aggregation strategy) | `split` with `aggregationStrategy` | `split(..).aggregation_strategy(..)` | [split and aggregate](https://pmsudhi.github.io/cameleer/docs/guides/eips/split-aggregate.html#split-and-aggregate-the-results) | | Aggregator | `aggregate` | `aggregate` | [split and aggregate](https://pmsudhi.github.io/cameleer/docs/guides/eips/split-aggregate.html#aggregator) | | Resequencer (batch, stream) | `resequence` | `resequence` | [split and aggregate](https://pmsudhi.github.io/cameleer/docs/guides/eips/split-aggregate.html#resequencer) | | Sort | `sort` | `sort` | [split and aggregate](https://pmsudhi.github.io/cameleer/docs/guides/eips/split-aggregate.html#sort) | ## Transformation | EIP | YAML | Rust DSL | page | |---|---|---|---| | Message Translator | `setBody`, `transform`, `setHeader`, `removeHeaders` | `set_body`, `transform`, `set_header`, `remove_headers` | [transformation](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html#message-translator-setbody-transform-setheader) | | Marshal, Unmarshal (data formats) | `marshal`, `unmarshal` | `marshal`, `unmarshal`, `marshal_json` | [transformation](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html#marshal-and-unmarshal) | | Convert Body | `convertBodyTo` | `convert_body_to` | [transformation](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html#convert-the-body-type) | | Content Enricher | `enrich` | `enrich` | [transformation](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html#content-enricher-enrich) | | Poll Enrich | `pollEnrich` | `poll_enrich` | [transformation](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html#poll-enrich-pollenrich) | | Claim Check | `claimCheck` | `claim_check` | [transformation](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html#claim-check) | | Script | `script` | `script`, `process(cameleer_script::script(..))` | [transformation](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html#script) | ## Flow control and resilience | EIP | YAML | Rust DSL | page | |---|---|---|---| | Idempotent Consumer | `idempotentConsumer` | `idempotent_consumer` | [flow control](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#idempotent-consumer) | | Throttle | `throttle` | `throttle` | [flow control](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#throttle) | | Delay | `delay` | `delay` | [flow control](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#delay) | | Sampling | `sample` | `sample`, `sample_every` | [flow control](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#sampling) | | Circuit Breaker | `circuitBreaker` / `onFallback` | `circuit_breaker().on_fallback()` | [flow control](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#circuit-breaker) | | Validate | `validate` | `validate` | [flow control](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#validate) | | Threads | `threads` | `threads`, `threads_with` | [flow control](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#threads) | ## Interceptors, completions and reuse | EIP | YAML | Rust DSL | page | |---|---|---|---| | Intercept Send To Endpoint | `interceptSendToEndpoint` | `intercept_send_to_endpoint` | [system](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#intercept-sends-to-an-endpoint) | | Intercept From, Intercept | `interceptFrom`, `intercept` | `intercept_from`, `intercept` | [system](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#intercept-every-input) | | On Completion | `onCompletion` | `on_completion` | [system](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#on-completion) | | Saga | `saga` | `saga` | [system](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#saga) | | Route Template | `routeTemplate`, `templatedRoute` | `add_route_template`, `TemplatedRouteBuilder` | [system](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#route-templates) | | Kamelet | `kamelet` | `kamelet` | [system](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#kamelets) | | Poll | `poll` | `poll` | [system](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#poll) | | Stop | `stop` | `stop` | [system](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html#stop) | ## Error handling | EIP | YAML | Rust DSL | page | |---|---|---|---| | Dead Letter Channel, redelivery | `errorHandler: deadLetterChannel` | `error_handler(dead_letter_channel(..))` | [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html#dead-letter-channel-with-redelivery) | | Exception clause | `onException` | `on_exception` | [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html#onexception-react-to-one-kind-of-failure) | | Try, catch, finally | `doTry` / `doCatch` / `doFinally` | `do_try().do_catch().do_finally().end_do_try()` | [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html#dotry-docatch-and-dofinally) | | Throw Exception, Rollback | `throwException`, `rollback` | `throw_exception`, `rollback`, `mark_rollback_only` | [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html#onexception-react-to-one-kind-of-failure) | ## Patterns without a guide example yet These are implemented and covered by Cameleer's own test suite, but this guide has no example for them yet; [FEATURES.md](https://pmsudhi.github.io/cameleer/docs/FEATURES.html#enterprise-integration-patterns) describes their options. | EIP | YAML | Rust DSL | |---|---|---| | Process, Bean (custom Rust code) | `process`, `bean` | `process`, `process_ref` (see [concepts](https://pmsudhi.github.io/cameleer/docs/guides/concepts.html#processors-and-eips)) | | Transacted, Policy | `transacted`, `policy` | `transacted`, `transacted_ref`, `policy` | | Route Policy | route `routePolicy` | `route_policy` | | Service Call | `serviceCall` | `service_call` | | Transformer, Validator (data types) | route `inputType`/`outputType`, `transformDataType` | `input_type`, `output_type`, `transform_data_type` | | Set Exchange Pattern (InOnly, InOut) | `setExchangePattern` | `set_exchange_pattern`, `in_only`, `in_out` | | Message History, Tracer | `camel.main.messageHistory`, `--trace` | `set_message_history`, `set_tracing` | ## Common questions ### Which EIPs does Cameleer support? All of Apache Camel's EIPs listed above, from the filter and content-based router to the saga and circuit breaker. Differences from Camel are noted on each page and in [FEATURES.md](https://pmsudhi.github.io/cameleer/docs/FEATURES.html). ### Are EIP options the same as in Apache Camel? Yes: names, defaults and semantics follow Camel 4 (for example `completionTimeout` is an inactivity timeout and a split without an aggregation strategy continues with the original message). Options Cameleer does not model, such as `executorService` on some EIPs, are accepted and ignored in YAML. ### How do I write an EIP in Rust instead of YAML? Use the method with the snake_case name on the route builder and close blocks with `.end()`: `b.from("direct:a").split(jsonpath("$.items")).to("mock:item").end();`. Each pattern page shows both forms. ## Related - [Routing](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html), [split and aggregate](https://pmsudhi.github.io/cameleer/docs/guides/eips/split-aggregate.html), [transformation](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html), [flow control](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html), [interceptors and reuse](https://pmsudhi.github.io/cameleer/docs/guides/eips/system.html), [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html) - [Concepts](https://pmsudhi.github.io/cameleer/docs/guides/concepts.html), [agents cookbook](https://pmsudhi.github.io/cameleer/docs/guides/agents.html), [edge cookbook](https://pmsudhi.github.io/cameleer/docs/guides/edge.html) - Apache Camel: [EIP catalogue](https://camel.apache.org/components/next/eips/enterprise-integration-patterns.html) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html # Error handling > Cameleer handles failures the way Apache Camel does: an error handler retries failed steps > with a redelivery policy and then gives up or moves the message to a dead letter channel, > `onException` reacts to specific exception types, and `doTry`/`doCatch`/`doFinally` handle > errors inline. Every example on this page runs in the test suite (`crates/cameleer-main/tests/docs_guides.rs`) as YAML and as Rust DSL. ## How failures work - A step fails by returning an error. The error has a **kind**, a Java-like exception class name such as `IOException` or `IllegalArgumentException`. Kinds have parents, like Java classes (`FileNotFoundException` is an `IOException`, every kind is an `Exception`), and `onException` and `doCatch` match a kind and its subkinds. Java class names in YAML (`java.io.IOException`) are matched by their simple name. - The **error handler** decides what happens next. The default one (`defaultErrorHandler`) does not retry and fails the exchange; the caller gets the error. The **dead letter channel** sends the failed message to an endpoint instead and marks it handled. `noErrorHandler` does nothing. - **Redelivery** happens at the point of failure: only the failed step runs again, not the whole route. The headers `CamelRedelivered`, `CamelRedeliveryCounter` and `CamelRedeliveryMaxCounter` tell the step which attempt it is. - After the last attempt the exception is on the exchange and the exchange property `CamelExceptionCaught` holds it for `onException` and dead letter routes; `${exception.message}` reads its text in Simple. The examples below call an inventory API bound as a processor bean. It fails with `IOException` on the first call and the first retry, and succeeds on the second retry; the body `poison` always fails: ```rust fn bind_inventory_api(ctx: &CamelContext) { ctx.registry().bind_processor("inventoryApi", |ex: &mut Exchange| { let attempt = ex.header("CamelRedeliveryCounter").map(|v| v.to_string()).unwrap_or_default(); if ex.body().to_string() == "poison" || attempt != "2" { return Err(CamelError::new("IOException", "connection reset by inventory API")); } ex.set_body(format!("reserved {}", ex.body())); Ok(()) }); } ``` ## Dead letter channel with redelivery The dead letter channel retries the failed step according to its `redeliveryPolicy`, then sends the message to `deadLetterUri`. With `useOriginalMessage: true` the dead letter endpoint gets the message as it entered the route, not the half-processed one, so it can be replayed. ```yaml - errorHandler: deadLetterChannel: deadLetterUri: mock:dead-letters useOriginalMessage: true redeliveryPolicy: maximumRedeliveries: 3 redeliveryDelay: 10 useExponentialBackOff: true backOffMultiplier: 2 - route: from: uri: direct:reserve steps: - process: ref: inventoryApi - to: mock:reserved ``` ```rust b.error_handler( dead_letter_channel("mock:dead-letters") .use_original_message() .maximum_redeliveries(3) .redelivery_delay(10) .use_exponential_back_off() .back_off_multiplier(2.0), ); b.from("direct:reserve") .process_ref("inventoryApi") .to("mock:reserved"); ``` `sku-1` fails twice, succeeds on the second retry and reaches `mock:reserved` as `reserved sku-1`. `poison` fails four times (one call and three retries, 10 ms, 20 ms and 40 ms apart) and lands on `mock:dead-letters`; the caller does not get an error, because the dead letter channel handled it. The YAML `errorHandler` applies to every route of the context; `b.error_handler(...)` applies to the routes of one builder, and a route can set its own with `errorHandler:` in YAML or `.error_handler(...)` in Rust. Redelivery policy options: `maximumRedeliveries` (`-1` = forever), `redeliveryDelay`, `useExponentialBackOff`, `backOffMultiplier`, `maximumRedeliveryDelay`, `useCollisionAvoidance`, `collisionAvoidanceFactor`, `delayPattern` (`0:1000;5:5000`), `retryAttemptedLogLevel`, `retriesExhaustedLogLevel`, `logStackTrace`, `logExhausted`, `logRetryAttempted` and `asyncDelayedRedelivery` (release seda and timer consumer threads during the delay). A dead letter channel can also set `deadLetterHandleNewException`. ## `onException`: react to one kind of failure `onException` catches the listed exception kinds (and their subkinds). `handled: true` stops the failure from reaching the caller, and the clause's steps build the reply. It can have its own redelivery policy, `onWhen` (a predicate), `continued: true` (ignore the error and go on with the route), `useOriginalMessage` and `retryWhile`. Put it in a `routeConfiguration` to apply it to every route, as in Camel 4. ```yaml - routeConfiguration: onException: - onException: exception: - java.lang.IllegalArgumentException handled: constant: true steps: - setHeader: name: CamelHttpResponseCode constant: 400 - setBody: simple: "rejected: ${exception.message}" - onException: exception: - java.io.IOException redeliveryPolicy: maximumRedeliveries: 1 handled: constant: true steps: - setBody: constant: "inventory unavailable, order queued" - to: mock:retry-later - route: from: uri: direct:order steps: - filter: simple: "${body} == ''" steps: - throwException: exceptionType: java.lang.IllegalArgumentException message: empty order - process: ref: inventoryApi - to: mock:orders ``` ```rust b.on_exception(&["IllegalArgumentException"]) .handled(true) .set_header("CamelHttpResponseCode", constant(400)) .set_body(simple("rejected: ${exception.message}")); b.on_exception(&["IOException"]) .maximum_redeliveries(1) .handled(true) .set_body(constant("inventory unavailable, order queued")) .to("mock:retry-later"); b.from("direct:order") .filter(simple("${body} == ''")) .throw_exception("IllegalArgumentException", "empty order") .end() .process_ref("inventoryApi") .to("mock:orders"); ``` An empty order gets the reply `rejected: empty order` with response code 400. An order for `sku-1` fails once, is retried once (not enough: the API needs two retries), and goes to `mock:retry-later` with the reply `inventory unavailable, order queued`. The most specific matching `onException` wins, as in Camel. ## `doTry`, `doCatch` and `doFinally` `doTry` handles errors inline, like `try`/`catch`/`finally`. `doCatch` takes a list of exception kinds and an optional `onWhen`; `doFinally` always runs. The error handler does not redeliver inside a `doTry`. ```yaml - route: from: uri: direct:parse steps: - doTry: steps: - unmarshal: json: {} - setBody: simple: "order ${body[id]}" doCatch: - exception: - java.lang.Exception steps: - setBody: simple: "not JSON: ${exception.message}" doFinally: steps: - to: mock:parse-attempts - to: mock:parsed ``` ```rust b.from("direct:parse") .do_try() .unmarshal_json() .set_body(simple("order ${body[id]}")) .do_catch(&["Exception"]) .set_body(simple("not JSON: ${exception.message}")) .do_finally() .to("mock:parse-attempts") .end_do_try() .to("mock:parsed"); ``` `{"id": 5}` becomes `order 5`; `hello` becomes `not JSON: ...` with the parser's message. Both pass through `mock:parse-attempts` and continue to `mock:parsed`. ## Error handling for AI and edge routes - Wrap model calls in redelivery for transient errors (`HttpOperationFailedException`, `IOException`) and in a [circuit breaker](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#circuit-breaker) for outages. See [the agents cookbook](https://pmsudhi.github.io/cameleer/docs/guides/agents.html#rate-limits-and-a-circuit-breaker-around-a-model-api). - On a gateway with an unreliable uplink, use a dead letter channel to a local `file:` directory and a second route that replays it. See [store and forward](https://pmsudhi.github.io/cameleer/docs/guides/edge.html#store-and-forward-when-the-uplink-is-down). - Use `useOriginalMessage: true` whenever the dead letter queue is meant for replay. ## Differences from Apache Camel - Exceptions are string kinds with a parent hierarchy, not Java classes. Custom kinds are registered with `register_kind_parent("MyException", "IOException")` so `onException` for `IOException` catches them. - With `asyncDelayedRedelivery`, the waiting exchange is handed to a released thread and a new consumer thread takes over, instead of a scheduler continuation. The effect for the route is the same. - A failure from a `ProducerTemplate` call is returned as `CamelExecutionException` with the original exception as its cause, as in Camel. ## Common questions ### How do I retry a failed HTTP call in Cameleer? Add a `redeliveryPolicy` with `maximumRedeliveries` and `redeliveryDelay` to the error handler or to an `onException` for `HttpOperationFailedException` or `IOException`. Only the failed call is retried. ### How do I send failed messages to a dead letter queue? Declare `errorHandler: deadLetterChannel: deadLetterUri: kafka:orders-dlq` (or any endpoint), with `useOriginalMessage: true` so the queue holds replayable input. ### How do I return a 400 error from a REST route? Throw `IllegalArgumentException` (or validate with `validate`), and handle it with an `onException` that sets `CamelHttpResponseCode` to 400 and the body to `${exception.message}`. ### What is the difference between `handled` and `continued`? `handled: true` ends the route after the `onException` steps and answers the caller normally; `continued: true` ignores the error and continues the route after the failed step. ## Related - [EIPs](https://pmsudhi.github.io/cameleer/docs/guides/eips.html), [flow control and resilience](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html) - Apache Camel: [Error handling](https://camel.apache.org/manual/error-handler.html), [Exception clause](https://camel.apache.org/manual/exception-clause.html), [Dead Letter Channel](https://camel.apache.org/components/next/eips/dead-letter-channel.html) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/agent-tool-calls.html # AI agent tool calls: direct vs through Cameleer > Direct tool calls leave retries, duplicates, half-done work and business rules to the model; > routing them through Cameleer makes each tool a tested route that validates, runs once, rolls > back, retries and audits, so the model only decides what to do. The short version, with the comparison table and FAQ, is on the [AI agent tool calls: direct vs Cameleer](https://pmsudhi.github.io/cameleer/compare/ai-agent-tool-calls-direct-vs-cameleer/) page. Function calling (also called tool use) is how an AI agent acts: the model answers with the name of a tool and its arguments as JSON, and your code executes it. This page compares two ways to execute those calls, on a real multi-step job, and shows the complete Cameleer route for it. The route runs end to end in the test suite (`crates/cameleer-main/tests/docs_guides.rs`) against mock order, payment, shipping and email APIs. ![Direct tool calls: an agent with API keys calls the orders, payments, shipping and email APIs itself in four tool calls and five model turns. Through Cameleer: the agent makes one tool call to a route that validates, runs once, checks policy, runs a saga with rollback, retries and audits, then calls the same APIs.](https://pmsudhi.github.io/cameleer/docs/guides/agent-tool-calls.svg) ## The difference in one table | concern | direct tool calls | through Cameleer | |---|---|---| | who runs a multi-step job | the model, one tool call per step, one model turn per call | a route; the model makes one call | | wrong or invented arguments | reach the API unless your glue code checks them | rejected by a JSON schema with every error listed, so the model can correct itself (`400`) | | the agent retries a call | the action runs again: a second refund, a second email | `idempotentConsumer` runs each `requestId` once; the repeat gets `duplicate` | | a step fails after an earlier one succeeded | half-done work the model has to notice and undo | a `saga` calls the compensation (here: cancel the refund) | | flaky APIs | the model sees the error and may retry, give up or improvise | retries with exponential back-off at the failing step, then a clear answer | | business rules (amount, limits, approvals) | written in the prompt and trusted to the model | code in the route: the amount comes from the order, large refunds wait for a person | | credentials | the agent process holds every API key | the route holds them; the agent can only call the tool | | audit | whatever the agent framework logs | every accepted call appended to an audit log | | rate limits and outages | per agent, if at all | `throttle` and `circuitBreaker` around any API ([agent cookbook](https://pmsudhi.github.io/cameleer/docs/guides/agents.html#rate-limits-and-a-circuit-breaker-around-a-model-api)) | | tokens and latency | every step is a model turn that re-sends the conversation and tool results | one tool call, one final turn; the steps run at code speed | | changing an API or vendor | change the tool, the prompt and maybe re-test the agent | change the route; the tool the model sees stays the same | | testing | needs the model in the loop | the route is tested without a model (as this page's route is) | ## The use case: a damaged order A customer writes to a support agent: > My order EU-4411 arrived broken. I want my money back. Resolving it takes four systems: read the order (is it delivered, what did it cost, who is the customer), refund it with the payment provider, create a return label with the carrier, and email the customer the label. Company rules: only delivered orders, the refund is the order total and not a number someone typed, and refunds over 200 need a person. ### Direct: the model orchestrates The agent gets four tools, roughly: ```json [ {"name": "get_order", "parameters": {"orderId": "string"}}, {"name": "issue_refund", "parameters": {"orderId": "string", "amount": "number", "reason": "string"}}, {"name": "create_return_label", "parameters": {"orderId": "string"}}, {"name": "send_email", "parameters": {"to": "string", "template": "string", "data": "object"}} ] ``` and a system prompt with the rules. A good run is four tool calls and five model turns: `get_order`, then `issue_refund` with the total the model read, then `create_return_label`, then `send_email`, then the reply to the customer. Each turn sends the conversation, the tool definitions and every tool result so far back to the model. What goes wrong in production: - **The refund is issued twice.** The payments API times out after it has refunded; the model, or the agent framework, retries `issue_refund`. Nothing in the call says it is the same request. - **Half-done work.** The refund succeeds, then the carrier API is down. The customer has the money and no label, and the model has to notice it and decide what to do, every time, without a rule. - **The model sets the amount.** `issue_refund` takes whatever `amount` the model passes: a misread total, a currency mix-up, or a customer's "refund me 1,299" injected into the chat. The 200 limit is a sentence in the prompt. - **Every key in one place.** The agent process holds the payment, carrier and email credentials, and the tool definitions show the model how to use them. - **No record.** Unless you add one, there is no log of which tool calls were made with which arguments, separate from the model's transcript. Each of these can be fixed in glue code around every tool, in every agent, in every language the team uses. That glue code is an integration layer; Cameleer is one. ### Through Cameleer: the model decides, a route executes The agent gets one tool. Its parameters are the facts only the conversation knows: ```json { "name": "resolve_damaged_order", "description": "Refund a delivered order that arrived damaged and send the customer a return label. The amount is taken from the order.", "parameters": { "type": "object", "required": ["requestId", "orderId", "reason"], "additionalProperties": false, "properties": { "requestId": {"type": "string", "pattern": "^[A-Za-z0-9-]{8,64}$"}, "orderId": {"type": "string", "pattern": "^[A-Z]{2}-[0-9]+$"}, "reason": {"type": "string", "enum": ["damaged", "missing_parts", "wrong_item"]} } } } ``` The same file, [schemas/damaged-order-request.json](https://github.com/pmsudhi/cameleer/blob/main/examples/agents/schemas/damaged-order-request.json), validates the call in the route, so the tool the model sees and the check that runs can't drift apart. A good run is one tool call and two model turns. The route does the four steps: 1. **Validate** the arguments against the schema. An invented `amount` or a free-text reason gets `400` with every error. 2. **Run once.** `idempotentConsumer` on `requestId`: a retried call answers `duplicate` and does nothing. 3. **Read the order** and take the amount and the customer's email from it. 4. **Apply the rules.** Not delivered: `409`. Over `APPROVAL_LIMIT` (200): the request is parked as a file for a person and the agent gets `202 pending_approval`, with a sentence it can pass on. The [human approval pattern](https://pmsudhi.github.io/cameleer/docs/guides/agents.html#human-in-the-loop-approval) shows the Slack message and the approval endpoint. 5. **Refund and label as a saga.** The sub-route `refund-with-label` retries each call twice with exponential back-off. If the label still fails, the saga calls `cancel-refund` with the `requestId`, and the agent gets `502` with "nothing was changed". 6. **Email and audit** through wire taps, so they don't slow the answer down; the audit log has one JSON line per accepted call. The answer is a small JSON object the model can turn into a reply: `{"status": "resolved", "refundId": "r-1", "amount": 129.9, "returnLabel": "https://..."}`. ```yaml - rest: path: /tools post: - path: /resolve-damaged-order to: direct:resolve-damaged-order - routeConfiguration: id: tool-errors onException: - onException: exception: - org.apache.camel.component.jsonvalidator.JsonValidationException handled: constant: true steps: - setHeader: name: CamelHttpResponseCode constant: 400 - setBody: simple: "invalid tool arguments: ${exception.message}" - onException: exception: - org.apache.camel.http.base.HttpOperationFailedException - java.io.IOException handled: constant: true steps: - setHeader: name: CamelHttpResponseCode constant: 502 - setHeader: name: Content-Type constant: application/json - setBody: simple: '{"status": "failed", "requestId": "${header.requestId}", "detail": "a backend did not answer; nothing was changed, retry later with a new requestId"}' - route: id: resolve-damaged-order routeConfigurationId: tool-errors from: uri: direct:resolve-damaged-order steps: - convertBodyTo: type: java.lang.String - to: "json-validator:file:{{env:SCHEMA_DIR:schemas}}/damaged-order-request.json" - unmarshal: json: {} - setHeader: name: requestId simple: "${body[requestId]}" - setHeader: name: orderId simple: "${body[orderId]}" - setHeader: name: reason simple: "${body[reason]}" - wireTap: uri: direct:audit-order-tool - idempotentConsumer: header: requestId steps: - removeHeaders: pattern: "CamelHttp*" - toD: "{{env:ORDERS_URL:http://localhost:9001}}/orders/${header.orderId}?httpMethod=GET" - unmarshal: json: {} - setHeader: name: amount simple: "${body[total]}" - setHeader: name: email simple: "${body[email]}" - choice: when: - simple: "${body[status]} != 'delivered'" steps: - setHeader: name: CamelHttpResponseCode constant: 409 - setBody: simple: '{"status": "rejected", "requestId": "${header.requestId}", "detail": "order ${header.orderId} is ${body[status]}; only delivered orders can be refunded"}' - simple: "${header.amount} > {{env:APPROVAL_LIMIT:200}}" steps: - setBody: simple: '{"requestId": "${header.requestId}", "orderId": "${header.orderId}", "amount": ${header.amount}, "reason": "${header.reason}"}' - to: "file:{{env:PENDING_DIR:pending}}?fileName=${header.requestId}.json" - setHeader: name: CamelHttpResponseCode constant: 202 - setBody: simple: '{"status": "pending_approval", "requestId": "${header.requestId}", "detail": "refunds over {{env:APPROVAL_LIMIT:200}} need a person; tell the customer they will hear back within one business day"}' otherwise: steps: - doTry: steps: - to: direct:refund-with-label - wireTap: uri: direct:notify-customer - setHeader: name: CamelHttpResponseCode constant: 200 - setBody: simple: '{"status": "resolved", "requestId": "${header.requestId}", "refundId": "${header.refundId}", "amount": ${header.amount}, "returnLabel": "${header.labelUrl}"}' doCatch: - exception: - org.apache.camel.http.base.HttpOperationFailedException - java.io.IOException steps: - setHeader: name: CamelHttpResponseCode constant: 502 - setBody: simple: '{"status": "failed", "requestId": "${header.requestId}", "detail": "the return label failed, so the refund was cancelled; nothing was changed. To try again later, call with a new requestId"}' - filter: simple: "${exchangeProperty.CamelDuplicateMessage} == true" steps: - setBody: simple: '{"status": "duplicate", "requestId": "${header.requestId}"}' - setHeader: name: Content-Type constant: application/json - route: id: refund-with-label errorHandler: defaultErrorHandler: redeliveryPolicy: maximumRedeliveries: 2 redeliveryDelay: 200 useExponentialBackOff: true from: uri: direct:refund-with-label steps: - saga: compensation: uri: direct:cancel-refund option: - key: requestId expression: simple: "${header.requestId}" steps: - setBody: simple: '{"requestId": "${header.requestId}", "orderId": "${header.orderId}", "amount": ${header.amount}, "reason": "${header.reason}"}' - enrich: uri: "{{env:PAYMENTS_URL:http://localhost:9002}}/refunds?httpMethod=POST" - unmarshal: json: {} - setHeader: name: refundId simple: "${body[refundId]}" - setBody: simple: '{"orderId": "${header.orderId}", "requestId": "${header.requestId}"}' - enrich: uri: "{{env:SHIPPING_URL:http://localhost:9003}}/return-labels?httpMethod=POST" - unmarshal: json: {} - setHeader: name: labelUrl simple: "${body[labelUrl]}" - route: id: cancel-refund from: uri: direct:cancel-refund steps: - removeHeaders: pattern: "CamelHttp*" - setBody: simple: '{"requestId": "${header.requestId}"}' - to: "{{env:PAYMENTS_URL:http://localhost:9002}}/refunds/cancel?httpMethod=POST" - route: id: notify-customer from: uri: direct:notify-customer steps: - removeHeaders: pattern: "CamelHttp*" - setBody: simple: '{"to": "${header.email}", "template": "refund-issued", "orderId": "${header.orderId}", "amount": ${header.amount}, "returnLabel": "${header.labelUrl}"}' - to: "{{env:EMAIL_URL:http://localhost:9004}}/messages?httpMethod=POST" - route: id: audit-order-tool from: uri: direct:audit-order-tool steps: - marshal: json: {} - setBody: simple: "${body}\n" - to: "file:{{env:AUDIT_DIR:audit}}?fileName=tool-calls.jsonl&fileExist=Append" ``` Three details: - The refund and the label are in their own route because the error handling has to be in the right place. A `handled` `onException` around the saga would clear the error before the saga sees it, so nothing would be compensated (Apache Camel behaves the same way). Inside `refund-with-label`, the default error handler retries and then lets the error through the saga, which compensates; the `doTry` in the main route turns it into the `502` answer. - A failed call is still an answered call, so its `requestId` is used up, and the `502` tells the agent to retry with a new one. The other answers are final, so a repeat is correctly a `duplicate`. - `removeHeaders: CamelHttp*` comes before the backend calls, because the incoming request's HTTP headers would otherwise change where the `http` producer sends them, as in Apache Camel. The calls with side effects use `enrich`, so a retry sends the original request again (see [the tool pattern](https://pmsudhi.github.io/cameleer/docs/guides/agents.html#expose-a-tool-to-an-agent-over-rest)). ### What the test checks | the agent sends | the route answers | what happened | |---|---|---| | `EU-4411`, `damaged` | `200 resolved`, amount 129.9 from the order | payments failed once with `503` and was retried; label created; customer emailed | | the same call again | `200 duplicate` | nothing executed a second time | | `orderId: "4411"`, a free-text reason and `amount: 1299` | `400` naming `orderId`, `reason` and `amount` | nothing executed, nothing audited | | `EU-1200`, not yet delivered | `409 rejected` | nothing executed | | `EU-9000`, total 899 | `202 pending_approval` | parked for a person, no refund | | `EU-5000`, carrier down | `502 failed` | refund issued once, label tried three times, refund cancelled, no email | ## When a direct call is enough Not every tool needs a route. Call the API directly when the call: - only reads, such as a search or a lookup, and a repeat does no harm; - is a single step with no other system to keep consistent; - is a prototype, and you will move the tool behind a route before real users touch it. Use a route as soon as a call changes something (money, stock, messages, tickets), spans several systems, needs a rule the model must not override, or must be audited. Read-only tools can still go through Cameleer to hide credentials and add limits. ## Cameleer's advantages for agent tools - **Fewer model turns.** A multi-step job is one tool call instead of one call per step, which cuts tokens, latency and the chances for the model to go off script. - **Reliability is code, not prompt.** Idempotency, retries, back-off, sagas, throttling and circuit breakers are Enterprise Integration Patterns, configured in a few lines and tested without a model. - **Rules the model can't override.** Amounts, limits and approvals are decided in the route from system data; the model only supplies what it learned in the conversation. - **Least privilege.** API keys stay in the route's environment; the agent can call the tool and nothing else. - **One audit trail** for every agent, framework and model. - **230 components.** The same route style reaches databases, queues, SaaS APIs, files and cloud services ([components](https://pmsudhi.github.io/cameleer/docs/components/index.html)), and the routes run as a small sidecar or inside a Rust agent with `cameleer_main::new_main()`. - **Model and framework neutral.** A tool is an HTTP endpoint with a JSON schema, so it works with any model provider and agent framework that supports function calling. ## Common questions ### Is Cameleer an AI agent framework? No. The agent framework runs the model loop and decides which tool to call. Cameleer executes the tool: it is the integration layer between the agent and your systems. ### Does routing tool calls through Cameleer replace function calling? No. The model still uses function calling; the tool it calls is a Cameleer route behind an HTTP endpoint, declared with the same JSON schema the route validates against. ### Which models and agent frameworks does it work with? Any that support function calling or tool use and can make an HTTP request: OpenAI, Anthropic, Google, AWS Bedrock and Azure OpenAI models, open models on Ollama or vLLM, and frameworks that let you define a tool as an HTTP call. An MCP server is on the [roadmap](https://pmsudhi.github.io/cameleer/docs/ROADMAP.html). ### What happens when an agent retries a tool call? The route sees the same `requestId` and answers `duplicate` without executing anything. Keep the ids across restarts and instances with the JDBC or Redis idempotent repository. ### Can the agent still handle a multi-step job itself? Yes, and for open-ended tasks it should. Put a step behind a route when its order and rules are known in advance, as in a refund: the model decides that the customer gets a refund, the route decides how. ### Does an extra hop make tool calls slower? A route adds microseconds to a call; a model turn takes hundreds of milliseconds to seconds. A multi-step job through one route saves model turns, so it is usually faster end to end. ## Related - [Cameleer for AI agents](https://pmsudhi.github.io/cameleer/docs/guides/agents.html): eight tested patterns, including the single-step [tool over REST](https://pmsudhi.github.io/cameleer/docs/guides/agents.html#expose-a-tool-to-an-agent-over-rest) and [human approval](https://pmsudhi.github.io/cameleer/docs/guides/agents.html#human-in-the-loop-approval) - [Error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html), [EIPs](https://pmsudhi.github.io/cameleer/docs/guides/eips.html), [AI components](https://pmsudhi.github.io/cameleer/docs/guides/ai-components.html) - The route file: [examples/agents/damaged-order-tool.camel.yaml](https://github.com/pmsudhi/cameleer/blob/main/examples/agents/damaged-order-tool.camel.yaml) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/ai-components.html # AI components > Cameleer calls large language models from routes with four components: `openai` (OpenAI and any OpenAI-compatible server such as Ollama or vLLM), `azure-openai`, `aws-bedrock` and `google-vertexai`; they cover chat, embeddings and, depending on the provider, streaming, tool calls and images. Every AI component is a producer: a step in a route that sends the message body to a model and replaces the body with the answer. Everything else (where the text comes from, what happens with the answer, retries, filtering, splitting) is ordinary Camel routing, so an "AI agent" in Cameleer is a route: a trigger, one or more model calls, and the systems the agent acts on. ## Which component to use | | [openai](https://pmsudhi.github.io/cameleer/docs/components/openai.html) | [azure-openai](https://pmsudhi.github.io/cameleer/docs/components/azure-openai.html) | [aws-bedrock](https://pmsudhi.github.io/cameleer/docs/components/aws-bedrock.html) | [google-vertexai](https://pmsudhi.github.io/cameleer/docs/components/google-vertexai.html) | |---|---|---|---|---| | Providers | OpenAI, Ollama, vLLM, LM Studio, LiteLLM, any `/v1/chat/completions` server | Azure OpenAI deployments | Models on Amazon Bedrock (Anthropic, Amazon Titan, Meta, Mistral, ...) | Gemini; partner models (Anthropic, Meta, Mistral) via `rawPredict` | | Chat | `chat-completion` | `chatCompletion` | `converse`, `invokeTextModel` | `generateText`, `generateChat`, `generateCode`, `generateMultimodal` | | Embeddings | `embeddings` | `embeddings` | `invokeEmbeddingsModel` | `generateEmbeddings` | | Streaming | no | yes (`streaming=true`) | yes (`converseStream`, `invoke*Streaming`) | yes (`generate*Streaming`, `streamRawPredict`) | | Tool calls | pass-through only: send `tools` in a request map, read the raw response with `outputType=full` | the model's tool calls in the `CamelAzureOpenAIToolCalls` header | `CamelAwsBedrockConverseToolConfig` header; tool-use blocks in `CamelAwsBedrockConverseOutputMessage` | pass-through only: send `tools` in a request map, read the raw response in `CamelGoogleVertexAIResponse` | | Images | no | `imageGeneration` (DALL-E) | `invokeImageModel` | `generateImage` (Imagen), image input with `generateMultimodal` | | Extras | | Assistants API (`runAssistant`, threads, runs) | Guardrails, knowledge bases and agents (`aws-bedrock-agent-runtime`) | token counting (`countTokens`) | | Auth | API key (Bearer) or none | API key or Entra ID client credentials | AWS credentials (SigV4) | Google service account / workload identity | All four give the answer text as the body and token usage and finish reason as headers. Check each component page for the exact option and header names. ## What is not supported Be aware of these limits before you design around them: - **No agent loop.** No component executes tools for you: when a model asks for a tool, your route must call the tool and send the result back in a new request. Server-side agents are the exception: Bedrock Agents (`aws-bedrock-agent-runtime`, `invokeAgent`) and Azure OpenAI Assistants (`runAssistant`) run the loop in the cloud service. - **`openai` has no streaming and no tool-call options.** It waits for the complete answer. - **No vector store component.** Embeddings are lists of numbers in the body; store and search them with the data store you already run ([mongodb](https://pmsudhi.github.io/cameleer/docs/components/mongodb.html), [elasticsearch](https://pmsudhi.github.io/cameleer/docs/components/elasticsearch.html), [spring-redis](https://pmsudhi.github.io/cameleer/docs/components/spring-redis.html), files). - **No LangChain4j components** (`langchain4j-chat`, `langchain4j-embeddings`, ...), no MCP client or server, and no direct Anthropic or Gemini API component: use Claude through Bedrock or Vertex AI, and Gemini through Vertex AI. - **No native Ollama API.** Ollama, vLLM and LM Studio are used through their OpenAI-compatible `/v1` endpoint with `openai:` and `baseUrl`. ## Classify and summarise with an LLM, then act An agent that triages incidents: the model writes a one-line summary, which becomes the Jira issue summary, while the original text stays the issue description. ```yaml - route: id: ai-triage-to-jira from: uri: "direct:incident" steps: - setProperty: name: report simple: "${body}" - to: "openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&apiKey={{env:OPENAI_API_KEY}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}&temperature=0&maxTokens=40&systemMessage=Write a one-line Jira summary of this incident report." - setHeader: name: IssueSummary simple: "${body}" - setBody: simple: "${exchangeProperty.report}" - setHeader: name: ProjectKey constant: "OPS" - setHeader: name: IssueTypeName constant: "Bug" - to: "jira:addIssue?jiraUrl={{env:JIRA_URL:https://your-site.atlassian.net}}&username={{env:JIRA_USER}}&password={{env:JIRA_API_TOKEN}}" - log: "opened ${header.IssueKey}: ${header.IssueSummary}" ``` The same shape works with any target: post the answer to [Slack](https://pmsudhi.github.io/cameleer/docs/components/slack.html), reply on [Telegram](https://pmsudhi.github.io/cameleer/docs/components/telegram.html), comment a [GitHub](https://pmsudhi.github.io/cameleer/docs/components/github.html) pull request, or use the answer in a `choice` to route the message (see the [ticket triage example](https://github.com/pmsudhi/cameleer/blob/main/examples/ai-ticket-triage.camel.yaml)). ## Embeddings into a store Embed every text file dropped into a folder and write the vector next to it as JSON, ready to be loaded into a vector index. Swap the last step for a database component to store it directly. ```yaml - route: id: embed-documents from: uri: "file:{{env:DOCS_DIR:docs-in}}?include=.*\\.txt&delay=2000" steps: - convertBodyTo: type: String - to: "openai:embeddings?model={{env:OPENAI_EMBEDDING_MODEL:text-embedding-3-small}}&apiKey={{env:OPENAI_API_KEY}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}" - marshal: json: {} - to: "file:{{env:VECTOR_DIR:vectors}}?fileName=${file:onlyname.noext}.json" ``` ## Local models: Ollama, vLLM, LM Studio Point `baseUrl` at the server's OpenAI-compatible endpoint; nothing else changes. This keeps the data on the machine, which matters on edge devices and in regulated networks. | server | `baseUrl` | |---|---| | Ollama | `http://localhost:11434/v1` | | vLLM | `http://localhost:8000/v1` | | LM Studio | `http://localhost:1234/v1` | The [openai page](https://pmsudhi.github.io/cameleer/docs/components/openai.html) has a complete local-model route. ## Tool calls With Azure OpenAI, send the request as a map with `tools`; when the model decides to call a tool, the finish reason is `tool_calls` and the calls (function name and JSON arguments) are in the `CamelAzureOpenAIToolCalls` header. Your route runs the tool and sends a follow-up request. ```yaml - route: id: weather-tool-call from: uri: "direct:ask-weather" steps: - setBody: constant: '{"messages":[{"role":"user","content":"What is the weather in Oslo?"}],"tools":[{"type":"function","function":{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}}}}}]}' - unmarshal: json: {} - to: "azure-openai://{{env:AZURE_OPENAI_RESOURCE:my-resource}}/{{env:AZURE_OPENAI_DEPLOYMENT:gpt-4o}}?apiKey=RAW({{env:AZURE_OPENAI_KEY}})" - log: "finish=${header.CamelAzureOpenAIFinishReason} calls=${header.CamelAzureOpenAIToolCalls}" ``` With `openai:` the same request map is sent unchanged; add `outputType=full` to get the whole response, including `choices[0].message.tool_calls`, as the body: ```yaml - route: id: openai-tools-passthrough from: uri: "direct:ask-with-tools" steps: - setBody: constant: '{"messages":[{"role":"user","content":"What is the weather in Oslo?"}],"tools":[{"type":"function","function":{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}}}}}]}' - unmarshal: json: {} - to: "openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&apiKey={{env:OPENAI_API_KEY}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}&outputType=full" - log: "finish reason: ${header.CamelOpenAIFinishReason}" ``` With Bedrock, put the tool configuration map in the `CamelAwsBedrockConverseToolConfig` header of a `converse` call; see [aws-bedrock](https://pmsudhi.github.io/cameleer/docs/components/aws-bedrock.html). ## Google Vertex AI (Gemini) ```yaml - route: id: gemini-answer from: uri: "direct:ask-gemini" steps: - to: "google-vertexai:{{env:GCP_PROJECT:my-project}}:us-central1:gemini-1.5-flash?temperature=0.2&maxOutputTokens=256&systemInstruction=Answer in one paragraph." - log: "finish=${header.CamelGoogleVertexAIFinishReason} tokens=${header.CamelGoogleVertexAITotalTokenCount}" ``` Credentials come from `serviceAccountKey` or the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. Embeddings use the same component with an embedding model: ```yaml - route: id: gemini-embeddings from: uri: "direct:embed-gemini" steps: - to: "google-vertexai:{{env:GCP_PROJECT:my-project}}:us-central1:text-embedding-004?operation=generateEmbeddings&taskType=RETRIEVAL_DOCUMENT" ``` More options (streaming, multimodal input, Imagen, Claude on Vertex) are on the [google-vertexai page](https://pmsudhi.github.io/cameleer/docs/components/google-vertexai.html). ## Amazon Bedrock The `converse` operation works the same way for every Bedrock chat model: ```yaml - route: id: bedrock-converse from: uri: "direct:ask-bedrock" steps: - setHeader: name: CamelAwsBedrockConverseSystem constant: "Answer in one sentence." - to: "aws-bedrock://chat?modelId=anthropic.claude-3-haiku-20240307-v1:0&operation=converse®ion={{env:AWS_REGION:us-east-1}}&useDefaultCredentialsProvider=true" - log: "stop=${header.CamelAwsBedrockConverseStopReason}" ``` See [aws-bedrock](https://pmsudhi.github.io/cameleer/docs/components/aws-bedrock.html) for streaming, embeddings, guardrails, knowledge bases and Bedrock Agents. ## Rust DSL ```rust ctx.add_routes(|b| { b.from("direct:summarise") .to("openai:chat-completion?model=gpt-4o-mini&systemMessage=Summarise in one sentence.") .to("slack:#ops?webhookUrl={{env:SLACK_WEBHOOK_URL}}"); })?; ``` ## Run it with Docker ```sh export OPENAI_API_KEY=sk-... JIRA_USER=bot@example.com JIRA_API_TOKEN=... JIRA_URL=https://your-site.atlassian.net docker run --rm -e OPENAI_API_KEY -e JIRA_URL -e JIRA_USER -e JIRA_API_TOKEN \ -v "$PWD:/routes:ro" pmsudhi/cameleer run /routes/ai-triage-to-jira.camel.yaml ``` ## Common questions ### How do I keep the original message after calling the model? Save it first (`setProperty` with `${body}`), call the model, move the answer into a header, then restore the body from the property, as in the triage example above. ### How do I make model calls reliable? Treat them like any remote call: `onException` with a redelivery policy for timeouts and 429 responses, a `circuitBreaker` around the call, and `temperature=0` for classification so the answers are stable. ### Can the same route run against OpenAI in the cloud and Ollama at the edge? Yes. Put `baseUrl`, `model` and `apiKey` in `{{env:...}}` placeholders, as in the examples, and set them per deployment. ### How are the examples tested? Every example on this page runs in the test suite against embedded mock servers of the OpenAI, Azure OpenAI, Bedrock, Vertex AI and Jira APIs, so the option names and the request formats are checked on every build. ## Related - [openai](https://pmsudhi.github.io/cameleer/docs/components/openai.html), [azure-openai](https://pmsudhi.github.io/cameleer/docs/components/azure-openai.html), [aws-bedrock](https://pmsudhi.github.io/cameleer/docs/components/aws-bedrock.html), [google-vertexai](https://pmsudhi.github.io/cameleer/docs/components/google-vertexai.html) - [slack](https://pmsudhi.github.io/cameleer/docs/components/slack.html), [jira](https://pmsudhi.github.io/cameleer/docs/components/jira.html), [github](https://pmsudhi.github.io/cameleer/docs/components/github.html), [telegram](https://pmsudhi.github.io/cameleer/docs/components/telegram.html) - [EIPs](https://pmsudhi.github.io/cameleer/docs/guides/eips.html) - [Apache Camel AI components](https://camel.apache.org/components/next/langchain4j-chat-component.html) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/build-features.html # Build features: choose what goes into your build > Cameleer's default build, image and `cargo install` include every component. Cargo features let > you build a smaller binary that contains only the component families your routes use, for a > serverless function, an edge device or a smaller container image. **You don't need this page to get started.** The Docker image `pmsudhi/cameleer`, `cargo install cameleer-main` and `cargo build -p cameleer-main` all contain every component, language and data format. Every page in these docs works with them as is. Come back here when size or cold start matters. ## What is always included Every build, even the smallest one (`core`), contains: - the routing engine, every EIP, error handling, sagas, route templates and Kamelets - the YAML DSL, the XML DSL, the Rust DSL and the REST DSL - HTTP/1.1 and TLS: `platform-http`, `http`/`https`, `rest`, `netty`, `netty-http`, `jetty`, `undertow` - the core components: `direct`, `seda`, `vm`, `timer`, `scheduler`, `cron`, `quartz`, `file`, `log`, `mock`, `stub`, `bean`, `class`, `ref`, `browse`, `controlbus`, `dataformat`, `dataset`, `dataset-test`, `exec`, `json-validator`, `kamelet`, `language`, `saga`, `stream`, `test` - the core languages: `simple`, `constant`, `header`, `exchangeProperty`, `variable`, `jsonpath`, `tokenize`, `bean`, `ref` - the core data formats: `json`, `jacksonxml`, `csv`, `base64`, `hex`, `string`, `gzip`, `zip-deflater`, `urlencoded` - the `cameleer` commands `run`, `init`, `ps`, `stop`, `log`, `get` and `version` ## The features Each feature adds one family. Every component, language and data format page in these docs names its feature in its header line (**Cargo feature:**). | feature | adds | crate | |---|---|---| | `amqp` | `activemq`, `amqp-stomp`, `jms`, `rabbitmq`, `spring-rabbitmq`, `stomp` (AMQP 0-9-1, STOMP and JMS) | `cameleer-amqp` | | `amqp10` | `amqp` (AMQP 1.0, JMS over AMQP), `azure-servicebus`, `azure-eventhubs` | `cameleer-amqp10` | | `cloud` | every `aws*`, `azure-*` and `google-*` component except the two above (62 components: S3, SQS, SNS, DynamoDB, Lambda, Bedrock, Blob, Cosmos DB, Azure OpenAI, Pub/Sub, BigQuery, Vertex AI, ...) and the `{{aws:...}}`, `{{azure:...}}` and `{{gcp:...}}` secret placeholders | `cameleer-cloud` | | `dataformats` | the data formats `asn1`, `avro`, `barcode`, `bindy`, `cbor`, `crypto`, `fory`, `ical`, `mime-multipart`, `pgp`, `tarfile`, `thrift`, `univocity`, `yaml`, `zipfile`, and the `thrift` component | `cameleer-dataformats` | | `datastores` | `spring-redis`, `mongodb`, `mongodb-gridfs`, `elasticsearch`, `opensearch`, `cql` (Cassandra), `couchdb` | `cameleer-datastores` | | `grpc` | the `grpc` component and the `protobuf` data format | `cameleer-grpc` | | `kafka-mqtt` | `kafka`, `paho`, `paho-mqtt5` | `cameleer-kafka-mqtt` | | `protocols` | `ftp`, `ftps`, `smtp`/`smtps`, `pop3`/`pop3s`, `imap`/`imaps`, `mllp`, `syslog`, `websocket`/`ws`/`wss`, `vertx-websocket`; the `hl7terser` language; the `hl7` and `syslog` data formats | `cameleer-protocols` | | `saas` | `openai`, `slack`, `telegram`, `github`, `twilio`, `jira`, `webhook`, `graphql`, `rss`, `atom` | `cameleer-saas` | | `script` | the languages `js`/`javascript`, `groovy`, `python`, `java`/`joor`, `csimple`, `mvel`, `ognl`, `spel`, `datasonnet`, and the `mvel` component | `cameleer-script` | | `soap` | `cxf`, `cxfrs`, `spring-ws`, `fhir`; the `jaxb` and `fhir` data formats | `cameleer-soap` | | `sql` | `sql`, `sql-stored`, `jdbc` (embedded SQL engine and PostgreSQL client) | `cameleer-sql` | | `ssh` | `sftp`, `ssh` | `cameleer-ssh` | | `xml` | `xslt`, `xslt-saxon`, `xquery`, full XML Schema validation for `validator`, `xj`, `xmlsecurity-sign`, `xmlsecurity-verify`; the `xquery` language and XPath 1.0 in full | `cameleer-xml` | | `templates` | `mustache`, `velocity`, `freemarker`, `string-template` | part of `cameleer-core` | | `jq` | the `jq` language and Simple's `${jq(...)}` | part of `cameleer-core` | | `xpath` | a smaller XPath 1.0 subset for the `xpath` language and Simple's `${xpath(...)}` when `xml` is left out | part of `cameleer-core` | | `http2` | `httpVersion=HTTP/2` on the `http` producer | part of `cameleer-http` | | `validate` | the `cameleer validate` and `cameleer schema` commands | part of `cameleer-main` | Two names cover sets of features: - `full`, the default, is every feature above. - `core` is none of them: only what is always included. ## Choose features for your routes 1. List the schemes your routes use: the part before `:` in every `from` and `to` URI, plus any `kamelet:` sources. 2. List the languages you use besides Simple and JSONPath (`jq`, `groovy`, `xpath`, ...), and the data formats in `marshal` and `unmarshal` steps. 3. Look each one up in the table above, or open its page under Components, Languages or Data formats. Anything that isn't in the table is always included. 4. Add `validate` if you run `cameleer validate` with this binary, and `http2` if an `http` endpoint sets `httpVersion=HTTP/2`. For example, a route that reads SQS messages, transforms them with jq and writes rows to PostgreSQL needs `cloud,jq,sql`. A webhook that calls Slack needs only `saas`, because the `platform-http` consumer is always included. **A feature list replaces the default; it doesn't add to it.** `--features cloud` gives `core` plus `cloud` and nothing else, so jq, the template engines, HTTP/2 and `cameleer validate` are left out. Name every feature you use, for example `--features cloud,jq,validate`. ## Build it The same feature names work in every kind of build. ### Container image ```sh git clone https://github.com/pmsudhi/cameleer && cd cameleer docker build --build-arg FEATURES=cloud,jq,sql -t my-cameleer . docker run --rm my-cameleer version ``` Leave out `FEATURES` for the full image, which is what `pmsudhi/cameleer` on Docker Hub is. `FEATURES=core` gives the smallest image. ### The `cameleer` command ```sh cargo install cameleer-main --no-default-features --features cloud,jq,sql ``` From a clone, `cargo build --profile dist -p cameleer-main --no-default-features --features cloud,jq,sql` builds `target/dist/cameleer`. The `dist` profile optimizes for size and speed; use `--release` if you want a quicker build. ### AWS Lambda ```sh scripts/package-lambda.sh --arch x86_64 --features cloud,jq --out function.zip orders.camel.yaml ``` The [deployment guide](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html#aws-lambda) explains the rest of the Lambda setup. ### Your own Rust program Depend only on the crates you need. `cameleer-core` and `cameleer-http` have default features too; turn them off to drop the parts you don't use: ```toml [dependencies] cameleer-core = { version = "0.4", default-features = false, features = ["jq"] } cameleer-http = { version = "0.4", default-features = false } # HTTP/1.1 only, no gRPC crate cameleer-cloud = "0.4" ``` To run YAML routes from Rust with a reduced set of components, depend on `cameleer-main` with the same feature list as the command above: ```toml cameleer-main = { version = "0.4", default-features = false, features = ["cloud", "jq"] } ``` ## Check what a binary contains `cameleer version` lists the features compiled in: ```text $ cameleer version Cameleer 0.4.0 (components: core, http, yaml, cloud, sql, jq) ``` A full build lists every feature. ## When something is missing A route that uses a component or language left out of the build fails when it starts, and the error names the feature to add: ```text No component found with scheme: kafka (this cameleer binary was built without the `kafka-mqtt` feature; rebuild cameleer-main with `--features kafka-mqtt`) ``` The same applies to the template components (`templates`), the `jq` language and Simple's `${jq(...)}` (`jq`), `${xpath(...)}` (`xpath` or `xml`), `httpVersion=HTTP/2` (`http2`) and the `cameleer validate` and `cameleer schema` commands (`validate`). Rebuild with the feature added to your list; nothing else needs to change. Two parts don't fail when their feature is left out. They fall back to a smaller version that is always included: - **`validator` without `xml`** only checks that the XML is well-formed and that its root element is declared in the schema. It doesn't check content models, types or facets. Add `xml` if you validate against an XML Schema. - **The `xpath` language without `xml`** uses the core XPath subset when the `xpath` feature is in, instead of XPath 1.0 in full. ## Sizes Linux x86-64, `dist` profile, stripped: | build | glibc | static musl | |---|---:|---:| | `core` | 5.8 MB | 6.0 MB | | every feature (`full`) | 23.9 MB | 24.1 MB | What each part adds to `core`: `templates` 0.50 MB, `jq` 0.42 MB, `validate` 0.27 MB, `http2` 0.26 MB, `xpath` 0.09 MB. A smaller binary starts a little faster and uses less memory; startup is about 1 ms on Linux either way. Container image sizes, cold-start times and memory are in [BENCHMARKS.md](https://pmsudhi.github.io/cameleer/docs/BENCHMARKS.html#faas-cold-start-and-size). ## Common questions ### Do I have to choose features? No. The default build has everything, and it is what the Docker image and `cargo install cameleer-main` give you. Features only matter when you want a smaller binary. ### Is the full build slower than a small one? No. Throughput is the same: a component that a route doesn't use costs nothing at run time. The full build is only bigger on disk and takes a little longer to load. ### I upgraded and `--features cloud` lost jq (or `cameleer validate`). Why? Since 0.4.0, the template engines, jq, the core XPath subset, HTTP/2 and `validate` are features too, and a feature list replaces the default. Add them to your list, for example `--features cloud,jq,validate`. ### How do I find out which feature a component needs? Open the component's page: the header line names its feature. You can also look it up in the table above, or build with the features you think you need and read the error, which names the missing one. ### Does leaving out a family change how the remaining components behave? No. Each component is the same code in every build; only which components exist changes. ## Related - [Getting started](https://pmsudhi.github.io/cameleer/docs/guides/getting-started.html): install Cameleer and run your first route - [Deploying Cameleer](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html): Docker, Compose, Kubernetes, Cloud Run and AWS Lambda - [Benchmarks](https://pmsudhi.github.io/cameleer/docs/BENCHMARKS.html): sizes, cold start and throughput ## Found a problem? If something on this page doesn't match what Cameleer does, please [open an issue](https://github.com/pmsudhi/cameleer/issues/new) that describes what you saw, or fork the repository, fix it, add or update a test, and open a pull request. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/edge.html # Cameleer at the edge > Cameleer runs integration routes on gateways and small devices: a single native binary of > about 10 MB of memory that ingests MQTT, syslog and HL7 locally, filters and aggregates on > site, buffers when the uplink is down, and forwards to Kafka or HTTP in the cloud. Edge gateways sit between machines and the cloud. They need to speak local protocols, keep working offline, and fit next to other software on small hardware. Apache Camel does all of this on a JVM; Cameleer runs the same routes without one. This cookbook shows five patterns, each a complete route file in [`examples/edge/`](https://github.com/pmsudhi/cameleer/tree/main/examples/edge) that runs end to end in the test suite (`crates/cameleer-main/tests/docs_guides.rs`) against the embedded MQTT and Kafka brokers and HTTP mocks. | pattern | file | what it shows | |---|---|---| | [Sensor rollups](#filter-and-aggregate-mqtt-sensor-readings) | `mqtt-sensor-rollup.camel.yaml` | MQTT in, validation, per-sensor aggregation, Kafka out | | [Store and forward](#store-and-forward-when-the-uplink-is-down) | `store-and-forward.camel.yaml` | retries, a local file buffer and replay when the uplink is down | | [Syslog to Kafka](#syslog-to-kafka) | `syslog-to-kafka.camel.yaml` | protocol bridging with severity filtering | | [HL7 over MLLP](#hl7-v2-over-mllp-to-an-http-api) | `hl7-mllp-to-http.camel.yaml` | healthcare messages with acknowledgements after delivery | | [Gateway presence](#gateway-presence-with-retained-messages-and-a-last-will) | `gateway-heartbeat.camel.yaml` | retained status and MQTT last will | ## Filter and aggregate MQTT sensor readings Sensors publish a reading every few seconds; the cloud needs a summary. The route subscribes to `sensors/+/temperature`, takes the sensor id from the topic, drops readings that are not numbers or are outside the sensor's range, and aggregates five readings per sensor (or what arrived within a minute) into one rollup with count, min, max and average. Only rollups go to Kafka, keyed by sensor, so a sensor's rollups stay in order on one partition. ```yaml - route: id: sensor-rollup from: uri: "paho:sensors/+/temperature?brokerUrl={{env:MQTT_BROKER:tcp://localhost:1883}}&qos=1&clientId={{env:GATEWAY_ID:gateway-1}}-rollup" steps: - convertBodyTo: type: java.lang.String - setHeader: name: sensor simple: "${header.CamelMqttTopic.split('/')[1]}" - choice: when: - simple: "${body} !regex '-?[0-9]+(\\.[0-9]+)?'" steps: - log: "dropped malformed reading '${body}' from ${header.sensor}" - stop: {} - convertBodyTo: type: java.lang.Double - choice: when: - simple: "${body} < -40 || ${body} > 125" steps: - log: "dropped out-of-range reading ${body} from ${header.sensor}" - stop: {} - aggregate: correlationExpression: header: sensor aggregationStrategy: "#class:org.apache.camel.processor.aggregate.GroupedBodyAggregationStrategy" completionSize: 5 completionInterval: 60000 steps: - setBody: jq: '{gateway: "{{env:GATEWAY_ID:gateway-1}}", sensor: header("sensor"), count: length, min: min, max: max, avg: (add / length)}' - marshal: json: {} - setHeader: name: kafka.KEY simple: "${header.sensor}" - to: "kafka:sensor-rollups?brokers={{env:KAFKA_BROKERS:localhost:9092}}" ``` The test publishes `20`, `garbage`, `21.5`, `999`, `22`, `23.5` and `18` for sensor `t1` and one reading for `t2`. Kafka receives one rollup for `t1` with count 5, min 18, max 23.5 and average 21; `garbage` and `999` are dropped, and `t2` waits for its minute. MQTT payloads arrive as bytes, so the route converts them to text first. The `jq` step computes the statistics on the list the `GroupedBodyAggregationStrategy` collected. To keep a rollup per minute regardless of the number of readings, drop `completionSize` and keep `completionInterval`. Aggregation state is in memory: rollups in progress are lost on a restart, which is usually fine for telemetry; use `forceCompletionOnStop: true` to flush them on a clean shutdown. ## Store and forward when the uplink is down Gateways lose their uplink: cellular dead zones, maintenance windows, cloud outages. This route keeps every message: 1. Telemetry from MQTT goes into a `seda` queue, so the MQTT consumer is never blocked by a slow uplink. 2. The uplink route posts each message to the cloud. A dead letter channel retries three times with exponential back-off, then writes the original message to a buffer folder. 3. A replay route polls the buffer folder every 30 seconds, posts each file, and moves it to `.sent` when the cloud accepts it. While the uplink is still down, the post fails and the file stays for the next poll. ```yaml - route: id: telemetry-in from: uri: "paho:telemetry/#?brokerUrl={{env:MQTT_BROKER:tcp://localhost:1883}}&qos=1&clientId={{env:GATEWAY_ID:gateway-1}}-uplink" steps: - convertBodyTo: type: java.lang.String - to: "seda:uplink?size=10000&blockWhenFull=true" - route: id: uplink errorHandler: deadLetterChannel: deadLetterUri: "file:{{env:BUFFER_DIR:buffer}}?fileName=${date:now:yyyyMMddHHmmssSSS}-${exchangeId}.msg" useOriginalMessage: true redeliveryPolicy: maximumRedeliveries: 3 redeliveryDelay: "{{env:RETRY_DELAY_MS:1000}}" useExponentialBackOff: true from: uri: "seda:uplink?size=10000" steps: - setHeader: name: X-Gateway constant: "{{env:GATEWAY_ID:gateway-1}}" - enrich: uri: "{{env:UPLINK_URL:http://localhost:9000}}/telemetry?httpMethod=POST" - route: id: replay-buffer from: uri: "file:{{env:BUFFER_DIR:buffer}}?include=.*\\.msg&sortBy=file:name&move=.sent&delay={{env:REPLAY_DELAY_MS:30000}}" steps: - convertBodyTo: type: java.lang.String - setHeader: name: X-Gateway constant: "{{env:GATEWAY_ID:gateway-1}}" - setHeader: name: X-Replayed constant: "true" - enrich: uri: "{{env:UPLINK_URL:http://localhost:9000}}/telemetry?httpMethod=POST" ``` The test starts with the cloud mock answering `503`: the message is tried four times and lands in the buffer. When the mock recovers, the replay posts it (with `X-Replayed: true`) and moves the file to `.sent`, and new messages go straight through. Why `enrich` and not `to` for the uplink call: `enrich` sends a copy, so every retry posts the original message. With `to`, Cameleer's `http` producer puts the error response into the message before it fails, and a retry would post that instead (Apache Camel leaves the message unchanged). Things to tune for production: - Mount the buffer on persistent storage (`-v cameleer-buffer:/buffer`); the container root can stay read-only. - `seda` is in memory: messages queued there when the process stops are lost. For zero loss across restarts, write to the buffer folder first and let the replay route be the only sender, at the cost of disk writes for every message. - Order is kept per buffer folder (`sortBy=file:name`), not between live and replayed messages. Put a timestamp in the payload if the cloud must reorder. ## Protocol bridging Bridges translate between what the site speaks and what the cloud accepts. The two routes above already bridge MQTT to Kafka and MQTT to HTTP. Two more common ones follow. ### Syslog to Kafka Routers, switches, PLCs and Linux hosts all speak syslog. The `syslog` component receives RFC 5424 and RFC 3164 messages over UDP or TCP (and TLS), parses them into a map, and sets headers such as `CamelSyslogSeverity`, `CamelSyslogFacility` and `CamelSyslogHostname`. This route keeps warnings and worse and sends them to Kafka as JSON. ```yaml - route: id: syslog-alerts from: uri: "syslog:udp://{{env:SYSLOG_HOST:0.0.0.0}}:{{env:SYSLOG_PORT:5514}}" steps: - filter: simple: "${header.CamelSyslogSeverity} in 'EMERG,ALERT,CRIT,ERR,WARNING'" steps: - setHeader: name: kafka.KEY simple: "${header.CamelSyslogHostname}" - marshal: json: {} - to: "kafka:syslog-alerts?brokers={{env:KAFKA_BROKERS:localhost:9092}}" ``` The test sends a `local4.notice` message (dropped) and an `auth.crit` message (forwarded to the `syslog-alerts` topic, keyed by `router-1`). ### HL7 v2 over MLLP to an HTTP API Hospital systems exchange HL7 v2 over MLLP. The `mllp` component frames and acknowledges messages and sets `CamelMllp*` headers (message type, event, control id, sending application); the `hl7terser` language reads fields by path (`PID-3-1` is the patient id). The acknowledgement goes back after the route finishes, so the sender gets `AA` only once the EHR accepted the message, and `AE` when it still failed after five retries, so it keeps the message and resends. ```yaml - route: id: hl7-to-ehr errorHandler: defaultErrorHandler: redeliveryPolicy: maximumRedeliveries: 5 redeliveryDelay: "{{env:RETRY_DELAY_MS:2000}}" from: uri: "mllp://{{env:MLLP_HOST:0.0.0.0}}:{{env:MLLP_PORT:2575}}" steps: - filter: simple: "${header.CamelMllpEventType} == 'ADT'" steps: - setHeader: name: X-Patient-Id hl7terser: "PID-3-1" - setHeader: name: X-Message-Control-Id simple: "${header.CamelMllpMessageControlId}" - setHeader: name: Content-Type constant: application/hl7-v2 - enrich: uri: "{{env:EHR_URL:http://localhost:9000}}/hl7/v2?httpMethod=POST" ``` The test sends an ADT message (posted, `AA`), an ORU message (acknowledged, not posted) and an ADT message the EHR rejects (six posts with the original message, then `AE`). For TLS between the systems, add `sslContextParameters=#ssl` to the `mllp` URI. ## Gateway presence with retained messages and a last will Dashboards need to know which gateways are alive without polling them. The heartbeat route publishes a retained `online` status every 30 seconds. Its MQTT connection registers a last will of `offline`, retained too: if the gateway loses power or network, the broker publishes it. Anyone subscribing to `gateways/+/status` gets the current state of every gateway at once. ```yaml - route: id: gateway-heartbeat from: uri: "timer:heartbeat?period={{env:HEARTBEAT_MS:30000}}" steps: - setBody: constant: online - to: "paho:gateways/{{env:GATEWAY_ID:gateway-1}}/status?brokerUrl={{env:MQTT_BROKER:tcp://localhost:1883}}&clientId={{env:GATEWAY_ID:gateway-1}}-status&qos=1&retained=true&willTopic=gateways/{{env:GATEWAY_ID:gateway-1}}/status&willPayload=offline&willQos=1&willRetained=true" ``` The test checks that the broker holds the retained `online` status and that the gateway is connected under its own client id. The broker sends the will only on an abnormal disconnect; a clean stop of the route does not mark the gateway offline, so publish `offline` from an `onCompletion` or a shutdown route if you need that too. ## Small footprint deployment The Docker image `pmsudhi/cameleer` is one native binary on a distroless base, for `linux/amd64` and `linux/arm64`, about 20 MB to download (12.5 MB built with `FEATURES=core`). A running route uses about 10 MB of memory (8 MB at startup in the [benchmarks](https://pmsudhi.github.io/cameleer/docs/BENCHMARKS.html)) and starts its routes in milliseconds. Nothing is written to the filesystem unless a route writes files, so the container runs with a read-only root: ```sh docker run -d --name edge-routes --restart unless-stopped \ --read-only --memory 64m --cpus 0.5 \ --network host \ -e MQTT_BROKER=tcp://localhost:1883 -e KAFKA_BROKERS -e GATEWAY_ID=plant-7 \ -v "$PWD/routes:/routes:ro" \ pmsudhi/cameleer run /routes/mqtt-sensor-rollup.camel.yaml ``` - **arm64 boards:** the same image runs on 64-bit Raspberry Pi OS (Pi 3, 4 and 5), NVIDIA Jetson, AWS Graviton and other arm64 Linux devices; Docker picks the right platform. There is no published 32-bit ARM image. - **No container runtime:** `cargo install cameleer-main` builds the `cameleer` binary on the device (or cross-compile it: Cameleer has no dependencies outside the Rust standard library), then run it under systemd, for example `cameleer run /etc/cameleer/rollup.camel.yaml`. - **Several route files:** pass several files to `run` (or none, to load every route file in the working directory); they share one process. - **Health:** add `--prop camel.server.enabled=true --prop camel.server.healthCheckEnabled=true` for `/q/health/live` and `/q/health/ready` on port 8080. See the [deployment guide](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html). ## Offline-first patterns - **Decide locally.** Filters, thresholds and aggregation run on the gateway, so alarms fire without the cloud. A `choice` that writes to a local actuator topic (`paho:...`) keeps working when the uplink is gone. - **Buffer, then forward.** A dead letter channel to `file:` plus a replay route is a durable outbox in a few lines of YAML. Use `seda` with `blockWhenFull=true` in front of slow calls. - **Make delivery idempotent.** Replays and MQTT QoS 1 can deliver a message twice. Give each message an id at the edge and use an `idempotentConsumer` at the receiving side. - **Report presence with the broker.** Retained status messages and a last will tell the cloud what is offline without a polling loop. - **Thin out telemetry.** `sample` and `throttle` keep the uplink bill under control; aggregate before sending. ## Common questions ### Can Cameleer run on a Raspberry Pi? Yes, on a 64-bit OS: the `pmsudhi/cameleer` image is published for `linux/arm64`, and a route uses about 10 MB of memory. 32-bit ARM images are not published. ### How do I bridge MQTT to Kafka? Use a route from `paho:topic/#?brokerUrl=tcp://...` to `kafka:topic?brokers=...`, converting the payload with `convertBodyTo` and setting `kafka.KEY` from the MQTT topic if you need per-device ordering. The [sensor rollup example](#filter-and-aggregate-mqtt-sensor-readings) shows it with filtering and aggregation. ### What happens to messages when the internet connection drops? Without precautions, a failed call fails the exchange. With a dead letter channel to a local `file:` folder and a replay route, as in [store and forward](#store-and-forward-when-the-uplink-is-down), messages wait on disk and are sent when the uplink returns. ### Does Cameleer support MQTT 5 and TLS? Yes. `paho` speaks MQTT 3.1.1 and `paho-mqtt5` MQTT 5.0, both with QoS 0, 1 and 2, retained messages, wills, persistent sessions, automatic reconnect and `ssl://` broker URLs. WebSocket transports and shared subscriptions are not supported. ### Which industrial and healthcare protocols are supported? MQTT, syslog, HL7 v2 over MLLP, TCP and UDP (`netty`), HTTP, WebSocket, FTP and SFTP, Kafka, AMQP and STOMP. There are no OPC UA, Modbus or PLC4X components; reach those through an MQTT or HTTP gateway. ## Related - [EIPs](https://pmsudhi.github.io/cameleer/docs/guides/eips.html): [aggregate](https://pmsudhi.github.io/cameleer/docs/guides/eips/split-aggregate.html#aggregator), [sample and throttle](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html) - [Error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html), [deployment guide](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html), [benchmarks](https://pmsudhi.github.io/cameleer/docs/BENCHMARKS.html) - Components: [paho](https://pmsudhi.github.io/cameleer/docs/components/paho.html), [kafka](https://pmsudhi.github.io/cameleer/docs/components/kafka.html), [syslog](https://pmsudhi.github.io/cameleer/docs/components/syslog.html), [mllp](https://pmsudhi.github.io/cameleer/docs/components/mllp.html), [file](https://pmsudhi.github.io/cameleer/docs/components/file.html) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/faq.html # Cameleer FAQ > Short, factual answers to the questions people ask about Cameleer: what it is, how it relates > to Apache Camel, how fast it is, where it runs, and how to use it with Kafka, LLMs and edge > devices. ## About Cameleer ### What is Cameleer? Cameleer is Apache Camel rewritten in Rust: an integration framework with the same routes, Enterprise Integration Patterns, endpoint URIs, Simple language and YAML DSL, shipped as one native binary with no dependencies outside the Rust standard library. See the [README](https://github.com/pmsudhi/cameleer/blob/main/README.md) and [concepts](https://pmsudhi.github.io/cameleer/docs/guides/concepts.html). ### Is Cameleer compatible with Apache Camel? It follows Camel 4's names and semantics: Camel YAML and XML routes, URIs, options, EIPs, Simple expressions and `Camel*` headers work unchanged for the supported components. Java code (processors, beans, custom components) does not run; it is rewritten in Rust. See [migrating from Apache Camel](https://pmsudhi.github.io/cameleer/docs/guides/migrating-from-apache-camel.html). ### Is Cameleer affiliated with the Apache Software Foundation? No. It is an independent project that follows Apache Camel's design and documentation but contains no Camel source code, and it is not endorsed by the ASF. Apache Camel is a trademark of the ASF. ### What was Cameleer called before? rust-camel, up to version 0.1.0. ### What license does Cameleer use? The Apache License 2.0 ([LICENSE](https://github.com/pmsudhi/cameleer/blob/main/LICENSE)). ## Performance and footprint ### How fast is Cameleer? With the same routes, Cameleer handles 1.7 to 8.7 times the throughput of Apache Camel 4.23 on Java 21 in every scenario measured: 2.1x on a REST API with JSON, 2.3x writing to PostgreSQL, 1.7x publishing to Kafka, 8.7x on Kafka stream processing and 3.0x on a content-based router. Every scenario, the method and the raw data are on the [Apache Camel vs Cameleer](https://pmsudhi.github.io/cameleer/compare/apache-camel-vs-cameleer/) page and in [BENCHMARKS.md](https://pmsudhi.github.io/cameleer/docs/BENCHMARKS.html). ### How much memory does it use? A route uses about 10 MB: 8 MB at startup, and 8 to 68 MB at peak across the benchmark scenarios, against 213 to 440 MB for Apache Camel on the JVM (up to 54 times less). See [BENCHMARKS.md](https://pmsudhi.github.io/cameleer/docs/BENCHMARKS.html). ### How long does it take to start? 3 ms from process start to routes running in the benchmark, against 1.1 s for Apache Camel on the JVM and 30 ms compiled natively with Quarkus. A function build with only the components it needs starts in under 1 ms. That makes scale-to-zero and serverless practical ([BENCHMARKS.md](https://pmsudhi.github.io/cameleer/docs/BENCHMARKS.html#faas-cold-start-and-size)). ### How big is the Docker image? The `pmsudhi/cameleer` image is about 20 MB to download: one native binary on a distroless base. Built with only the component families you need it is smaller: 14.5 MB with `FEATURES=cloud`, 12.5 MB with `FEATURES=core`. See the [deployment guide](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html). ## Running it ### How do I install Cameleer? Run the Docker image `pmsudhi/cameleer`, install the CLI with `cargo install cameleer-main`, or add the `cameleer-core` crate (plus component crates) to a Rust project. See [getting started](https://pmsudhi.github.io/cameleer/docs/guides/getting-started.html). ### Do I need Java, Maven or a JVM? No. Cameleer is a native binary. Rust is needed only to build it from source or to embed it in a Rust program. ### Can I run it on a Raspberry Pi? Yes, on a 64-bit OS: the image is published for `linux/arm64` (Raspberry Pi 3, 4 and 5 with 64-bit Raspberry Pi OS), and a route uses about 10 MB of memory. There is no 32-bit ARM image. See [Cameleer at the edge](https://pmsudhi.github.io/cameleer/docs/guides/edge.html#small-footprint-deployment). ### Does it run on Kubernetes, Cloud Run or AWS Lambda? Kubernetes, Docker Compose, Google Cloud Run, Azure Container Apps, Knative and ECS/Fargate run the container image as it is ([deployment guide](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html)). On AWS Lambda, `cameleer` is its own custom runtime for `provided.al2023`: API Gateway and function URL events go to your `platform-http` routes, other events to `direct:lambda` ([AWS Lambda](https://pmsudhi.github.io/cameleer/docs/DEPLOYMENT.html#aws-lambda)). ### Does it run on Windows and macOS? Yes. The CLI builds and runs on Linux, macOS and Windows (the test suite passes on Windows 11); the Docker image is Linux (amd64 and arm64). ### Can it run with a read-only filesystem? Yes. The container writes nothing unless a route writes files, and it works with `--read-only` and Kubernetes' `readOnlyRootFilesystem: true`. ### How do I check that it is healthy? Start it with `--prop camel.server.enabled=true --prop camel.server.healthCheckEnabled=true` and probe `/q/health/live` and `/q/health/ready` on port 8080, or run `cameleer get health --url http://localhost:8080`. ## Features ### Which Enterprise Integration Patterns does it support? All the patterns of Apache Camel's EIP catalogue, including split, aggregate, multicast, recipient list, routing slip, dynamic router, wire tap, enrich, idempotent consumer, load balancer, resequencer, throttle, circuit breaker, saga and transacted. See [EIPs](https://pmsudhi.github.io/cameleer/docs/guides/eips.html). ### Does it support Kafka? Yes. The `kafka` component has its own Kafka wire protocol client: producer and consumer groups, keys and partitions, offsets and commits, TLS and SASL (PLAIN, SCRAM, AWS MSK IAM). It is verified with librdkafka, kcat and the kfake Kafka simulator. See the [kafka component](https://pmsudhi.github.io/cameleer/docs/components/kafka.html). ### Does it support MQTT? Yes: `paho` (MQTT 3.1.1) and `paho-mqtt5` (MQTT 5.0) with QoS 0 to 2, retained messages, wills, persistent sessions and TLS, tested against Mosquitto, RabbitMQ and ActiveMQ. See [the edge cookbook](https://pmsudhi.github.io/cameleer/docs/guides/edge.html). ### Which databases and clouds does it connect to? PostgreSQL (`sql`, `jdbc`), Redis, MongoDB, Elasticsearch and OpenSearch, Cassandra and CouchDB, plus all of Apache Camel's AWS, Azure and Google Cloud components (S3, SQS, SNS, DynamoDB, Lambda, Bedrock, Blob Storage, Service Bus, Event Hubs, Cosmos DB, Cloud Storage, Pub/Sub, BigQuery, Vertex AI and more). See [FEATURES.md](https://pmsudhi.github.io/cameleer/docs/FEATURES.html). ### Does it have every Camel component? No. It has about 230 components, languages and data formats, chosen from the most used ones, but not Camel's long tail of vendor connectors. [FEATURES.md](https://pmsudhi.github.io/cameleer/docs/FEATURES.html) lists what is there. ### Can I write routes in YAML, XML or code? All three: Camel's YAML DSL, Camel's XML DSL, and a Rust DSL equivalent to Camel's Java DSL. They support the same EIPs. See [concepts](https://pmsudhi.github.io/cameleer/docs/guides/concepts.html#the-three-dsls). ### Does it support REST APIs and OpenAPI? Yes. The REST DSL (`rest:` in YAML, `b.rest(...)` in Rust) declares endpoints with path and query parameters, JSON binding and CORS, and generates an OpenAPI 3 document. ## AI and agents ### How do I use Cameleer with an LLM? Send the prompt as the message body to `openai:chat-completion?model=...` (OpenAI or any OpenAI-compatible server such as Ollama or vLLM), `aws-bedrock`, `azure-openai` or `google-vertexai`; the answer becomes the body, and you route on it with `choice`. See [Cameleer for AI agents](https://pmsudhi.github.io/cameleer/docs/guides/agents.html). ### Can an AI agent call Cameleer routes as tools? Yes. Expose a route over REST and add JSON schema validation, an idempotent consumer, retries and an audit log, as in the [tool API pattern](https://pmsudhi.github.io/cameleer/docs/guides/agents.html#expose-a-tool-to-an-agent-over-rest). A packaged MCP server is planned ([roadmap](https://pmsudhi.github.io/cameleer/docs/ROADMAP.html)). ### Does Cameleer run models or store embeddings? No. It calls model APIs and writes embeddings to the stores you run, such as Elasticsearch, OpenSearch or PostgreSQL with pgvector. See [RAG ingestion](https://pmsudhi.github.io/cameleer/docs/guides/agents.html#rag-ingestion-chunk-embed-and-store). ## Quality and support ### Is Cameleer production ready? The feature list is complete against its 228-row parity table, with 2,300+ hermetic tests and verification against real servers, official clients and emulators ([INTEROP.md](https://pmsudhi.github.io/cameleer/docs/INTEROP.html)). Two caveats: it is young (version 0.2), and its from-scratch TLS and crypto code has not had an independent security audit yet ([STATUS.md](https://pmsudhi.github.io/cameleer/docs/STATUS.html)). Evaluate it on your own workload. ### How is it tested? With 2,300+ hermetic tests against embedded servers, live suites against real PostgreSQL, Redis, RabbitMQ, ActiveMQ, Mosquitto, vsftpd, Postfix/Dovecot and nginx, official clients (librdkafka, pymongo, the DataStax driver, boto3, the Azure SDK, gcloud, grpcio) and emulators (moto, Azurite, fake-gcs-server, FerretDB). Every example in these guides runs in the test suite. ### Why are there no third-party dependencies? The project was written with only the Rust standard library: TLS, HTTP/2, gRPC, the Kafka, AMQP, MQTT, PostgreSQL, Redis, MongoDB and Cassandra protocols, XML and JSON are implemented from scratch. There is no dependency tree to audit or patch, and builds need no downloads. ### How do I report a bug or a security issue? Open an issue on [GitHub](https://github.com/pmsudhi/cameleer/issues) for bugs; report vulnerabilities privately as described in [SECURITY.md](https://github.com/pmsudhi/cameleer/blob/main/SECURITY.md). ## Related - [Getting started](https://pmsudhi.github.io/cameleer/docs/guides/getting-started.html), [concepts](https://pmsudhi.github.io/cameleer/docs/guides/concepts.html), [EIPs](https://pmsudhi.github.io/cameleer/docs/guides/eips.html) - [Migrating from Apache Camel](https://pmsudhi.github.io/cameleer/docs/guides/migrating-from-apache-camel.html) - [Benchmarks](https://pmsudhi.github.io/cameleer/docs/BENCHMARKS.html), [features](https://pmsudhi.github.io/cameleer/docs/FEATURES.html), [roadmap](https://pmsudhi.github.io/cameleer/docs/ROADMAP.html) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/agents.html # Cameleer for AI agents > Cameleer is the action layer for AI agents: the model decides what, a tested route decides > how. It feeds events to models, routes on their answers, and turns agent tool calls into > reliable actions with validation, idempotency, retries, sagas, human approval, rate limits, > fallbacks and an audit log. An agent decides what to do; a route does it. This cookbook shows eight patterns, each a complete route file in [`examples/agents/`](https://github.com/pmsudhi/cameleer/tree/main/examples/agents) that runs end to end in the test suite (`crates/cameleer-main/tests/docs_guides.rs`) against an OpenAI-compatible mock, the embedded Kafka broker, an Elasticsearch emulator and Slack and payment API mocks. The routes use the [`openai` component](https://pmsudhi.github.io/cameleer/docs/components/openai.html), which works with OpenAI and every OpenAI-compatible server: Azure OpenAI gateways, Ollama, vLLM, LM Studio and LiteLLM. Point it at another server with `OPENAI_BASE_URL` and `OPENAI_MODEL`. The component reads the API key from the `OPENAI_API_KEY` environment variable, so the key never appears in a route. AWS Bedrock (`aws-bedrock`), Azure OpenAI (`azure-openai`) and Google Vertex AI (`google-vertexai`) work the same way in these patterns; see [AI components](https://pmsudhi.github.io/cameleer/docs/guides/ai-components.html). | pattern | file | what it shows | |---|---|---| | [Classify and route](#classify-with-an-llm-and-route-on-the-answer) | `llm-classify-route.camel.yaml` | an LLM label drives a content-based router, with a fixed list of trusted labels | | [Summarize](#summarization-pipeline) | `llm-summarize-pipeline.camel.yaml` | map-reduce summaries of long documents | | [RAG ingestion](#rag-ingestion-chunk-embed-and-store) | `rag-ingest.camel.yaml` | chunk, embed and index documents in Elasticsearch | | [Tools over REST](#expose-a-tool-to-an-agent-over-rest) | `agent-tool-api.camel.yaml` | schema validation, idempotency, retries and an audit log for agent tool calls | | [Human approval](#human-in-the-loop-approval) | `human-approval.camel.yaml` | park risky actions until a person approves them | | [Guard LLM output](#guard-llm-output-with-a-json-schema) | `llm-output-guard.camel.yaml` | validate model JSON against a schema, re-ask once | | [Rate limits and breakers](#rate-limits-and-a-circuit-breaker-around-a-model-api) | `llm-rate-limit-breaker.camel.yaml` | throttle and circuit breaker around a model API | | [Multi-model fallback](#multi-model-fallback) | `llm-multi-model-fallback.camel.yaml` | fail over from a hosted model to a local one | ## Classify with an LLM and route on the answer Tickets arrive over HTTP. The model labels each one, and a `choice` sends the original ticket to a Kafka topic per label. Two details make this safe to run: - The ticket is kept in an exchange property and put back after the model call, so the topic receives the ticket, not the model's answer. - The router compares the label against a fixed list. Anything else, including a ticket that tries "ignore previous instructions", goes to the general topic. Never use the model's answer directly in `toD` or as a topic name. ```yaml - route: id: classify-ticket from: uri: "platform-http:/tickets?httpMethodRestrict=POST" steps: - convertBodyTo: type: java.lang.String - setProperty: name: ticket simple: "${body}" - to: "openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}&temperature=0&systemMessage=Label the support ticket with exactly one word. billing for payments and invoices, technical for errors and outages, other for everything else." - setHeader: name: category simple: "${body.trim().toLowerCase()}" - setBody: simple: "${exchangeProperty.ticket}" - choice: when: - simple: "${header.category} == 'billing'" steps: - to: "kafka:tickets-billing?brokers={{env:KAFKA_BROKERS:localhost:9092}}" - simple: "${header.category} == 'technical'" steps: - to: "kafka:tickets-technical?brokers={{env:KAFKA_BROKERS:localhost:9092}}" otherwise: steps: - setHeader: name: category constant: other - to: "kafka:tickets-general?brokers={{env:KAFKA_BROKERS:localhost:9092}}" - setHeader: name: CamelHttpResponseCode constant: 202 - setHeader: name: Content-Type constant: application/json - setBody: simple: '{"category": "${header.category}"}' ``` The test sends three tickets: an invoice question goes to `tickets-billing`, an error report to `tickets-technical`, and a prompt-injection attempt (the model answers with an invented label) to `tickets-general`. ```sh docker run --rm --network host -e KAFKA_BROKERS=localhost:9092 -e OPENAI_API_KEY \ -v "$PWD:/routes:ro" pmsudhi/cameleer run /routes/llm-classify-route.camel.yaml ``` To classify events from Kafka instead of HTTP, replace the `from` URI with `kafka:support-tickets?brokers=...&groupId=triage`, as in [ai-ticket-triage.camel.yaml](https://github.com/pmsudhi/cameleer/blob/main/examples/ai-ticket-triage.camel.yaml). ## Summarization pipeline Long documents do not fit in one prompt, and one huge prompt is slow and expensive. The route splits each document into paragraphs, summarizes each paragraph, and asks the model to combine the partial summaries. The split's `GroupedBodyAggregationStrategy` collects the summaries into a list, and a list body is sent to the `openai` component as one user message per item. ```yaml - route: id: summarize-documents from: uri: "file:{{env:INBOX_DIR:inbox}}?include=.*\\.txt&move=.done&moveFailed=.failed" steps: - convertBodyTo: type: java.lang.String - split: tokenize: "\n\n" aggregationStrategy: "#class:org.apache.camel.processor.aggregate.GroupedBodyAggregationStrategy" steps: - to: "openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}&temperature=0&systemMessage=Summarize this paragraph in one sentence." - to: "openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}&temperature=0&systemMessage=The messages are summaries of consecutive paragraphs of one document. Combine them into a summary of at most five sentences." - to: "file:{{env:OUTBOX_DIR:outbox}}?fileName=${file:onlyname.noext}.summary.txt" - log: "summarized ${file:name} (${header.CamelOpenAITotalTokens} tokens for the last call)" ``` Drop `report.txt` into the inbox and `report.summary.txt` appears in the outbox. The test checks that a three-paragraph document makes four model calls (three paragraphs, one combine). Add `parallelProcessing: true` to the split to summarize paragraphs concurrently, and a [throttle](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#throttle) to stay under the provider's rate limit. ## RAG ingestion: chunk, embed and store Retrieval-augmented generation needs an index of your documents. This route watches a folder, splits each document into paragraph chunks, embeds each chunk with `openai:embeddings`, and indexes `{source, chunk, text, embedding}` in Elasticsearch or OpenSearch under the id `-`, so re-dropping a changed file overwrites its chunks. ```yaml - route: id: rag-ingest from: uri: "file:{{env:INBOX_DIR:docs}}?include=.*\\.(md|txt)&move=.indexed&moveFailed=.failed" steps: - convertBodyTo: type: java.lang.String - setHeader: name: docId simple: "${file:onlyname.noext}" - split: tokenize: "\n\n" steps: - filter: simple: "${body.trim()} != ''" steps: - setProperty: name: chunk simple: "${body.trim()}" - to: "openai:embeddings?model={{env:EMBEDDING_MODEL:text-embedding-3-small}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}" - setHeader: name: indexId simple: "${header.docId}-${exchangeProperty.CamelSplitIndex}" - setBody: jq: '{source: header("CamelFileName"), chunk: property("CamelSplitIndex"), text: property("chunk"), embedding: .}' - to: "elasticsearch://rag?hostAddresses={{env:ES_HOSTS:localhost:9200}}&indexName={{env:ES_INDEX:docs}}&operation=Index" - log: "indexed ${file:name}" ``` The `jq` step builds the document: `.` is the embedding vector returned by the model, and `header()` and `property()` read the file name and the chunk text. Empty chunks are skipped by the filter. The same shape works for other sources (`aws2-s3`, `google-drive`, `sftp`, `mail`) and other stores: PostgreSQL with pgvector through the [`sql` component](https://pmsudhi.github.io/cameleer/docs/components/sql.html), or a vector database's REST API through [`http`](https://pmsudhi.github.io/cameleer/docs/components/http.html). Create the index with a `dense_vector` (Elasticsearch) or `knn_vector` (OpenSearch) mapping before you ingest. ## Expose a tool to an agent over REST Agents call tools; a tool is a route behind an HTTP endpoint. This refund tool shows the four things every side-effecting tool needs: 1. **Validated arguments.** `json-validator` checks the call against [schemas/refund-request.json](https://github.com/pmsudhi/cameleer/blob/main/examples/agents/schemas/refund-request.json), the same schema you give the agent in its tool definition. A bad call gets `400` with every error, so the agent can correct itself. 2. **Idempotency.** Agents retry. `idempotentConsumer` on the `requestId` argument makes sure a repeated call does not refund twice; the repeat gets `{"status": "duplicate"}`. 3. **Retries.** The payments call is retried three times with exponential back-off on HTTP errors and I/O errors, then answered with `502`. 4. **An audit log.** A wire tap appends every accepted call to `tool-calls.jsonl` without slowing the call down. ```yaml - rest: path: /tools post: - path: /refund to: direct:refund-tool - routeConfiguration: onException: - onException: exception: - org.apache.camel.component.jsonvalidator.JsonValidationException handled: constant: true steps: - setHeader: name: CamelHttpResponseCode constant: 400 - setBody: simple: "invalid tool arguments: ${exception.message}" - onException: exception: - org.apache.camel.http.base.HttpOperationFailedException - java.io.IOException redeliveryPolicy: maximumRedeliveries: 3 redeliveryDelay: 200 useExponentialBackOff: true handled: constant: true steps: - setHeader: name: CamelHttpResponseCode constant: 502 - setBody: simple: "payments API unavailable, the refund was not confirmed; retry later with the same requestId" - route: id: refund-tool from: uri: direct:refund-tool steps: - convertBodyTo: type: java.lang.String - to: "json-validator:file:{{env:SCHEMA_DIR:schemas}}/refund-request.json" - unmarshal: json: {} - setHeader: name: requestId simple: "${body[requestId]}" - wireTap: uri: direct:audit - idempotentConsumer: header: requestId steps: - removeHeaders: pattern: "CamelHttp*" - marshal: json: {} - enrich: uri: "{{env:PAYMENTS_URL:http://localhost:9000}}/refunds?httpMethod=POST" - setBody: simple: '{"status": "refunded", "requestId": "${header.requestId}", "payment": ${body}}' - filter: simple: "${exchangeProperty.CamelDuplicateMessage} == true" steps: - setBody: simple: '{"status": "duplicate", "requestId": "${header.requestId}"}' - setHeader: name: Content-Type constant: application/json - route: id: audit-tool-calls from: uri: direct:audit steps: - marshal: json: {} - setBody: simple: "${body}\n" - to: "file:{{env:AUDIT_DIR:audit}}?fileName=tool-calls.jsonl&fileExist=Append" ``` Two details in the payments call: - `removeHeaders: CamelHttp*` comes first, because the incoming request's `CamelHttpUri` and `CamelHttpPath` headers would otherwise change where the `http` producer sends the request, as in Apache Camel. - The call is an `enrich`, not a `to`. `enrich` sends a copy of the message, so a retry sends the original request again. With `to`, a retry after an HTTP error would send the error response instead, because Cameleer's `http` producer puts the response into the message before it fails (Apache Camel leaves the message unchanged). The reply of a successful call replaces the body, as with `to`. The test makes the payments mock fail the first call with `503`, and checks the retry, the duplicate, the `400` for a call without `orderId` and with an amount over the schema's maximum, and the two audit lines. For a tool that spans several systems (refund, return label, email) in one call, with a saga that rolls back half-done work, see [AI agent tool calls: direct vs through Cameleer](https://pmsudhi.github.io/cameleer/docs/guides/agent-tool-calls.html). For one endpoint per tool without one route per tool, use `toD: direct:tool-${header.name}` on a path parameter, with a fixed set of `direct:` routes behind it. A packaged MCP server is on the [roadmap](https://pmsudhi.github.io/cameleer/docs/ROADMAP.html). ## Human-in-the-loop approval Some actions should not run on the model's word alone. Here refunds up to `APPROVAL_LIMIT` run at once. Larger ones are written to a `pending` folder, and a Slack message asks a person to approve. The approval endpoint takes the parked action with `pollEnrich` (deleting the file) and executes it, so an action runs at most once even if the link is clicked twice. ```yaml - rest: post: - path: /actions to: direct:propose - path: /approvals/{id} to: direct:approve - route: id: propose-action from: uri: direct:propose steps: - convertBodyTo: type: java.lang.String - setProperty: name: action simple: "${body}" - unmarshal: json: {} - setHeader: name: actionId simple: "${body[actionId]}" - choice: when: - simple: "${body[amount]} <= {{env:APPROVAL_LIMIT:100}}" steps: - setBody: simple: "${exchangeProperty.action}" - to: direct:execute - setBody: simple: '{"status": "executed", "actionId": "${header.actionId}"}' otherwise: steps: - setBody: simple: "${exchangeProperty.action}" - to: "file:{{env:PENDING_DIR:pending}}?fileName=${header.actionId}.json" - setBody: simple: "Agent wants to refund ${jsonpath($.amount)} on order ${jsonpath($.orderId)}. Approve: {{env:PUBLIC_URL:http://localhost:8080}}/approvals/${header.actionId}" - to: "slack:#approvals?webhookUrl={{env:SLACK_WEBHOOK_URL}}" - setHeader: name: CamelHttpResponseCode constant: 202 - setBody: simple: '{"status": "pending_approval", "actionId": "${header.actionId}"}' - route: id: approve-action from: uri: direct:approve steps: - setProperty: name: actionId simple: "${header.id}" - pollEnrich: uri: "file:{{env:PENDING_DIR:pending}}?fileName=${header.id}.json&delete=true" timeout: 1000 - choice: when: - simple: "${body} == null" steps: - setHeader: name: CamelHttpResponseCode constant: 404 - setBody: simple: "no pending action ${exchangeProperty.actionId}" otherwise: steps: - to: direct:execute - setBody: simple: '{"status": "executed", "actionId": "${exchangeProperty.actionId}"}' - route: id: execute-action from: uri: direct:execute steps: - removeHeaders: pattern: "CamelHttp*" - to: "{{env:PAYMENTS_URL:http://localhost:9000}}/refunds?httpMethod=POST" ``` The test proposes a 40 refund (executed at once) and a 350 refund (parked, Slack message with the approval link), approves it, checks that the payments API got it, and checks that a second approval answers `404`. Protect `/approvals` with your SSO or API gateway: anyone who can call it can approve. To approve from Slack buttons instead of links, point a Slack interactivity URL at a `platform-http` route that parses the payload. ## Guard LLM output with a JSON schema When a model's answer feeds another system, check it first. The route asks for JSON, validates it with `json-validator` against [schemas/invoice.json](https://github.com/pmsudhi/cameleer/blob/main/examples/agents/schemas/invoice.json), and on failure asks once more, passing the validation errors back to the model. A second failure becomes `422`, so bad data never reaches the caller. ```yaml - route: id: extract-invoice from: uri: "platform-http:/extract?httpMethodRestrict=POST" steps: - convertBodyTo: type: java.lang.String - setProperty: name: document simple: "${body}" - doTry: steps: - to: direct:ask-for-json doCatch: - exception: - org.apache.camel.ValidationException steps: - setBody: simple: "Your previous answer was rejected. ${exception.message}. Reply with corrected JSON only. Document follows. ${exchangeProperty.document}" - doTry: steps: - to: direct:ask-for-json doCatch: - exception: - org.apache.camel.ValidationException steps: - setHeader: name: CamelHttpResponseCode constant: 422 - setBody: simple: "the model did not return a valid invoice: ${exception.message}" - setHeader: name: Content-Type constant: application/json - route: id: ask-for-json from: uri: direct:ask-for-json steps: - to: "openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}&temperature=0&systemMessage=Extract the invoice as a JSON object with the fields vendor (string), total (number) and currency (ISO 4217 code). Reply with the JSON object only." - to: "json-validator:file:{{env:SCHEMA_DIR:schemas}}/invoice.json" ``` The outer `doTry` catches the first `ValidationException` (an answer that is not JSON at all is a `JsonValidationException`, a subkind of it) and asks again with the errors; the inner `doTry` turns a second failure into `422`. Both attempts call the same `direct:ask-for-json` route. The test covers all three outcomes: valid at once, repaired on the second call, and rejected with `422`. ## Rate limits and a circuit breaker around a model API Model APIs have rate limits and outages. The `throttle` keeps calls under five per second (others wait, instead of getting `429` from the provider). The `circuitBreaker` counts failures; when half of the last ten calls failed, it stops calling the API for 30 seconds and answers from `onFallback` with `503` and `Retry-After`. ```yaml - route: id: ask-model from: uri: "platform-http:/ask?httpMethodRestrict=POST" steps: - convertBodyTo: type: java.lang.String - throttle: constant: 5 timePeriodMillis: 1000 - circuitBreaker: resilience4jConfiguration: slidingWindowSize: 10 minimumNumberOfCalls: 5 failureRateThreshold: 50 waitDurationInOpenState: 30 timeoutEnabled: true timeoutDuration: 60000 steps: - to: "openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}&maxTokens=300&systemMessage=Answer in at most three sentences." onFallback: steps: - setHeader: name: CamelHttpResponseCode constant: 503 - setHeader: name: Retry-After constant: 30 - setBody: constant: "The assistant is unavailable right now. Please retry in 30 seconds." ``` The test runs it twice: against the model mock (six calls take at least a second because of the throttle), and against an API that is down (every call gets the fallback `503` at once). For a limit per tenant or per API key, add `correlationExpression: {header: tenant}` to the throttle. ## Multi-model fallback Use a hosted model first and a local one when it fails. A failover load balancer tries the primary endpoint and moves to the fallback on any exception: a connection error, a timeout, or an HTTP error such as `429` or `401` (the `openai` component fails with `HttpOperationFailedException` on API errors). ```yaml - route: id: chat-with-fallback from: uri: "platform-http:/chat?httpMethodRestrict=POST" steps: - convertBodyTo: type: java.lang.String - loadBalance: failover: maximumFailoverAttempts: 1 steps: - to: "openai:chat-completion?model={{env:PRIMARY_MODEL:gpt-4o-mini}}&baseUrl={{env:PRIMARY_BASE_URL:https://api.openai.com/v1}}&timeout=30s" - to: "openai:chat-completion?model={{env:FALLBACK_MODEL:llama3.1}}&baseUrl={{env:FALLBACK_BASE_URL:http://localhost:11434/v1}}&timeout=60s" - setHeader: name: X-Answered-By simple: "${header.CamelOpenAIResponseModel}" ``` The `X-Answered-By` header comes from `CamelOpenAIResponseModel`, the model name the API reported. The test checks both paths: primary down (the local model answers) and primary up (the fallback is never called). To prefer a cheaper model and fall back to a stronger one, swap the two endpoints; to spread load instead, use `roundRobin` (see [load balancer](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#load-balancer)). ## Patterns that combine well - Put a [`filter`](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#message-filter) and an [`idempotentConsumer`](https://pmsudhi.github.io/cameleer/docs/guides/eips/flow-control.html#idempotent-consumer) in front of model calls: spam and duplicates cost tokens too. - [`wireTap`](https://pmsudhi.github.io/cameleer/docs/guides/eips/routing.html#wire-tap) every prompt and answer to Kafka or a file for an audit trail and evaluation data. - Use [`enrich`](https://pmsudhi.github.io/cameleer/docs/guides/eips/transformation.html#content-enricher-enrich) to add customer data from SQL or a CRM to the prompt. - Run the routes as a sidecar that starts in milliseconds next to your agent (Docker image `pmsudhi/cameleer`) or embed them in a Rust agent with `cameleer_main::new_main()`. ## Common questions ### How do I call an LLM from a Cameleer route? Send the prompt as the body to `openai:chat-completion?model=...`; the body becomes the model's answer and the token counts are in `CamelOpenAI*Tokens` headers. Set `baseUrl` for Ollama, vLLM or another OpenAI-compatible server, or use `aws-bedrock`, `azure-openai` or `google-vertexai`. ### Can an AI agent call Cameleer routes as tools? Yes. Expose each tool as a REST endpoint (`rest` or `platform-http`), validate the arguments with `json-validator`, and add idempotency and retries, as in the [tool pattern](#expose-a-tool-to-an-agent-over-rest). An MCP server component is planned. ### How do I stop an agent from executing the same action twice? Have the agent send a unique request id with every tool call and wrap the side effect in `idempotentConsumer` on that id. Use the JDBC or Redis idempotent repository to keep the ids across restarts and instances. ### How do I test routes that call an LLM without paying for tokens? Point `baseUrl` at a mock. `cameleer_saas::testing::OpenAiMock` is an OpenAI-compatible mock for chat and embeddings, and the tests of this guide use it. ### Does Cameleer run models or store vectors? No. It calls the models and moves embeddings into the stores you already run (Elasticsearch, OpenSearch, PostgreSQL with pgvector, or any store with an HTTP API). ## Related - [AI agent tool calls: direct vs through Cameleer](https://pmsudhi.github.io/cameleer/docs/guides/agent-tool-calls.html): a multi-step tool with a saga, compared with letting the model call each API - [AI components](https://pmsudhi.github.io/cameleer/docs/guides/ai-components.html), [openai component](https://pmsudhi.github.io/cameleer/docs/components/openai.html) - [EIPs](https://pmsudhi.github.io/cameleer/docs/guides/eips.html), [error handling](https://pmsudhi.github.io/cameleer/docs/guides/error-handling.html), [edge cookbook](https://pmsudhi.github.io/cameleer/docs/guides/edge.html) - [Roadmap: agent tool server and agent loop](https://pmsudhi.github.io/cameleer/docs/ROADMAP.html) ## 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](https://github.com/pmsudhi/cameleer/issues/new) 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](https://github.com/pmsudhi/cameleer/blob/main/CONTRIBUTING.md) explains the ground rules. --- Source: https://pmsudhi.github.io/cameleer/docs/guides/cloud-credentials.html # Cloud credentials, regions and endpoints > How Cameleer's AWS and Azure components find credentials (keys, profiles, instance and container roles, web identity, managed identity, SAS tokens), choose a region, and talk to emulators such as LocalStack and Azurite. This guide covers every `aws*` component (for example [aws2-s3](https://pmsudhi.github.io/cameleer/docs/components/aws2-s3.html), [aws2-sqs](https://pmsudhi.github.io/cameleer/docs/components/aws2-sqs.html), [aws-bedrock](https://pmsudhi.github.io/cameleer/docs/components/aws-bedrock.html)) and every `azure-*` component (for example [azure-storage-blob](https://pmsudhi.github.io/cameleer/docs/components/azure-storage-blob.html), [azure-servicebus](https://pmsudhi.github.io/cameleer/docs/components/azure-servicebus.html), [azure-openai](https://pmsudhi.github.io/cameleer/docs/components/azure-openai.html)). All of them are implemented in Rust without the AWS or Azure SDKs; the behaviour below is that of Cameleer's own code, which follows the SDKs' conventions. ## AWS ### Where credentials come from When an endpoint has both `accessKey` and `secretKey`, they are used (with `sessionToken` for temporary credentials). Otherwise Cameleer walks the default credentials chain, in this order: | order | source | details | |---|---|---| | 1 | Environment variables | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` (or `AWS_SECRET_KEY`), optional `AWS_SESSION_TOKEN`. | | 2 | Shared credentials file | `AWS_SHARED_CREDENTIALS_FILE`, else `$HOME/.aws/credentials`; profile `profileCredentialsName`, else `AWS_PROFILE`, else `default`. Keys `aws_access_key_id`, `aws_secret_access_key`, `aws_session_token`. | | 3 | Web identity (EKS IRSA, any OIDC federation) | `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` (optional `AWS_ROLE_SESSION_NAME`): STS `AssumeRoleWithWebIdentity`. The STS endpoint is `AWS_ENDPOINT_URL_STS`, `AWS_ENDPOINT_URL`, the endpoint override, or `https://sts.{region}.amazonaws.com`. | | 4 | Container credentials (ECS task roles, EKS Pod Identity) | `AWS_CONTAINER_CREDENTIALS_FULL_URI`, or `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` on `http://169.254.170.2`; authorization from `AWS_CONTAINER_AUTHORIZATION_TOKEN` or `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE`. | | 5 | EC2 instance metadata (instance profiles) | IMDSv2 with a session token, falling back to IMDSv1, on `AWS_EC2_METADATA_SERVICE_ENDPOINT` (default `http://169.254.169.254`). Disable with `AWS_EC2_METADATA_DISABLED=true`. | Remote credentials are cached per process until five minutes before they expire. An unreachable metadata endpoint is remembered for a minute, so the chain stays fast on machines that are not on EC2. Two options narrow the chain: - `useDefaultCredentialsProvider=true` ignores `accessKey` / `secretKey` on the endpoint and always uses the chain. - `useProfileCredentialsProvider=true` (with `profileCredentialsName`) reads only the shared credentials file. `useSessionCredentials`, `useIAMCredentials` and `autoDiscoverClient` are accepted for Camel compatibility and have no effect: session tokens and roles are handled by the rules above. Differences from the AWS SDKs: only the credentials file is read, not `~/.aws/config` (so the region and `credential_process` / SSO settings in that file are not used), and on Windows the file is found through `HOME` or `AWS_SHARED_CREDENTIALS_FILE`. ### Region `region` on the endpoint, else `AWS_REGION`, else `AWS_DEFAULT_REGION`, else `us-east-1`. Names are lower-cased and `_` becomes `-`, so Camel-style `EU_WEST_1` works. Requests go to `https://{service}.{region}.amazonaws.com` (`.amazonaws.com.cn` for `cn-` regions); global services (IAM) use their global endpoint and sign for `us-east-1`. ### AWS options Every AWS component accepts these options: | option | default | description | |---|---|---| | `accessKey`, `secretKey` | | Static credentials. Wrap them in `RAW(...)` so `+` and `/` survive. | | `sessionToken` | | Session token of temporary credentials. | | `region` | see above | Region. | | `useDefaultCredentialsProvider` | `false` | Always use the default chain. | | `useProfileCredentialsProvider` | `false` | Only the shared credentials file. | | `profileCredentialsName` | | Profile of the credentials file. | | `uriEndpointOverride` | | Base URL of an emulator or a VPC endpoint. | | `overrideEndpoint` | `false` | Must be `true` for `uriEndpointOverride` to be used. | | `proxyHost`, `proxyPort`, `proxyProtocol` | , `3128`, `HTTP` | Explicit proxy. Without it, the usual proxy environment variables apply. | | `trustAllCertificates` | `false` | Disables certificate checks. Only for local test servers with self-signed certificates; never in production. | ### LocalStack, MinIO and other emulators Set `uriEndpointOverride` and `overrideEndpoint=true`. LocalStack accepts any key, conventionally `test` / `test`: ```yaml - route: id: localstack-orders from: uri: "timer:localstack?repeatCount=1" steps: - setBody: constant: "hello from Cameleer" - to: "aws2-sqs://orders?region=us-east-1&accessKey=test&secretKey=test&uriEndpointOverride={{env:AWS_ENDPOINT_URL:http://localhost:4566}}&overrideEndpoint=true&autoCreateQueue=true" ``` S3 switches to path-style addressing (`http://host:port/bucket/key`) when the endpoint is overridden, which is what MinIO and LocalStack expect. Note that Cameleer does not read `AWS_ENDPOINT_URL` by itself for the components (only for STS web identity); pass it through a placeholder as above. ## Azure ### Storage accounts (Blob, Queue, Data Lake, Files) | `credentialType` | what it uses | |---|---| | `SHARED_ACCOUNT_KEY` (default when `accessKey` is set) | The base64 account key in `accessKey` (`accountKey` is accepted too). Requests are signed with Shared Key. | | `AZURE_SAS` (default when only `sasToken` is set) | A SAS token (`sasToken`, with or without the leading `?`), appended to every request. | | `AZURE_IDENTITY` | Microsoft Entra ID bearer tokens for `https://storage.azure.com/.default`, see below. | | `CLIENT_SECRET` | Entra ID with an explicit service principal (`tenantId`, `clientId`, `clientSecret`). Setting `clientSecret` without a key or SAS selects it too. | The account name is the first part of the uri path (`azure-storage-blob://myaccount/container`). Requests go to `https://{account}.{blob|queue|dfs|file}.core.windows.net`. ### Microsoft Entra ID (service principals, workload identity, managed identity) `credentialType=AZURE_IDENTITY` resolves like the SDK's `DefaultAzureCredential`. Options on the endpoint win over the environment: | order | source | details | |---|---|---| | 1 | Client secret | `clientSecret` / `AZURE_CLIENT_SECRET` with `tenantId` / `AZURE_TENANT_ID` and `clientId` / `AZURE_CLIENT_ID`. | | 2 | Workload identity (AKS, any OIDC federation) | `federatedTokenFile` / `AZURE_FEDERATED_TOKEN_FILE` with the tenant and client id; the token file is read again on every refresh. | | 3 | Managed identity | `managedIdentityEndpoint`, else the App Service / Functions / Container Apps endpoint (`IDENTITY_ENDPOINT` + `IDENTITY_HEADER`), else the instance metadata service `http://169.254.169.254`. `managedIdentityClientId` (or `clientId` / `AZURE_CLIENT_ID`) selects a user-assigned identity. | The token endpoint is `{authorityHost}/{tenantId}/oauth2/v2.0/token`, with `authorityHost` from the option, `AZURE_AUTHORITY_HOST` or `https://login.microsoftonline.com` (set it for sovereign clouds). Tokens are cached per process until a minute before they expire, and a request that gets `401` is retried once with a fresh token. ### Other Azure services | component | credentials | |---|---| | [azure-key-vault](https://pmsudhi.github.io/cameleer/docs/components/azure-key-vault.html), [azure-schema-registry](https://pmsudhi.github.io/cameleer/docs/components/azure-schema-registry.html) | Entra ID: `CLIENT_SECRET` (default; `tenantId`, `clientId`, `clientSecret`) or `AZURE_IDENTITY`. | | [azure-cosmosdb](https://pmsudhi.github.io/cameleer/docs/components/azure-cosmosdb.html) | `accountKey` (master key signature, default) or `credentialType=AZURE_IDENTITY`. | | [azure-openai](https://pmsudhi.github.io/cameleer/docs/components/azure-openai.html) | `apiKey`, or Entra ID client credentials. | | [azure-functions](https://pmsudhi.github.io/cameleer/docs/components/azure-functions.html) | `functionKey` / `masterKey`, or Entra ID tokens with `scope`. | | [azure-eventgrid](https://pmsudhi.github.io/cameleer/docs/components/azure-eventgrid.html) | `accessKey`, a SAS token (`credentialType=AZURE_SAS`), or Entra ID. | | [azure-servicebus](https://pmsudhi.github.io/cameleer/docs/components/azure-servicebus.html), [azure-eventhubs](https://pmsudhi.github.io/cameleer/docs/components/azure-eventhubs.html) | `connectionString`, or namespace + `sharedAccessName` + `sharedAccessKey` (SAS tokens on the AMQP `$cbs` node), or `credentialType=AZURE_IDENTITY` / `TOKEN_CREDENTIAL`. | ### Azure options Storage components accept `accessKey` (or `accountKey`), `sasToken`, `credentialType`, `tenantId`, `clientId`, `clientSecret`, `authorityHost`, `federatedTokenFile`, `managedIdentityClientId`, `managedIdentityEndpoint`, `uriEndpointOverride` + `overrideEndpoint`, `proxyHost` + `proxyPort` and `trustAllCertificates` (local tests only). The other Azure components accept the subset that applies to them, listed on their pages. ### Azurite and other emulators Storage endpoints with `uriEndpointOverride` and `overrideEndpoint=true` use Azurite's path-style addressing, `http://host:port/{account}/...`. Azurite's development account is `devstoreaccount1` with a well-known, public key: ```yaml - route: id: azurite-upload from: uri: "timer:azurite?repeatCount=1" steps: - setBody: constant: "hello from Cameleer" - setHeader: name: CamelAzureStorageBlobBlobName constant: hello.txt - to: "azure-storage-blob://devstoreaccount1/uploads?operation=uploadBlockBlob&accessKey=RAW(Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==)&uriEndpointOverride={{env:AZURITE_BLOB_URL:http://127.0.0.1:10000}}&overrideEndpoint=true" ``` For Key Vault, Functions, OpenAI and Schema Registry, `uriEndpointOverride` replaces the service base URL. Service Bus and Event Hubs emulators are reached with a connection string that contains `UseDevelopmentEmulator=true`. ## Configure once for all endpoints Every endpoint option can be set as a component default with `camel.component..