Expressions (CEL)
Where CEL expressions appear in flows and which variables are available.
Every condition, payload, and transform in a flow is a CEL expression evaluated against the current message. CEL, the Common Expression Language, is a small, non-Turing-complete language from Google: expressions always terminate, cannot mutate anything, and are compiled once at startup, so a malformed expression fails when the config loads. CEL by Example is a good tour of the syntax.
Where expressions appear
You write CEL in four places.
Block settings marked as expressions: a log block's message, a set-payload block's value, a queue-dispatch block's subject, an object-read block's key.
- type: set-payload
settings:
value: '{"orderId": vars.id, "total": body.qty * body.price}'Conditions: an if block's condition, a switch case's when, and a validate block's rules are boolean CEL expressions.
- type: if
condition: 'body.amount >= 1000.0'Source payloads: a cron source's payload builds the message body and can read now, the fire time.
source:
type: cron
settings:
schedule: "@every 30s"
payload: '{"firedAt": string(now)}'Transform values: each step of a multi-transform, an enrich block's setBody and setVars, and template files' {{ }} sections are all CEL over the message.
Available variables
Every message expression can reference six variables:
| Variable | Holds |
|---|---|
body | The message payload (decoded JSON). |
vars | The message variables: scratch state set by blocks and sources. |
eventID | The unique id of this message. |
correlationID | The id correlating this message with an external interaction. |
env | The resolved environment variables, as env.NAME. |
now | The evaluation time (the fire time in source payloads). |
See State and Data for how body and vars get populated.
The quoting gotcha
YAML and CEL both use quotes, and the layers stack. A CEL string literal needs its own double quotes inside the YAML value, so the text hello is written '"hello"':
# CORRECT: the YAML value is the CEL expression "hello world" + body.name
message: '"hello world, " + body.name'
# WRONG: this is the CEL expression `hello`, an undefined variable
message: 'hello'The samples follow one convention: single-quote the YAML value, double-quote CEL string literals inside it.
If a flow fails to load with an error like undeclared reference to 'hello', you wrote a bare word where CEL expected a string literal. Wrap it in double quotes inside the YAML quoting.
Experimenting with octo eval
The CLI evaluates an expression against an ad-hoc message, with no config or runtime services:
octo eval --expr 'body.qty * body.price' --data '{"qty": 3, "price": 10.0}'
# {"ok":true,"result":30}
octo eval --expr '"hello, " + vars.name' --vars '{"name":"Ada"}' --data '{}'
# {"ok":true,"result":"hello, Ada"}
octo eval --expr 'env.GREETING' --env '{"GREETING":"hi"}' --data '{}'
# {"ok":true,"result":"hi"}--data binds to body (stdin is read when omitted), --vars to vars, and --env to env. On failure ok is false and error holds the compile or evaluation message.
What an expression can call
Beyond standard CEL, every expression gets utility functions for strings
(trim, split, join, replace, upperAscii, format), lists (distinct,
sort, sortBy, flatten, slice), base64, math, set membership, and
regex extraction. Octo adds toJson, fromJson, toFormData, fromFormData,
toYaml, fromYaml, toEnv, fromEnv, templateResource, and the multipart
family (multipart, addPart, fromMultipart, toMultipart) for file
uploads. Most reshaping that looks like it needs a foreach is a single
expression. See Extension Libraries.
Going deeper
The CEL reference tours the language itself: operators, macros like has() and map(), type conversions, the extension libraries, and the custom functions Octo adds (such as templateResource and fromFormData).