Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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

RootContents
triggerThe 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.failedInside 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
outputInside read.assert: / query.assert: and a read/query trigger's condition: โ€” the value being gated (alias result); query NUMERIC results compare as exact numbers
simInside assert_sim: only โ€” sim.ok, sim.gas_used, sim.return_data
item / item_indexInside a foreach: iteration โ€” the current element (or batch) and its index
matrixInside a matrix combination run โ€” matrix.<axis>
stateThe Postgres-backed key/value store โ€” see state_set. Guard first reads with is defined
previous_runThe workflow's last succeeded run: .id, .status, .steps.<id>.output/.status โ€” undefined on the first run, see previous_run
reorgInside 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
constantsYour constants:
secretsYour secrets: โ€” log-redacted
lists.<name>Watchlist members (seed โˆช runtime โˆช source), for the in operator
runrun.id, run.workflow โ€” plus run.status inside finally: steps
errorInside retry_if: only โ€” error.kind, error.message
inputsManual 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:

UseNot
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

FunctionDescription
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:

  • uint256 values 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 use mul_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 out

Addresses 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 seed

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