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 OpenZeppelin Defender

OpenZeppelin Defender was sunset on July 1, 2026. If you ran Monitors, Relayers and Actions there, rflow covers the same ground — self-hosted, in one binary, with stronger execution guarantees. This page maps every Defender concept to its rflow equivalent and shows the one-command importer.

The one command

If you managed Defender with the defender-as-code serverless plugin, point the importer at your serverless.yml:

rflow import defender ./serverless.yml --output ./my-project

It scaffolds a complete project — rflow.yaml, .env / .env.example, abis/, docker-compose.yml and a MIGRATION-NOTES.md — and prints a summary table of what mapped, what needs review, and what could not be converted. The generated project passes rflow validate as-is: everything that has no rflow equivalent yet stays in the file as a clearly-marked # TODO(rflow import) comment instead of being silently dropped.

 status          | source item                                | result
 mapped          | monitor 'large-usdc-transfers' event ...   | → workflow 'large-usdc-transfers'
 mapped          | relayer 'relayer-2'                        | merged into relayers.payout-relayer
 needs attention | action 'daily-reporter'                    | cron mapped; the JS body does NOT auto-convert
 needs attention | action 'action-webhook'                    | webhook trigger mapped; the JS body does NOT auto-convert
 
imported into ./my-project - 9 mapped, 12 need attention, 3 not migrated

Concept map — Defender gave you X, in rflow it is Y

DefenderrflowImported automatically?
Monitor (event condition)workflow with an event: trigger✅ one workflow per event signature
Monitor condition expression (value > 100)where: "${{ trigger.args.value > 100 }}"✅ best-effort translated, flagged for review
Monitor confirm-leveltrigger confirmations: N | finalized
Monitor function / transaction conditionsno direct equivalent — watch an emitted event, or use a read: trigger (poll a view function, fire on a threshold)❌ TODO comment + note
Notification channel (slack / telegram / discord)notifications.channels + notify steps✅ credentials via .env
Notification channel (pagerduty / opsgenie)native pagerduty: / opsgenie: channels✅ credentials via .env
Notification channel (email / datadog)an http_call to the provider's API❌ noted
Notification channel (webhook)http_call step with templated body + optional HMAC❌ noted (one-liner to add)
Relayerrelayers: entry
address-from-relayerone relayer on several networks — rflow creates the wallet once and clones the address everywhere✅ merged into a single entry
Relayer policy (gas cap, receiver allowlist)relayers.<name>.policy (max_gas_price, whitelist_receivers) + per-workflow permissions.max_value_per_tx❌ noted with the exact keys to set
Relayer min-balanceautomatic_top_up passthrough on the network (relayers fund each other / from a Safe)❌ noted
Monitor alert threshold (X events in Y)trigger throttle:✅ semantics caveat flagged — Defender alerts on the Nth hit, rflow runs the first N then cools down
Action (schedule: cron or frequency)workflow with a cron: trigger✅ trigger only — see below
Action JavaScript bodynative steps (read, http_call, send_transaction, notify) where they fit; otherwise keep the JS and run it from a command: step❌ logic cannot auto-convert, but a command: step runs your preserved script as-is — it decides/prepares and a later send_transaction moves money. A TODO placeholder keeps the workflow valid meanwhile
Action (webhook trigger)a webhook: trigger (path + optional HMAC auth)✅ trigger only — point callers at rflow's port; the JS body stays a TODO
Action environment variablesconstants:
Secretssecrets: backed by .env placeholders✅ names only — Defender never exposes secret values, re-enter them
Contracts + ABIscontracts: registry; ${file(...)} ABIs are copied, missing ones are synthesized from the monitored event signatures

The two honest caveats

Your relayer keys do not come with you. Defender custodied the signing keys and they were never exportable. The importer scaffolds a fresh signer: (dev mnemonic via .env, or swap in AWS KMS / GCP / Turnkey / Fireblocks / Privy / PKCS#11) — you get new relayer addresses. Fund them, and update any onchain allowlists that referenced the old Defender addresses. rflow relayers ls shows the new addresses after first start.

JavaScript does not become YAML. Autotask/Action code bodies are real programs; converting them mechanically would be dishonest. The importer converts the trigger (schedule → cron) and leaves a placeholder step with a TODO pointing at your source file. Most action bodies port in minutes: an onchain check becomes read: + assert:, an API call becomes http_call:, a transaction becomes send_transaction: (which also gets you pre-flight simulation and idempotency keys for free).

Worked example

A typical Defender monitor:

serverless.yml
monitors:
  large-usdc-transfers:
    name: 'Large USDC Transfers'
    type: 'BLOCK'
    network: 'mainnet'
    addresses:
      - '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
    confirm-level: 1
    notify-config:
      channels:
        - ${self:resources.notifications.slack-1}
    conditions:
      event:
        - signature: 'Transfer(address,address,uint256)'
          expression: 'value > 10000000000'

becomes:

rflow.yaml (generated)
contracts:
  large-usdc-transfers-target:
    abi: ./abis/large-usdc-transfers-target.json # synthesized from the event signature
    addresses:
      ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
 
notifications:
  channels:
    workspace-slack:
      slack:
        webhook_url: ${WORKSPACE_SLACK_WEBHOOK_URL}
 
workflows:
  large-usdc-transfers:
    trigger:
      event:
        contract: large-usdc-transfers-target
        name: Transfer
        network: ethereum
        where: "${{ trigger.args.value > 10000000000 }}"
        confirmations: 1
        start_block: latest
        end_block: live
    steps:
      - id: notify-workspace-slack
        notify:
          channel: workspace-slack
          message: "Large USDC Transfers: Transfer on ethereum - tx ${{ trigger.tx_hash }}"
    on_failure: dead_letter

Then:

cd my-project
docker compose up -d   # postgres
vim .env               # rpc urls, slack webhook, signer
rflow validate
rflow start

Holding OpenZeppelin Monitor (OSS) configs instead?

OpenZeppelin's suggested Defender replacement, openzeppelin-monitor, uses JSON monitor files whose shape is very close to what rflow imports — match_conditions.events[].signature/expression map 1:1 onto event: triggers with where:, and its triggers (slack/telegram/discord/webhook/script) map onto notifications.channels and steps. A dedicated rflow import oz-monitor is planned; until it lands, the table above is the translation guide — and unlike a monitor-only tool, the same file that watches the event can also send the response transaction.

What you gain in the move

  • Exactly-once execution — journaled steps + idempotency keys instead of best-effort retries. See Reliability.
  • Pre-flight simulation on every send by default — plus gas caps, assert_sim and recheck.
  • Reorg-aware confirmations per trigger — alert at head, pay at depth, and on_reorg: compensations Defender never had. See Reorgs.
  • Historical backfill + live tail in one config (start_block: earliest) — and rflow replay to backtest a workflow before it goes live.
  • Human approval gates on transactions — see Approvals.
  • Your custody — keys never live in a vendor's cloud again.