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

Doctor & explain — pre-deploy diagnosis and PR review

Two commands cover "is this safe to boot?":

  • rflow doctor — live diagnosis against the real world (DB, RPCs, relayer balances, channels) plus static posture guidance, each check 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 trigger and broadcast. 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. In a project the deep suite follows automatically (--deep only makes it explicit; outside a project only the tool checks run). With --profile, doctor diagnoses the merged 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 idseveritywhat it verifies
config.validerrorthe config (post-profile-merge) passes strict validation
config.profile.<name>error / warninga 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>errorthe ABI file parses; every static address is EIP-55 checksum-valid
db.connecterrorPostgres reachable via config.db_connection (bounded SELECT 1)
db.schemainfothe rflow schema and its migration sentinel exist (a fresh project passes this only after the first rflow start)
rpc.<network>[.N]errorevery RPC url (primary and every fallback) answers eth_chainId matching the declared chain_id (at runtime a wrong-chain endpoint is permanently evicted). ${VAR} placeholders resolve before probing; an unresolved variable is its own clear failure
rpc.<network>.finalizedadvicethe node answers a finalized-tag block query; probed only when a workflow uses finalized (event confirmations: finalized or send wait_for: finalized)
rpc.<network>.archiveadvicebest-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>warningan rflow.relayers row exists for the chain (rflow start creates the wallet on first boot)
relayer.<name>.importwarninga 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>warningthe relayer's native balance is above 0.01 ether; below that it likely cannot pay for gas
relayer.<name>.funding.<network>warningthe balance covers the funding-plan estimate; only checked when a money workflow targets the relayer and the balance/gas price could be read
relayer.<name>.queue.<network>adviceno in-flight (queued/in-mempool) relayer transaction looks stuck (a long-pending nonce blocks every send behind it); remediate with rflow relayers pending / cancel / replace
signer.raw / signer.private_keyswarningthe mnemonic / key env vars actually resolve (raw mnemonics also get a "dev-only" nudge)
signer.<cloud-provider>infoconfig-shape only for aws_kms, aws_secret_manager, gcp_secret_manager, privy, turnkey, pkcs11, fireblocks: no side-effect-free probe exists; rflow start verifies them
channel.<name>warningdelivery probe per sender: Telegram getMe, Slack/Discord webhook-host TCP, PagerDuty/Opsgenie/Twilio API reachability
command.<workflow>.<step>errorevery command: step's executable resolves (covers steps, on_reorg and finally)
webhook.<workflow>warning / advicewebhook 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>warningapproval-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>advicehead-fired triggers feeding sends / ignored confirmations (validation's send-safety warnings, grouped per workflow)
history.<workflow>advicestart_block: earliest with no bounded end_block: the trigger backfills every historical event before tailing live
cursor.<workflow>.<network>warninga persisted trigger cursor more than 1,000 blocks behind the chain head: a long (re)backfill or a stalled indexer
coverage.<workflow>infoa 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 doctor exits 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 finalized support), 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.json

The first step fails on its own exit code when an error-severity check fails, so no parsing is required. Key any custom policy off summary counts or stable check ids.

rflow validate --preflight runs the connectivity subset of the same checks but keeps its stricter historical rule: any failed check fails preflight, regardless of severity. It resolves ${VAR} placeholders in rpc urls exactly like the deep suite; a missing variable fails with a clear "missing env var" message.

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 raw, so ${VAR} references stay visible and secret values never print. When the raw view cannot parse (an env placeholder in a numeric field like chain_id: ${CHAIN_ID}), explain substitutes env values to parse, then restores every string field to its ${VAR} form before rendering. 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). simulate: steps are listed as read-only: they cannot send money (no relayer handle exists in their path), so a workflow whose only chain interaction is a simulate: step keeps the monitor_only label.

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 and the absolute gas.hard_max_price ceiling, 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
  [ ] absolute gas ceiling (gas.hard_max_price)
  [ ] 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 12 reviewer safeguards configured

Enough context to approve or reject a production change without opening the YAML. 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 for diffing between branches.

Honesty note

Every explain render ends with the same footer:

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 what rflow doctor, staging replays (Backtesting) and unattended-ops alerting are for.