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

History explorer — what ran, why, what it touched

rflow already journals every run and step (see reliability). The history explorer turns that durable journal into an operational surface that answers the questions logs can't: which workflows ran in the last hour, which runs sent transactions and what was the final status, what is parked right now, who approved a send, what changed between a good run and a bad one, and can I export a redacted audit trail?

It ships as three surfaces over one query layer, so the CLI, the API and the UI never diverge and every one of them is secret-redacted by default:

  • an embedded web UI at GET / — the run-trace viewer evolved into a full history explorer (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 CLIrflow history and rflow runs diff / rflow runs timeline, useful over SSH with --json for automation.

The shared query layer

There is exactly one read-optimised query module — rflow_core::history::HistoryReader — and both the CLI and the API call it. It gives three properties to every result:

  1. Keyset (cursor) pagination, not just limit. Pages key on (created_at, id) newest-first and hand back an opaque cursor plus has_more; passing that cursor to the next request continues exactly where the last one stopped — a row is never skipped or returned twice, even as new runs land between pages.
  2. 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_call headers/hmac, command.env, …) and every ${VAR} env value the raw config references, before it leaves the process. The marker is <redacted>. This is the same scrubber the archive exporter uses — one redaction discipline, everywhere.
  3. 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 — backed by guarded, idempotent database indexes for the common ones.

The embedded UI — GET /

Open http://localhost:3940/ while rflow start is running. It is a single dependency-free HTML/CSS/JS file embedded in the binary — no build step, no external CDN, works offline. Views:

ViewAnswers
Workflows overviewper 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 historythe 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
Transactionsevery send rflow attempted or would have sent — network, relayer, status lifecycle, tx hash, idempotency key, explorer link where the network resolves
Approvalspending + decided, who decided and why
Waitingruns parked right now (tx / delay / event / approval), with due/expiry
Failures & dead-lettersfailed and dead-lettered runs
Reorg responsesdurable on_reorg responses and their steps
Replay/test sessionsisolated 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

The UI is deliberately restrained (dense tables with durable column widths, copy buttons on every hash/id/address, clear <redacted> markers, dark + light themes, loading/error/empty states). Untrusted run data is never rendered as HTML — event args, errors and HTTP bodies reach the DOM only as text, so a value containing 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.

EndpointReturns
GET /api/history/runsruns 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/timelineone run's chronological timeline: { run, events: [...] } where each event is a trigger, step, approval or reorg_step
GET /api/history/txssend attempts (queued/parked/submitted/settled)
GET /api/history/approvalsapprovals (pending + decided)
GET /api/history/waitingruns parked right now
GET /api/history/reorgsdurable on_reorg responses
GET /api/history/replaysreplay/test sessions
GET /api/history/workflowsthe 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 the layer is read-only, since accepts a duration (24h, 7d) or an RFC3339 timestamp.

The CLI

Everything the UI shows is reachable over SSH without it — see the rflow history and rflow runs references:

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, chronologically

Each 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.yaml
config:
  server:
    auth:
      token: "${{ secrets.RFLOW_API_TOKEN }}"   # never a yaml literal

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 requires Authorization: Bearer <token>.
  • the UI shows a clean login card on any 401, stores the token in sessionStorage, sends it on every API call, and returns to login on a later 401. The token is never rendered or logged.
  • behind an auth-terminating proxy (oauth2-proxy, Cloudflare Access, most tunnels and embedded preview browsers) the Authorization header is often stripped or replaced with the proxy's own bearer. The server therefore also accepts the token via an X-Rflow-Token request header, and the UI falls back to it automatically when a login 401s despite a sent token. Caveat: log/APM pipelines that redact Authorization by default usually record custom headers verbatim — the UI only uses the fallback in environments that actually need it, but if your intermediary captures request headers into logs, treat those logs as secret or rotate with rflow token revoke.
  • /health stays open for k8s probes (unless health_public: false); /metrics follows auth unless metrics_public: true. /hooks/* webhook routes keep their own per-trigger HMAC auth: 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 that don't resolve to a known explorer simply show the hash with a copy button and no link. All links open in a new tab with rel="noopener".

Retention

The explorer reads history; it never prunes it. Bounding the journal is the job of config.retention + rflow retention/rflow archive, which share the exact same redaction discipline — an exported audit trail never leaks a secret whether it comes from rflow history export or rflow archive export.