/v1 API-key model you
already use.
There are two tiers for changing a graph:
- The deterministic tier (available now). Precise, scripted control: state and event CRUD, a batch op vocabulary, and the kernel validate/lint gate. This is what third-party editors and your own tooling build on. Everything on this page is in this tier.
- The agent tier. A natural-language endpoint —
POST /v1/worlds/{id}/edit— where you describe the change in prose and the kernel agent authors it for you. It returns the validated result only, never the authoring brain. See Editing with natural language. Both tiers funnel through the exact same validation gate described below, so a world edited either way behaves identically in the editor and at runtime.
Reading the graph needs a publishable (
pk_) or secret (sk_) key with
worlds:read. Every write needs a secret key with worlds:write. Never ship a
secret key to a browser.The validation gate
This is the load-bearing rule of the whole surface: every write persists only after passing the kernel grammar and the full doctrine lint suite, fail-closed. If a mutation would produce an invalid world, the API rejects it with422 and a
list of diagnostics — and nothing is persisted. There is no weaker path; the
kernel is the sole authority that can bless a change, and the API is only a way to
propose one.
A diagnostic looks like this:
- Structural lints (
dangling-ref,transition-needs-to,kernel-cycle, theslot-*family, …) are always fatalerrors and block the write. - Doctrine and budget lints (
negation,whiteout,lethal-override,budget, …) are advisory — they come back aswarning/infoand don’t block a save, so you can fix them iteratively. A common one: prompt prose must describe pixels, not authorial intent (“a dim stone corridor,” not “the player feels trapped”).
world, any advisory
diagnostics, and a fresh rev (see concurrency).
Read the graph
GET /v1/worlds/{id}/scene returns the whole graph; /states and /events
return the node map and edge list on their own. States are addressed by their id;
events are always addressed by their unique name, never by index.
rev both in the body and as the ETag response
header — hold onto it for your next write.
Add a state and wire an edge
Adding a node, then an edge into it, is the bread-and-butter of programming a graph. Omitid on a state to have one auto-assigned; the response tells you
which id (or event name) was created.
transition must carry a to; an override (a self-loop that re-renders
the current state without moving) must not. Break either rule — or reference a
state that doesn’t exist — and the op can’t be applied: the write comes back 400
with a GraphOpError, caught before the kernel ever runs. A 422
(GraphValidationError) is the other failure mode: the ops applied cleanly, but
the resulting graph failed the kernel grammar or doctrine lint, so its blocking
diagnostics (like dangling-ref or kernel-cycle) come back in the body and nothing is
persisted.
Patch a node or edge with PATCH (send the partial fields, or wrap them in
{ "patch": { … } }); remove one with DELETE. Deleting a state that is the
entrance, or that any event still references, is rejected with 400 — detach
those edges first.
Retarget the entry point with PATCH /v1/worlds/{id}/entrance:
Batch ops
POST /v1/worlds/{id}/ops applies an ordered batch of operations atomically — it
backs nearly every mutation the visual editor makes. The op vocabulary is a
stable, curated surface (add_state, update_state, delete_state, add_event,
update_event, delete_event, set_entrance, add_variant, remove_variant).
The resulting world is validated once, as a whole: if the batch leaves the
world invalid, the entire batch is rejected 422 and nothing persists.
applyErrors — mirroring the editor’s behavior — so
one bad op in a batch doesn’t sink the rest. A 422, by contrast, means the
final world failed the kernel gate as a whole.
Validate and lint without saving
Before you commit a change — or to check a world you’ve assembled client-side — run it through the same gate without persisting.POST /v1/worlds/{id}/validateruns the strict gate. It returns200with the resolvedworldwhen clean, or422withdiagnosticswhen not.POST /v1/worlds/{id}/lintis advisory — it never returns4xxon doctrine hits. It reports every finding pluscountsby severity and thepromptBudget(1900) your assembled prompts must stay under.
world (or data) in the body to check a
candidate world; omit it to check the stored one.
Optimistic concurrency
The editor, your scripts, and (soon) the agent can all edit one world. To keep a stale write from clobbering a fresh one, every write is guarded by a revision token,rev.
Each read and each successful write returns the current rev — in the body and
as the ETag header. Pass it back on your next write, either as the If-Match
request header or as an expectedRev body field. If it no longer matches the
stored revision, the write is rejected with 409 and you should reload and
retry.
Status codes
| Status | Meaning |
|---|---|
200 | Write applied. The body carries the new world, diagnostics, and rev. |
400 | An op couldn’t be applied — a missing reference, a duplicate event name, deleting a referenced or entrance state. Caught before validation. |
404 | The world (or addressed state/event) doesn’t exist, or isn’t yours. |
409 | The rev you asserted is stale — another writer got there first. Reload and retry. |
422 | The result failed the kernel gate. The body carries the blocking diagnostics; nothing was persisted. |