> ## Documentation Index
> Fetch the complete documentation index at: https://sandtable.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> sandtable is an early-alpha, open-source CLI (TypeScript, MIT): https://github.com/sandtable-ai/sandtable. Run it as `npx sandtable-ai <command>`, never with the bare name: npm's `sandtable` is an unrelated package. If a flag here disagrees with `npx sandtable-ai <command> --help`, trust the help output.
> Grading is deterministic: pass/fail comes from the world's end state, invariants and the tool-call log, never from an LLM judge.
> To draft a domain for a user, read /guides/init and /reference/spec, then run `npx sandtable-ai validate`, `lint` and `review` before any model-backed run. Keep all example data fictional.
> In Claude Code, the sandtable plugin teaches these steps as skills: `/plugin marketplace add sandtable-ai/sandtable`, then `/plugin install sandtable@sandtable`.
> Every page is available as Markdown by appending .md to its URL. /llms.txt lists every page, /llms-full.txt contains the whole site, and https://sandtable.dev/mcp is a search MCP server for these docs.

# Draft a domain from an OpenAPI spec, SOP and transcripts

> Use sandtable init to draft world tables, tools, invariants and tasks from a customer's OpenAPI spec, SOP and call transcripts, then review each item.

Writing a domain by hand is the main cost of adopting sandtable. `sandtable init` drafts one from what a customer hands over during discovery, then leaves a person to review it. Every inferred item stays visible as a `TODO` or `review: pending` until someone accepts it.

| Input | Drafts | Model |
| - | - | - |
| `--openapi` | world tables, seed rows from examples, tools | none: deterministic |
| `--sop` | invariants, each quoting its source sentence | Claude |
| `--transcripts` | personas, tasks and fictional seed rows | Claude |

> [!WARNING]
>
> The SOP and transcript steps call Claude. They have been verified in live runs on the bundled freight sources ([results](/results)), but a model can still misread a rule or a call. Treat every drafted item as a proposal until a person accepts it.

## The whole flow

**1. Draft from the API**

```bash theme={null}
npx sandtable-ai init --openapi path/to/openapi.yaml -o sandtable
npx sandtable-ai validate sandtable
```

Writes `sandtable/sandtable.yaml`, a placeholder `policy.md` and an empty `tasks.yaml`. It refuses to overwrite existing files unless you pass `--force`. It reads OpenAPI 3.x, in YAML or JSON. It writes nothing for a Swagger 2.0 spec (convert it to OpenAPI 3 first) or for a spec it can't draft a single tool from.

**2. Add invariants from the SOP**

```bash theme={null}
npx sandtable-ai init --sop policy.md -o sandtable
```

Appends invariants to the existing domain and replaces the placeholder `policy.md` with the SOP.

**3. Add tasks from transcripts**

```bash theme={null}
npx sandtable-ai init --transcripts calls/ -o sandtable
```

Appends personas, tasks and any fictional seed rows the tasks need.

**4. Review, lint, then run**

```bash theme={null}
npx sandtable-ai review sandtable              # what still needs a human decision
npx sandtable-ai lint sandtable                # ready, solvable, non-vacuous, graded (no model calls)
npx sandtable-ai run sandtable --agent noop --user scripted   # the floor, before any real agent
```

You can also pass all three inputs at once: `npx sandtable-ai init --openapi api.yaml --sop policy.md --transcripts calls/ -o sandtable`. The steps run in the order above.

## From an OpenAPI spec (deterministic)

No model is involved: the same spec always produces the same draft.

| OpenAPI | Draft |
| - | - |
| `/loads/{load_id}` | table `loads` with key `load_id` |
| a collection without an item path | key from the schema (`<singular>_id`, then `id`), else `<singular>_id` |
| `GET /loads/{load_id}` | tool with `get: loads, key: $load_id` |
| `GET /loads?origin=&status=` | tool with `list: loads, where: {...}`; free-text strings match with `$contains`, enums and numbers with equality |
| query param `default` | param `default`, applied when the agent omits it |
| `limit`, `offset`, `page`, `cursor`, … | ignored (pagination) |
| `POST /bookings` | tool with `create: bookings, values: {body fields}` |
| `PUT/PATCH /orders/{id}` | tool with `update: orders, key: $id, set: {body fields}` |
| `/customers/{customer_id}/orders` | parent params become filters and create values |
| `POST /orders/{id}/cancel` (no GET) | action: a `get` on the parent plus a review note to model the effect |
| a body param named like another table's key | existence `check` on that table |
| a schema composed with `allOf` | its members' properties and required lists merged, in bodies, responses and params |
| a field or param named `@type` or `pickup-window` | param `at_type` or `pickup_window`, since `$` references only name letters, digits and `_`; the op still writes `"@type": $at_type` |
| a `{param}` in the path that the spec doesn't declare | a required string param |
| `example` on a response schema or its media type, and each `value` in the media type's `examples` (an `externalValue` is not fetched) | seed rows; an array is several rows. When several examples share a key, the one with the most fields is kept, with a warning |

It cannot infer some things, so they become `# TODO(review):` comments next to the tool:

* **409/422/400/403 responses**: encode the condition as a `check` with a `where`.
* **Side effects in the description** ("the load becomes booked"): add the `update` op on the other table.
* **Action endpoints**: replace the placeholder `get` with the ops that model the action.

It also leaves out several things and lists them as warnings:

* header and cookie params
* non-scalar fields (arrays, objects)
* `DELETE` operations
* webhooks, which the API sends to the client rather than serves: OpenAPI 3.1 `webhooks`, operations' `callbacks`, and the paths where a client receives events or subscribes to them. A path counts as one only when it has a listener's shape:

  * every operation on it is tagged "notification listener" or "events subscription", and it takes only `POST` or `DELETE`, or its `POST` takes a callback URL (a string `callback` field)
  * it is `/listener/<event>`, with `<event>` a name ending in `Event`, and it takes only `POST`
  * it is `/hub` or `/hub/{id}`, and `POST /hub` takes a callback URL

  A resource such as `/hubs`, `/listener/restart` or `/events/{id}/subscriptions` is still drafted.

Tables with no examples get a `TODO: add seed rows` comment that lists the fields seen in the spec.

## From an SOP (Claude, grounded)

```bash theme={null}
npx sandtable-ai init --openapi api.yaml --sop policy.md -o sandtable   # draft, then add invariants
npx sandtable-ai init --sop policy.md -o sandtable                       # add invariants to an existing domain
```

Claude reads the SOP alongside the domain's tools, params, tables and known fields, and proposes invariants in the six existing [kinds](/reference/spec#invariants), using structured outputs. Nothing it proposes is trusted as-is:

* Every invariant must **quote** the SOP. A quote that does not appear verbatim gets a warning.
* Proposals that reference an unknown tool, param, table or field, or that are incomplete, are **dropped**, and the reason is printed.
* Survivors are appended to the domain (existing comments are kept) with `review: pending` and `source: { file, quote }`.
* Rules that cannot be checked from tool calls and records (wording, tone, read-backs) are written as **soft rule** comments, for the planned LLM-judged checks ([#16](https://github.com/sandtable-ai/sandtable/issues/16)). They are notes, not `soft: true` invariants.

The SOP also becomes `policy.md` when the domain still has the placeholder. Pass `--model` to choose the drafting model (default `claude-opus-5-5`).

## From call or chat transcripts (Claude, grounded)

```bash theme={null}
npx sandtable-ai init --transcripts calls/ -o sandtable          # a directory of .txt/.md files, or one file
npx sandtable-ai init --openapi api.yaml --sop policy.md --transcripts calls/ -o sandtable   # all three steps
```

Each transcript becomes one task:

* a hidden-goal instruction for a simulated caller who behaves like the real one
* a scripted version of the caller's turns, so the task also runs offline
* tags, and a new or reused persona
* the end state the world must reach **if the agent follows the policy**. When the human rep broke the policy, the expectation follows the policy, not the rep.

Guardrails:

* **PII is scrubbed locally before anything is sent**: emails, phone numbers and card-like numbers. Short identifiers such as order or MC numbers are kept. The model is told to replace names and companies with fictional ones.
* When the world lacks rows for the scenario, the draft adds **fictional seed rows**. A row whose key collides with an existing one drops the task.
* Tasks that would grade nothing (no state and no `called` expectations), or that fail validation, are dropped with the reason.
* Survivors are `review: pending`, with the transcript and a verbatim excerpt as `source`.

> [!TIP]
>
> Local scrubbing catches patterns, not every identifying detail. Read transcripts before sending them anywhere, and keep the drafted domain's seed rows fictional.

## Reviewing a draft

Drafted invariants and tasks carry `review: pending` and a `source` (the file and the quote they came from). Tools with review notes are marked the same way.

```bash theme={null}
npx sandtable-ai review sandtable              # list what is pending, with sources
npx sandtable-ai validate sandtable --strict   # fails while anything is pending (use in CI)
```

While items are pending, runs still work, but their reports are marked **provisional**. Run `npx sandtable-ai lint` too: it catches drafted tasks that are vacuous, unreachable or unsolvable before anyone spends money running them. To accept an item, change `review: pending` to `review: accepted` or delete the line. [Review workflow](/concepts/review-workflow) covers what to check.

## What a real draft looks like

In a live run on the bundled freight sources (an OpenAPI spec, an SOP and two transcripts, in October 2026, before the example's SOP grew), `init` drafted 4 tables and 5 tools with two review notes, 9 invariants that each quote their sentence, and 2 tasks with personas and fictional seed rows. `lint` then flagged both drafted tasks as not ready, because a side effect the OpenAPI step had marked for review was not modelled yet. Details are on the [results](/results) page.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.