Expressions (CEL)
Look up the syntax, operators, and functions available inside a Shipfox expression.
Shipfox expressions use CEL. A workflow writes them in two forms.
A template puts a value into text with ${{ }}. A predicate gives a true
or false result that controls a trigger, job, step, or gate.
For the data an expression can read, see Contexts.
Syntax
Write an expression between ${{ and }}:
run: echo "Building ${{ trigger.event }}"| Form | Example | Result |
|---|---|---|
| Property access | event.ref | The ref property of event |
| Index access | vars["API_URL"] | A property whose name is not an identifier |
| List index | execution.events[0].data | The first element of a list |
| Escape | $${{ | The literal text ${{ |
A predicate field takes the expression on its own. An if field takes exactly
one ${{ }} and nothing else:
if: ${{ trigger.event == "push" }}vars and secrets accept a literal key only. vars["API_URL"] is valid and a
computed key such as vars[inputs.name] is not.
Run identifiers
The run context has a UUID id and a sequential number. The UUID identifies
the run in API and client routes. The number is scoped to the workflow
definition and is available for labels, predicates, and display text. It is not
resolved as a route address.
execution_name: 'Deploy #${{ run.number }}'Types
| Type | Written as |
|---|---|
string | "refs/heads/main" |
int | 42 |
double | 1.5 |
bool | true |
list | ["main", "release"] |
map | {"env": "prod"} |
dyn | A value whose shape is not known when the expression is checked |
timestamp | timestamp("2026-01-01T00:00:00Z") |
Operators
| Operator | Example | Result |
|---|---|---|
==, != | trigger.event == "push" | Compares compatible values; dyn is checked at evaluation time |
<, <=, >, >= | run.created_at > timestamp("2026-01-01T00:00:00Z") | Orders numbers, strings, and timestamps |
&&, ||, ! | job.key == "deploy" && !step.is_retry | Combines boolean values |
in | "prod" in inputs.environments | True when a list contains the value |
in | "API_URL" in vars | True when a map contains the key |
+ | "v" + inputs.version | Adds numbers, joins strings, joins lists |
-, *, /, % | executions.size() % 2 == 0 | Arithmetic on numbers |
? : | run.number == 1 ? "first" : "rerun" | The middle value when true, the last when false |
( ) | (a || b) && c | Groups an expression against precedence |
Static checking rejects incompatible known types rather than treating them as a
false result. Dynamic values are checked when the expression runs, and numeric
comparisons support both int and double. A predicate that errors never
passes.
Functions and macros
| Call | Example | Result |
|---|---|---|
list.size(), string.size() | executions.size() > 1 | Number of elements or characters |
list.first() | execution.events.first().data | The first element of a list |
list.last() | execution.events.last().data | The final element of a list |
list.all(item, predicate) | executions.all(e, e.status == "succeeded") | True when every element matches |
list.exists(item, predicate) | execution.events.exists(e, e.event == "push") | True when at least one element matches |
list.exists_one(item, predicate) | needs.exists_one(j, j.status == "failed") | True when exactly one element matches |
list.filter(item, predicate) | executions.filter(e, e.status == "failed") | The elements that match |
list.map(item, expression) | needs.map(j, j.key) | The expression applied to each element |
string.contains(part) | event.ref.contains("release") | True when the string contains part |
string.startsWith(part), string.endsWith(part) | event.ref.startsWith("refs/heads/") | Prefix and suffix tests |
string.matches(pattern) | event.ref.matches("^refs/tags/v[0-9]+$") | True when the string matches a regular expression |
string.split(separator) | event.ref.split("/")[2] | The parts between each separator |
string.lowerAscii(), string.upperAscii() | event.ref.lowerAscii() | The string in one case |
has(field) | has(event.pull_request) | True when the field is present |
string(value), int(value), double(value) | "run-" + string(run.number) | The value converted to that type |
timestamp(string) | timestamp("2026-01-01T00:00:00Z") | A timestamp from an RFC 3339 string |
range(start, stop, step) | range(1, 3, 1) | An inclusive list of integers |
toJson(value) | toJson(event.pull_request.labels) | The value serialized as compact JSON text |
fromJson(string) | fromJson(event.payload) | The JSON text parsed into a dynamic CEL value |
list.first() and list.last() keep the element's type. Calling either method
on an empty list causes an evaluation error. Workflow job and step conditions
record that outcome as errored rather than rejected.
range() accepts a positive step and can materialize at most 1,000 values and
1,000,000 context-byte fan-out units in one evaluation. toJson() can produce
at most 1,000,000 UTF-8 bytes in one evaluation. fromJson() expects a valid
JSON string and converts safe integer numbers to CEL integers. toJson() writes
integers outside the range JSON numbers represent exactly as quoted strings.
Lookups through an open map, schema-less tool output, or an unknown JSON Schema
are dyn. They can be used with field access, methods, and comparisons, but a
runtime failure still makes the surrounding predicate fail.
The CEL language definition documents the full standard library.
Related pages
Contexts
Every context, its properties, and the fields that can read it.
Workflow schema
The fields that carry these expressions.
Context and templating
Why templates and predicates are separate, and how commands receive values.