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

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; every later event attaches to the correlation it satisfies. ${{ event.args.* }} in a where: 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. A single on-chain event never fans out into multiple runs (so one settlement transfer can never trigger two payouts). If a cross-event where: is not fully discriminating (e.g. two deposits by the same user), the oldest window wins and the others keep waiting for their own later event.
  • When a correlation holds every listed event, the run is claimed exactly once (a durable dedup key over the matched set — 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 zero window is rejected). Cross-event where: references must point at an earlier event in the list (validation enforces this), and a composite where: may reference only event.* and events.<earlier-id>.* — any other root (constants, secrets, trigger, state, …) is undefined at match time and is rejected by validation.

Ordering assumption (honest): 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) are delivered by independent head callbacks, so a later event emitted in the same block as its starter but at a lower log index can arrive first; with no correlation open yet it is dropped, and that set never fires. Order the events: list so the event most likely to be seen first (or the one others correlate back to) is the starter, or keep the 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 — doing it exactly-once would need a durable consumed-identity tombstone (the arbitrary-CEP join this feature excludes).

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

    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 an event: trigger uses for an event with no confirmations: of its own. Exactly-once completion does not depend on the indexing depth: it rides the durable rflow.composite_matches accumulator, the composite:{workflow}:{hash} dedup key, and the sliding within window (a re-delivered completing event re-derives the same key and never fires a second run). The reorg caveat is the same as 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. Keep composite money workflows to values you are comfortable acting on at head, or gate the payout behind a send_transaction wait_for: confirmed.

A composite where: sees only event.* (this event) and events.<earlier-id>.* (correlated earlier events) — the same self-contained match-time discipline as wait_for. 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 (honest): the fixture asserts the correlated set directly — the sliding within window and the cross-event where: matching are not re-run in rflow test. Use it to exercise the workflow's steps over a known-correlated set; the correlation state machine itself is covered by rflow's DB-gated tests.