CEL expressions
Every place a CEL expression appears in Axiom — the compose-mapping formula bar, the edge-adapter cel() transform, a gate condition's CEL op, and a HITL node's interrupt_payload_template — the value/srcField bindings, the inputs.<edge>.<path> multi-source syntax, the built-in string functions, and Axiom's two custom functions toUpper/toLower.
View as MarkdownAxiom uses Common Expression Language (CEL) — via
cel-go — anywhere a mapping needs more
than a straight field pick: computing a target field from one or more source
fields, or gating an edge with a boolean condition. This page documents the
CEL surface available to you: what identifiers are bound, the multi-source
inputs.<edge>.<path> syntax, the built-in string functions, and Axiom's two
custom functions.
Where you write CEL
- The compose-mapping formula bar. When a consumer node has more than one
incident edge (a join), select a target field and the ƒx formula bar
becomes the one place to type its binding. Typing a bare field name (e.g.
email) collapses to a plain pick if it is unambiguous; typing anything else —email != "" ? email : full_name, a multi-source expression, a string-function call — is stored and evaluated as a CEL expression. Click the ? next to ƒx to reopen this page from the panel. - The single-source edge adapter. On a single-source edge, each mapping's
right-hand side is a single bare CEL expression evaluated against the source
message (or a plain field pick when it names a source field). Write the
expression directly —
int(value),text + "!",toUpper(text)— with no pipe wrapper. See the error catalog for how a failing adapter reports its error. - An edge's gate condition, scoped to one field. Set
op: CELandfield: <path>on aflow.yamlcondition (or the equivalent graph JSON) to evaluate the expression against that field's own value, bound tovalue(same convention as the single-source edge adapter above) — the condition passes iff the expression returnstrue. There is no field-less/whole-message CEL condition today: aCELop with nofieldset fails to compile (field "" not found in message ...) rather than evaluating against the whole input — so aflow.yamlop: CELblock always needsfieldtoo. (The canvas gate editor's own "Use CEL expression" toggle doesn't currently expose a field to scope to, so a condition authored that way hits the same compile failure — use the row-based comparison operators in the canvas editor instead until that toggle is field-aware.) - A HITL node's
interrupt_payload_template. Evaluated once when the pause fires, withvaluebound to the whole node input message (not a single field) —valuealone reproduces the whole input,value.textextracts one field, and normal CEL string concatenation ("approve order " + value.id) builds a custom string. No source-field alias here since there is no single source field to alias.
The value binding and source-field aliases
Every CEL expression has a variable named value bound to its input — the
field (or, in a pipeline, the running value after any prior transforms). When
the adapter or binding also knows the source field's name, that name is
declared as a second variable bound to the same value, so an expression
written naturally against the field's own name works without having to know
about value at all:
value == "" # always works
email == "" # works too, when the source field is named `email`If the source message happens to have a field literally named value, the
real field wins — Axiom does not silently return the wrong type just to keep
the placeholder name meaningful.
Picking a whole message-typed field
A field pick doesn't have to name a scalar — it can name a whole
message-typed field, singular or repeated, and copy it across the edge
verbatim. This is the basis of the canonical-envelope pattern: a node that
outputs a shared Image (or Document, Graph, …) envelope wires straight
into a node that consumes the same envelope with a one-line pick.
# source node outputs `annotated` (an Image message); consumer takes `image` (also Image)
adapter:
annotated: image # whole-message pick — copies the Image verbatimThe one rule: a whole-message pick is a byte-for-byte copy, so it is only valid when the source field and the destination field are the same message type. Picking one message type into a different message field — even a structurally identical one from another package that doesn't resolve to the same type — is a compile-time error, not a silent misread:
adapter: field pick "image" maps message type "pkgA.Image" into differently-typed
message field "annotated" (type "pkgB.Image") … reconstruct the destination in CEL
by mapping each leaf field explicitly (e.g. {"annotated.<field>": "image.<field>"})To bridge two different message types, reconstruct the destination in CEL,
field by field (annotated.width: image.width, …) or with a CEL object
literal — the same explicit-reshape rule every cross-type mapping follows. A
cross-type message pick never compiles green and then fails at run.
Multi-source composition: inputs.<edge>.<path>
A join's consumer can have several incident edges, each identified by its canvas edge id. A CEL expression in the compose-mapping formula bar addresses any field on any incident edge with:
inputs.<edge_id>.<field_path><field_path> is dotted for nested fields (inputs.edge-a.reviewer.email).
Axiom builds the CEL type environment from each edge's declared source
message, so referencing an edge id that isn't actually incident to this
join, or a field path that doesn't exist on that edge's message, is a
compile-time error — it fails before any execution runs, not partway
through one. An edge id that isn't a legal CEL identifier (for example one
containing a hyphen from the canvas) can't be referenced this way; wire it
with a plain pick instead.
The formula bar accepts a bare field name as shorthand — email — and
qualifies it to the full inputs.<edge>.<path> form automatically as long as
exactly one incident edge has a field with that name. An expression that
touches more than one source field always displays and stores the fully
qualified form, since that is the only form cel-go's checker (and Axiom's
wire-recovery parser) can resolve unambiguously.
If an incident edge hasn't produced a payload yet, every field on it reads as
its type's zero value (empty string, 0, false, …) rather than failing the
expression.
String functions (cel-go ext.Strings())
Axiom enables the full cel-go strings extension, called on a string value
with member-call syntax (value.trim()), plus the two-argument join/
split/replace/substring overloads:
| Function | Signature | Example |
|---|---|---|
charAt | string.charAt(int) -> string | value.charAt(0) |
indexOf | string.indexOf(string) -> int, string.indexOf(string, int) -> int | value.indexOf("@") |
lastIndexOf | string.lastIndexOf(string) -> int, string.lastIndexOf(string, int) -> int | value.lastIndexOf(".") |
lowerAscii | string.lowerAscii() -> string | value.lowerAscii() |
upperAscii | string.upperAscii() -> string | value.upperAscii() |
replace | string.replace(string, string) -> string, string.replace(string, string, int) -> string | value.replace("-", "_") |
split | string.split(string) -> list<string>, string.split(string, int) -> list<string> | value.split(",") |
substring | string.substring(int) -> string, string.substring(int, int) -> string | value.substring(0, 5) |
trim | string.trim() -> string | value.trim() |
join | list<string>.join() -> string, list<string>.join(string) -> string | value.split(",").join(" | ") |
reverse | string.reverse() -> string | value.reverse() |
format | string.format(list<dyn>) -> string | "%s wins".format([name]) |
lowerAscii/upperAscii only affect ASCII bytes — for Unicode-aware
uppercasing/lowercasing, use Axiom's toUpper/toLower below instead.
Axiom custom functions: toUpper / toLower
Axiom registers two custom global CEL functions alongside the cel-go
extension — called as plain functions, not member calls:
toUpper(value) # "hello" -> "HELLO"
toLower(value) + " world" # "HELLO" -> "hello world"Both take exactly one string argument and return a string; calling either
with a non-string argument is a runtime CEL error
(toUpper requires string / toLower requires string).
These are Go's strings.ToUpper/strings.ToLower under the hood (full
Unicode case folding, not the ASCII-only ext.Strings() equivalents above).
toUpperandtoLowerare the only two custom functions Axiom adds on top of the standard CEL library documented below.
Function catalog
Everything below is enabled everywhere CEL runs in Axiom — the ƒx formula
bar, the cel() adapter transform, gate conditions, and HITL templates all
share one environment (env_version: v3), so this catalog is the
single surface. The same list drives the ƒx bar's autocomplete/function
palette and the axiom flow mapping-scope CLI verb, both fed live from
GET /api/cel/manifest. Only pure, deterministic functions are enabled:
nothing here does I/O, produces randomness, or reads the clock (that
guarantee is what keeps preview-equals-runtime and durable replay sound).
Named functions are grouped by family; operators (+, ==, in, ?:, …)
are CEL syntax and are not listed.
| Family | Functions | Notes |
|---|---|---|
optionals | first, hasValue, last, optional.none, optional.of, optional.ofNonZeroValue, optional.unwrap, or, orValue, unwrapOpt, value | Optional-value plumbing — required by (and enabled with) regex.extract's optional<string> return. |
strings | charAt, format, indexOf, join, lastIndexOf, lowerAscii, replace, reverse, split, strings.quote, substring, trim, upperAscii | String member helpers (value.trim()) plus list<string>.join — full signatures in the table above. |
math | math.abs, math.bitAnd, math.bitNot, math.bitOr, math.bitShiftLeft, math.bitShiftRight, math.bitXor, math.ceil, math.floor, math.isFinite, math.isInf, math.isNaN, math.round, math.sign, math.sqrt, math.trunc | Rounding, absolute value, square root, and bitwise operations on numeric values (math.ceil(value)). |
lists | distinct, flatten, lists.range, slice, sort | List member helpers for de-duplicating, flattening, ordering, windowing, and generating ranges of repeated fields. |
sets | sets.contains, sets.equivalent, sets.intersects | Set-semantics comparisons over two lists (sets.contains(value, ["a", "b"])). |
encoders | base64.decode, base64.encode, json.encode | Base64 between bytes and string, and json.encode for a value → JSON string. |
regex | regex.extract, regex.extractAll, regex.replace | RE2 regular expressions: extract the first match (an optional), all matches, or replace matches. |
toUpper/toLower/parseJSON/toJson | parseJSON, toJson, toLower, toUpper | Axiom's custom functions: Unicode-correct toUpper/toLower, and the schema-free JSON pair <string>.parseJSON() (Tekton-style receiver method: parse a JSON string to a traversable value, integers kept precise) / toJson (serialize with sorted keys, byte-stable across builds). |
Macros
Macros are expanded at compile time (they are not first-class functions). The token is what the manifest reports; the call form is how you write it:
| Macro | Call form | What it does |
|---|---|---|
all | list.all(x, pred) | true iff every element satisfies the predicate (two-var list.all(i, x, pred) also available) |
bind | cel.bind(name, init, expr) | bind a computed value to a name once and reuse it in expr |
exists | list.exists(x, pred) | true iff at least one element satisfies the predicate |
existsOne | list.exists_one(x, pred) | true iff exactly one element satisfies the predicate |
exists_one | list.exists_one(x, pred) | legacy spelling of the exactly-one quantifier |
filter | list.filter(x, pred) | the sublist of elements satisfying the predicate |
greatest | math.greatest(a, b, …) | the maximum of the arguments (or of a single list argument) |
has | has(msg.field) | test whether a (possibly nested) message field is set |
least | math.least(a, b, …) | the minimum of the arguments (or of a single list argument) |
map | list.map(x, expr) | project each element through an expression (list.map(x, pred, expr) filters first) |
optFlatMap | opt.optFlatMap(x, expr) | like optMap but expr itself returns an optional (no double-wrapping) |
optMap | opt.optMap(x, expr) | map an optional's value through expr if present, else stay empty |
sortBy | list.sortBy(x, key) | the list sorted ascending by each element's computed key |
transformList | list.transformList(i, x, expr) | index-and-value list comprehension (list.transformList(i, x, pred, expr) filters first) |
transformMap | map.transformMap(k, v, expr) | rebuild a map, transforming each value (keys preserved) |
transformMapEntry | map.transformMapEntry(k, v, expr) | rebuild a map, transforming each entry into new key/value pairs |
stdlib (always on)
<details> <summary>Standard built-in functions — conversions, collection/string membership, size, regex `matches`, and the timestamp accessors — enabled everywhere, plus the standard operators and comprehension macros above.</summary>| Family | Functions | Notes |
|---|---|---|
stdlib | bool, bytes, contains, double, duration, dyn, endsWith, getDate, getDayOfMonth, getDayOfWeek, getDayOfYear, getFullYear, getHours, getMilliseconds, getMinutes, getMonth, getSeconds, in, int, matches, size, startsWith, string, timestamp, type, uint | Conversions (int, double, string, bytes, bool, timestamp, duration, dyn, type), membership/size (in, contains, size, startsWith, endsWith, matches), and timestamp accessors (getFullYear, getHours, …). |
Two ways to explore this catalog interactively instead of reading it here:
- In the canvas: the ƒx bar's ƒ( ) button opens the function palette (grouped by the same families); typing in the bar autocompletes both in-scope fields and these functions.
- From the CLI / an agent:
axiom flow mapping-scope [--edge <edge-id>] [--json]prints the functions and the exact variables in scope for a specific edge's mapping.
Errors
A CEL expression that fails to parse or compile, or whose type checker
rejects a reference, is reported before the flow runs. A CEL expression that
raises at evaluation time (a custom function given the wrong type, a runtime
error from cel-go) surfaces on the failing edge. See
Edge adapter (CEL) errors and the
CEL evaluation failed / CEL parse error rows in
Flow run errors in the editor.