Skip to content

Variables and expressions

All dynamic values in a duckflux workflow — conditions, guard clauses, input mappings, payload values — are written as CEL expressions (Common Expression Language). CEL is a simple, safe, and fast expression language developed by Google and used in production systems like Kubernetes, Firebase, and Envoy.


Expressions appear in any field that needs to reference runtime data:

FieldExample
if conditioncondition: tests.status == "success" && lint.status == "success"
when guardwhen: reviewer.output.approved == true
loop exit conditionuntil: reviewer.output.approved == true
input mappingtask: workflow.inputs.taskDescription
http url, headers, bodyurl: '"https://api.example.com/users/" + workflow.inputs.userId'
emit payloadpayload: coder.output
wait match conditionmatch: event.requestId == submitRequest.output.id
cwd pathcwd: workflow.inputs.packagePath

The following variables are available in any expression within a workflow.

VariableTypeDescription
workflow.idstringWorkflow definition identifier.
workflow.namestringHuman-readable name.
workflow.versionstringDefinition version.
workflow.inputsmapInput parameters as defined in the inputs field.
workflow.outputstring or mapWorkflow output as defined in the output field.
flow:
- as: clone
type: exec
run: git clone
input:
repo: workflow.inputs.repoUrl
branch: workflow.inputs.branch
VariableTypeDescription
execution.idstringUnique identifier for this execution.
execution.numberintSequential execution number.
execution.startedAttimestampExecution start time.
execution.statusstringCurrent status: running, success, failure.
execution.contextmapRead/write shared data scratchpad.
execution.cwdstringResolved base working directory.

execution.context is the only writable shared state in a workflow — useful for passing data across iterations in a loop or between branches:

flow:
- loop:
max: 5
steps:
- coder
- reviewer:
when: execution.context.lastScore < 8

Within a participant’s context, input and output refer to that participant’s own I/O — not the workflow’s.

VariableDescription
inputThe resolved input received by the current participant (read-only).
outputThe output produced by the current participant (write-only, set by the runtime).
participants:
notify:
type: http
url: https://hooks.example.com/done
method: POST
body: input # the chained input received by this participant

These are distinct from workflow.inputs (the workflow’s declared input parameters) and workflow.output (the workflow’s final result).

Read-only. Injected by the runtime. Never defined in YAML.

participants:
fetchData:
type: http
url: https://api.example.com/data
headers:
Authorization: '"Bearer " + env.API_KEY'

Every named participant (reusable or named inline) is accessible by its name after it executes:

VariableTypeDescription
<step>.outputstring or mapStep output. Auto-parsed as JSON if valid.
<step>.statusstringsuccess, failure, or skipped.
<step>.startedAttimestampStep start time.
<step>.finishedAttimestampStep end time.
<step>.durationdurationExecution duration.
<step>.retriesintNumber of execution attempts.
<step>.errorstringError message (when status == "failure").
<step>.cwdstringEffective working directory (exec only).
flow:
- reviewer
- deploy:
when: reviewer.output.approved == true && reviewer.output.score >= 8
- if:
condition: deploy.status == "failure"
then:
- rollback

Available only inside loop blocks. Renamed via the as field on the loop.

VariableTypeDescription
loop.indexint0-based iteration index.
loop.iterationint1-based iteration number.
loop.firstbooltrue on the first iteration.
loop.lastbooltrue on the last iteration (only when max is defined).
flow:
- loop:
as: round
max: 5
steps:
- coder
- reviewer:
when: round.index > 0 # skip reviewer on the first iteration

Available only inside wait blocks when waiting for an event. Contains the received event payload as a map.

flow:
- submitRequest
- wait:
event: "approval.response"
match: event.requestId == submitRequest.output.id && event.approved == true
timeout: 24h

Available anywhere. Returns the current timestamp at evaluation time.

flow:
- wait:
until: now >= timestamp("2026-06-01T09:00:00Z")
poll: 1m
timeout: 48h

workflow.* Workflow definition metadata
workflow.inputs.* Workflow input parameters
workflow.output Workflow final output
execution.* Current run metadata
execution.context.* Shared read/write scratchpad
input Current participant's input (chain + explicit, merged)
output Current participant's output (write-only)
env.* Environment variables (read-only)
<step>.* Named participant result
loop.* Loop iteration context (or renamed via 'as')
event Event payload (inside wait blocks)
now Current timestamp

CEL supports standard comparison and logic operators, string and collection operations, and type conversions.

reviewer.output.approved == true
tests.status != "failure"
reviewer.output.score >= 8 && lint.status == "success"
deploy.status == "success" || deploy.status == "skipped"
workflow.inputs.branch.startsWith("feat/")
workflow.inputs.env == "production"
"Bearer " + env.API_TOKEN
coder.output.message.contains("error")
reviewer.output.tags.size() > 0
"admin" in reviewer.output.roles
workflow.inputs.targets.filter(t, t.startsWith("prod"))
int(reviewer.output.score)
string(execution.number)
timestamp(workflow.inputs.scheduledAt)
now >= timestamp("2026-06-01T09:00:00Z")
execution.startedAt + duration("1h")
deploy.finishedAt - deploy.startedAt

A conforming runtime supports the full CEL standard library:

CategoryFunctions
Stringscontains, startsWith, endsWith, matches (RE2), size, lowerAscii, upperAscii, replace, split, join
Lists and mapssize, in, + (concatenation), [] (index/key access)
Macroshas, all, exists, exists_one, filter, map
Type conversionsint(), uint(), double(), string(), bool(), bytes(), timestamp(), duration()
TimestampsArithmetic, comparisons, component access

- deploy:
when: build.status == "success"
# If reviewer outputs: {"approved": true, "score": 9}
- when: reviewer.output.approved == true && reviewer.output.score >= 7
participants:
fetchUser:
type: http
url: '"https://api.example.com/users/" + workflow.inputs.userId'
method: GET
participants:
callApi:
type: http
url: https://api.example.com/data
headers:
Authorization: '"Bearer " + env.API_SECRET'

Gate a step on iteration index inside a loop

Section titled “Gate a step on iteration index inside a loop”
flow:
- loop:
as: attempt
max: 3
steps:
- coder
- reviewer:
when: attempt.index > 0
flow:
- wait:
until: now >= timestamp("2026-06-01T09:00:00Z")
poll: 5m
timeout: 72h