What is rflow?
rflow is an open-source, self-hosted onchain workflow engine. You describe automations
in one rflow.yaml — when this event fires on this chain, and this condition holds,
send this transaction over there, then tell me about it — and rflow runs them with
exactly-once execution, pre-flight simulation, reorg awareness, and a durable journal
you can inspect at any time.
The positioning in one sentence: GitHub-Actions-shaped YAML gets exactly-once, reorg-aware, simulation-gated onchain automation — self-hosted, any custody, any EVM chain.
One binary, three engines
rflow embeds two battle-tested Rust libraries in a single process:
| Concern | Owner |
|---|---|
| Block fetching, event decoding, reorg detection, factory tracking, historical backfill | rindexer (embedded) |
| Nonce management, gas pricing & bumping, rebroadcast, tx expiry, signing (9 providers), allowlists, automatic top-up | rrelayer (embedded) |
| Workflow state machine, exactly-once journal, expressions, simulation gate, approval gates, sagas, backtesting, notifications, HTTP, cursors, CLI, MCP | rflow |
Events flow in through the embedded indexer, expressions and the step journal run
in-process, and transactions go out through the embedded relayer — all function calls,
no HTTP hop on the hot path. State lives in a single Postgres database (schemas:
rflow, relayer, rindexer).
The engines are lazy: the relayer only boots if you declare a signer/relayers
block, and the indexer only boots if you have chain triggers. A monitoring-only
project needs no keys; a cron → HTTP → Telegram project needs no networks at all.
What a workflow looks like
workflows:
mirror:
trigger:
event:
contract: USDC
name: Transfer
network: ethereum
where: "${{ trigger.args.value > wei('1000', 6) }}"
confirmations: 12
steps:
- id: gate
read:
contract: USDC
network: base
function: "balanceOf(address)"
args: ["${{ relayers.payout.address }}"]
assert: "${{ output >= trigger.args.value }}"
- id: mirror
send_transaction:
network: base
relayer: payout
contract: USDC
function: "transfer(address,uint256)"
args: ["${{ trigger.args.to }}", "${{ trigger.args.value }}"]
valid_for: 5m
wait_for: confirmed
- id: report
notify:
channel: ops
message: "mirrored ${{ format_units(trigger.args.value, 6) }} USDC — ${{ steps.mirror.tx.hash }}"
on_failure: dead_letterSteps run strictly sequentially. Any prior step's output is addressable from any later step through the expression language. Sends are pre-flight simulated by default and carry an idempotency key, so a crash mid-run can never double-spend — see Reliability.
What rflow is not
- Not a hosted service. You run it — your infra, your keys (or your KMS/Privy/ Turnkey/Fireblocks — any of rrelayer's 9 signing providers), your Postgres.
- Not a smart-contract framework. rflow reacts to chains and sends transactions; it does not deploy or manage contracts.
- Not an indexer replacement. If you want a full indexing platform (GraphQL
APIs, custom schemas, non-workflow consumers), use rindexer directly. rflow uses
indexing as a trigger surface — and lets workflows make decisions over the event
tables it already writes via
query:(withindex_eventsto index extra contracts for exactly that) — but its tables serve workflows, not apps.