# Build an agent audit trail from Reelier run receipts

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.

Markdown twin of https://www.reelier.com/guides/agent-audit-trail-receipts — a step-by-step Reelier how-to. Every command below is a real Reelier command and every output block is verbatim, never a mockup.

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

## 01 · 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.)

```bash
reelier run registry-latest.skill.md
```

```
✓ Step 1 — Latest dist-tag from the registry [passed] 134ms

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

## 02 · 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.

```
{"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}}
```

## 03 · 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.

```bash
reelier run posthog-weekly-pull.skill.md
```

```
✗ 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
```

## 04 · 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.

```bash
reelier push registry-latest.skill.md --dry-run
```

```
  [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.
```

## The 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}}
```

The single line the failed run in step 3 appended to `.reelier/runs/posthog-weekly-pull.jsonl` — `passed: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.

## Related

- [Run receipt — the audit-trail record this guide builds, defined field by field](https://www.reelier.com/learn/run-receipt)
- [Level-0 (zero-token) replay — why the receipt's token counters read 0](https://www.reelier.com/learn/level-0-replay)
- [Guide: diff two runs in the trail and fail CI on drift](https://www.reelier.com/guides/diff-two-agent-runs)
- [Guide: compile the SKILL.md you replay here from a recorded session](https://www.reelier.com/guides/from-session-compile)
- [Reelier for GitHub Actions — keep the audit trail running on every push](https://www.reelier.com/for/github-actions)

## Sources

- [seldonframe/reelier — the open-source (AGPL-3.0) CLI every command here runs](https://github.com/seldonframe/reelier)
- [reelier src/runner.ts — the RunRecord schema: the exact shape of every line on the trail](https://github.com/seldonframe/reelier/blob/main/src/runner.ts)
- [NIST CSRC glossary: audit trail — the pre-agent definition this record extends](https://csrc.nist.gov/glossary/term/audit_trail)

Other guides: [How to snapshot-test a Claude Code workflow](https://www.reelier.com/guides/snapshot-test-claude-code-workflows) · [How to record MCP tool calls with a wrap proxy](https://www.reelier.com/guides/record-mcp-tool-calls) · [How to replay an agent workflow at 0 LLM tokens](https://www.reelier.com/guides/replay-agent-workflow-zero-tokens) · [How to gate CI on agent drift](https://www.reelier.com/guides/ci-gate-for-agent-drift) · [Replay an agent skill on every PR with the Reelier GitHub Action](https://www.reelier.com/guides/github-action-agent-replay) · [Test your MCP server for regressions by replaying a golden run](https://www.reelier.com/guides/test-mcp-servers) · [Run a deterministic cron agent by scheduling a recorded replay](https://www.reelier.com/guides/deterministic-cron-agents) · [Configure BYOK escalation levels: L0, L1, and L2](https://www.reelier.com/guides/byok-escalation-levels) · [Compile a skill from an existing session with reelier from-session](https://www.reelier.com/guides/from-session-compile) · [Diff two agent runs with reelier diff (exit 1 on drift)](https://www.reelier.com/guides/diff-two-agent-runs) · [Convert an LLM agent skill into a deterministic, assertion-checked skill](https://www.reelier.com/guides/convert-agent-skill-deterministic)
