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

wait_for

Park the run durably until the first of several conditions. This is the cross-chain saga construct: send on chain A, wait for the matching event on chain B, continue (or compensate on timeout).

rflow_version: 1
name: bridge-saga
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
  - name: base
    chain_id: 8453
    rpc: ${BASE_RPC}
 
# DEV ONLY raw mnemonic - swap in a production signer before real funds
signer:
  raw:
    mnemonic: ${RAW_DANGEROUS_MNEMONIC}
 
relayers:
  payout:
    networks: [base]
 
contracts:
  Bridge:
    abi: ./abis/bridge.json
    addresses:
      ethereum: "0x0000000000000000000000000000000000000000" # replace
  BridgeReceiver:
    abi: ./abis/bridge-receiver.json
    addresses:
      base: "0x0000000000000000000000000000000000000000" # replace
 
notifications:
  channels:
    ops:
      console: {}
 
workflows:
  bridge-and-sweep:
    trigger:
      event: { contract: Bridge, name: DepositInitiated, network: ethereum, confirmations: 20 }
    steps:
      - id: send-l2
        send_transaction: { network: base, relayer: payout, ... }
      - id: await-arrival
        wait_for: 
          any: 
            - event: 
                contract: BridgeReceiver
                network: base
                name: DepositFinalized
                where: "${{ trigger.args.depositId == 12345 }}"
            - timeout: 30m
          on_timeout: { goto: { step: compensate } }   # or: fail | continue 
      - id: sweep
        send_transaction: { ... }
      - id: compensate
        if: "${{ steps.await-arrival.output.timed_out is defined }}"
        notify: { channel: ops, message: "bridge deposit did not arrive in 30m" }
FieldRequiredDescription
any✅Conditions raced against each other (non-empty, validated): event: {...} and/or timeout: <duration>
on_timeoutfailWhat a winning timeout: does: fail | continue | goto: { step: <id> }

The awaited event

any: - event: takes contract (registry name), network, name, an optional where: and an optional confirmations:. Inside the wait's where:, trigger.* refers to the awaited event, not the run's original trigger; no other root (steps.*, state.*, constants.*, item, ...) exists at match time, so bind concrete values via the surrounding YAML. rflow validate enforces this: a where: referencing anything besides trigger.* can never match, so it is a hard error when the wait has no timeout: condition (the run would park forever) and an Advice when it has one.

A match settles the step succeeded with the decoded event as steps.<id>.output; on an on_timeout: continue/goto, the output is {"timed_out": true}.

confirmations: — how deep before the wait settles

ValueBehaviour
absent (default)A confirmed delivery settles the wait immediately; a head delivery only ARMS it: the parked run then settles once the chain reaches the network's confirmations depth past the event
0Settle the moment the event is first seen (head). rflow validate prints a reorg Advice when the workflow sends transactions
NThe match is journaled (armed) and the wait settles once the event is N blocks deep (verified against the chain, like a send's confirmed(N))
finalizedArmed until the chain's finalized block reaches the event (rflow doctor probes finality support; validate advises on chains rflow has no guidance for)

Internal wait subscriptions

Every wait_for: event condition gets its own indexer subscription: at boot, the (contract, event, network) triples across all workflows (main steps, foreach bodies, finally: blocks) are collected and any triple no declared event trigger already feeds the matcher for gets subscribed. Only a trigger's confirmed phase feeds the matcher (run_on: confirmed, the default, or both); a head-only run_on: unconfirmed trigger covers nothing, and a confirmations: 0 wait always gets its own head-indexing subscription. Wait-only subscriptions route decoded events only to the matcher (they never create runs) and start at the latest block: they catch future events while a run is parked, never backfill. A contract declared on exactly one network resolves for waits on any network: the subscription indexes that single address on the awaited chain (validate and the engine apply the same fallback).

rflow validate rejects impossible waits before runtime: an awaited event missing from the contract's ABI (with a did-you-mean hint), an unknown contract/network, a wait whose contract has no address on the awaited network and no timeout: condition (with a timeout it degrades to an Advice), and a where: that references anything besides trigger.* (same error/Advice split).

Durability

Every event condition is persisted (rflow.run_event_waits, including its confirmations requirement and any armed match) and the timeout deadline is journaled before the run parks (waiting_event). All survive restarts, and a parked saga costs nothing while it waits. The goto: decision is part of the settle output, so a crash right after a timeout-goto resumes at the same named step. A goto that re-enters a foreach: step starts a fresh pass: the collection is re-evaluated and every iteration re-runs with fresh attempt numbers (fresh send idempotency keys), while a plain retry of a failed fan-out still reuses the pass's journaled plan and never re-runs its settled iterations.

Placement

wait_for also runs inside foreach iterations and finally: blocks. In a foreach, each item parks its own <id>[<i>] journal row and wait rows, resumes independently and settles exactly once (a parked item holds its max_parallel slot). In a finally: block, the step parks and resumes but the run's terminal status never changes. Two caveats: on_timeout: goto is rejected inside foreach/finally (no step sequence to jump to), and the match-time where: only sees trigger.*, so foreach items share their conditions: one matching event settles every parked item it matches.

Reorg safety

Dry-run behaviour

In rflow replay / rflow test sessions, wait_for steps settle immediately with a would_wait_for output: a historical event must never resume a live parked saga, and a rehearsal must terminate deterministically. --with-waits pins deterministic per-step outcomes (matched event or timeout) for saga rehearsals; fixtures key on the config step id, so foreach items and finally waits resolve through their base id.