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-projectIt 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 migratedConcept map — Defender gave you X, in rflow it is Y
| Defender | rflow | Imported 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-level | trigger confirmations: N | finalized | ✅ |
| Monitor function / transaction conditions | no 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) |
| Relayer | relayers: entry | ✅ |
address-from-relayer | one 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-balance | automatic_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 body | native 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 variables | constants: | ✅ |
| Secrets | secrets: backed by .env placeholders | ✅ names only — Defender never exposes secret values, re-enter them |
| Contracts + ABIs | contracts: 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:
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:
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_letterThen:
cd my-project
docker compose up -d # postgres
vim .env # rpc urls, slack webhook, signer
rflow validate
rflow startHolding 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_simandrecheck. - 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) — andrflow replayto 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.