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

# Editing the graph

> Program a world's states, events, and entrance over HTTP — every write validated fail-closed against the kernel.

A world is a **programmable graph**: nodes are **states** (what a scene is) and
edges are **events** (what the player can do to move between them). Generating a
world gives you a complete, valid graph; the Graph-Editing API lets you keep
programming it — add a room, wire a new exit, retarget the entrance, or push a
whole batch of changes at once — all over the same `/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](/agent-editing). 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.

<Note>
  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.
</Note>

## 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 with `422` 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:

```json theme={null}
{
  "lint": "dangling-ref",
  "severity": "error",
  "path": "event[Step outside]",
  "message": "event 'Step outside' points at unknown state 'ghost_room'"
}
```

* **Structural lints** (`dangling-ref`, `transition-needs-to`, `kernel-cycle`,
  the `slot-*` family, …) are always fatal `error`s and block the write.
* **Doctrine and budget lints** (`negation`, `whiteout`, `lethal-override`,
  `budget`, …) are advisory — they come back as `warning`/`info` and 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").

When a write succeeds, the response carries the new `world`, any advisory
`diagnostics`, and a fresh `rev` (see [concurrency](#optimistic-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.

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.alakazam.gg/v1/worlds/WORLD_ID/scene' \
    -H 'Authorization: Bearer pk_live_…'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://api.alakazam.gg/v1/worlds/WORLD_ID/scene",
    { headers: { Authorization: "Bearer pk_live_…" } },
  );
  const { states, events, rev } = await res.json();
  console.log(Object.keys(states), events.length, "edges; rev", rev);
  ```

  ```python Python theme={null}
  import requests

  res = requests.get(
      "https://api.alakazam.gg/v1/worlds/WORLD_ID/scene",
      headers={"Authorization": "Bearer pk_live_…"},
  )
  graph = res.json()
  print(list(graph["states"]), len(graph["events"]), "edges; rev", graph["rev"])
  ```
</CodeGroup>

Every read returns the current `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. Omit `id` on a state to have one auto-assigned; the response tells you
which id (or event `name`) was created.

<CodeGroup>
  ```bash cURL theme={null}
  # 1. Add a node.
  curl -X POST 'https://api.alakazam.gg/v1/worlds/WORLD_ID/states' \
    -H 'Authorization: Bearer sk_live_…' \
    -H 'Content-Type: application/json' \
    -d '{
      "id": "cellar",
      "base": "a low stone cellar, one guttering candle, damp flagstones underfoot"
    }'

  # 2. Wire a transition into it from an existing state.
  curl -X POST 'https://api.alakazam.gg/v1/worlds/WORLD_ID/events' \
    -H 'Authorization: Bearer sk_live_…' \
    -H 'Content-Type: application/json' \
    -d '{
      "kind": "transition",
      "from": "hallway",
      "to": "cellar",
      "name": "Descend the stairs"
    }'
  ```

  ```javascript JavaScript theme={null}
  const auth = {
    Authorization: "Bearer sk_live_…",
    "Content-Type": "application/json",
  };

  // 1. Add a node.
  const added = await fetch(
    "https://api.alakazam.gg/v1/worlds/WORLD_ID/states",
    {
      method: "POST",
      headers: auth,
      body: JSON.stringify({
        id: "cellar",
        base: "a low stone cellar, one guttering candle, damp flagstones underfoot",
      }),
    },
  ).then((r) => r.json());
  console.log("created state", added.stateId);

  // 2. Wire a transition into it.
  const wired = await fetch(
    "https://api.alakazam.gg/v1/worlds/WORLD_ID/events",
    {
      method: "POST",
      headers: auth,
      body: JSON.stringify({
        kind: "transition",
        from: "hallway",
        to: "cellar",
        name: "Descend the stairs",
      }),
    },
  ).then((r) => r.json());
  console.log("created event", wired.eventName, "rev", wired.rev);
  ```

  ```python Python theme={null}
  import requests

  auth = {
      "Authorization": "Bearer sk_live_…",
      "Content-Type": "application/json",
  }

  # 1. Add a node.
  added = requests.post(
      "https://api.alakazam.gg/v1/worlds/WORLD_ID/states",
      headers=auth,
      json={
          "id": "cellar",
          "base": "a low stone cellar, one guttering candle, damp flagstones underfoot",
      },
  ).json()
  print("created state", added["stateId"])

  # 2. Wire a transition into it.
  wired = requests.post(
      "https://api.alakazam.gg/v1/worlds/WORLD_ID/events",
      headers=auth,
      json={
          "kind": "transition",
          "from": "hallway",
          "to": "cellar",
          "name": "Descend the stairs",
      },
  ).json()
  print("created event", wired["eventName"], "rev", wired["rev"])
  ```
</CodeGroup>

A `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`:

```bash theme={null}
curl -X PATCH 'https://api.alakazam.gg/v1/worlds/WORLD_ID/entrance' \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -d '{ "state": "cellar" }'
```

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

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://api.alakazam.gg/v1/worlds/WORLD_ID/ops' \
    -H 'Authorization: Bearer sk_live_…' \
    -H 'Content-Type: application/json' \
    -d '{
      "ops": [
        { "op": "add_state", "id": "vault", "base": "a steel vault, fluorescent hum, one heavy door" },
        { "op": "add_event", "kind": "transition", "from": "cellar", "to": "vault", "name": "Force the door" },
        { "op": "set_entrance", "state": "cellar" }
      ]
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://api.alakazam.gg/v1/worlds/WORLD_ID/ops",
    {
      method: "POST",
      headers: {
        Authorization: "Bearer sk_live_…",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        ops: [
          { op: "add_state", id: "vault", base: "a steel vault, fluorescent hum, one heavy door" },
          { op: "add_event", kind: "transition", from: "cellar", to: "vault", name: "Force the door" },
          { op: "set_entrance", state: "cellar" },
        ],
      }),
    },
  );
  const { world, applyErrors, diagnostics, rev } = await res.json();
  console.log("rev", rev, "skipped", applyErrors);
  ```

  ```python Python theme={null}
  import requests

  res = requests.post(
      "https://api.alakazam.gg/v1/worlds/WORLD_ID/ops",
      headers={
          "Authorization": "Bearer sk_live_…",
          "Content-Type": "application/json",
      },
      json={
          "ops": [
              {"op": "add_state", "id": "vault", "base": "a steel vault, fluorescent hum, one heavy door"},
              {"op": "add_event", "kind": "transition", "from": "cellar", "to": "vault", "name": "Force the door"},
              {"op": "set_entrance", "state": "cellar"},
          ]
      },
  )
  body = res.json()
  print("rev", body["rev"], "skipped", body["applyErrors"])
  ```
</CodeGroup>

Individual ops that can't be applied (say, deleting a state that doesn't exist)
are collected non-fatally in `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}/validate` runs the **strict** gate. It returns `200`
  with the resolved `world` when clean, or `422` with `diagnostics` when not.
* `POST /v1/worlds/{id}/lint` is **advisory** — it never returns `4xx` on
  doctrine hits. It reports every finding plus `counts` by severity and the
  `promptBudget` (1900) your assembled prompts must stay under.

Both accept an optional inline `world` (or `data`) in the body to check a
candidate world; omit it to check the stored one.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://api.alakazam.gg/v1/worlds/WORLD_ID/lint' \
    -H 'Authorization: Bearer pk_live_…'
  ```

  ```javascript JavaScript theme={null}
  const report = await fetch(
    "https://api.alakazam.gg/v1/worlds/WORLD_ID/lint",
    { method: "POST", headers: { Authorization: "Bearer pk_live_…" } },
  ).then((r) => r.json());
  console.log(report.counts, "budget", report.promptBudget);
  ```

  ```python Python theme={null}
  import requests

  report = requests.post(
      "https://api.alakazam.gg/v1/worlds/WORLD_ID/lint",
      headers={"Authorization": "Bearer pk_live_…"},
  ).json()
  print(report["counts"], "budget", report["promptBudget"])
  ```
</CodeGroup>

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

<CodeGroup>
  ```bash cURL theme={null}
  # Read returns ETag: "2026-06-28T12:00:00.000Z"
  curl -X PATCH 'https://api.alakazam.gg/v1/worlds/WORLD_ID/states/cellar' \
    -H 'Authorization: Bearer sk_live_…' \
    -H 'Content-Type: application/json' \
    -H 'If-Match: 2026-06-28T12:00:00.000Z' \
    -d '{ "base": "a low stone cellar, the candle guttering lower now" }'
  ```

  ```javascript JavaScript theme={null}
  const read = await fetch(
    "https://api.alakazam.gg/v1/worlds/WORLD_ID/scene",
    { headers: { Authorization: "Bearer sk_live_…" } },
  );
  const rev = read.headers.get("ETag");

  const res = await fetch(
    "https://api.alakazam.gg/v1/worlds/WORLD_ID/states/cellar",
    {
      method: "PATCH",
      headers: {
        Authorization: "Bearer sk_live_…",
        "Content-Type": "application/json",
        "If-Match": rev,
      },
      body: JSON.stringify({ base: "a low stone cellar, the candle guttering lower now" }),
    },
  );
  if (res.status === 409) console.log("stale — reload and retry");
  ```

  ```python Python theme={null}
  import requests

  read = requests.get(
      "https://api.alakazam.gg/v1/worlds/WORLD_ID/scene",
      headers={"Authorization": "Bearer sk_live_…"},
  )
  rev = read.headers["ETag"]

  res = requests.patch(
      "https://api.alakazam.gg/v1/worlds/WORLD_ID/states/cellar",
      headers={
          "Authorization": "Bearer sk_live_…",
          "Content-Type": "application/json",
          "If-Match": rev,
      },
      json={"base": "a low stone cellar, the candle guttering lower now"},
  )
  if res.status_code == 409:
      print("stale — reload and retry")
  ```
</CodeGroup>

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