Migrating from Gelato
Gelato deprecated Web3 Functions on March 31, 2026, and the classic Automate task products are winding down with them. Time-based tasks, event listeners and recurring contract calls all have direct rflow equivalents — self-hosted, with exactly-once execution instead of best-effort. This page maps every Gelato concept and shows the one-command importer.
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, both work.
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 | ✅ the millisecond cadence maps to every: faithfully (rflow is second-granular, so sub-second or non-round intervals round to the nearest second and are flagged) |
EVENT trigger (filter.address + topics) | workflow with an event: trigger | ⚠️ the topic hash can't be reversed into an event name — but if an ABI json next to the export (or in its abis/ dir) declares that topic0, the real event name + ABI are used; otherwise a placeholder is written and flagged |
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, but a command: step runs your preserved Web3 Function as-is — it returns JSON that a later send_transaction consumes, so rflow keeps signing/simulation/idempotency. 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. A Web3 Function is a real program; the importer
converts the trigger faithfully and leaves a placeholder step naming the W3F hash
or path to port. Most W3F bodies port in minutes: the offchain fetch becomes
http_call:, the onchain check becomes read: + assert:, and the returned
calldata becomes send_transaction: — which also gets you pre-flight simulation,
idempotency keys and a durable journal for free.
Worked example
The five-minute counter task above becomes:
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 is kept in a comment
right above the placeholder, and the comment names the two helpers —
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
- Exactly-once execution — every trigger occurrence is claimed once in Postgres,
every send carries an idempotency key. Survives
kill -9. See Reliability. - 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. - Backtesting —
rflow replayrehearses a workflow over real historical blocks before it goes live; Gelato had no equivalent. - 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.