Skip to main content
sandtable run writes two files to its output directory (--out, or .sandtable/runs/<timestamp>-<domain>/):
  • results.json: a SuiteResult, 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.json re-renders it.
The TypeScript types are exported from the package (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:
A tool event’s violations lists the ids of hard invariants that call violated. When it also broke soft invariants, soft lists their ids.

Change

Usage

Tokens are grouped by role and model. 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.

Recipes