Archives¶
Every plan execution writes a JSON archive: aat run plan, each plan in aat run batch, aat prompt when it executes the plan, and the MCP server's execution tool. An archive records each request and response, how every input got its value, every assertion result, cleanup, and the outcome, so you can inspect a run long after it finished. The web UI is the viewer; the files are plain JSON for scripts too.
Where Archives Go¶
Archives are written to the directory given by --output, else the manifest's archives: entry, else _output/runs in the working directory. aat web, aat web view, aat run show, aat import, aat run clean, and aat run rebuild-summaries look for them the same way.
Layout¶
A single run gets its own directory:
_output/runs/
run-20260910-225958-d819f460/
archive.json
attempt-01.json
summary.json
A batch gets a directory holding batch.json and one run directory per executed plan (duplicate permutations skipped by dedup appear in batch.json but get no directory):
_output/runs/
batch-20260910-225919-0754c0ea/
batch.json
run-20260910-225919-5a04ca45/
archive.json
summary.json
run-20260910-225921-80a38128/
archive.json
summary.json
...
Directory names are run- or batch-, the local date and time (YYYYMMDD-HHMMSS), and eight random hex characters.
| File | Contents |
|---|---|
archive.json |
The full record of the run. With --retries, the final attempt |
attempt-NN.json |
An earlier attempt that --retries retried (attempt-01.json, attempt-02.json, …), in the same format as archive.json. Present only when the run was retried |
summary.json |
A small summary the web UI reads for its lists: run ID, timestamp, outcome, step counts, duration, plan name, attempt numbers, layers, and issues counts. Derived from archive.json; see Rebuilding Summaries |
batch.json |
Batch metadata (batch ID, source directory, layers, layer groups), one entry per run including skipped duplicates (plan name, run ID, outcome, counts, layers, permutation, duplicateOf), and the aggregate result |
What an Archive Contains¶
archive.json has these top-level keys:
| Key | Contents |
|---|---|
metadata |
Run ID, timestamp, environment name, graph version, AAT version, the plan as loaded (plan), the plan after graph defaults and layers were merged in (instantiatedPlan), the layers applied, and attempt/totalAttempts for retried runs |
steps |
One record per main and verification step (see below) |
cleanup |
Cleanup step records in the same format; absent when no cleanup ran |
cleanupSkipped |
Registered cleanups that did not run because they were no longer needed: node, cleanupFor, reason (released or when), and releasedBy or when. See API Graphs: Cleanup. Absent when none were skipped |
result |
outcome (passed, failed, error, aborted, or stopped), error, and durationMs, the run's wall-clock time (archives written before it was recorded omit it, and the web UI then sums the step durations) |
Each step record holds:
| Field | Contents |
|---|---|
stepId, node, startTime, durationMs |
Which step ran, and when: a retried step's start and duration cover every attempt and the waits between them. Archives written before 0.1.0 name the duration duration_ms; AAT reads both |
inputs |
The resolved input values |
request |
Method, full URL, headers, and body. When an override routed the step elsewhere, originalUrl holds the URL it would have used |
response |
Status, headers, and body |
outputs, displayOutputs |
Extracted outputs (after any Lua transform, whose script is in transformScript) and the plan's display outputs |
resolutions |
How each input got its value: source (such as plan_default, graph_default, layer, expression, plan_from, select_edge, or fallback_pool), the layer that set the value, the raw and final value, the step and output it came from, and whether a constraint passed |
selections |
For inputs picked from an array: source step and field, array size, filter and how many elements passed it, strategy, and the selected index |
validation |
Each assertion's type, pass or fail, and message |
expectFailure |
For negative steps: expected statuses, actual status, pass or fail |
oasValidation |
Request and response checks against the OpenAPI spec |
errorClassification, error, retryCount, retriedOn |
Error category and detail for a failed step, and the category of each step-level retry |
cleanupFor |
On a cleanup step: the step whose resource it releases, or the cleanup step before it in a cleanup chain. Cleanup step IDs are unique within a run (deleteCart, then deleteCart_2) |
whenError |
On a cleanup step: why its pairing's when condition couldn't be evaluated, such as an output the step didn't return. The cleanup ran anyway |
What Is Redacted, and What Is Not¶
Archives redact the credentials AAT knows about, not every piece of sensitive data, so an archive, an exported .aar/.aab, or a CI artifact is not automatically safe to commit or share. Before you attach one to a ticket, check what it holds.
Redacted:
- Credential headers, by name. In requests and responses, the values of
Authorization,Proxy-Authorization,X-API-Key,X-Auth-Token,Cookie, andSet-Cookie(any capitalization) are replaced with[REDACTED]. The header name stays. - Known secrets, everywhere. AAT collects the resolved secret credentials of every
authblock that can apply to the run — the environment's, its host overrides' (such as the shop's payments key), the plan's, and those of overlays and their overrides — plus the LLM API key and, inaat run planandaat run batch, the access token the run authenticated with. A secret credential is any credential except the oauth2usernameandclientId, which identify an account rather than prove it. Each secret is redacted from every string in the archive: URLs and query parameters, headers under any name, request and response bodies, inputs and resolved values, outputs and display outputs, error and assertion messages, and the archived plan. In a JSON body only string values change; keys, numbers, and layout are kept.batch.jsonis redacted the same way. - How a secret matches. A string that equals a secret is always replaced. A secret of at least eight characters is also replaced inside longer strings, including its URL-escaped forms. A shorter secret is not, because it is an ordinary word in data too: with the shop sandbox's
demopassword,"password": "demo"is redacted butdemo@example.comis kept. - Plan credentials. In
metadata.planandmetadata.instantiatedPlan, the value of everysource: literalcredential in the plan'sauthblock and the plan's credential headers are replaced with[REDACTED], whatever their length.source: envreferences keep their variable names.
Stored as-is:
- Tokens AAT does not know as secrets anywhere other than a credential header, such as a session token a login step returns, or the access token of a run started by
aat promptor the MCP server'sexecute_plan, echoed in a body. - Other sensitive data an API returns or a plan sends, such as personal data, and a secret written as a literal step value, which is not a credential AAT knows about.
- A short secret inside a longer string, as described above.
Terminal output and the --json summary are not redacted: they are the live view of the run, like the requests themselves.
summary.json holds no request data. The live-state dump written by --dump-state redacts nothing at all; see Checkpoints: Security.
Viewing Archives¶
aat web serves the archive directory; aat web view opens one run or batch in the browser:
aat web view # the run list
aat web view latest # the newest run or batch
aat web view run-20260910-225958-d819f460 # a run
aat web view batch-20260910-225919-0754c0ea # a batch
A reference is a directory name in the archive directory, saved names included; AAT treats it as a batch when the directory holds batch.json. latest opens whichever run or batch in the archive directory has the newest timestamp. If a server is already listening on the port (--port, default 9119), aat web view opens the page there; otherwise it starts a temporary server that runs until you press Ctrl+C.
Viewing a File¶
The reference can also be a file: an exported .aar or .aab, or a bare archive.json or batch.json, for example from a downloaded CI artifact. AAT loads it into memory and serves it from a temporary server, with no project, manifest, or archive directory needed, and writes nothing to disk:
aat web view exported-run.aar
aat web view nightly.aab
aat web view downloaded-artifacts/run-20260910-225958-d819f460/archive.json
Next to an archive.json, sibling attempt-NN.json files are loaded so the attempt selector works; next to a batch.json, the member run directories are loaded. A file is read-only: save, rename, export, and import are unavailable.
Known issue: aat web view <file> always starts its own server, so it fails with bind: address already in use while aat web (or another aat web view) holds the port. Pass a different --port.
Inspecting a Run from the CLI¶
aat run show prints what an archive recorded, for a terminal, a script, or an AI coding assistant that cannot open the web UI. It only reads: it writes nothing, not even summary.json.
aat run show latest # the steps
aat run show latest --step checkout # one step
aat run show latest --step checkout --response --shape # the structure of its response
aat run show latest --step checkout --response --path orderId
The run is one of:
latest: the newest run, runs inside batches included, even those of a batch that is still running- a run ID, such as
run-20260910-225958-d819f460, looked up at the top of the archive directory and then inside each batch - a batch ID and a run ID joined by a slash, such as
batch-20260910-225919-0754c0ea/run-20260910-225919-5a04ca45 - a path to a run directory, an
archive.jsonorattempt-NN.json, or an exported.aarfile - a batch ID, such as
batch-20260910-225919-0754c0ea, or a path to a batch directory or itsbatch.json, which shows the batch instead of a run (see A Batch)
IDs are looked up in the archive directory, as Where Archives Go describes.
Without --step, it lists the steps: ID, node, HTTP status, pass or fail, duration, and output names, then the verification steps and the cleanup steps, each with the step it releases. After the shop's smoke recipe:
run-20260912-224235-0842d2b8 PASSED 2ms
plan: Buy one in-stock product and pay by card
archive: /path/to/shop/_output/runs/run-20260912-224235-0842d2b8/archive.json
# STEP NODE STATUS RESULT TIME OUTPUTS
1 listProducts listProducts 200 pass 0ms currency, products
2 createCart createCart 201 pass 0ms cartId, status
3 addItem addItem 201 pass 0ms cartId, lineCount, subtotal
4 checkout checkoutCart 201 pass 0ms currency, discount, orderId, receiptNumber, shipping, status, subtotal, tax, ta…
5 paymentCharge paymentCharge 201 pass 0ms amountDisplay, orderStatus, paymentId, status
cleanup:
# STEP NODE STATUS RESULT TIME FOR
1 deleteOrder deleteOrder 204 pass 0ms checkout
2 deleteCart deleteCart 204 pass 0ms createCart
--step takes a step ID, or a node name when that node ran only once. On its own, it prints:
- the step's method and URL, status, result, duration and retries, and error
- the inputs, each with where its value came from
- the outputs, with arrays and objects by their size
- assertion counts, with each failure
- warnings, such as a selection tie
- the sizes of the request and response bodies
A step that failed before its request was sent still shows the inputs resolved up to the failure, and the one that failed.
step checkout (node checkoutCart)
POST http://localhost:8765/us/v1/carts/cart_0001/checkout
status 201 pass 0ms
inputs:
cartId "cart_0001" plan_from createCart.cartId
customerEmail - optional_skip
deliveryDate - optional_skip
notes - optional_skip
postalCode "78701" expression {{env.postalCode}}
shippingTier "standard" graph_default
outputs:
currency "USD"
orderId "ord_0001"
status "created"
total 10340
...
assertions: 1 passed, 0 failed
request body: 48 bytes
response body: 518 bytes
These flags print one part of the step instead:
| Flag | Prints |
|---|---|
--request, --response |
The request or response body, as indented JSON |
--inputs, --outputs |
The resolved inputs or the extracted outputs, as JSON |
--resolutions |
How each input got its value, as JSON. Each entry is the input's resolution record (source, fromStep and fromOutput, expression, the pool pick, a constraint) with the selection that picked the value, and an error for an input that couldn't be resolved |
--path PATH |
Only what a gjson path selects: lines.0.sku, or lines.#.sku for every element. The $.lines[0].sku form works too. Without a part flag, it reads the response body |
--shape |
The part's structure instead of its values. Without a part flag, the response body's |
--max-bytes N |
Cut a printed part after N bytes, 65536 by default, with a note on stderr; 0 prints everything |
--json |
The step list or the step as JSON with snake_case keys, or the shape as a JSON array. A step's JSON includes resolutions, warnings, and validation, its assertion results as archive.json names them (archive.json itself uses camelCase keys) |
--compact |
JSON on one line, for a script or jq: a part, with or without --path, and with --json the step list, the step, or the shape. With --shape, or without a part, it needs --json |
Without --step, a part flag or --path prints that part of every step that has it, cleanup steps included, one line each. One command then answers a question about the whole run:
$ aat run show latest --response --path error.code
STEP NODE VALUE
captureBeforeConfirm capturePaymentIntent "payment_intent_unexpected_state"
refundTwice createRefund "charge_already_refunded"
With --json or --compact, it prints a JSON array of step_id, node, and value. --shape still needs --step.
--shape is the way to learn a large response. It prints one line per path: the path's type, an array's item count, how many objects hold a key when not all of them do, and a sample value. The elements of an array are merged, so a key that only some elements hold, or a value that is sometimes null (string|null), shows up. Each path works as an extract rule in a template and as --path:
$ aat run show latest --step checkout --response --shape
orderId string "ord_0001"
receiptNumber string "RCPT-US-0001"
status string "created"
...
total number 10340
totalDisplay string "$103.40"
lines array 1 item
lines.# object
lines.#.sku string "SKU-1001"
lines.#.quantity number 1
lines.#.lineTotal number 8999
createdAt string "2026-09-13T03:42:35Z"
Archives several hundred megabytes in size still list their steps in under a second, and so does the shape of a response tens of megabytes long.
A path that matches nothing, an unknown step, or an unknown run exits with code 2, and the message says what does exist: the top-level keys, the step IDs, or the forms a run reference takes. Archives are redacted when they are written, so aat run show prints only what the archive holds; see What Is Redacted, and What Is Not. The MCP server's get_sample_response takes the same path and shape; see MCP Server.
A Batch¶
A batch ID, or a path to a batch directory or its batch.json, shows the batch: its totals from batch.json, one row per permutation, and what cleanup did across its runs.
$ aat run show batch-20260913-160341-4a6502e8
batch-20260913-160341-4a6502e8 PASSED 4m30s
batch: _output/runs/batch-20260913-160341-4a6502e8/batch.json
runs: 220, 94 passed, 0 failed, 0 errors, 126 skipped as duplicates
# RUN PLAN LAYERS RESULT STEPS TIME
1 run-20260913-160341-5a04ca45 full-refund card-visa,currency-usd pass 9/9 1.2s
2 - full-refund card-visa,currency-eur skip - - duplicate of full-refund [card-visa,currency-usd]
...
cleanup:
NODE RAN FAILED RELEASED WHEN
cancelPaymentIntent 8 0 0 24
deleteCustomer 93 0 1 0
reconcilePaymentIntent 32 0 61 0
- RAN and FAILED count cleanup steps across the runs.
- RELEASED and WHEN count the pairings skipped: because a step released the resource, or because the pairing's
whenwas false. - Cleanup failures: a
cleanup failures:list names each failed cleanup step by run, with its status or error. --jsonprints the same as a document withsnake_casekeys.- Steps: a batch has no steps of its own, so
--stepand the part flags need a run:batch-ID/run-ID.
Exporting and Importing¶
A run or batch can travel as a single zip file.
Export¶
Export is a web UI feature; there is no CLI command for it. In the run list, each row has a download arrow (Export run or Export batch), and run and batch detail pages have an Export button.
- A run exports as
.aar, a zip of its directory's JSON files:archive.json,summary.json, and anyattempt-NN.json. - A batch exports as
.aab:batch.jsonplus every member run's directory.
The file is named after the run or batch ID, or its saved name (run-20260910-225958-d819f460.aar, checkout-baseline.aar). The content is the archive as written, so the redaction limits above apply to exports too. The same downloads are available at GET /api/runs/{id}/export and GET /api/batches/{id}/export.
Import¶
- Web UI: the Import button on the run list accepts
.aarand.aabfiles, adds them to the served archive directory, and opens the imported run or batch. - CLI:
aat import FILEextracts into the archive directory. - API:
POST /api/importwith a multipart form whosefilefield holds the archive (100 MB limit). The response is{"ref": …, "name": …, "type": "run"|"batch"}; a name that already exists returns409.
$ aat import "nightly run #3.aar"
Imported run "nightly-run-3" → /path/to/shop/_output/runs/nightly-run-3
| Flag | Type | Default | Description |
|---|---|---|---|
--name |
string | derived from the file name | Directory name for the imported archive |
--output |
path | manifest archives, else _output/runs |
Archive directory to import into |
--manifest |
path | auto-discovered | Explicit path to aat-project.yaml |
Without --name, the web UI, the API, and the CLI derive the directory name from the file name: the .aar/.aab extension is dropped, every character other than letters, digits, ., _, and - becomes -, repeated dashes collapse, and leading and trailing dashes go. A result that starts with run- or batch- gets a ! prefix, so run-20260910-225958-d819f460.aar imports as !run-20260910-225958-d819f460. An archive imported without --name is therefore always a named run that aat run clean never deletes.
--name must be a single directory name, without path separators, and a name that starts with run- or batch- gets the same ! prefix.
An import never overwrites or merges. If the target directory exists, it fails (exit code 2 from the CLI):
aat: import: directory "nightly-run-3" already exists
The file must be a zip with archive.json (a run) or batch.json (a batch) at its root. Symlinks, absolute paths, and .. entries are rejected, as are zips with more than 10,000 entries or more than 500 MB uncompressed.
Naming and Saving Runs¶
A directory whose name starts with run- or batch- is auto-generated and a candidate for aat run clean. Any other name, or a name starting with !, marks a named (saved) run or batch: it is never deleted by aat run clean and shows under the Saved filter in the web UI's run list.
On a run or batch detail page in the web UI:
- Save opens a name field, prefilled with the plan name (for a batch, its source). Save in that form renames the directory to the name you typed; Save as-is keeps the ID and adds the
!prefix (!run-20260910-225829-a4c05ee5). A typed name that starts withrun-orbatch-also gets the!prefix. - A saved run shows its name, a pencil (Edit name) to rename it again, and Unsave, which restores the original ID from the archive's metadata.
A name cannot contain / or \ or be . or .., and renaming fails if a directory with the new name exists. Naming works only when serving a directory, not a file.
The same operations are PUT /api/runs/{id}/name with a body such as {"name": "run-keep"} (an empty name saves as-is) and DELETE /api/runs/{id}/name to unsave; batches use /api/batches/{id}/name. The response carries the new directory name to use in URLs:
{"ref":"!run-keep","name":"run-keep"}
Because only the directory name matters, renaming a directory by hand works too. Quote a ! name in the shell, for example '!run-20260910-225829-a4c05ee5'.
Pruning Old Runs (aat run clean)¶
The archive directory grows by one directory per run or batch. aat run clean deletes auto-generated run and batch directories older than a cutoff:
aat run clean # delete runs older than 7 days
aat run clean --days 30 # keep a month
aat run clean --dry-run # list what would be deleted, remove nothing
| Flag | Type | Default | Description |
|---|---|---|---|
--days |
int | 7 |
Delete runs older than this many days |
--dry-run |
bool | false |
Preview without deleting |
--output |
path | manifest archives, else _output/runs |
Archive directory to clean |
--manifest |
path | auto-discovered | Explicit path to aat-project.yaml |
Only top-level directories whose names start with run- or batch- are candidates, and their age comes from the timestamp in the name, not from file times. Deleting a batch directory deletes its member runs with it. Named directories are skipped and counted:
$ aat run clean --dry-run --days 0
aat: dry run — previewing cleanup in _output/runs (older than 0 days)...
would delete: batch-20260910-225858-2e1dcfc5 (age: < 1 day)
would delete: batch-20260910-225919-0754c0ea (age: < 1 day)
would delete: run-20260910-225742-06f1a24d (age: < 1 day)
...
would delete: run-20260910-230834-1a340655 (age: < 1 day)
warning: run-garbage: cannot parse timestamp: unrecognized format: run-garbage
aat: would delete 16 run(s), skipped 2 saved, 1 errors
Known issue: a run- or batch- directory whose name carries no parseable timestamp, like run-garbage above, is reported as an error and left alone, but the command still exits 0. In scripts, check the summary line for errors.
Rebuilding Summaries (aat run rebuild-summaries)¶
The web UI builds its lists from each run's summary.json. After an AAT upgrade adds summary fields, older summaries lack them. Rebuild every summary from the full archives without re-running anything:
$ aat run rebuild-summaries
aat: rebuilding summaries in /path/to/shop/_output/runs...
aat: rebuilt 3 summaries
It rewrites summary.json for every top-level run directory with an archive.json and for every run inside a batch directory (auto-generated or named), using --output or the manifest's archives like the other commands. The next web UI listing picks the new summaries up. A run with no summary.json at all does not need this: the web UI computes and writes the summary the first time it lists the run.
Known issue: batch.json is not recomputed, so batch-level counts stay as they were written.
Source: archive/types.go, archive/writer.go, archive/redact.go, archive/transfer.go, archive/naming.go, archive/clean.go, archive/rebuild.go, engine/archive.go, cmd/aat/run_shared.go, cmd/aat/import_cmd.go, cmd/aat/run_clean_cmd.go, cmd/aat/run_rebuild_cmd.go, cmd/aat/web_cmd.go, server/handlers.go, server/service.go, server/web/src/routes/*.svelte.