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. Its official replacement is a pair of open-source, self-hosted services: openzeppelin-relayer and openzeppelin-monitor (both AGPL, plus Redis). Neither replaces Actions: OZ's own migration guide says that logic "will need to be manually recreated". Monitor notifies but never sends; Relayer plugins are TypeScript functions invoked over HTTP, with no trigger binding or scheduler. rflow covers Monitors, Relayers and the missing Actions layer in one binary. 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, MIGRATION-NOTES.md) and prints a summary of what mapped, what needs review, and what could not convert. The generated project passes rflow validate as-is: anything with no rflow equivalent stays in the file as a # TODO(rflow import) comment, never 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❌ no auto-convert; a command: step runs the preserved script as-is (it decides/prepares, a later send_transaction moves money). A TODO placeholder keeps the workflow valid
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; 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. The importer converts the trigger (schedule → cron) and leaves a placeholder step with a TODO pointing at your source file. For action bodies that fit the native steps, an onchain check becomes read: + assert:, an API call becomes http_call:, a transaction becomes send_transaction: (with 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)
rflow_version: 1
name: defender-import
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETHEREUM_RPC}
 
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 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, translate with the table above. Monitor paired with the open-source OpenZeppelin Relayer can send transactions, but you wire everything between them yourself (Monitor webhook → your glue → Relayer HTTP API, plus your own retry and dedupe). In rflow the same file that watches the event submits the response transaction with a persisted idempotency key and a recovery journal.

What you gain in the move

  • Durable execution — deduplicated trigger claims, journaled steps and idempotent relayer submissions. HTTP, notifications and commands need their own external-effect idempotency. 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), plus rflow replay to rehearse historical triggers. Reads and simulations use the configured RPC state, not an automatic historical snapshot.
  • Human approval gates on transactions; see Approvals.
  • Your choice of custody — choose the signing provider and account that your deployment uses, including your own cloud KMS or a supported remote custodian.