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

send_transaction

Queue a transaction through the embedded relayer.

- id: mirror
  send_transaction:
    network: base
    relayer: payout
    contract: USDC
    function: "transfer(address,uint256)"
    args: ["${{ contracts.Treasury.address }}", "${{ trigger.args.value }}"]
    valid_for: 5m
    wait_for: confirmed
    gas: { max_price: 100 gwei, max_cost: 0.01 ether }
    assert_sim: ["${{ sim.gas_used < 500000 }}"]
  retry:
    max_attempts: 3
    retry_if: "${{ error.kind in ['rpc_timeout', 'rate_limited', 'gas_cap_exceeded'] }}"

Fields

FieldRequiredDescription
networkA declared network — this is how cross-chain works: trigger on one, send on another
relayerA named relayer to send from
contractone ofTarget: a contract registry name...
toone of...or a raw 0x address (no registry entry needed)
functionSolidity signature, e.g. transfer(address,uint256)
argsArguments — each may be an expression
valueNative value to attach — unit literals work: 0.1 ether, or "${{ wei('0.1', 18) }}"
dataRaw calldata alternative to function/args
simulatetruePre-flight simulation — see below
on_simulation_failabortabort | skip | continue | notify
assert_simAssertions over the simulation result (sim.*) — see below
gasClient-side gas guardrails — see below
recheckExpression re-evaluated immediately before broadcast — see below
valid_forExpiry for queued sends, e.g. 5m — a stale send never fires late
wait_forconfirmednone | submitted | included | confirmed | confirmed(N) | finalized — see below
speedrelayer'sOverride the relayer's speed for this send: SLOW | MEDIUM | FAST | SUPER
approvalHuman gate on the prepared transaction: a single via: gate, a named N-of-M policy:, or ordered when: tiers (value = the send's native value in wei) — see Approvals
propose_to_safePROPOSE to a Safe (delegate-signed, via the Transaction Service) instead of broadcasting — see Safe proposal mode
multicallN calls in one transaction via Multicall3 — see below

Simulation is the default

Every send is pre-flight simulated (eth_call with the exact calldata) before it is queued. A revert is decoded — custom errors included — into steps.<id>.error and the step aborts. You only write simulate: false or on_simulation_fail: to change that behavior:

  • abort (default) — the step fails with kind simulation_failed
  • skip — the step is skipped, the run continues
  • continue — send anyway (you probably don't want this)
  • notify — intended to skip-and-surface; today it still aborts the step with a warning (the notify path lands in a later phase — documented so you are not surprised)

assert_sim — gates over the simulation

assert_sim: expressions run after the simulation and see a sim root:

assert_sim:
  - "${{ sim.ok }}"
  - "${{ sim.gas_used < 500000 }}"
PathDescription
sim.okThe call succeeded
sim.gas_usedFrom eth_estimateGas
sim.return_dataThe raw return bytes (0x-hex)

Any false assertion aborts the step before broadcast with kind assert_failed — a deliberate gate, never retried.

gas — client-side guardrails

gas:
  max_price: 100 gwei        # abort if eth_gasPrice is above this
  max_cost: 0.01 ether       # abort if projected limit × price is above this
  limit: 500000              # the limit used by the max_cost projection
  limit_from_simulation: true  # ...or derive it from eth_estimateGas
  multiplier: 1.2            # applied to the estimated limit (default 1.2)

A tripped cap fails the step with kind gas_cap_exceededretryable, since gas prices fall: a retry: with backoff: legitimately waits a spike out.

recheck — don't fire stale

recheck: is re-evaluated immediately before broadcast — after approval waits, queue delays, retries — against the current context. False means the send is dropped rather than fired stale:

recheck: "${{ now() - trigger.block_number * 12 < duration('10m') }}"

Pair it with approval: — conditions can change while a human decides.

multicall — N calls, one transaction

- id: harvest-all
  send_transaction:
    network: ethereum
    relayer: keeper
    multicall:
      - { contract: VaultA, function: "harvest()" }
      - { contract: VaultB, function: "harvest()" }
      - { contract: VaultC, function: "compound(uint256)", args: ["${{ steps.plan.output }}"] }

Batches every call into a single Multicall3 aggregate3 transaction. Mutually exclusive with contract/to/function/data (validated).

wait_for

What the step waits for before the next step runs:

TargetMeaning
noneFire and forget — the step succeeds once queued. Consecutive wait_for: none sends are nonce-ordered by the relayer and land back-to-back
submittedIn the mempool
included (alias mined)In a block
confirmed (default)At the network's confirmations depth (default 12) — later steps can rely on steps.<id>.receipt
confirmed(N)An explicit depth. N at or below the network's relayer depth settles at the relayer's CONFIRMED (never shallower); N above it makes rflow verify the chain itself — polling head until the receipt is N blocks deep
finalizedReal finality: rflow polls eth_getBlockByNumber('finalized') until the finalized block reaches the receipt block — not a confirmation-count proxy

While waiting, the run parks durably (waiting_tx) — a restart resumes the wait, it never re-sends.

(For waiting on someone else's event — cross-chain sagas — see the wait_for: step, a different construct.)

What later steps see

PathDescription
steps.<id>.tx.hashTransaction hash
steps.<id>.tx_idStable relayer id — survives gas-bump hash changes; prefer it for bookkeeping
steps.<id>.receiptThe receipt, once mined (with wait_for: included+)
steps.<id>.output.valueThe native value the send attached (wei, as a decimal string) — journaled, so spend reports can aggregate output->>'value' straight from rflow.step_runs
steps.<id>.status / steps.<id>.errorStep outcome

Failure semantics — who owns what

rflow owns everything before the queue: simulation, assert_sim, gas caps, relayer policy checks, encoding, recheck, expiry (valid_for), approval gates, and retries of pre-queue failures (rpc_timeout, rate_limited, gas_cap_exceeded).

The relayer owns everything after: nonce management, gas pricing, gas bumping, rebroadcast, stuck-tx replacement. rflow never re-implements these and never blind-retries a transaction that made it onchain and reverted — a reverted tx is a step failure (kind reverted) routed to on_failure.

Every send carries a client-side idempotency key journaled before the request, so a crash between "sent" and "recorded" is resolved by lookup, not by re-sending. The full story: Reliability.