CEL EXPRESSIONS
Swamp uses CEL (Common Expression Language) for inline expressions in model definitions, workflow configurations, and data queries.
Expression Syntax
Embed CEL expressions in YAML values with the ${{ }} wrapper:
globalArguments:
greeting: "${{ 'Hello, ' + self.name }}"
vpc_id: "${{ model.my_vpc.resource.result.result.attributes.vpcId }}"When the entire YAML value is a single ${{ }} expression, the evaluated result
preserves its type (number, boolean, list, map). When mixed with surrounding
text, the result is coerced to a string.
Where Expressions Are Evaluated
Each location has access to a different set of context variables:
| Location | Available Context |
|---|---|
Model globalArguments |
model, self, env, vault, data, file |
Model inputs defaults |
model, self, env, vault, data, file |
Workflow step task.inputs |
model, self, inputs, env, vault, data, file, run, webhook* |
Workflow step forEach.in |
model, self, inputs, env, vault, data, file, run, webhook* |
| Data output overrides | model, self, env, vault, data, file |
swamp data query predicate |
DataRecord fields only (see Query Fields) |
swamp data query --select |
DataRecord fields only |
* webhook is only available when the workflow was triggered by a
webhook.
Note
Grant conditions (--when) are subject to additional complexity limits beyond
the standard CEL expression context. See
Condition cost limits
for thresholds.
Context Variables
self
The current model definition being evaluated.
| Field | Type | Description |
|---|---|---|
self.id |
string |
Definition UUID |
self.name |
string |
Model name |
self.version |
int |
Definition version number |
self.tags |
map<string, string> |
Definition tags |
self.globalArguments |
map<string, dyn> |
Evaluated global arguments |
When forEach is active, the iteration variable is available as a field on
self. For example, forEach: { item: "env", in: [...] } adds self.env.
model
A map of all models in the repository, indexed by name and UUID.
model.<name>.input
| Field | Type | Description |
|---|---|---|
id |
string |
Definition UUID |
name |
string |
Model name |
version |
int |
Definition version |
tags |
map<string, string> |
Definition tags |
globalArguments |
map<string, dyn> |
Global arguments |
model.<name>.definition
| Field | Type | Description |
|---|---|---|
id |
string |
Definition UUID |
name |
string |
Model name |
version |
int |
Definition version |
tags |
map<string, string> |
Definition tags |
globalArguments |
map<string, dyn> |
Global arguments |
inputs |
map<string, dyn> |
Inputs schema |
model.<name>.resource.<specName>.<instanceName>
| Field | Type | Description |
|---|---|---|
attributes |
map<string, dyn> |
Parsed JSON content |
tags |
map<string, string> |
Metadata tags |
version |
int |
Version number |
model.<name>.file.<specName>.<instanceName>
| Field | Type | Description |
|---|---|---|
path |
string |
File path on disk |
size |
int |
File size in bytes |
contentType |
string |
MIME type |
model.<name>.execution
| Field | Type | Description |
|---|---|---|
id |
string |
Execution ID |
methodName |
string |
Method that was executed |
status |
string |
succeeded, failed, etc. |
startedAt |
string |
ISO 8601 timestamp |
completedAt |
string |
ISO 8601 timestamp (if done) |
durationMs |
int |
Execution duration |
error |
map |
Error details (if failed) |
Hyphenated Model Names
CEL interprets hyphens as subtraction. Swamp automatically transforms
model.deploy-vpc.resource to model["deploy-vpc"].resource. Both forms work
in expressions. Use bracket notation explicitly when needed:
model["deploy-vpc"].resource.result.result.attributes.vpcIdinputs
Input values provided when instantiating a definition. Available in workflow step task inputs.
inputs.vpc_id
inputs["hyphenated-name"]env
The full process environment as a flat string map.
env.API_ENDPOINT
env.HOMECaution
Values accessed via env are persisted to .swamp/data/ on disk. Use
vault.get() for sensitive values.
vault
Secure secret access. Secrets are fetched at runtime and never written to model output data. Vault expressions are resolved before CEL evaluation — the secret value is substituted into the expression before it runs.
vault.get("my-vault", "api-key")
vault.get("infra", "Client ID")data
Namespace for querying versioned data artifacts. See Data Functions below.
file
Namespace for reading file contents. See File Functions below.
run
Metadata about the current workflow execution. Only available in workflow step
expressions (task.inputs, forEach.in), not in workflow-level fields like
description.
| Field | Type | Description |
|---|---|---|
run.id |
string (UUID) |
Unique ID of this workflow run |
run.workflowId |
string (UUID) |
Workflow definition ID |
run.workflowName |
string |
Workflow name |
run.startedAt |
string (ISO 8601) |
Timestamp when the run started |
run.tags |
map<string, string> |
Merged workflow + runtime tags |
run.id
run.workflowName
run.tagsThe flat workflowRunId variable is also available (equivalent to run.id) for
backward compatibility with data.query() predicates.
Because run.id is unique per execution, it serves as a collision-safe key when
the same workflow runs concurrently:
inputs:
outputKey: "filtered-vms-${{ run.id }}"webhook
Webhook request data, available in workflow step expressions (task.inputs,
forEach.in) only when the workflow was triggered by a
webhook. Not present for CLI-triggered
or scheduled runs.
| Field | Type | Description |
|---|---|---|
webhook.body |
dyn |
Parsed JSON body (object or array), or raw string for non-JSON payloads |
webhook.headers |
map<string, string> |
Lowercased request headers (signature header excluded) |
webhook.route |
string |
Matched webhook route path |
webhook.body.action == "opened"
webhook.headers["x-github-event"] == "issues"Use webhook.body to route workflow behavior based on the incoming payload:
inputs:
action: "${{ webhook.body.action }}"
repo: "${{ webhook.body.repository.full_name }}"DataRecord Fields
Data functions and query predicates operate on records with these fields:
| Field | Type | Description |
|---|---|---|
id |
string |
Record UUID |
name |
string |
Data item name |
version |
int |
Version number |
createdAt |
string |
ISO 8601 timestamp |
attributes |
map<string, dyn> |
Parsed JSON content (resource data) |
tags |
map<string, string> |
Metadata tags |
modelName |
string |
Owning model name |
modelType |
string |
Model type (e.g., command/shell) |
specName |
string |
Output spec name |
dataType |
string |
resource or file |
contentType |
string |
MIME type |
lifetime |
string |
Data lifetime |
ownerType |
string |
Owner type |
streaming |
bool |
Whether data was streamed |
size |
int |
Content size in bytes |
content |
string |
Raw text content |
Data Functions
All data functions are methods on the data namespace. They return DataRecord
objects or arrays of them.
data.latest(modelName, dataName)
Returns the latest version of a named data item, or null.
data.latest("scanner", "result")
data.latest("scanner", "result").attributes.findings
# Null-safe — returns null instead of throwing if the data doesn't exist:
data.latest("scanner", "result").?attributes.?findings
data.latest("scanner", "result").?attributes.?findings.orValue([])data.latest(modelName, dataName, varyValues)
Returns the latest version for a specific vary dimension.
data.latest("scanner", "result", ["dev"])
data.latest("scanner", "result", ["us-east-1", "prod"])data.version(modelName, dataName, version)
Returns a specific version of a data item, or null.
data.version("scanner", "result", 1)
data.version("scanner", "result", 3).attributes.status
# Null-safe:
data.version("scanner", "result", 1).?attributes.?status.orValue("unknown")data.version(modelName, dataName, varyValues, version)
Returns a specific version for a vary dimension.
data.version("scanner", "result", ["dev"], 2)data.listVersions(modelName, dataName)
Returns all version numbers for a data item, in ascending order.
data.listVersions("scanner", "result")With vary values:
data.listVersions("scanner", "result", ["prod"])data.findByTag(tagKey, tagValue)
Returns all data records (latest version) matching a tag.
data.findByTag("env", "production")Note
In workflow step task.inputs, data.findByTag() is deferred as a
step-output dependency. The expression is left unevaluated during workflow
evaluation and resolved at step execution time, when upstream step outputs are
available. This also applies when using --last-evaluated.
data.findBySpec(modelName, specName)
Returns all data records for a model matching a given output spec name. Inside a workflow run, results are scoped to data produced during that run.
data.findBySpec("scanner", "result")data.query(predicate)
Runs a CEL predicate against all data artifacts. Returns matching records.
data.query("modelName == \"scanner\" && size > 1000")data.query(predicate, select)
Runs a predicate with a projection expression that transforms each matching row into the specified shape.
data.query("modelName == \"scanner\"", "attributes.status")
data.query("size > 1000", "{\"name\": name, \"size\": size}")Note
In workflow step task.inputs, data.query() is deferred as a
step-output dependency. The expression is left unevaluated during workflow
evaluation and resolved at step execution time, when upstream step outputs are
available. This also applies when using --last-evaluated.
Cross-Namespace Queries
When a repository has a namespace assigned, data functions support
cross-namespace access through a prefix syntax on the modelName argument.
Namespace prefix
Prefix the model name with the namespace and a colon to read from a specific namespace:
data.latest("infra:my-vpc", "result")
data.version("compute:web-server", "result", 3)
data.findBySpec("security:scanner", "findings")Wildcard prefix
Use * to search all namespaces:
data.latest("*:my-vpc", "result")If the model name exists in more than one namespace, the wildcard lookup fails with an ambiguity error.
Default scoping
Without a prefix, point lookups (data.latest, data.version,
data.listVersions, data.findByTag, data.findBySpec) scope to the current
repository's namespace.
data.query() returns results from all namespaces by default.
ns query field
The ns field is available in data.query() predicates and projections to
filter or project by namespace:
data.query("ns == \"infra\" && modelName == \"my-vpc\"")
data.query("ns == \"infra\"", "{\"model\": modelName, \"ns\": ns}")The field name is ns, not namespace. CEL reserves namespace as a keyword.
File Functions
file.contents(modelName, specName)
Returns the text content of a file data item, or null.
file.contents("config-gen", "output")Data Query Predicates
The data.query() function and swamp data query command evaluate CEL
predicates against data artifacts. Each row exposes the DataRecord fields as
top-level variables.
Available Query Fields
id, name, version, createdAt, attributes, tags, modelName,
modelType, specName, dataType, contentType, lifetime, ownerType,
streaming, size, content, ns
When using data.query(), the attributes and content fields are lazy-loaded
only when referenced in the predicate or projection. Other data functions
(data.latest(), data.version(), etc.) load these fields eagerly.
Projections
A projection transforms each matching row into the shape specified by the
expression. The second argument to data.query() (or --select flag on the
CLI) is evaluated against each matching row, and the result replaces the full
DataRecord in the output.
data.query("modelName == \"hello-world\"", "attributes.stdout")
data.query("size > 1000", "{\"name\": name, \"size\": size}")Types
Primitives
| Type | CEL Literal | Description |
|---|---|---|
int |
42, 0xFF |
64-bit signed integer |
uint |
42u |
64-bit unsigned integer |
double |
3.14, 3e2 |
64-bit floating-point |
string |
"hello", 'hello' |
Unicode text |
bool |
true, false |
Boolean |
bytes |
b"data" |
Binary data |
null |
null |
Null value |
Aggregates
| Type | Literal | Description |
|---|---|---|
list<T> |
[1, 2, 3] |
Ordered collection |
map<K, V> |
{"key": value} |
Key-value mapping |
Strings
Triple-quoted strings span multiple lines. Raw strings (r"...") disable escape
processing.
"""multi
line"""
r"no\escape"Operators
Arithmetic
| Operator | Types | Result |
|---|---|---|
+ |
int, double, string, list, bytes |
Addition, concatenation |
- |
int, double |
Subtraction |
* |
int, double |
Multiplication |
/ |
int, double |
Division |
% |
int, uint |
Remainder |
- |
int, double (unary) |
Negation |
Arithmetic operators work across int and double without explicit conversion.
This covers +, -, *, /, and %.
Comparison
| Operator | Description |
|---|---|
== |
Equal |
!= |
Not equal |
< |
Less than |
<= |
Less than or equal |
> |
Greater than |
>= |
Greater than or equal |
Logical
| Operator | Description |
|---|---|
&& |
Logical AND (short-circuit) |
|| |
Logical OR (short-circuit) |
! |
Logical NOT |
Membership
| Expression | Description |
|---|---|
value in list |
true if list contains value |
key in map |
true if map has key |
For string containment, use the .contains() method:
"hello world".contains("world") // trueAccess
| Syntax | Description |
|---|---|
obj.field |
Field access (throws if obj is null) |
obj.?field |
Optional select (null if obj is null) |
obj[key] |
Index / bracket access |
cond ? a : b |
Conditional (ternary) |
Optional Select (.?)
The .? operator provides null-safe field access. When the receiver is null,
.? evaluates to null instead of throwing an error. Chain multiple .?
operators to traverse deeply nested structures that may contain null at any
level.
data.latest("factory", "code-review").?attributes.?findingsRegular . access throws when the receiver is null — this is intentional, as
it catches real bugs where data is expected to exist. Use .? only when the
data genuinely might not exist (e.g., prior-cycle artifacts in rework loops,
optional cross-model references).
.orValue() — Inline Defaults
Pair .? with .orValue() to provide a fallback value when the chain resolves
to null:
data.latest("factory", "code-review").?attributes.?findings.orValue([])
data.version("scanner", "result", 1).?attributes.?status.orValue("unknown")When to Use .? vs .
| Scenario | Use | Why |
|---|---|---|
| Step depends on another step's output | . |
Data must exist — a missing value is a real bug |
| Prior-cycle artifact that may not exist yet | .? |
First iteration has no prior data |
| Optional cross-model reference | .? |
The referenced model may not have run |
| Required model output in same workflow | . |
Throw early if the contract is violated |
Precedence (Highest to Lowest)
(),.,.?,[]-- grouping, field access, optional select, indexing-(unary),!-- negation*,/,%-- multiplicative+,--- additive==,!=,<,>,<=,>=,in-- comparison&&-- logical AND||-- logical OR? :-- conditional
Built-in Functions
Type Conversion
| Function | Description |
|---|---|
int(x) |
Convert to int from double, string |
uint(x) |
Convert to uint |
double(x) |
Convert to double from int, string |
string(x) |
Convert to string from int, double, bool, bytes |
bool(x) |
Parse boolean from string |
bytes(x) |
UTF-8 encode string |
type(x) |
Returns type of value |
dyn(x) |
Wrap as dynamic type |
Size
size("hello") // 5
size([1, 2, 3]) // 3
size({"a": 1}) // 1
"hello".size() // 5 (method form)String Methods
| Method | Returns | Description |
|---|---|---|
s.contains(sub) |
bool |
Substring check |
s.startsWith(prefix) |
bool |
Prefix check |
s.endsWith(suffix) |
bool |
Suffix check |
s.matches(regex) |
bool |
Regex match |
s.lowerAscii() |
string |
Lowercase |
s.upperAscii() |
string |
Uppercase |
s.trim() |
string |
Trim whitespace |
s.indexOf(sub) |
int |
First occurrence (-1 if absent) |
s.indexOf(sub, offset) |
int |
From offset |
s.lastIndexOf(sub) |
int |
Last occurrence |
s.lastIndexOf(sub, offset) |
int |
Backward from offset |
s.substring(start) |
string |
From start to end |
s.substring(start, end) |
string |
From start to end index |
s.split(sep) |
list |
Split by separator |
s.split(sep, limit) |
list |
Split with max parts |
List/String Join
["a", "b", "c"].join() // "abc"
["a", "b", "c"].join(", ") // "a, b, c"Timestamp and Duration
Constructors
| Function | Returns | Description |
|---|---|---|
timestamp(string) |
Timestamp |
Parse ISO 8601 string |
duration(string) |
Duration |
Parse Go-style duration (e.g., "1h30m", "300ms") |
Valid duration units: ns, us, ms, s, m, h.
Timestamp Methods
All accept an optional timezone string (e.g., "America/New_York"). Default is
UTC.
| Method | Returns | Description |
|---|---|---|
t.getFullYear() |
int |
Four-digit year |
t.getMonth() |
int |
Month (0-based, 0 = January) |
t.getDate() |
int |
Day of month (1-based) |
t.getDayOfWeek() |
int |
Day of week (0 = Sunday) |
t.getDayOfYear() |
int |
Day of year (0-based) |
t.getHours() |
int |
Hour (0-23) |
t.getMinutes() |
int |
Minute (0-59) |
t.getSeconds() |
int |
Second (0-59) |
t.getMilliseconds() |
int |
Millisecond (0-999) |
timestamp("2024-01-15T10:30:00Z").getHours("America/New_York") // 5Duration Methods
| Method | Returns | Description |
|---|---|---|
d.getHours() |
int |
Total hours |
d.getMinutes() |
int |
Total minutes |
d.getSeconds() |
int |
Total seconds |
d.getMilliseconds() |
int |
Total milliseconds |
Arithmetic
timestamp("2024-01-15T10:00:00Z") + duration("1h") // Timestamp
timestamp("2024-01-16T00:00:00Z") - timestamp("2024-01-15T00:00:00Z") // Duration
duration("1h") + duration("30m") // DurationMacros
has()
Tests whether a field exists without throwing on missing properties.
has(model.scanner.resource)
has(attributes.optional_field)all()
True if all elements satisfy the predicate. True for empty lists.
[1, 2, 3].all(x, x > 0) // trueexists()
True if any element satisfies the predicate.
[1, 2, 3].exists(x, x > 2) // trueexists_one()
True if exactly one element satisfies the predicate.
[1, 2, 3].exists_one(x, x > 2) // truefilter()
Returns elements that satisfy the predicate.
[1, 2, 3, 4].filter(x, x > 2) // [3, 4]map()
Transforms each element. With three arguments, filters then transforms.
[1, 2, 3].map(x, x * 2) // [2, 4, 6]
[1, 2, 3, 4].map(x, x > 2, x * 10) // [30, 40]