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, steps, webhook* |
Workflow step guard |
model, self, inputs, env, vault, data, file, run, steps, webhook* |
Workflow assert expr |
model, self, inputs, env, vault, data, file, run, steps, webhook* |
Workflow assert message |
model, self, inputs, env, vault, data, file, run, steps, webhook* |
Workflow step forEach.in |
model, self, inputs, env, vault, data, file, run, steps, webhook* |
| Workflow step task target** | model, self, inputs, env, vault, data, file, run, steps, webhook* |
| Data output overrides | model, self, env, vault, data, file |
| Datastore and repo config | env, vault only (see Datastore Configuration) |
swamp data query predicate |
DataRecord fields only (see Query Fields) |
swamp data query --select |
DataRecord fields only |
workflow history search |
Run metadata fields only (see Workflow History Filtering) |
* webhook is only available when the workflow was triggered by a
webhook.
** The task target is modelIdOrName, modelName or workflowIdOrName — see
when the target resolves.
Note
Grant conditions (--when) and
workflow history filters (--filter) are
subject to additional complexity limits beyond the standard CEL expression
context. See
Condition cost limits
for thresholds.
Evaluated and Inert Sources
Swamp evaluates an expression only if its text was written in an evaluated source. Expression text that arrives from any other source is inert: it stays literal and Swamp logs a warning naming the path.
| Source | Expressions |
|---|---|
| Model definition files | Evaluated |
| Workflow files | Evaluated |
swamp model method run --input, --input-file, --stdin |
Evaluated |
swamp workflow run and resume --input, --input-file, --stdin |
Inert |
| Webhook and trigger payloads | Inert (trigger.inputs expressions are evaluated) |
Data content read with data.latest() and related functions |
Inert |
forEach items |
Inert |
Because model method run inputs are evaluated, a ${{ vault.get(...) }} or
${{ env.* }} in text passed that way resolves with the operator's credentials
and reaches the method in plaintext. Pass untrusted text through a workflow
input or model data instead. See
Model Method Run Inputs Are Evaluated.
Grant Condition Fields
Grant conditions (--when) are evaluated against resource fields at
access-check time. The following fields are available:
| Field | Type | Description |
|---|---|---|
tags |
map<string, string> |
Resource tags |
methodName |
string |
Model method being executed (model resources only) |
tags.env == "staging"
methodName == "deploy"
methodName == "read" || methodName == "list"Resource fields can be passed to
swamp access check
using the --field flag for testing condition evaluation.
Other Services' Template Syntax
Only ${{ ... }} is Swamp's expression syntax, and only when the expression
reads a Swamp namespace: model, self, inputs, env, vault, data,
file, workers, and inside workflows run, steps, workflow and
webhook. Other template text in an argument is passed to the method unchanged.
Passed through
These forms reach the method as literal text:
{{ ... }}and${ ... }whose names are not Swamp namespaces, such as Datadog{{host.name}}, Go templates{{ .Values.x }}, and shell${HOME}${{ ... }}whose names are not Swamp namespaces, such as GitHub Actions${{ github.sha }}, including in global arguments
swamp model validate reports them in a warning that never fails validation:
⚠ Template syntax passed through
methods.execute.arguments.env.HOST passes {{host.name}}
methods.execute.arguments.env.SHA passes ${{ github.sha }}
→ This text is not a swamp expression and is passed to the method unchanged. If you meant a swamp expression, write ${{ ... }} and reference one of swamp's namespaces (model, self, inputs, env, vault, data). If it is another service's template syntax, the model type can declare the field with .meta({ foreignTemplate: true }) to silence this warning.A mistyped namespace, such as ${{ modle.x }} or ${{ input.region }}, is not
a Swamp namespace either. It produces the same warning and passes through at run
time, so read validation warnings.
Reported as errors
Validation fails, and in a workflow the step fails, when the text reads as a broken Swamp expression:
- A dropped
$or brace on a Swamp namespace, such as{{self.name}}or${self.private_ip} - A missing
model.prefix, when the first name is one of the repository's models, such as${{ my-vpc.VpcId }} ${{ ... }}text that CEL cannot parse
Each failed expression gets its own arrow, with its details indented under it. A fix shared by every error of the same form prints once, after the list:
✗ Expression paths
→ {{self.name}}
Expression uses {{...}} instead of ${{...}} at "methods.execute.arguments.env.NAME"
Add "$" prefix: ${{...}}.
If this is another service's template syntax, pass it through with literal(), e.g. ${{ literal('{{name}}') }}, or have the model type declare the field with .meta({ foreignTemplate: true }).With --json, the failed Expression paths item carries an expressionErrors
array alongside its error text, one entry per failed expression:
| Field | Type | Description |
|---|---|---|
expression |
string | The expression text that failed |
error |
string | What is wrong, and the field path where it was found |
suggestion |
string | How to fix this expression (optional) |
remedy |
string | A fix shared by every error of the same form (optional) |
availableKeys |
string[] | Keys that exist where a path lookup failed (optional) |
Colliding names
Some vendor syntax uses a name that is also a Swamp namespace. Swamp treats this text as its own and reports it as an error:
- Datadog
{{env.name}} - Argo Workflows
{{workflow.name}}and{{steps.gen.outputs.result}} - Terraform
${data.aws_ami.ubuntu.id}and${self.private_ip} - GitHub Actions
${{ steps.x }}inside a workflow, and${{ inputs.version }}whereversionis a declared input
To pass such text through, wrap it in literal(). The function returns its
string argument unchanged, and Swamp does not read expressions inside a CEL
string:
PRIVATE_IP: "${{ literal('${self.private_ip}') }}"
DD_TAG: "${{ literal('{{env.name}}') }}"Building the braces with CEL string concatenation, such as
${{ "$" + "{self.private_ip}" }}, evaluates to the same text. A model type can
instead declare the field as another service's template text with
.meta({ foreignTemplate: true }) on its Zod schema. Validation then skips that
field for this check, but ${{ ... }} expressions in it are still evaluated.
command/shell declares its run argument this way.
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.
forEach also adds self._index, the zero-based iteration index (int): list
elements in order, map entries in key order. An iteration variable named
_index shadows it. Concatenate it with string(self._index), as CEL does not
add an int to a string. See
ForEach.
model
A map of all models in the repository, indexed by name and UUID. Models of Swamp's access-control types are left out.
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>
Warning
Deprecated. Use data.latest(modelName, dataName).path instead — see
path.
| Field | Type | Description |
|---|---|---|
path |
string |
File path on disk |
size |
int |
File size in bytes |
contentType |
string |
MIME type |
The replacement reads the path from the data record:
methods:
execute:
arguments:
run: "wc -l ${{ data.latest('r-lab', 'table').path }}"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) |
model.method(modelName, methodName) / model.method(modelName, methodName, inputs)
Runs a model method and returns the parsed data output content. For
command/shell models, the return value has the shape
{exitCode, stdout, stderr, ...}.
model.method("dns-checker", "verify")
model.method("dns-checker", "verify", {"domain": "app.example.com"})
model.method("infra", "check-status", {"name": inputs.instanceName}).exitCode == 0Available in workflow step guard, assert expr, assert message
interpolation (${{ }}), and task.inputs.
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.HOMEenv expressions resolve at method-run time, not at model evaluate or
model get — those commands show the literal ${{ env.* }} text. This makes
env the right tool for host-portable paths in committed globalArguments:
globalArguments:
repo_root: "${{ env.HOME }}/src/my-extensions"A missing variable fails with the variable name and argument path:
error: No such key: EXT_REPO_ROOT
path: globalArguments.repo_root
envVar: EXT_REPO_ROOTCaution
Values accessed via env are persisted to .swamp/data/ on disk. Use
vault.get() for sensitive values.
Over swamp serve, env is the server's environment. Serve allows env only
to principals with write on the model; see
Expressions and grants.
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.
steps
Metadata about completed steps in the current workflow run. Only available in
workflow step expressions (task.inputs, the task target, guard,
forEach.in) — downstream steps can reference the status and outputs of earlier
steps. An expression that reads steps resolves when its step runs, never at
run start.
| Field | Type | Description |
|---|---|---|
steps.<name>.status |
string |
Terminal status of the step |
steps.<name>.outputs |
map<string, dyn> |
Merged attributes of the step's JSON resources; see below |
steps.<name>.outputs.<field> |
dyn |
Individual output field |
These are the only fields. The step context does not carry timestamps.
steps.provision.status == "succeeded"
steps.provision.outputs.instanceIdoutputs depends on the step's task type:
model_method— the flat merge of the attributes of every JSON resource the step wrote. When two resources share an attribute name, the one written later wins. For keyed access to one resource instance, usemodel.<model>.resource.<specName>.<instanceName>.attributes.workflow— the child run's outputs, keyed by child step name:steps.<parent-step>.outputs.<child-step>.<field>. Only the child's succeededmodel_methodsteps contribute. Workflow steps inside the child are not followed, so grandchild outputs are not available.- Non-JSON resources and file outputs contribute nothing. A step with no
contributing attributes has no
outputs.
Step outputs are not stored on the run record. During a live run they come from
the step's in-memory result. On resume, and in swamp workflow history get and
swamp workflow history outputs, they are read back from the datastore, so data
with an ephemeral lifetime, garbage-collected versions, or an uncached remote
datastore yield no outputs.
steps:
- name: deploy
task:
type: model_method
modelIdOrName: deployer
methodName: apply
inputs:
instanceId: "${{ steps.provision.outputs.instanceId }}"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 |
path |
string |
Local path of the stored content |
path
path is the local filesystem path of the version's stored content file. It
applies to any data type; a resource's path is its stored JSON file. It
replaces the deprecated
model.<name>.file.<specName>.<instanceName>.path.
path is an empty string:
- for data with an ephemeral lifetime
- for records from another namespace in a shared datastore
- when the file cannot be made local
- in
swamp data queryoutput and serve API responses, which always return an emptypath
On a lazy-hydration datastore, data.latest() and data.version() download the
file before returning the record. List lookups (data.findBySpec(),
data.findByTag(), data.query()) do not download; they return an empty path
for any file not already present.
The path is on the host that evaluates the expression, not a remote worker. A
run replayed with --last-evaluated reuses the path as it was first resolved.
Select the .path field rather than passing a whole record into an input:
inputs:
tablePath: ${{ data.latest('r-lab', 'table').path }}Data Functions
All data functions are methods on the data namespace. They return DataRecord
objects or arrays of them. They never return records of Swamp's
access-control types,
locally or on a server.
data.latest(modelName, dataName)
Returns the latest version of a named data item, or null. When dataName also
matches a specName shared by multiple records, the call throws with an
ambiguity error — use the specific data name or data.query() to disambiguate.
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 and the task target, 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)
Queries data artifacts matching a CEL predicate. 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 and the task target, 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]Workflow History Filtering
The --filter flag on
swamp workflow history search
accepts a CEL expression evaluated against each run's metadata. Only runs where
the expression evaluates to a truthy value are returned.
Available Fields
| Field | Type | Description |
|---|---|---|
workflowName |
string |
Name of the workflow. |
status |
string |
Run status (succeeded, failed, cancelled, …). |
startedAt |
string |
ISO 8601 timestamp (lexicographic comparison works). |
completedAt |
string |
ISO 8601 timestamp. |
duration |
double |
Run duration in milliseconds. |
inputs |
map<string,dyn> |
Input values supplied to the run. |
tags |
map<string,dyn> |
Tags on the run. |
instanceId |
string |
Instance UUID of the run. |
triggerSource |
string |
What triggered the run. |
failedStep |
string |
Name of the step that failed (if any). |
failureReason |
string |
Failure reason text (if any). |
Missing map keys (e.g. inputs.commit when no commit input was supplied)
return false rather than throwing an error.
Complexity Limits
Workflow history filter expressions are subject to the same complexity limits as grant conditions:
- 1024 character maximum
- AST depth 24
- Comprehension nesting 2
- Cost budget 500
Examples
status == "failed"
inputs.commit == "b3ff3a8a"
inputs.branch == "main"
duration > 60000
status == "succeeded" && duration < 30000