Migrating from Gelato
This guide maps Gelato Automate task definitions to rflow. Time-based tasks, event listeners and recurring contract calls have rflow equivalents: self-hosted workflows with durable trigger claims and idempotent relayer submission. Review the mapping and execution boundaries before moving consequential tasks.
The one command
Export your task definitions as JSON (the same shape as the
automate-sdk createTask /
createBatchExecTask options, plus a chainId) and run:
rflow import gelato ./tasks.json --output ./my-projectThe input file may hold one task object, an array of tasks, or
{ "tasks": [...] }:
[
{
"name": "Oracle Keeper",
"chainId": 1,
"trigger": { "type": 1, "cron": "*/10 * * * *" },
"web3FunctionHash": "QmPXsYNiw...",
"web3FunctionArgs": { "oracle": "0x71B9...4da", "currency": "ethereum" }
},
{
"name": "Counter Increment",
"chainId": 84532,
"trigger": { "type": 0, "interval": 300000 },
"execAddress": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"execSelector": "0xe8927fbc",
"dedicatedMsgSender": true
}
]trigger.type is the SDK's TriggerType enum: numeric (0 TIME, 1 CRON, 2
EVENT, 3 BLOCK) or the spelled-out string.
The importer scaffolds a complete project (rflow.yaml, .env / .env.example,
abis/, docker-compose.yml, MIGRATION-NOTES.md) that passes rflow validate
as-is, with every unconvertible piece kept visible as a # TODO(rflow import)
comment plus a MIGRATION-NOTES entry. Nothing is silently dropped.
Concept map — Gelato gave you X, in rflow it is Y
| Gelato | rflow | Imported automatically? |
|---|---|---|
| CRON trigger | workflow with a cron: trigger | ✅ expression verbatim |
| TIME trigger (interval ms) | workflow with an interval: trigger | ✅ ms cadence maps to every:; rflow is second-granular, so sub-second/non-round intervals round to the nearest second and are flagged |
EVENT trigger (filter.address + topics) | workflow with an event: trigger | ⚠️ a topic hash alone can't name the event; ABI json next to the export (or its abis/ dir) resolves it, else a flagged placeholder |
blockConfirmations | trigger confirmations: N | ✅ |
| BLOCK trigger (every block) | a block: trigger — block: { every: 1, network: ... } | ✅ raise every: to run less often |
Classic task (execAddress + execSelector/execData) | send_transaction step from a scaffolded relayer | ✅ selector-only calldata is flagged if the function takes arguments |
| Resolver contract (checker) | read: step with assert: (or an if:) before the send | ❌ noted: port the checker logic |
dedicatedMsgSender | a named relayer — your workflows share one address per chain | ⚠️ new address: update onchain whitelists (rflow relayers ls shows it) |
web3FunctionArgs / userArgs | constants: — ${{ constants.<name> }} in steps | ✅ |
| Web3 Function TypeScript body | native steps (read, http_call, send_transaction, notify) where they fit; otherwise keep the code and run it from a command: step | ❌ logic cannot auto-convert; a command: step runs the preserved W3F as-is (its JSON feeds a later send_transaction), and a TODO placeholder keeps the workflow valid meanwhile |
| W3F secrets | secrets: backed by .env | manual |
| Gelato executors + 1Balance | the embedded relayer: your own funded wallet, automatic gas bumping/rebroadcast, optional automatic_top_up | ✅ scaffolded (fund the new signer) |
The two honest caveats
msg.sender changes. Gelato executed through its own executors /
dedicatedMsgSender proxy. rflow sends from your relayer wallet. The importer
scaffolds a fresh signer: plus a main relayer. Fund it and
update any contract whitelist that expected the Gelato sender.
TypeScript does not become YAML. The importer converts the trigger
faithfully and leaves a placeholder step naming the W3F hash or path to port.
For bodies that fit the native steps, the offchain fetch becomes http_call:, the
onchain check becomes read: + assert:, the returned calldata becomes
send_transaction:.
Worked example
The five-minute counter task above becomes:
rflow_version: 1
name: my-project
config:
port: 3940
db_connection: ${DATABASE_URL}
networks:
- name: base-sepolia
chain_id: 84532
rpc: ${BASE_SEPOLIA_RPC}
# a fresh signer is scaffolded - fund it (or swap in aws_kms / turnkey / ...)
signer:
raw:
mnemonic: ${RAW_DANGEROUS_MNEMONIC}
relayers:
# scaffolded to replace Gelato's dedicated msg.sender #
main:
networks: [base-sepolia]
workflows:
counter-increment:
trigger:
interval:
# Gelato interval: 300000ms
every: 5m
steps:
- id: exec-call
send_transaction:
network: base-sepolia
relayer: main
to: "0x5FbDB2315678afecb367f032d93F642f64180aa3"
data: "0xe8927fbc"
on_failure: dead_letterThen:
cd my-project
docker compose up -d # postgres
vim .env # rpc urls + fresh signer mnemonic
rflow validate
rflow startFor event tasks, drop the contracts' ABI json files next to your task export
(or in an abis/ dir beside it) before importing: the importer matches
each task's topic hash against them and writes the real event name + ABI. If
none matched, finish the one flagged TODO. The topic hash sits in a comment
above the placeholder, alongside
rflow abi find-event <file|dir> <topic0> to match it
against ABIs you already have and
rflow abi fetch --network <net> --address <0x..> to pull the verified ABI
from sourcify/etherscan.
What you gain in the move
- Durable execution — retained trigger identities deduplicate run claims, and every relayer submission carries a persisted idempotency key. HTTP, notifications and commands need their own external-effect idempotency. See Reliability for recovery and retention boundaries.
- Pre-flight simulation on every send by default — reverts are caught before gas
is spent, plus gas caps and
assert_simgates. - Reorg-aware event triggers —
confirmations: 0 | N | finalizedper trigger, withon_reorg:responses. See Reorgs. - Historical-trigger rehearsal —
rflow replayruns a workflow against historical events/blocks. Reads and simulations use the configured RPC state; it does not reconstruct a historical execution environment automatically. - Human approval gates on transactions — see Approvals.
- No per-execution fees — your infra, your RPC, open source.
- Off-chain automations too — cron → HTTP → Telegram with zero networks configured.