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
durable trigger deduplication, idempotent transaction submission, pre-flight
simulation and reorg awareness.
GitHub-Actions-shaped YAML, any custody, any EVM chain.
The journal resumes completed work without repeating settled steps. HTTP calls, notifications and commands can repeat if a crash interrupts recording their outcome, so consequential external effects need receiver-side idempotency. See the execution guarantees.
One binary, three engines
rflow.yaml
│
┌──────────────┴──────────────┐
│ rflow (one binary) │
│ │
│ rindexer ──► workflow ──► rrelayer
│ events in engine txs out
│ │
│ ▼
│ Postgres
│ (journal: every run,
│ every step, durable)
└───────────────┬──────────────┘
│
any EVM chain(s)rflow embeds two 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, durable journal, expressions, simulation gate, approval gates, sagas, historical-trigger rehearsal, 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. The hot path is
all function calls, with no HTTP hop. 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
rflow_version: 1
name: usdc-mirror
config:
port: 3947
db_connection: ${DATABASE_URL}
networks:
- name: ethereum
chain_id: 1
rpc: ${ETH_RPC}
- name: base
chain_id: 8453
rpc: ${BASE_RPC}
signer:
raw:
mnemonic: ${RAW_DANGEROUS_MNEMONIC}
relayers:
payout:
networks: [base]
contracts:
USDC:
abi: ./abis/erc20.json
addresses:
ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
base: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
notifications:
channels:
ops:
telegram:
bot_token: ${TG_BOT_TOKEN}
chat_id: ${TG_CHAT_ID}
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 a persisted idempotency key, so recovery reconciles the same relayer submission for a step attempt; see Reliability for the database-retention and external-effect boundaries.
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. For a full indexing platform (GraphQL APIs,
custom schemas, non-workflow consumers), use rindexer directly. rflow uses
indexing as a trigger surface; workflows can query the event tables it writes
via
query:(add extra contracts withindex_events), but its tables serve workflows, not apps.