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

Composite triggers — all / any

A composite trigger fires a workflow from several on-chain events instead of one.

  • trigger.all — fire one run when every listed event has matched within a sliding correlation window (within).
  • trigger.any — fire when any listed event matches (first match).

Each matched event is exposed to the workflow steps as events.<id>.* (its decoded args, tx_hash, block_number, address, …).

all — correlate two related events

rflow_version: 1
name: deposit-then-transfer
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
 
contracts:
  Vault:
    abi: ./abis/vault.json
    network: ethereum
    address: "0x0000000000000000000000000000000000000000" # your vault
  USDC:
    abi: ./abis/erc20.json
    network: ethereum
    address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
 
notifications:
  channels:
    ops:
      telegram:
        bot_token: ${TG_BOT_TOKEN}
        chat_id: ${TG_CHAT_ID}
 
workflows:
  deposit-then-transfer:
    trigger: 
      all: 
        within: 10m           # sliding correlation window (REQUIRED for `all`) 
        events: 
          - id: deposit
            event: 
              contract: Vault
              name: Deposit
              network: ethereum
          - id: transfer
            event: 
              contract: USDC
              name: Transfer
              network: ethereum
              # cross-event correlation: a LATER event's where may reference #
              # events.<earlier-id>.args 
              where: "${{ event.args.to == events.deposit.args.user }}"
    steps:
      - notify:
          channel: ops
          message: "deposit ${{ events.deposit.args.amount }} then transfer ${{ events.transfer.tx_hash }}"

Semantics:

  • The first listed event opens a correlation; later events attach to the correlation they satisfy. In a where:, ${{ event.args.* }} is this event; events.<earlier-id>.args are the already-matched earlier events.
  • A later event is consumed by at most one correlation: the oldest open one it satisfies. One on-chain event never fans out into multiple runs. If a cross-event where: is not fully discriminating (two deposits by the same user), the oldest window wins and the others keep waiting.
  • When a correlation holds every listed event, the run is claimed exactly once: a durable dedup key over the matched set, so a re-delivered completing event never fires a second run.
  • Partial matches accumulate in a durable table (rflow.composite_matches) and are swept once their within window passes. An incomplete set never fires.

within is required for all and must be a positive duration. A composite where: may reference only event.* and events.<earlier-id>.* (events earlier in the list); any other root (constants, secrets, trigger, state, …) is rejected by validation.

Ordering assumption: only the first-listed event opens a correlation, so all assumes the starter is observed no later than the events that correlate to it. Events on different contracts (or networks) arrive via independent head callbacks, so a same-block event at a lower log index can arrive before its starter; with no correlation open it is dropped and that set never fires. List the event most likely to be seen first as the starter, or keep correlated events on the same contract (same-contract logs are delivered in on-chain order). Buffering out-of-order arrivals is intentionally out of scope.

A single-event all is just an event: trigger. rflow validate advises you to model a "react to a follow-up event" flow as an event: trigger plus a wait_for: step (the run then correlates with the full run context).

any — fire on the first of several events

rflow_version: 1
name: emergency-pager
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
 
contracts:
  Vault:
    abi: ./abis/vault.json
    network: ethereum
    address: "0x0000000000000000000000000000000000000000" # your vault
  Guardian:
    abi: ./abis/guardian.json
    network: ethereum
    address: "0x0000000000000000000000000000000000000000" # your guardian
 
notifications:
  channels:
    pager:
      pagerduty:
        routing_key: ${PAGERDUTY_ROUTING_KEY}
 
workflows:
  emergency-stop:
    trigger: 
      any: 
        events: 
          - id: paused
            event: { contract: Vault, name: Paused, network: ethereum } 
          - id: guardian
            event: { contract: Guardian, name: EmergencyStop, network: ethereum } 
    steps:
      - notify: { channel: pager, message: "emergency: ${{ events.paused.tx_hash or events.guardian.tx_hash }}" } 

any fires immediately on the first matching event (deduped by that event's log identity). within is accepted but does not gate any.

Match-time scope + reorgs

Composite events match at chain head (the same default as an event: trigger with no confirmations:). Exactly-once completion does not depend on indexing depth: it rides the durable rflow.composite_matches accumulator, the composite:{workflow}:{hash} dedup key, and the sliding within window. The reorg caveat matches a head-firing event: trigger: a reorg can revert a matched event, so a matched-but-not-completed correlation may linger until its window expires.

Addresses are case-normalized, so event.args.to == events.deposit.args.user compares correctly regardless of casing.

Testing composites

rflow test <wf> --fixture <file> rehearses a composite path by supplying the matched events directly:

{ "events": {
    "deposit":  { "args": { "user": "0xabc", "amount": "1000000" } },
    "transfer": { "args": { "to": "0xabc", "value": "500000" } }
} }

Replay limit: the fixture asserts the correlated set directly; the sliding within window and cross-event where: matching are not re-run in rflow test. The correlation state machine itself is covered by rflow's DB-gated tests.