sandtable run writes two files to its output directory (--out, or .sandtable/runs/<timestamp>-<domain>/):
results.json: aSuiteResult, described below. It is the source of truth for a run.report.html: a self-contained rendering of it, with no external assets, that you can attach to a pull request or send to a customer.npx sandtable-ai report results.jsonre-renders it.
SuiteResult, TrialResult, CheckResult and the rest). While sandtable is alpha, fields may still be added or change; version will change when the format breaks.
SuiteResult
TaskSummary
FailureMode
TagScore
SoftScore
How often a soft invariant (soft: true) was broken. Soft invariants never fail a trial or change pass^k.
PendingItem
TrialResult
CheckResult
Violation
SoftCheck
TrajectoryEvent
One of:violations lists the ids of hard invariants that call violated. When it also broke soft invariants, soft lists their ids.
Change
Usage
input excludes cache reads and writes, following the Anthropic convention. The Claude, Bedrock Converse and OpenAI-compatible adapters record usage automatically; custom modules can call recordUsage; the HTTP adapter cannot see your agent’s model calls, so it records none.
maxTokensStops counts the calls whose reply stopped at the model’s output-token limit: stop reason max_tokens on Claude and Converse, finish_reason: "length" on an OpenAI-compatible server. What the agent or caller said after such a call may be cut off or empty, and the grader scores it as said. The count also includes replies sandtable discarded and asked for again, such as a Converse reply whose <reasoning> never closed, since every call is recorded. The field appears only once a model has such a call, so a missing field means none did, or that the run is older than the field. The terminal summary and the HTML report show the count when it isn’t zero.
costUsd is an estimate at list prices from PRICES in src/run/usage.ts, which holds verified prices for Claude models, plus any prices passed with --prices. If any model in the run has no known price, costUsd is null and reports show n/a.
Claude’s usage is keyed by the model that served it. On the Anthropic API, that is the model Claude reports, which a refusal fallback can change. On Bedrock, it is the requested ID, such as global.anthropic.claude-opus-5-5 or eu.anthropic.claude-opus-5-5. A geo cross-region profile (eu., us., apac., us-gov. …) is priced at 1.1× the built-in price, since “Regional and multi-region endpoints include a 10% premium over global endpoints” for Claude 4.5 models and later (Anthropic pricing). A global. profile and the Anthropic API pay the built-in price. A price passed with --prices for the exact ID is used as is. An inference profile ARN names no model, so its usage is recorded under the bare name Claude reports and always priced at the built-in (global) price; a --prices entry for the ARN is never read. A bare anthropic. ID, such as an in-region call, also gets the built-in (global) price, which is unverified for in-region calls. Runs from before keyed Bedrock Claude usage by the bare model name, so they’re priced as global.