Skip to main content

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.vpcId

inputs

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.HOME

Caution

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.tags

The 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")   // true

Access

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.?findings

Regular . 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)

  1. (), ., .?, [] -- grouping, field access, optional select, indexing
  2. - (unary), ! -- negation
  3. *, /, % -- multiplicative
  4. +, - -- additive
  5. ==, !=, <, >, <=, >=, in -- comparison
  6. && -- logical AND
  7. || -- logical OR
  8. ? : -- 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")  // 5

Duration 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")   // Duration

Macros

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)           // true

exists()

True if any element satisfies the predicate.

[1, 2, 3].exists(x, x > 2)        // true

exists_one()

True if exactly one element satisfies the predicate.

[1, 2, 3].exists_one(x, x > 2)    // true

filter()

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]