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

networks

One networks: list feeds both embedded engines: indexer settings and relayer settings live side by side on the same entry. Optional: pure off-chain projects (cron → HTTP → notify) declare no networks and neither engine boots.

rflow_version: 1
name: transfer-alert
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks: 
  - name: ethereum
    chain_id: 1
    rpc:                                     # structured form: roles + failover policy 
      primary: ${ETH_RPC_ALCHEMY}
      fallbacks: 
        - ${ETH_RPC_INFURA}
      policy: 
        failover_after: 3                    # consecutive errors before failover 
        max_lag: 3                           # blocks behind the best head before eviction 
    ws: ${ETH_WS}                            # RESERVED - parsed but not wired yet (validate warns) 
    confirmations: 12                        # relayer CONFIRMED depth (default 12) 
  - name: base
    chain_id: 8453
    rpc: ${BASE_RPC}                         # the simple single-url form still works 
 
contracts:
  USDC:
    abi: ./abis/erc20.json
    addresses:
      ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
 
notifications:
  channels:
    ops:
      console: {}
 
workflows:
  transfer-alert:
    trigger:
      event:
        contract: USDC
        name: Transfer
        network: ethereum
    steps:
      - id: alert
        notify:
          channel: ops
          message: "transfer: ${{ trigger.tx_hash }}"

Core fields

FieldRequiredDescription
name✅Your label for the chain, referenced by contracts, triggers and steps
chain_id✅The EVM chain id
rpc✅Provider endpoint(s): a single url, a list (first = primary, rest = fallbacks), or the structured { primary, fallbacks, policy } form. See failover for per-consumer semantics
wsReserved, not wired yet. Neither engine consumes it (head tracking is HTTP polling; tune block_poll_frequency instead). rflow validate warns when set
confirmationsDepth at which the relayer marks a tx CONFIRMED (default 12). Not to be confused with per-trigger confirmations
signing_providerPer-network signing provider override

Multiple providers & failover

rpc: accepts three equivalent shapes. All normalize to an ordered endpoint list (primary first) plus a failover policy:

rpc: ${ETH_RPC}                          # one url
rpc: [${ETH_RPC}, ${ETH_RPC_FALLBACK}]   # list: first = primary, rest = fallbacks (default policy)
rpc:                                     # structured: explicit roles + policy
  primary: ${ETH_RPC_ALCHEMY}
  fallbacks:
    - ${ETH_RPC_INFURA}
    - ${ETH_RPC_QUICKNODE}
  policy:
    failover_after: 3
    max_lag: 3

policy fields

FieldDefaultDescription
failover_after3Consecutive errors (in-band calls or the health probe) before an endpoint is marked unhealthy and traffic moves to the first healthy fallback. Re-admission takes two consecutive within-lag probes (a recovered primary reclaims the traffic), then probation under real traffic; the requirement doubles per flap (capped), so a flapping endpoint cannot thrash the active slot
max_lag3Blocks an endpoint may trail the best-known head (probed per endpoint every ~10s) before eviction. Catches a lagging-but-alive provider that error counting never would. "Best-known" remembers past rounds (a stale endpoint cannot measure lag against its own frozen head), and only the probe loop clears a lag eviction. Dev-node escape hatch: when every endpoint agrees the head went backwards (a restarted anvil), the remembered best resets

These are the only policy keys. The once-reserved read_quorum / simulation_quorum are rejected as unknown; remove them from configs that carried them.

Chain ids are verified per endpoint: an endpoint that answers eth_chainId with anything other than the declared chain_id is permanently evicted and never serves a request. rflow start verifies every endpoint eagerly and refuses to boot on a mismatch (a merely unreachable endpoint only warns and counts a failure), and rflow validate --preflight fails naming the exact endpoint before you ever boot. The guarantee does not depend on the eager pass: the transport gate re-runs eth_chainId on an endpoint's first admitted request, so an endpoint that was down at boot and comes back on the wrong chain is evicted before answering a single read, on every path (replay/test, MCP, CLI one-shots). When every endpoint is unhealthy the stack fails open to the full set rather than refusing to serve (wrong-chain endpoints stay excluded). Every request carries a 15s transport timeout, so a black-holed provider surfaces as a counted failure instead of a hang.

What fails over — and how, per consumer

All three consumers honor the full url list; their failover semantics differ:

  • rflow's own RPC path (reads/simulation). read: steps, read/block trigger polls, send_transaction pre-flight simulation and gas estimation, simulate: steps, wait_for confirmation/finality polls, receipt checks, replay/test, MCP reads and the stall monitor all ride the health-aware stack. Strict primary/standby: the first healthy endpoint in list order serves; failover_after/max_lag evict it; a recovered primary reclaims the slot.
  • The indexer. The embedded rindexer receives the full list and serves from one sticky active endpoint, rotating on transport failures, a stalled or lagging tip (its own ~10s prober), or a wrong chain id. It suspends reorg parent-hash validation across a switch so a laggier fallback cannot trigger a spurious rollback. Every switch is surfaced (/metrics rflow_rpc_endpoint_events_total, the last failover column in rflow status, /health).
  • Broadcasts. The embedded rrelayer tracks per-endpoint health (call outcomes + a ~10s tip prober) and selects per call among healthy, chain-verified, non-lagging endpoints: health-aware weighted selection, not a sticky primary (a cooling-down or lagging endpoint is skipped; when everything looks unhealthy the full set serves rather than nothing). Gas estimation follows the same pool (not pinned to url[0]), a wrong-chain url is rejected at boot, and receipt polls for a freshly broadcast tx stick to the endpoint that accepted it until first sighting, so a lagging endpoint cannot trigger premature same-nonce gas bumps.

policy numbers (failover_after, max_lag) tune rflow's own stack; the engines apply their own equivalent thresholds internally. rflow status and /health show all three views (consumers reads / indexer / relayer). See observability.

rflow validate also warns on duplicated urls (a duplicate fails exactly when its twin does) and on non-http(s) schemes.

rindexer passthroughs

All optional; passed verbatim to the embedded indexer (semantics documented at rindexer.xyz):

FieldWhat it controls
block_poll_frequencyHow often the indexer polls for new blocks
max_block_rangeMax block span per eth_getLogs request
compute_units_per_secondRate limiting for CU-metered providers
get_logs_settingsAdvanced log-fetch tuning
multicall3_addressOverride the Multicall3 address on exotic chains
reorg_handlingrindexer reorg detection tuning

rrelayer passthroughs

All optional; passed verbatim to the embedded relayer (semantics documented at rrelayer.xyz):

FieldWhat it controls
gas_providerGas price oracle (fallback / blocknative / infura / tenderly / etherscan / custom)
gas_bump_blocks_everyHow many blocks before a pending tx's gas is bumped
max_gas_price_multiplierCap on gas escalation
enable_sending_blobsAllow EIP-4844 blob transactions
permissionsAllowlists, disable_native_transfer, disable_transactions, ...
api_keysNetwork-scoped API keys for the (opt-in) relayer API
automatic_top_upRelayers fund each other / from a Safe when balances run low

Example top-up where the treasury relayer keeps every other relayer above 0.1 native:

rflow_version: 1
name: treasury-top-up
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
# DEV ONLY raw mnemonic - swap in a production signer before real funds
signer:
  raw:
    mnemonic: ${RAW_DANGEROUS_MNEMONIC}
 
relayers:
  treasury:
    networks: [ethereum]
  payout:
    networks: [ethereum]
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
    automatic_top_up: 
      - from: 
          relayer: 
            address: "${{ relayers.treasury.address }}"
            internal_only: true
        relayers: "*"
        native: { min_balance: "0.1", top_up_amount: "0.2" } 
 
workflows:
  payouts:
    trigger:
      webhook: { path: /hooks/payout }
    steps:
      - id: send
        send_transaction:
          network: ethereum
          relayer: payout
          to: "${{ trigger.args.to }}"
          value: "${{ trigger.args.amount }}"

rflow validates these blocks when it generates the runtime engine configs at boot (.rflow/runtime/{rindexer,rrelayer}/). Env placeholders are preserved, so secrets never land on disk.