# Gridnode — build an importable grid JSON

This document gives an AI everything needed to generate a **Gridnode document** as JSON
that a user can import into <https://gridnode.app>.

Gridnode is a constrained canvas: **nodes** (small markdown cards) placed on a
**coarse bounded grid**, connected by **directed arrows** (cycles allowed). A node can
link to a URL and/or open a **sub-grid** (nested grid). The whole state is one JSON
document.

## How the user imports it

Menu **⋯ → Import JSON** → pick the `.json` file. On import the app validates the
document, then **normalizes** it: it fills missing `cols`/`rows`, and accepts a few
legacy shapes. So you only need to produce a valid document (below); you don't have to
compute pixel positions — a node lives in a **cell** `(col, row)`, integers only.

## Mental model

- A **grid** is a bounded board of `cols` columns × `rows` rows. Default **24 × 24**.
  Coordinates are integers: `0 ≤ col < cols`, `0 ≤ row < rows`. `(0,0)` is top-left.
  Reading direction is left→right, then top→bottom.
- A **node** occupies exactly **one cell**. Two nodes can never share a cell in the
  same grid. It may carry one optional muted color marker for lightweight visual
  classification; the marker does not change the node background or meaning.
- An **edge** is a directed arrow `from` one node `to` another, within the same grid.
  Cycles (A→B→C→A) and self-loops (A→A) are allowed. No duplicate exact `(from,to)` pair.
- A **sub-grid** is another grid in the same document. A node points to it via
  `childGridId`; the app shows a 3×3 grid badge that navigates into it. Nesting is how you
  build larger structures while each board stays small and readable. The root is
  depth 0; sub-grids may be nested to depth **3** maximum.

## JSON schema

Top level:

```json
{
  "version": 1,
  "rootGridId": "root",
  "grids": { "<gridId>": { /* Grid */ } }
}
```

- `version` (number) — always `1`.
- `rootGridId` (string) — id of the top grid the user lands on. Must be a key of `grids`.
  Use `"root"`.
- `grids` (object) — map of **gridId → Grid**. Must contain `rootGridId`.

**Grid**

```json
{
  "id": "root",
  "name": "gridnode",
  "parentGridId": null,
  "parentNodeId": null,
  "cols": 24,
  "rows": 24,
  "nodes": [ /* Node[] */ ],
  "edges": [ /* Edge[] */ ]
}
```

- `id` (string) — must equal its key in `grids`. Root grid uses `"root"`; sub-grids use
  any unique id (convention: `g_<random>`).
- `name` (string) — shown in the breadcrumb. Root is usually `"gridnode"`.
- `parentGridId` (string | null) — `null` for the root grid; for a sub-grid, the id of
  the grid that contains the node linking to it.
- `parentNodeId` (string | null) — `null` for the root grid; for a sub-grid, the id of
  the node whose `childGridId` points here. (Powers the breadcrumb chain.)
- `cols`, `rows` (number) — grid bounds. Default 24 and 24. You may enlarge them if you
  need more room; keep them reasonable.
- `nodes` (Node[]), `edges` (Edge[]).

**Node**

```json
{
  "id": "n_auth",
  "col": 0,
  "row": 0,
  "text": "# Auth\nCheck the **token**, then call `refresh()`",
  "color": "blue",
  "links": ["https://example.com/auth", "https://docs.example.com/auth"],
  "link": "https://example.com/auth",
  "childGridId": null,
  "createdAt": "2026-07-08T09:00:00.000Z",
  "updatedAt": "2026-07-08T09:00:00.000Z",
  "revisions": []
}
```

- `id` (string) — unique within the whole document. Convention: `n_<something>`.
- `col`, `row` (integer) — cell within the grid bounds; unique per grid.
- `text` (string) — **Markdown** (GitHub-flavored: headings, lists, bold, `code`, links,
  blockquotes). The first line is often a `# Title`. Keep it short — a node is a small
  card. May be `""`.
- `color` (string, optional) — a prominent marker inside the node's lower-left corner.
  Allowed values: `gray`, `red`, `orange`, `yellow`, `green`, `blue`, `purple`. Omit it
  for no marker. Colors are intentionally semantic-free: define their meaning in the
  document.
- `links` (array, optional) — up to **3 external URLs**, accessible from the node
  menu only. Each URL is limited to 2 048 characters.
- `link` (string URL | null) — backward-compatible mirror of `links[0]`. Old documents
  with only `link` are migrated automatically; when generating new content, prefer
  `links` and set `link` to its first entry (or `null` when the array is empty).
- `childGridId` (string | null) — id of a sub-grid in `grids`, or `null`.
- `createdAt`, `updatedAt` (ISO 8601 string) — timestamps. Include them; if omitted the
  card still imports but dates display as unknown.
- `revisions` (array) — text history, newest first: `[{ "text": "...", "at": "ISO" }]`.
  Use `[]` when generating fresh content (max 5 kept).
- `comments` (array, optional) — per-node discussion, in-document:
  `[{ "id": "cm_x", "author": "you@example.com", "text": "...", "at": "ISO", "resolved": false }]`.
  Omit the field (or use `[]`) when there are none. **Pro plan only** — a document that
  contains any comment is rejected on free-plan (or anonymous) docs with
  `400 plan_limit_comments`. Caps: ≤ 10 comments per node, `text` ≤ 2 000 chars,
  `author` ≤ 100 chars.

**Edge**

```json
{ "id": "e_ab", "from": "n_auth", "to": "n_validate" }
```

- `id` (string) — unique within the document. Convention: `e_<something>`.
- `from`, `to` (string) — node ids **in the same grid**. `from === to` = self-loop.

## Rules (must hold, or import fails / renders wrong)

1. `grids[rootGridId]` must exist.
2. Every `grid.id` equals its key in `grids`.
3. Within a grid, `(col, row)` is **unique** and within `0..cols-1` / `0..rows-1`.
4. Every edge `from`/`to` references a node **in the same grid**. No duplicate exact
   `(from, to)`.
5. Every `childGridId` references an existing grid; that grid's `parentGridId` /
   `parentNodeId` should point back for a correct breadcrumb.
6. All `id`s are unique across the whole document.
7. The parent chain of every grid reaches `rootGridId`; sub-grid depth is at most **3**.
8. If present, `color` is one of the seven allowed values listed above.

## Limits (server caps — a document over these is rejected)

The API enforces anti-abuse caps. Stay well within them and imports always succeed:

- **Grid size**: `cols` ≤ **64**, `rows` ≤ **64**. Default: **24 × 24**.
  Push detail into sub-grids rather than enlarging a board.
- **Node text**: ≤ **10 000** characters per node (each `revisions[].text` too). Keep a
  node a small card — one idea, a title line.
- **Grids per document**: ≤ **32** (technical cap). Recommended **4** or fewer for a
  readable space (root + a few sub-grids).
- **Sub-grid depth**: ≤ **3** below the root, for both Free and Pro documents.
- **Link**: ≤ **2 048** characters. **Grid name**: ≤ **200** characters.
- **Whole document**: serialized JSON ≤ **512 KB**.

Rejections return an error code: `too_many_grids`, `grid_too_large`, `text_too_long`,
`link_too_long`, `too_many_links`, `name_too_long`, `cell_conflict`, `out_of_bounds`, `invalid_edge`,
`invalid_grid_hierarchy`, `subgrid_depth_limit`, `invalid_node_color`,
`too_many_comments`, `comment_too_long`, `comment_author_too_long`,
`plan_limit_comments` (all HTTP 400), or `doc_too_large` (HTTP 413).

## Drive a live grid via the API (Pro edit links)

If the user gives you an **edit link** (`https://gridnode.app/?doc=<id>&k=<writeKey>`),
you can read and modify their live grid directly — no JSON file needed. Extract `<id>`
and `<writeKey>` from the link.

Edit links and remote writes are a **Pro feature**. Free has no account: its grid stays
on the user's device, and sharing publishes a view-only snapshot. The user keeps editing
and resharing the original personal grid; recipients cannot write to the snapshot.

**Read the document** (the doc id alone is the read capability):

```
GET https://gridnode.app/api/docs/<id>
→ { "doc": { …same JSON shape as this spec… }, "seq": 12 }
```

**Apply mutations** (writes require the key, sent as a header):

```
POST https://gridnode.app/api/docs/<id>/mutations
x-write-key: <writeKey>
content-type: application/json

{ "baseSeq": 12, "mutations": [
  { "op": "addNode",  "args": { "gridId": "root", "col": 2, "row": 1, "text": "# New step" } },
  { "op": "addEdge",  "args": { "gridId": "root", "from": "n_abc", "to": "<use returned id>" } }
] }
→ { "seq": 13, "results": [ { "ok": true, "id": "n_xyz" }, … ] }
```

Operations (`op` / `args`): `addNode {gridId, col, row, text?}` ·
`moveNode {gridId, nodeId, col, row}` · `updateNodeText {gridId, nodeId, text}` ·
`setNodeLink {gridId, nodeId, link}` (legacy single URL) ·
`setNodeLinks {gridId, nodeId, links}` (≤ 3) ·
`setNodeColor {gridId, nodeId, color}` (allowed value or `""`/`null` to clear) ·
`deleteNode {gridId, nodeId}` (cascades sub-grids
and edges) · `addEdge {gridId, from, to}` · `deleteEdge {gridId, edgeId}` ·
`createSubGrid {gridId, nodeId}` · `renameGrid {gridId, name}` ·
`restoreRevision {gridId, nodeId, index}`.

Notes:
- Mutations apply **in order**; each returns `{ok}` or `{ok:false, reason}` (e.g.
  `cell_occupied`, `out_of_bounds`). Ids are generated server-side and returned — but
  a later mutation in the same batch cannot reference an id created earlier in that
  batch; use two requests for that.
- Always GET first, then send `baseSeq` = the `seq` you read. A `409` means someone
  else wrote in between: GET again and rebuild your batch.
- Without a valid `x-write-key` (or for a view-only link without `&k=`), writes return
  `403 write_forbidden`. Reads always work with the id.
- The server enforces the caps below and the free plan limit (4 grids per document →
  `400 plan_limit_grids`). Rate limit: ~30 writes/min per document (`429`) — batch your
  mutations instead of sending them one by one.

## Connect via MCP (Claude / ChatGPT / Cursor)

Instead of calling the HTTP API by hand, you can drive a live grid through Gridnode's
**remote MCP server** — a set of tools your MCP client (Claude, a custom connector,
Cursor…) can call directly.

Writing through MCP requires a Pro grid and its edit key. A Free or view-only
connection can still use `read_document`.

**Connection URL** (configure it in your client as a remote MCP / "custom connector"):

```
https://gridnode.app/mcp?doc=<docId>&k=<writeKey>
```

- `doc` — the document id (from a share link). **Required** for the tools to act on a
  grid.
- `k` — the write key. **Optional**: omit it for a **read-only** connection (the write
  tools then return an error: *read-only: missing or invalid write key*; `read_document`
  still works). Include it to edit.

Extract both from an **edit link** (`https://gridnode.app/?doc=<id>&k=<writeKey>`): the
part after `?doc=` is the id, the part after `&k=` is the key.

**Example — Claude Desktop / custom connector**: add a remote MCP server whose URL is the
connection URL above. The transport is **Streamable HTTP** (stateless). No auth header is
needed — the capability lives in the URL. Example config shape:

```json
{
  "mcpServers": {
    "gridnode": {
      "url": "https://gridnode.app/mcp?doc=<docId>&k=<writeKey>"
    }
  }
}
```

**Tools** (call `read_document` first to learn grid ids, node ids and free cells):

- `read_document {}` — the whole document (same JSON shape as this spec) + `seq`.
- `add_node {gridId, col, row, text?}` → `{id}`.
- `update_node_text {gridId, nodeId, text}` — pushes a revision.
- `move_node {gridId, nodeId, col, row}`.
- `set_node_links {gridId, nodeId, links}` — canonical operation, up to 3 URLs; `[]` clears them.
- `set_node_link {gridId, nodeId, link}` — backward-compatible single-URL operation;
  replaces the whole list, empty string clears it.
- `set_node_color {gridId, nodeId, color}` — sets the optional marker; use one of the
  seven allowed color names, or an empty string to clear it.
- `delete_node {gridId, nodeId}` — cascades edges and sub-grids.
- `add_edge {gridId, from, to}` → `{id}` (cycles / self-loops OK, no exact duplicate).
- `delete_edge {gridId, edgeId}`.
- `create_subgrid {gridId, nodeId}` → `{gridId}` of the new (or existing) sub-grid;
  returns `subgrid_depth_limit` when called from level 3.
- `rename_grid {gridId, name}`.

Each write tool applies **one** mutation and returns the result (or an error such as
`cell_occupied`, `out_of_bounds`, `subgrid_depth_limit`, `plan_limit_grids`). The same caps and free-plan limit
(4 grids/document) and rate limit (~30 writes/min per document) as the HTTP API apply.

## Layout tips (for a readable board)

- Lay a workflow **left→right** (steps in increasing `col`) and use rows for branches.
- Arrows that go **against reading order** (to a smaller `col`, or upward in the same
  `col`) are drawn **amber** automatically — great for "loop back" / "retry" edges.
- A self-loop (A→A) renders as a small loop on the node's corner.
- Keep each grid small; push detail into sub-grids rather than enlarging `cols`/`rows`.
- One idea per node; the title line is what the reader scans first.

## Minimal valid document

```json
{
  "version": 1,
  "rootGridId": "root",
  "grids": {
    "root": {
      "id": "root", "name": "gridnode",
      "parentGridId": null, "parentNodeId": null,
      "cols": 24, "rows": 24,
      "nodes": [
        { "id": "n_1", "col": 0, "row": 0, "text": "# Hello", "link": null, "childGridId": null, "createdAt": "2026-07-08T09:00:00.000Z", "updatedAt": "2026-07-08T09:00:00.000Z", "revisions": [] }
      ],
      "edges": []
    }
  }
}
```

## Full example (workflow + cycle + self-loop + link + sub-grid)

```json
{
  "version": 1,
  "rootGridId": "root",
  "grids": {
    "root": {
      "id": "root", "name": "Auth flow",
      "parentGridId": null, "parentNodeId": null,
      "cols": 24, "rows": 24,
      "nodes": [
        { "id": "n_auth", "col": 0, "row": 0, "text": "# Auth\nCheck **token**", "link": null, "childGridId": null, "createdAt": "2026-07-08T09:00:00.000Z", "updatedAt": "2026-07-08T09:00:00.000Z", "revisions": [] },
        { "id": "n_validate", "col": 1, "row": 0, "text": "# Validate\n- parse header\n- verify sig", "link": null, "childGridId": "g_store", "createdAt": "2026-07-08T09:00:00.000Z", "updatedAt": "2026-07-08T09:00:00.000Z", "revisions": [] },
        { "id": "n_retry", "col": 3, "row": 0, "text": "# Retry\nback to start", "links": ["https://example.com/retry"], "link": "https://example.com/retry", "childGridId": null, "createdAt": "2026-07-08T09:00:00.000Z", "updatedAt": "2026-07-08T09:00:00.000Z", "revisions": [] }
      ],
      "edges": [
        { "id": "e_1", "from": "n_auth", "to": "n_validate" },
        { "id": "e_2", "from": "n_retry", "to": "n_auth" },
        { "id": "e_3", "from": "n_auth", "to": "n_auth" }
      ]
    },
    "g_store": {
      "id": "g_store", "name": "Store",
      "parentGridId": "root", "parentNodeId": "n_validate",
      "cols": 24, "rows": 24,
      "nodes": [
        { "id": "n_session", "col": 0, "row": 0, "text": "# Session\nWrite to D1", "link": null, "childGridId": null, "createdAt": "2026-07-08T09:00:00.000Z", "updatedAt": "2026-07-08T09:00:00.000Z", "revisions": [] }
      ],
      "edges": []
    }
  }
}
```

## Checklist before returning JSON

- [ ] Valid JSON, `version: 1`, `rootGridId` present in `grids`.
- [ ] Each grid `id` matches its key; `cols`/`rows` set (24/24 unless you need more).
- [ ] Node cells unique and in bounds; ids unique document-wide.
- [ ] Edges reference same-grid nodes; no exact duplicates.
- [ ] Sub-grids linked both ways (`childGridId` ↔ `parentGridId`/`parentNodeId`).
- [ ] Node `text` is concise Markdown with a title line.
