Skip to main content
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.
The SOP and transcript steps call Claude. They have been verified in live runs on the bundled freight sources (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

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

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

Add tasks from transcripts

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

Review, lint, then run

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

Claude reads the SOP alongside the domain’s tools, params, tables and known fields, and proposes invariants in the six existing kinds, 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). 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)

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