Doctor & explain — pre-deploy diagnosis and PR review
Two commands cover the "is this safe to boot?" question from both ends:
rflow doctor— live diagnosis: every check that can be verified against the real world (DB, RPCs, relayer balances, channels) plus static posture guidance, each with a severity and a fix hint. Safe to gate CI and deploys on.rflow explain— static narrative: what a workflow does, what sends money, and what stands between a trigger firing and a transaction broadcasting. Built for PR review.
Run doctor before every deployment; paste explain --risk into every PR that
touches a money workflow.
rflow doctor — the operational suite
rflow doctor [--path <dir>] [--profile <name>] [--deep] [--json]The quick readiness table (Docker, Foundry, Postgres, Node, the rflow.yaml
validate summary — see Scaffolding)
prints first. When a project is present the deep suite follows
automatically (--deep only makes it explicit; outside a project only the tool
checks run). With --profile, doctor diagnoses the merged view — the exact
config rflow start --profile <name> would boot.
deep checks
check | severity | status | detail
config.valid | error | pass | valid - 1 workflow(s), 1 warning(s)
contract.Token | error | pass | abi parses (2 event(s), 9 function(s)); 1 static address(es) checksum-valid
db.connect | error | pass | connected
db.schema | info | pass | rflow schema present (migrations applied)
relayer.treasury.mapping.ethereum | warning | FAIL | no rflow.relayers row for chain 1
rpc.ethereum | error | pass | chain id 1 matches
signer.raw | warning | pass | mnemonic resolves (24 words) - dev-only; use a managed signer in production
approval.treasury-sweep-approval.sweep | warning | pass | gates the broadcast via cli, timeout 1h -> fail
coverage.treasury-sweep-approval | info | FAIL | money workflow without: rate_limit, circuit_breaker, permissions.max_value_per_tx
How to fix:
- relayer.treasury.mapping.ethereum [warning]: `rflow start` creates the wallet on first boot (or `rflow relayers sync`)
- coverage.treasury-sweep-approval [info]: each is optional - but they are the blast-radius caps when something upstream goes wrong
doctor: ok - 7 check(s) passed, 1 warning(s), 0 advice, 1 info (warnings/advice never block)The check catalogue
Check ids are stable — key CI logic off them, not off titles or details.
| check id | severity | what it verifies |
|---|---|---|
config.valid | error | the config (post-profile-merge) passes strict validation |
config.profile.<name> | error / warning | a profiles.<name> overlay applies cleanly and the merged config validates — error for the --profile you selected, warning for other declared profiles checked in passing |
contract.<name> | error | the ABI file parses; every static address is EIP-55 checksum-valid |
db.connect | error | Postgres reachable via config.db_connection (bounded SELECT 1) |
db.schema | info | the rflow schema and its migration sentinel exist (a fresh project passes this only after the first rflow start) |
rpc.<network>[.N] | error | every RPC url answers eth_chainId and it matches the declared chain_id — a mismatch means every address/nonce assumption is wrong |
rpc.<network>.finalized | advice | the node answers a finalized-tag block query — probed only when a workflow actually uses finalized (event confirmations: finalized or send wait_for: finalized) |
rpc.<network>.archive | advice | best-effort archive probe (eth_getBalance at block 0x1) — probed only when a workflow backfills history; passing it is not proof of full archive depth |
relayer.<name>.mapping.<network> | warning | an rflow.relayers row exists for the chain (rflow start creates the wallet on first boot) |
relayer.<name>.import | warning | a declared import: { id } has been adopted (a mapping row exists — the boot derived-address check passed); rflow relayers verify-imports re-checks against the live signer |
relayer.<name>.balance.<network> | warning | the relayer's native balance is above 0.01 ether — below that it likely cannot pay for gas |
relayer.<name>.funding.<network> | warning | the balance covers the funding-plan estimate for the money workflows targeting the relayer — only checked when a money workflow targets it and the balance/gas price could be read |
signer.raw / signer.private_keys | warning | the mnemonic / key env vars actually resolve (raw mnemonics also get a "dev-only" nudge) |
signer.<cloud-provider> | info | config-shape only for aws_kms, aws_secret_manager, gcp_secret_manager, privy, turnkey, pkcs11, fireblocks — no side-effect-free probe exists, so rflow start is what verifies them |
channel.<name> | warning | delivery probe per sender: Telegram getMe, Slack/Discord webhook-host TCP, PagerDuty/Opsgenie/Twilio API reachability |
command.<workflow>.<step> | error | every command: step's executable resolves (covers steps, on_reorg and finally) |
webhook.<workflow> | warning / advice | webhook auth posture — no auth on a workflow that queues transactions is a warning (anyone who reaches the port can fire it); no auth on a monitor-only workflow is advice |
approval.<workflow>.<step> | warning | approval-gate posture — on_timeout: proceed fails the check because an undecided approval broadcasts once the timeout elapses (covers steps and finally; validation rejects an approval in on_reorg) |
reorg.<workflow> | advice | head-fired triggers feeding sends / ignored confirmations (validation's send-safety warnings, grouped per workflow) |
history.<workflow> | advice | start_block: earliest with no bounded end_block — the trigger backfills every historical event before tailing live |
cursor.<workflow>.<network> | warning | a persisted trigger cursor more than 1,000 blocks behind the chain head — a long (re)backfill or a stalled indexer |
coverage.<workflow> | info | a money-moving workflow lacking any of rate_limit, circuit_breaker, permissions.max_value_per_tx — optional, but they are the blast-radius caps |
Severities and the exit rule
- error — broken as configured; boot would misbehave. The only severity
that gates:
rflow doctorexits non-zero iff an error-severity check fails. - warning — operationally risky (unfunded relayer, unauthenticated money
webhook,
on_timeout: proceed), never blocks. - advice — posture guidance (reorg exposure, backfill volume, missing
finalizedsupport), never blocks. - info — context worth knowing (schema not yet migrated, coverage notes), never blocks.
Skipped checks (e.g. a balance fetch against an unreachable node) report as
skipped, never as failures.
Gating CI and deploys
--json prints exactly one machine-readable object on stdout (logs go to
stderr), with the same exit rule:
{
"version": 1,
"quick": [ { "name": "docker", "status": "ok", "detail": "...", "hint": null } ],
"checks": [ { "id": "rpc.ethereum", "title": "rpc ethereum",
"severity": "error", "status": "pass",
"detail": "chain id 1 matches", "hint": null } ],
"summary": { "passed": 7, "failed": 2, "skipped": 0,
"errors": 0, "warnings": 1, "advice": 0, "info": 1 },
"ok": true
}ok is true unless an error-severity check failed. The shape is versioned
(version: 1) and pinned by a golden test. A GitHub Actions gate:
- name: doctor gate
run: |
rflow doctor --path ./ops --profile prod --json > doctor.json
jq '.summary' doctor.json # visibility in the job log
# optional stricter policy: also refuse warnings
- name: no-warnings policy
run: jq -e '.summary.warnings == 0' doctor.jsonThe first step fails on its own exit code when an error-severity check fails —
no parsing required. Key any custom policy off summary counts or stable
check ids.
rflow validate --preflight runs the connectivity subset of the same checks
(same functions, no duplicated logic) but keeps its stricter historical rule:
any failed check fails preflight, regardless of severity.
rflow explain — the plain-English narrative
rflow explain [<workflow>] [--risk] [--json] [--path <dir>] [--profile <name>]Fully static — no network, no database. It reads rflow.yaml (the raw
view, so ${VAR} references stay visible and no secret values are ever
printed) and narrates it. When the raw view cannot parse (an env placeholder
in a numeric field like chain_id: ${CHAIN_ID}), explain substitutes the env
values to parse and then restores every string field to its ${VAR} form
before rendering — resolved secrets still never print. With no argument, one
summary block per workflow:
workflow 'treasury-sweep-approval' [money_moving]
trigger: fires on cron schedule '0 * * * *'
steps: balance (read) -> sweep (send_transaction)The risk label is the template-registry vocabulary:
monitor_only (no sends), prepares_tx (a command: step with tx-shaped
input keys — a heuristic), money_moving (any send_transaction).
With a workflow name, the full breakdown: TRIGGER (kind, schedule/event,
conditions, confirmations), STEPS (in order, with if: conditions and
dependencies), WHAT SENDS MONEY, WHAT BLOCKS MONEY (simulation,
assert_sim, recheck, valid_for, approval gates, gas caps,
permissions/budgets, rate limit, circuit breaker, concurrency), ON FAILURE,
ON REORG, REPLAY / DRY-RUN (sends stop pre-broadcast; waits settle
unless --with-waits) and EXTERNAL SYSTEMS (HTTP hosts, channels, local
commands).
--risk — the PR-review view
--risk prints only the money sections plus a reviewer checklist:
REVIEWER CHECKLIST
[x] pre-flight simulation on every send
[x] simulation assertions (assert_sim)
[x] recheck immediately before broadcast
[x] human approval gate
[ ] send expiry (valid_for)
[x] gas price / cost caps
[ ] per-tx spend cap (permissions.max_value_per_tx)
[ ] relayer allowlist (permissions.relayers)
[ ] rate limit
[ ] circuit breaker
[ ] reorg compensation steps (on_reorg)
verdict: 5 of 11 reviewer safeguards configuredThat is deliberately enough context to approve or reject a production change
without opening the YAML. A lightweight review pattern: require
rflow explain <workflow> --risk output in the PR description for any diff
touching a workflow with sends, and treat a decreasing verdict count as a
question to answer in review. --json emits the same data structured (and
deterministic) if you want to diff it mechanically between branches.
Honesty note
Every explain render ends with the same footer, and it means it:
explain describes the CONFIGURATION, not runtime guarantees — it is not formal verification and not a security audit.
explain tells you what rflow was asked to do. Whether the RPC lies, the
contract behaves, or the approval channel pages the right human is exactly what
rflow doctor, staging replays
(Backtesting) and unattended-ops alerting
are for.