History explorer — what ran, why, what it touched
rflow journals every run and step (see reliability). The history explorer is the operational surface over that journal: what ran, what sent transactions, what is parked, who approved a send, what changed between a good run and a bad one, and a redacted audit export.
Three surfaces share one query layer, so the CLI, API and UI never diverge and all are secret-redacted by default:
- an embedded web UI at
GET /(workflows overview, runs, run detail timeline, transactions, approvals, waiting, failures, reorgs, replay/test sessions) - a read-only JSON API under
GET /api/history/*: cursor-paginated, stable shapes, auth-gated - a CLI:
rflow historyandrflow runs diff/rflow runs timeline, with--jsonfor automation over SSH
The shared query layer
One read-optimised module, rflow_core::history::HistoryReader, backs both
the CLI and the API. Every result gets:
- Keyset (cursor) pagination, not just
limit. Pages key on(created_at, id)newest-first and return an opaquecursorplushas_more; a row is never skipped or returned twice, even as new runs land between pages. - Secret redaction by default. Every rendered input, output, trigger
payload, tx summary, approval message and error is scrubbed of every declared
secret:, every secret-typed config field (signer creds, channel tokens, the server bearer,http_callheaders/hmac,command.env, …) and every${VAR}env value the raw config references, before it leaves the process. The marker is<redacted>. The archive exporter uses the same scrubber. - Filterability. Runs filter by workflow, status, trigger kind, network, contract, relayer, tx hash, block, run id, step id, error kind, approval status, session (live / replay / test) and time range. Guarded, idempotent database indexes back the common ones.
The embedded UI — GET /

Open http://localhost:3940/ while rflow start is running. A single
dependency-free HTML/CSS/JS file embedded in the binary: no build step, no
external CDN, works offline. Views:
| View | Answers |
|---|---|
| Workflows overview | per workflow: pause state, trigger kind, last run / success / failure, success & failure counts, runs waiting, median / p95 duration, tx count, recent error kind, cursor / head lag |
| Runs history | the full filter/search set above, reflected in the URL hash so a filtered view is shareable |
| Run detail (timeline) | trigger summary, condition results, step timeline with redacted inputs/outputs, retries & attempts, simulation result, gas-cap checks, approval decisions, tx lifecycle, wait/delay parking, reorg linkage, replay/test marker |
| Transactions | every send rflow attempted or would have sent: network, relayer, status lifecycle, tx hash, idempotency key, explorer link where the network resolves |
| Approvals | pending + decided, who decided and why |
| Waiting | runs parked right now (tx / delay / event / approval), with due/expiry |
| Failures & dead-letters | failed and dead-lettered runs |
| Reorg responses | durable on_reorg responses and their steps |
| Replay/test sessions | isolated backtest/dry-run sessions and their run counts |
| Relayers (address book) | every relayer wallet: network, address + copy button, live balance with low-balance flag, funding-plan estimate/shortfall, recent gas spend (backed by GET /api/relayers); a row deep-links to that relayer's runs |

Dense tables, copy buttons on every hash/id/address, clear <redacted>
markers, dark + light themes. Untrusted run data is never rendered as
HTML: event args, errors and HTTP bodies reach the DOM only as text, so a
<script> tag shows as text and never executes.
The JSON API — GET /api/history/*
Read-only, cursor-paginated, redacted, and gated by the same bearer as the rest
of /api/*. List endpoints return { "items": [...], "cursor": "<opaque|null>", "has_more": <bool> }; pass cursor back as a query param to page.
| Endpoint | Returns |
|---|---|
GET /api/history/runs | runs page (filters via query params: workflow, status, trigger, network, contract, relayer, tx_hash, block, run_id, step_id, error_kind, approval_status, session, since, until, limit, cursor) |
GET /api/history/runs/:id/timeline | one run's chronological timeline: { run, events: [...] } where each event is a trigger, step, approval or reorg_step |
GET /api/history/txs | send attempts (queued/parked/submitted/settled) |
GET /api/history/approvals | approvals (pending + decided) |
GET /api/history/waiting | runs parked right now |
GET /api/history/reorgs | durable on_reorg responses |
GET /api/history/replays | replay/test sessions |
GET /api/history/workflows | the per-workflow overview that powers the landing page |
GET /api/history/export | ?format=json → { runs, steps, sends, approvals, failures } (application/json); ?format=csv → a flat runs CSV (text/csv). Redacted; no raw-payload flag. |
# newest 50 runs of one workflow, then the next page
curl -s -H "Authorization: Bearer $RFLOW_API_TOKEN" \
'localhost:3940/api/history/runs?workflow=treasury-sweep&limit=50' | jq '.cursor'
curl -s -H "Authorization: Bearer $RFLOW_API_TOKEN" \
'localhost:3940/api/history/runs?workflow=treasury-sweep&limit=50&cursor=<cursor>' | jq '.items | length'Bad cursors / malformed run ids answer 400 {"error": ...}; an unknown run id
answers 404. since accepts a duration (24h, 7d) or an RFC3339
timestamp.
The CLI
Everything the UI shows is reachable over SSH. See
rflow history and rflow runs:
rflow history --status failed --since 24h # what failed today
rflow history txs --since 7d # what money moved this week
rflow history waiting # what's parked right now
rflow history approvals --all # every decision, not just pending
rflow history export --workflow treasury-sweep --since 30d --format csv --out audit.csv
rflow runs diff <run-a> <run-b> # a good run vs a bad one
rflow runs timeline <run-id> # one run, chronologicallyEach list command prints a next page: --cursor <c> hint when more rows exist,
and --json emits the same { items, cursor, has_more } shape the API returns.
Authentication
By default rflow's port is unauthenticated (localhost / private-network
operator tool). Set config.server.auth.token
to require a bearer on / and /api/*:
rflow_version: 1
name: treasury-ops
config:
port: 3940
db_connection: ${DATABASE_URL}
server:
auth:
token: "${{ secrets.RFLOW_API_TOKEN }}" # never a yaml literal
secrets:
RFLOW_API_TOKEN: ${RFLOW_API_TOKEN}
notifications:
channels:
ops:
console: {}
workflows:
heartbeat:
trigger:
cron:
expression: "0 * * * *"
steps:
- notify:
channel: ops
message: "hourly heartbeat"With auth on:
- the UI shell at
GET /stays public: it is a secret-free static login page that must load unauthenticated so the browser can prompt for the token. Every/api/*request (including/api/history/*) then requiresAuthorization: Bearer <token>. - the UI shows a login card on any
401, stores the token insessionStorage, sends it on every API call, and returns to login on a later401. The token is never rendered or logged. - behind an auth-terminating proxy (oauth2-proxy, Cloudflare Access, most
tunnels and embedded preview browsers) the
Authorizationheader is often stripped or replaced. The server also accepts the token via anX-Rflow-Tokenrequest header; the UI falls back to it automatically when a login401s despite a sent token. Caveat: log/APM pipelines that redactAuthorizationusually record custom headers verbatim, so if your intermediary captures request headers into logs, treat those logs as secret or rotate withrflow token revoke. /liveand/healthstay open for k8s probes (unlesshealth_public: false);/metricsfollows auth unlessmetrics_public: true./hooks/*webhook routes keep their own per-trigger HMACauth:and are never covered by the bearer.
Beyond the shared token you can mint named, revocable bearers with
rflow token. Any of them authenticates the API and UI.
Explorer links
The transactions and run-detail views build per-network block-explorer links
from a built-in chain map (keyed on the network name / chain id). Networks
without a known explorer show the hash with a copy button and no link. Links
open in a new tab with rel="noopener".
Retention
The explorer reads history; it never prunes it. Bound the journal with
config.retention + rflow retention/rflow archive,
which share the same redaction discipline: an exported audit trail never leaks
a secret whether it comes from rflow history export or rflow archive export.