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

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-project

The input file may hold one task object, an array of tasks, or { "tasks": [...] }:

tasks.json
[
  {
    "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

GelatorflowImported automatically?
CRON triggerworkflow 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
blockConfirmationstrigger 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
dedicatedMsgSendera named relayer — your workflows share one address per chain⚠️ new address: update onchain whitelists (rflow relayers ls shows it)
web3FunctionArgs / userArgsconstants: — ${{ constants.<name> }} in steps✅
Web3 Function TypeScript bodynative 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 secretssecrets: backed by .envmanual
Gelato executors + 1Balancethe 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.yaml (generated)
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_letter

Then:

cd my-project
docker compose up -d   # postgres
vim .env               # rpc urls + fresh signer mnemonic
rflow validate
rflow start

For 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_sim gates.
  • Reorg-aware event triggers — confirmations: 0 | N | finalized per trigger, with on_reorg: responses. See Reorgs.
  • Historical-trigger rehearsal — rflow replay runs 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.