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 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 history and rflow runs diff / rflow runs timeline, with --json for 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:

  1. Keyset (cursor) pagination, not just limit. Pages key on (created_at, id) newest-first and return an opaque cursor plus has_more; 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>. The archive exporter uses the same scrubber.
  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. Guarded, idempotent database indexes back the common ones.

The embedded UI — GET /

The workflows overview: per-workflow health, success rates, durations, tx counts and cursors

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:

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

Run detail: the trigger key, each step's status, and the send's tx hash and idempotency key

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.

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 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, 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
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 requires Authorization: Bearer <token>.
  • the UI shows a 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. The server also accepts the token via an X-Rflow-Token request header; the UI falls back to it automatically when a login 401s despite a sent token. Caveat: log/APM pipelines that redact Authorization usually record custom headers verbatim, so if your intermediary captures request headers into logs, treat those logs as secret or rotate with rflow token revoke.
  • /live and /health stay 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 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.