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 the "is this safe to boot?" question from both ends:

  • rflow doctorlive 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 explainstatic 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 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 answers eth_chainId and it matches the declared chain_id — a mismatch means every address/nonce assumption is wrong
rpc.<network>.finalizedadvicethe 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>.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 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_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, so rflow start is what 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 — 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 configured

That 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.