Guides · how-to

Build an agent audit trail from Reelier run receipts

▸ What you'll do

Every reelier run appends one line to .reelier/runs/<skill>.jsonl, written by the harness from observed results — not by the model, from memory. That append-only file is the audit trail: one JSON object per run, with each step's outcome, timing, and the run's measured token count. Nothing to stand up, no extra service; the record is a side effect of replaying.

This guide walks the loop end to end: replay a compiled skill, read the receipt file it grows, see what a failed run looks like on the record, and optionally sync the whole trail to a durable dashboard. The definition of a run receipt — every field, the honesty rules — lives in the glossary; here we just build and read one.

Before you start

  • Reelier installed — npm i -g reelier (or use npx -y reelier@latest in place of reelier in every command below).
  • One compiled skill (SKILL.md) to replay — compile one from a session you already ran, then come back here.
  • Reelier replays tool calls (mcp__server__tool, http.get/http.post), not Bash/Read/Edit/Write — so the trail records the API/MCP calls, which is exactly what an auditor cares about.
  • Nothing else. No account, no API key: deterministic replay never calls a model, so building the trail costs 0 tokens.

Replay the skill — the run writes the receipt

reelier run re-executes the recorded tool calls against the live systems, checks each assertion, and appends one JSON line to .reelier/runs/<skill>.jsonl. At the default --max-level 0 no LLM client is ever constructed, so the deterministic replay costs 0 tokens and the run's token counters are a measured zero, not an estimate.

The append is the whole point: the file is never overwritten. Run the skill once per scheduled tick and the record grows one line at a time into a dated history. (A --dry-run executes nothing and writes no receipt — only a real run lands on the trail.)

terminal
reelier run registry-latest.skill.md
Reelier · output
✓ Step 1 — Latest dist-tag from the registry [passed] 134ms

PASSED: 1/1 steps ok, 0 failed, 134ms total
verbatim output

Open the receipt file — the audit record itself

The trail is plain JSON Lines: one run per line, in the order they happened. Here is .reelier/runs/registry-latest.jsonl after three replays — three lines, appended, never edited.

Read a line left to right and you have the audit answer without a human in the loop: skill and the startedAt/finishedAt timestamps (when), passed (the verdict), steps (each with its outcome, escalation level, ms, and a verbatim failures list), and totals — step counts by outcome plus the two token counters, both 0 on this read-only run. The full field-by-field spec, and why a zero-assertion step reads unchecked and never passed, is in the glossary; for auditing you mostly grep it.

Because it is one machine-readable object per run, the file is queryable: grep '"passed":false' pulls every failed run, and reelier bench <skill> reads the same file to roll the history up into a pass rate and per-outcome totals.

Reelier · output
{"skill":"registry-latest","startedAt":"2026-07-22T10:41:23.836Z","finishedAt":"2026-07-22T10:41:23.970Z","passed":true,"steps":[{"n":1,"title":"Latest dist-tag from the registry","level":0,"outcome":"passed","ms":134,"failures":[]}],"totals":{"steps":1,"passed":1,"unchecked":0,"skipped":0,"failed":0,"ms":134,"llmInputTokens":0,"llmOutputTokens":0}}
{"skill":"registry-latest","startedAt":"2026-07-22T10:41:24.194Z","finishedAt":"2026-07-22T10:41:24.344Z","passed":true,"steps":[{"n":1,"title":"Latest dist-tag from the registry","level":0,"outcome":"passed","ms":148,"failures":[]}],"totals":{"steps":1,"passed":1,"unchecked":0,"skipped":0,"failed":0,"ms":148,"llmInputTokens":0,"llmOutputTokens":0}}
{"skill":"registry-latest","startedAt":"2026-07-22T10:41:24.565Z","finishedAt":"2026-07-22T10:41:24.702Z","passed":true,"steps":[{"n":1,"title":"Latest dist-tag from the registry","level":0,"outcome":"passed","ms":137,"failures":[]}],"totals":{"steps":1,"passed":1,"unchecked":0,"skipped":0,"failed":0,"ms":137,"llmInputTokens":0,"llmOutputTokens":0}}
verbatim output

A failed run lands on the record too

An audit trail that only logged successes would be useless. When a run diverges, Reelier records it rather than hiding it: the failing step keeps its verbatim reason and a why.trigger, every step after it is recorded skipped, and the run's passed flips to false. Below, a skill is replayed with a required variable left unset, so step 1 fails on the unbound {{project_id}} and step 2 is skipped — and that exact shape is appended to the file (see the receipt at the foot of this page).

This is what makes the file an audit log and not a success log: a red run is on the record with the same fidelity as a green one. Replay is also read-only by default — a recorded write step does not re-fire unless you pass --allow-writes (idempotent writes only; a destructive step still needs --yes), and a held-back write is itself recorded as a failed step with the refusal in its failures list.

terminal
reelier run posthog-weekly-pull.skill.md
Reelier · output
✗ Step 1 — Total events, last 7 days [failed] 1ms
    - Template fill failed: Unbound template variable {{project_id}}
○ Step 2 — Pageviews, last 7 days [skipped] 0ms

FAILED: 0/2 steps ok, 1 failed, 1ms total
verbatim output

Optional — push the trail to a durable, shareable dashboard

The receipt file is yours, on your machine. To keep a durable copy an auditor or teammate can browse, reelier push syncs new records to a dashboard — opt-in, and only when both REELIER_CLOUD_URL and REELIER_CLOUD_KEY are set. A per-skill cursor tracks what has already been sent, so re-running only pushes new lines.

Rehearse it with --dry-run first: it reads the local trail and reports exactly what would sync, with no network call and no cloud credentials required. Add --share on a real push to mint a public receipt link for a single run.

terminal
reelier push registry-latest.skill.md --dry-run
Reelier · output
  [0] would push
  [1] would push
  [2] would push

Dry run: would push 3 new record(s) for skill 'registry-latest' (cursor 0 -> 3).
Dry run: no network calls made, no state written.
verbatim output

The receipt

Reelier · replay receipt
{"skill":"posthog-weekly-pull","startedAt":"2026-07-22T10:42:24.812Z","finishedAt":"2026-07-22T10:42:24.814Z","passed":false,"steps":[{"n":1,"title":"Total events, last 7 days","level":0,"outcome":"failed","ms":1,"failures":["Template fill failed: Unbound template variable {{project_id}}"],"why":{"trigger":"Template fill failed: Unbound template variable {{project_id}}"}},{"n":2,"title":"Pageviews, last 7 days","level":0,"outcome":"skipped","ms":0,"failures":[]}],"totals":{"steps":2,"passed":0,"unchecked":0,"skipped":1,"failed":1,"ms":1,"llmInputTokens":0,"llmOutputTokens":0}}
verbatim output

The single line the failed run in step 3 appended to .reelier/runs/posthog-weekly-pull.jsonlpassed:false, step 1 failed with its verbatim reason and a why.trigger, step 2 skipped, and the token counters a measured 0 (no model was ever constructed at Level 0). Produced on 2026-07-22 with reelier v0.14.0 by replaying a verbatim copy of examples/loops/posthog-weekly-pull.skill.md with no --var set. A red run on the record with the same fidelity as a green one — that's the audit trail.

FAQ

Is the .jsonl file the whole run history, or just the last run?

The whole history. Every reelier run appends one line and never overwrites, so the file grows one dated record per run (a --dry-run writes nothing). To audit it, read or grep the file directly, or run reelier bench <skill> to roll every line up into a pass rate and per-outcome totals.

Could the agent fake its own audit trail?

The record is written by the harness from observed tool results, not by the model — so it isn't the agent's account of its own work. But it is plain JSON on disk: nothing signs it, and anyone with the file can edit it. Treat it like any artifact you commit and review, and anchor a drift check to a baseline you have read.

Does building the trail cost tokens?

No, for the deterministic core. At the default --max-level 0 the runner never constructs an LLM client, so replay spends 0 tokens and the receipt's counters report a measured zero. Judgment steps (opt-in BYOK Level 1/2 escalation) do spend, and that usage is recorded on the step and in the totals; a destructive step is never auto-escalated.

Reading as an agent? This page has a markdown twin: /guides/agent-audit-trail-receipts/md