Expressions
One ${{ ... }} language everywhere: conditions (where / if / assert /
retry_if / condition / recheck) and values (args, messages, urls, bodies)
all render through the same engine. It is GitHub-Actions-shaped on the outside and
minijinja-powered underneath, tuned for
EVM work: strict U256 math, case-insensitive addresses, and strict-undefined
behavior (a typo'd key is an error, never a silent false).
where: "${{ trigger.args.sender in lists.watched_traders and trigger.args.amount0 > wei('1', 18) }}"
message: "mirrored ${{ format_units(trigger.args.value, 6) }} USDC โ ${{ steps.mirror.tx.hash }}"Contexts
| Root | Contents |
|---|---|
trigger | The firing occurrence. Events: trigger.args.<name>, tx_hash, log_index, block_number, block_hash, address, network, chain_id, topics, phase. Other triggers document their own shapes: cron ยท webhook ยท read ยท query ยท block |
trigger.failed | Inside a workflow_error-triggered workflow: .workflow, .run_id, .trigger_key, .status, .step_id, .attempt, .error.kind, .error.message of the failed source run |
events.<id> | Inside a composite (all/any)-triggered workflow โ each correlated event by its list id: events.<id>.args.<name>, .tx_hash, .block_number, .address, โฆ A composite where: also sees events.<earlier-id>.* at match time |
steps.<id> | Any settled prior step: .output, .status, .error, .tx (.tx.hash), .tx_id (stable across gas bumps), .receipt |
output | Inside read.assert: / query.assert: and a read/query trigger's condition: โ the value being gated (alias result); query NUMERIC results compare as exact numbers |
sim | Inside assert_sim: only โ sim.ok, sim.gas_used, sim.return_data |
item / item_index | Inside a foreach: iteration โ the current element (or batch) and its index |
matrix | Inside a matrix combination run โ matrix.<axis> |
state | The Postgres-backed key/value store โ see state_set. Guard first reads with is defined |
previous_run | The workflow's last succeeded run: .id, .status, .steps.<id>.output/.status โ undefined on the first run, see previous_run |
reorg | Inside on_reorg: steps only โ tx_hash, block_number, network, run_id, workflow, fork_block, detection_block |
contracts.<name> | .address of registry contracts (on the relevant network) |
relayers.<name> | .address of named relayers |
constants | Your constants: |
secrets | Your secrets: โ log-redacted |
lists.<name> | Watchlist members (seed โช runtime โช source), for the in operator |
run | run.id, run.workflow โ plus run.status inside finally: steps |
error | Inside retry_if: only โ error.kind, error.message |
inputs | Manual trigger inputs โ rflow trigger <wf> --input k=v exposes each pair as inputs.<k> |
rflow validate knows which roots are legal where โ a reorg.* reference in a
normal step, or sim.* outside assert_sim:, is a validation error, not a
runtime surprise.
Operators
Boolean logic uses words, not symbols:
| Use | Not |
|---|---|
and | && |
or | || |
not | ! |
Everything else is as expected: == != < <= > >=, arithmetic + - * / %,
membership x in list, string concat ~, definedness tests
(x is defined / x is not defined โ and/or short-circuit, so guards
work), and inline conditionals in Python style:
args: ["${{ steps.gate.output if steps.gate.output < trigger.args.amount0 else trigger.args.amount0 }}"](there is no C-style cond ? a : b ternary).
Functions
| Function | Description |
|---|---|
wei(v, decimals) | Parse a decimal amount into its integer representation: wei('1.5', 18) โ 1500000000000000000. Alias of parse_units |
parse_units(v, decimals) | Same contract as wei |
format_units(v, decimals) | The inverse โ integer โ decimal string, for messages |
mul_div(a, b, d) | a * b / d computed at 512-bit width โ scale amounts without overflow: mul_div(amount, constants.scale_bps, 10000) |
min(a, b, ...) / max(a, b, ...) | Across native ints and U256s |
checksum(addr) | EIP-55 checksummed form of an address |
lower(s) | Lowercase a string |
keccak256(s) | Keccak-256 hash, 0x-hex |
abi_encode(sig, args...) | ABI-encode values, 0x-hex |
now() | Current unix timestamp (seconds) |
duration('5m') | Parse a duration string to seconds |
from_json(s) | Parse a JSON string into a value |
All functions are strict: bad inputs raise template errors that route to failure handling โ nothing degrades to floats or silent zeros.
U256 math is strict
Token amounts never touch floats:
uint256values that fit in a native integer stay native โ every operator works, and the math is checked (overflow raises, never wraps)- larger values stay exact as U256 objects โ comparisons (
> < ==) work directly; arithmetic on them is a loud error telling you to usemul_div/wei(which compute wide) - decimal strings from event payloads coerce transparently in comparisons against
wei(...)results
where: "${{ trigger.args.value > wei('100000', 6) }}" # exact
args: ["${{ mul_div(trigger.args.value, 9950, 10000) }}"] # 0.5% haircut, no overflow
message: "${{ format_units(trigger.args.value, 6) }} USDC" # human-readable outAddresses are case-insensitive
Every string in the context that looks like an address (0x + 40 hex) is normalized,
so equality and list membership behave regardless of casing:
where: "${{ trigger.args.to == relayers.payout.address }}" # just works
where: "${{ trigger.args.sender in lists.watched_traders }}" # any casing in the seedFor literals you write yourself, either casing works when compared against context
values; use lower('0xAbC...') or checksum(...) if you need a canonical form in
output.
Conditions are strictly boolean
A where / if / assert / retry_if must evaluate to a real boolean. A non-bool
result, an unknown key, or any evaluation error is a typed error routed to failure
handling โ never a silent false. You find out about typos on the first event,
not never.
rflow validate additionally checks expressions statically: unknown context roots
and references to steps/contracts/lists that don't exist are caught before boot.
Values keep their types
When a field is exactly one expression (args: ["${{ trigger.args.value }}"]), the
value keeps its type โ numbers stay numbers, U256s stay exact. Mixed text
("amount: ${{ x }}") renders to a string. Output is single-pass and never
re-parsed, so event data can't inject expressions.