galaxybrain-api
v0.9.1
Published
Local WebSocket API server for GalaxyBrain — read and write to your workspace programmatically.
Maintainers
Readme
galaxybrain-api
Local WebSocket API server for GalaxyBrain — an information operating system powered by local files. Read and write to your workspace programmatically.
- Website: https://galaxybrain.com
- About GalaxyBrain: https://galaxybrain.com/docs/galaxybrain/
- API documentation: https://galaxybrain.com/docs/api/
Prerequisites
- Bun runtime installed
- GalaxyBrain open in a Chromium-based browser (Chrome or Edge)
Usage
npx galaxybrain-apiBy default it listens on port 1924. Override with:
npx galaxybrain-api 3000 # command-line argument
GB_API_PORT=3000 npx galaxybrain-api # environment variableConnect
const ws = new WebSocket("ws://localhost:1924");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "command",
requestId: "1",
cmd: "ORIENTATION"
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log(msg);
};GalaxyBrain API Reference
The GalaxyBrain API is a local WebSocket server that lets you programmatically read and write to your GalaxyBrain workspace. You can use it to build scripts, automations, and integrations that interact with your pages, templates, and workspace layout.
Table of Contents
- Getting Started
- Architecture
- App URL Parameters
- Transport
- IDs, Versions, and Snapshots
- Data Model
- Commands
- Event System
- Error Reference
- Recommended Client Flow
Getting Started
1. Install and start the API server
This downloads the server from npm and starts it on port 1924:
npx galaxybrain-apiTo use a different port:
GB_API_PORT=3000 npx galaxybrain-api2. Open GalaxyBrain in the browser
The app automatically connects to ws://localhost:1924 and registers itself as an instance.
3. Connect your client
const ws = new WebSocket("ws://localhost:1924");
ws.onopen = () => {
// Get an overview of the workspace
ws.send(JSON.stringify({
type: "command",
requestId: "1",
cmd: "ORIENTATION"
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log(msg);
};Architecture
The API has three moving pieces:
- The API server — a local WebSocket server, started with
npx galaxybrain-api. An HTTPGET /returns the health check string"GalaxyBrain API Server/{protocolVersion}"(e.g."GalaxyBrain API Server/1"). The protocol version increments on breaking changes to the WebSocket command/event format. - GalaxyBrain browser tabs — each tab connects to the server and registers as an addressable instance.
- External clients — your scripts, tools, or integrations connect to the same server and send commands.
External clients do not connect directly to the browser tab. Commands are routed through the server to a specific instance, and responses are routed back. External clients never send identify messages — the browser app handles that automatically.
GalaxyBrain instances can be in one of three states:
| State | Description |
|-------|-------------|
| picker | No project is open |
| folder | A folder-backed project is open |
| demo | A demo project is open |
Commands divide into two groups:
Always available (any state): LIST_INSTANCES, LIST_FOLDERS, OPEN_FOLDER, OPEN_DEMO, CLOSE_PROJECT, REMOVE_RECENT_FOLDER, FILES_WATCH, FILES_UNWATCH, SUBSCRIBE, UNSUBSCRIBE
Require an open project: READ_PAGES, CREATE_PAGES, UPDATE_PAGES, DELETE_PAGES, QUERY, READ_TEMPLATES, CREATE_TEMPLATES, UPDATE_TEMPLATES, DELETE_TEMPLATES, CREATE_FROM_TEMPLATE, PUSH_PAGE_ITEMS, PUSH_TEMPLATE_ITEMS, POP_PAGE_ITEMS, POP_TEMPLATE_ITEMS, READ_WORKSPACE, WRITE_WORKSPACE, MAP, ANCESTORS, ORIENTATION
Sending a project-level command with no project open returns:
{
"type": "response",
"requestId": "1",
"cmd": "READ_PAGES",
"ok": false,
"error": "NO_PROJECT",
"message": "No project is open"
}App URL Parameters
The browser app reads these query parameters:
gb_port
API server port to connect to. Default: 1924.
?gb_port=3000gb_id
Instance ID the browser tab registers with. If omitted, a random 6-character ID is generated.
?gb_id=desk-mainUse a stable gb_id when you want to target a specific browser tab from a script, or run multiple tabs simultaneously. If two tabs use the same gb_id, the older one is evicted.
demo
Startup demo selection. An app convenience — API clients use OPEN_DEMO instead.
Transport
Message Format
All messages are JSON objects with a type field. There are five message types on the wire:
identify— sent by the browser app to register with the server (see below)command— sent by external clients to the serverresponse— sent back for commands and identifyerror— server-level routing or protocol errorsevent— server-pushed subscription events
External clients only use the last four. The identify handshake is documented here because it governs instance registration and affects behavior visible to external clients.
Identify (Browser App → Server)
When a GalaxyBrain browser tab connects, it identifies itself:
{
"type": "identify",
"instanceId": "desk-main",
"protocolVersion": 1,
"state": "picker",
"folder": null,
"demo": null,
"offline": false,
"version": "0.5.0"
}The server responds with:
{ "type": "response", "requestId": "identify", "ok": true, "serverVersion": "0.2.1" }The serverVersion field contains the API server's npm package version string.
If protocolVersion does not match the server's version, the connection receives a PROTOCOL_MISMATCH error. If another tab is already registered with the same instanceId, the older tab receives an eviction event and is disconnected — the new tab takes over.
External clients never send identify.
Command (Client → Server)
{
"type": "command",
"requestId": "req-1",
"instance": "desk-main",
"cmd": "READ_PAGES",
"...": "command-specific fields"
}| Field | Type | Required | Description |
|-------|------|----------|-------------|
| type | "command" | Yes | Message type |
| requestId | string | Yes | Unique ID for correlating the response |
| cmd | string | Yes | Command name |
| instance | string | No | Target instance ID (see Instance Routing) |
Response (Server → Client)
{
"type": "response",
"requestId": "req-1",
"cmd": "READ_PAGES",
"...": "command-specific result"
}Every response includes type, requestId, and cmd. The rest of the shape depends on the command (see Response Patterns).
Error (Server → Client)
Server-level errors (routing failures, protocol mismatches) — distinct from command-level errors in responses:
{
"type": "error",
"requestId": "req-1",
"code": "INSTANCE_REQUIRED",
"message": "Multiple instances connected. Specify \"instance\" field.",
"instances": [ /* instance list */ ]
}Event (Server → Client)
Pushed to subscribed clients:
{
"type": "event",
"event": "pages_updated",
"seq": 42,
"instanceId": "desk-main",
"timestamp": 1712766240,
"...": "event-specific fields"
}Instance Routing
- One instance connected:
instanceis optional — auto-routed. - Multiple instances:
instanceis required, otherwiseINSTANCE_REQUIRED(response includes the instance list). - No instances:
NO_INSTANCESerror. - Unknown instance:
UNKNOWN_INSTANCEerror. - Instance disconnects mid-request:
INSTANCE_DISCONNECTEDerror.
Response Patterns
Commands use three distinct response shapes:
1. Singleton — top-level ok with command-specific fields:
{ "type": "response", "requestId": "1", "cmd": "OPEN_FOLDER", "ok": true, "folder": "Work Notes", "skippedFiles": [] }Used by: LIST_FOLDERS, OPEN_FOLDER, OPEN_DEMO, CLOSE_PROJECT, REMOVE_RECENT_FOLDER, QUERY, MAP, ANCESTORS, ORIENTATION, READ_WORKSPACE, WRITE_WORKSPACE, SUBSCRIBE, UNSUBSCRIBE, CREATE_FROM_TEMPLATE.
2. Batch — top-level results array aligned to input order, each entry succeeds or fails independently:
{
"type": "response",
"requestId": "1",
"cmd": "DELETE_PAGES",
"results": [
{ "ok": true },
{ "ok": false, "error": "PAGE_NOT_FOUND", "message": "Page not found: ..." }
]
}Used by: READ_PAGES, CREATE_PAGES, UPDATE_PAGES, DELETE_PAGES, READ_TEMPLATES, CREATE_TEMPLATES, UPDATE_TEMPLATES, DELETE_TEMPLATES, PUSH_PAGE_ITEMS, PUSH_TEMPLATE_ITEMS, POP_PAGE_ITEMS, POP_TEMPLATE_ITEMS.
3. Top-level parse failure — when the payload is malformed before execution can begin:
{ "type": "response", "requestId": "1", "cmd": "READ_PAGES", "ok": false, "error": "PARSE_ERROR", "message": "pageIds must be an array" }Ordering and Delivery
- Commands are processed FIFO per target instance. If you send several commands to the same instance, they execute and respond in order.
- Batch arrays execute in array order — later operations can observe earlier mutations from the same batch.
- For API-triggered mutations, the response is sent first, then any resulting events are flushed. For example:
OPEN_FOLDERresponds, then emitsproject_opened.
IDs, Versions, and Snapshots
Page IDs
20-character alphanumeric strings (e.g. AbcDef1234567890GhIj). Templates also use page IDs.
Block IDs
Non-negative integers, unique within a page.
Variable IDs
Non-negative integers, unique within a page.
Page Versions
Each page and template has an independent integer version. You receive it from read and mutation commands. Mutation commands that accept readVersion compare it against the current version:
- Match → mutation proceeds.
- Mismatch →
CONFLICTerror. - Omitted or
null→ conflict check is skipped.
Workspace Version
Independent from page versions. Received from READ_WORKSPACE and WRITE_WORKSPACE.
snapshotSeq
Read commands include snapshotSeq — the instance's latest committed event sequence number at read time. Use it to detect whether your event subscription is still in sync.
Commands that include snapshotSeq: READ_PAGES, READ_TEMPLATES, READ_WORKSPACE, QUERY, MAP, ANCESTORS, ORIENTATION.
Write commands do not include snapshotSeq.
Data Model
Writable Schemas
These shapes are used when creating or updating pages and templates.
Page Body (Create / Full Replace)
PageBody = {
icon: string -- single emoji
title: TitleUnitInput[] -- title units (text or templateValue only)
subtitle: UnitInput[] -- subtitle units (all unit types)
blocks: BlockInput[] -- non-empty array
}
BlockInput = {
blockId: number -- non-negative integer, unique within page
items: ItemInput[] -- non-empty array
linkOrder?: string | null -- sort rule (see Link Order)
lastSelectedTemplateId?: string | null
}CREATE_PAGES and CREATE_TEMPLATES accept null entries in the pages array to create default blank pages (default icon "📄" for pages, "📋" for templates, empty title/subtitle, one empty text block).
Item Input
TextItemInput = {
type: "text"
style: "" | "#" | "##" | "###" | "*" | "[ ]" | "[X]" | "ol"
content: UnitInput[]
indentLevel?: number -- clamped to 0-8, default 0
orderedListStart?: number | null -- only for "ol" style
}
VarItemInput = {
type: "var"
id: number -- non-negative, unique within page
name: string
formula: FormulaUnitInput[] -- text, metaRef, or templateValue only
}
ImageItemInput =
| { type: "image", imageId: string } -- existing image by ID
| { type: "image", imageFile: string } -- reference into images map
PageLinkItemInput = {
type: "pageLink"
pageId: string -- cannot self-link
}Unit Input
TextUnitInput = {
type: "text"
text: string -- non-empty
unitStyle?: "bold" | "italic" | "boldItalic"
}
WebLinkUnitInput = {
type: "webLink"
text: string
url: string
unitStyle?: "bold" | "italic" | "boldItalic"
}
PageLinkUnitInput = {
type: "pageLink"
pageId: string
}
MetaRefUnitInput = {
type: "metaRef"
ref: string -- see Meta References
}
TemplateValueUnitInput = {
type: "templateValue"
valueType: "nextNumber" | "nextDay" | "dateToday"
}Restrictions by context:
- Titles accept only unstyled
textandtemplateValueunits. - Formulas accept only unstyled
text,metaRef, andtemplateValueunits. - Subtitle and text item content accept all unit types.
Surgical Update
UPDATE_PAGES and UPDATE_TEMPLATES support surgical block-level edits as an alternative to full block replacement:
SurgicalUpdate = {
icon?: string
title?: TitleUnitInput[]
subtitle?: UnitInput[]
updateBlocks?: [{ blockId, items?, linkOrder?, lastSelectedTemplateId? }]
insertBlocks?: [{ blockId, items, linkOrder?, lastSelectedTemplateId? }]
deleteBlockIds?: number[]
blockOrder?: number[] -- must list every surviving block exactly once
}blocks (full replacement) is mutually exclusive with all surgical fields.
In surgical updateBlocks, tri-state behavior applies for linkOrder and lastSelectedTemplateId:
- Omitted → keep current value.
null→ clear to null.- String → set new value.
Serialized (Read) Schemas
Read operations return these shapes. Fields marked (read-only) are computed by the server and must not be round-tripped back into writes.
Page
{
"pageId": "AbcDef1234567890GhIj",
"icon": "📄",
"title": [ /* serialized units */ ],
"subtitle": [ /* serialized units */ ],
"blocks": [ /* serialized blocks */ ],
"blockOrder": [0, 1, 2],
"createdAt": 1712766240,
"updatedAt": 1712766300,
"templateValues": { "nextNumber": "5" },
"counts": {
"words": 150, "characters": 820, "blocks": 3,
"checkboxes": 2, "checkboxesChecked": 1, "checkboxesUnchecked": 1,
"pageLinks": 4, "listItems": 5
}
}blockOrderalways lists all block IDs, even whenblockIdsfiltering is applied.countsalways reflect the full page.icon,title,subtitle, andblocksare optional based on read options.templateValuesis populated on regular pages with resolved values; template reads return{}.
(read-only): pageId, blockOrder, createdAt, updatedAt, templateValues, counts.
Block
{
"blockId": 0,
"linkOrder": "A.M.tt",
"lastSelectedTemplateId": null,
"items": [ /* serialized items */ ],
"createdAt": 1712766240,
"updatedAt": 1712766300,
"counts": { "words": 50, "characters": 280, "checkboxes": 0, "checkboxesChecked": 0, "checkboxesUnchecked": 0, "pageLinks": 2, "listItems": 1 }
}(read-only): createdAt, updatedAt, counts.
Serialized Items
indentLevel is only present when > 0 (default 0). orderedListStart is only present when style is "ol".
// Text (indentLevel omitted when 0; orderedListStart only for "ol" style)
{ "type": "text", "style": "", "content": [ /* units */ ], "indentLevel": 2 }
{ "type": "text", "style": "ol", "content": [ /* units */ ], "orderedListStart": 3 }
// Var (value is read-only, computed from formula)
{ "type": "var", "id": 0, "name": "Total", "formula": [ /* units */ ], "value": "150" }
// Image
{ "type": "image", "imageId": "a1b2c3...64hex.png" }
// Page Link (title is read-only, resolved at read time)
{ "type": "pageLink", "pageId": "AbcDef1234567890GhIj", "title": "My Page" }(read-only): var value, pageLink title.
Serialized Units
// Text
{ "type": "text", "text": "Hello", "unitStyle": "bold" }
// Web Link
{ "type": "webLink", "text": "Example", "url": "https://example.com" }
// Page Link (title is read-only)
{ "type": "pageLink", "pageId": "AbcDef1234567890GhIj", "title": "My Page" }
// Meta Ref — success
{ "type": "metaRef", "ref": "M.tw", "value": "150" }
// Meta Ref — error
{ "type": "metaRef", "ref": "V.AbcDef1234567890GhIj.0", "value": null, "error": "NOT_FOUND" }
// Template Value — on template page (no resolved value)
{ "type": "templateValue", "valueType": "dateToday" }
// Template Value — on regular page (resolved)
{ "type": "templateValue", "valueType": "dateToday", "value": "2026-04-10" }(read-only): unitStyle is preserved but not semantically read-only. metaRef.value, metaRef.error, pageLink.title, templateValue.value on regular pages.
Meta ref errors: NOT_FOUND, VAR_MISSING_REFERENCE, VAR_CIRCULAR_REFERENCE.
Normalization
The parser applies these normalization steps to written content:
- Adjacent compatible text and web-link units are merged.
- A lone page-link unit inside an unstyled text item is converted into a standalone
pageLinkitem. - Bullet, checkbox, and ordered-list items keep a lone inline page-link unit as text content.
Link Order
linkOrder controls automatic sorting of page-link items within a block. Format: "{direction}.{sortKey}".
Direction: A (ascending) or D (descending).
Sort keys:
- Metadata:
M.tt(title),M.ca(created at),M.ua(updated at),M.tb(total blocks),M.tw(total words),M.tc(total characters),M.tli(total list items),M.tpl(total page links),M.tr(reference count),M.tcb(total checkboxes),M.tcbc(checked),M.tcbu(unchecked) - Variable:
V.{varName}(sort by a named variable's value)
Examples: A.M.tt, D.M.ua, A.V.score.
Meta References
metaRef units reference live computed values. The ref string uses dot-separated segments:
| Pattern | Description | Example |
|---------|-------------|---------|
| CT.{format} | Current time (absolute formats only) | CT.a.d |
| CA.{pageId}.{format} | Page created-at | CA.AbcDef1234567890GhIj.a.d |
| CA.{pageId}.{blockId}.{format} | Block created-at | CA.AbcDef1234567890GhIj.0.a.d |
| UA.{pageId}.{format} | Page updated-at | UA.AbcDef1234567890GhIj.r.d |
| UA.{pageId}.{blockId}.{format} | Block updated-at | UA.AbcDef1234567890GhIj.0.r.d |
| V.{pageId}.{varId} | Variable value | V.AbcDef1234567890GhIj.0 |
| PLCV.{pageId}.{blockId}.{fn}.{varName} | Page-link common variable aggregation | PLCV.AbcDef1234567890GhIj.0.sum.score |
| M.{type} | Global metadata | M.tp |
| M.{type}.{pageId} | Page-level metadata | M.tw.AbcDef1234567890GhIj |
| M.{type}.{pageId}.{blockId} | Block-level metadata | M.tw.AbcDef1234567890GhIj.0 |
Time formats: a.ut (UNIX timestamp), a.d (date), a.dow (day of week), r.s (seconds ago), r.m (minutes ago), r.h (hours ago), r.d (days ago). Relative formats (r.*) only valid for CA/UA.
Metadata types: tp (total pages), tb (total blocks), tw (total words), tc (total characters), tcb (total checkboxes), tcbc (checked), tcbu (unchecked), tli (total list items), tpl (total page links), tr (total references — page-level only).
PLCV derivations: cnt, sum, avg, min, max.
Images
Image IDs are content-addressed: {64 lowercase hex sha256}.{ext}.
Allowed extensions: png, jpg, jpeg, webp, gif, svg.
Commands that accept new images (CREATE_PAGES, CREATE_TEMPLATES, UPDATE_PAGES, UPDATE_TEMPLATES, PUSH_PAGE_ITEMS, PUSH_TEMPLATE_ITEMS) support a top-level images map:
{
"images": {
"cover.png": "data:image/png;base64,iVBORw0KGgo..."
}
}Image items reference these via imageFile (key into the map) or imageId (existing image). Cannot use both on the same item.
Commands
Server Commands
LIST_INSTANCES
Lists all connected GalaxyBrain browser instances. Handled directly by the API server router — does not require an instance target or an open project.
Request:
{ "type": "command", "requestId": "1", "cmd": "LIST_INSTANCES" }Response:
{
"type": "response", "requestId": "1", "cmd": "LIST_INSTANCES",
"ok": true,
"instances": [
{ "instanceId": "desk-main", "connectedAt": 1712766240, "state": "folder", "folder": "Work Notes", "demo": null, "offline": false, "version": "0.4.0" },
{ "instanceId": "desk-side", "connectedAt": 1712766300, "state": "picker", "folder": null, "demo": null, "offline": false, "version": "0.4.0" }
]
}Each instance entry includes:
instanceId— the instance's self-assigned IDconnectedAt— when the instance connected (Unix seconds)state— current application state ("picker","folder","demo")folder— workspace folder name when state is"folder",nullotherwisedemo— demo name when state is"demo",nullotherwiseoffline— whether the instance is running in offline (file://) modeversion— application version string
Project Management
LIST_FOLDERS
Returns recent folders and available demos.
Request:
{ "type": "command", "requestId": "1", "cmd": "LIST_FOLDERS" }Response:
{
"type": "response", "requestId": "1", "cmd": "LIST_FOLDERS",
"ok": true,
"recentFolders": [
{ "id": 1, "name": "My Project", "lastUsed": 1712766240, "permission": "granted" },
{ "id": 2, "name": "Old Project", "lastUsed": 1712600000, "permission": "prompt" }
],
"demos": ["Simple", "F1", "The Premier League", "The Universe"]
}permission: "granted"— can open via API immediately.permission: "prompt"— requires user gesture in the browser first;OPEN_FOLDERreturnsPERMISSION_REQUIRED.- There is no API command for opening an arbitrary filesystem path directly.
OPEN_FOLDER
Opens a recent folder by its numeric ID.
Request:
{ "type": "command", "requestId": "1", "cmd": "OPEN_FOLDER", "id": 1 }Response:
{ "type": "response", "requestId": "1", "cmd": "OPEN_FOLDER", "ok": true, "folder": "My Project", "skippedFiles": [] }skippedFileslists project files that failed to load during initialization.- On success, the instance enters
folderstate and emitsproject_opened. - Errors:
PARSE_ERROR,FOLDER_NOT_FOUND,PERMISSION_REQUIRED,FOLDER_UNAVAILABLE,FOLDER_LOAD_FAILED.
Note: FOLDER_NOT_FOUND, PERMISSION_REQUIRED, and FOLDER_UNAVAILABLE are checked before the current project is closed. FOLDER_LOAD_FAILED occurs after teardown, leaving the instance in picker state.
OPEN_DEMO
Opens a demo project by name.
Request:
{ "type": "command", "requestId": "1", "cmd": "OPEN_DEMO", "name": "Simple" }Response:
{ "type": "response", "requestId": "1", "cmd": "OPEN_DEMO", "ok": true, "demo": "Simple" }- The app accepts any string, but only configured demo names load successfully.
- On success, emits
project_opened. - Errors:
PARSE_ERROR,OFFLINE,DEMO_LOAD_FAILED.
CLOSE_PROJECT
Closes the current project and returns to picker state.
Request:
{ "type": "command", "requestId": "1", "cmd": "CLOSE_PROJECT" }Response:
{ "type": "response", "requestId": "1", "cmd": "CLOSE_PROJECT", "ok": true }No-op if already in picker (returns success). Emits project_closed when closing a real project.
REMOVE_RECENT_FOLDER
Removes a folder from the recent folders list. Does not delete the filesystem folder.
Request:
{ "type": "command", "requestId": "1", "cmd": "REMOVE_RECENT_FOLDER", "id": 2 }Response:
{ "type": "response", "requestId": "1", "cmd": "REMOVE_RECENT_FOLDER", "ok": true }Errors: PARSE_ERROR, FOLDER_NOT_FOUND.
File Watching
FILES_WATCH
Subscribes to filesystem change events inside a subfolder of the open project. Wraps the Chromium FileSystemObserver API.
Request:
{ "type": "command", "requestId": "1", "cmd": "FILES_WATCH",
"subpath": "inbox", "recursive": true }subpath is forward-slash-delimited and must resolve to an existing directory under the project root. Leading /, \, Windows-absolute paths (C:\…), backslashes, .., ., and empty segments are rejected.
Success:
{ "type": "response", "requestId": "1", "cmd": "FILES_WATCH",
"ok": true, "subscriptionId": "fwatch_..." }Errors: PARSE_ERROR, FOLDER_NOT_FOUND, NO_PROJECT, FOLDER_UNAVAILABLE, UNSUPPORTED_BROWSER.
FOLDER_UNAVAILABLE— project is open but is a demo / in-memory (no real folder to observe).UNSUPPORTED_BROWSER— the browser does not exposeFileSystemObserver.
Subscriptions live on the GalaxyBrain instance (the open browser tab) until one of: explicit FILES_UNWATCH, project close, folder switch, demo switch, app reload, or Chrome emits an errored record (which tears the subscription down server-side).
FILES_UNWATCH
Stops a previously started folder watch.
Request:
{ "type": "command", "requestId": "2", "cmd": "FILES_UNWATCH",
"subscriptionId": "fwatch_..." }Unknown subscription IDs are intentionally no-ops; the server always returns ok: true.
Response:
{ "type": "response", "requestId": "2", "cmd": "FILES_UNWATCH", "ok": true }Errors: PARSE_ERROR (subscriptionId missing or not a string).
Sharp edges
changeType: "unknown"means the OS dropped events. Rescan the folder if correctness matters.changeType: "errored"means the observation is dead. GalaxyBrain removes the subscription; callFILES_WATCHagain to resume.- Windows reports cross-directory moves as
disappeared+appeared, notmoved. Same-directory renames aremoved. - File events are not file-readiness events. A large file copy produces
appeared(and/ormodified) before the writer has finished. Use temp-then-rename or wait for size/mtime stability if completion matters. - Per-origin observation limit exists, is OS-dependent, and surfaces as
errored.
Pages Commands
READ_PAGES
Reads regular pages by ID or by open tabs.
Request (by IDs):
{
"type": "command", "requestId": "1", "cmd": "READ_PAGES",
"pageIds": ["AbcDef1234567890GhIj"],
"icon": true, "title": true, "subtitle": true,
"blockIds": [0, 4]
}Request (by tabs):
{ "type": "command", "requestId": "1", "cmd": "READ_PAGES", "tabs": true }pageIds and tabs are mutually exclusive.
Read options (all optional, default true): icon, title, subtitle, blocks. When blocks is false, the blocks key is omitted entirely from the page object (lightweight read). blockOrder and counts are always included regardless. Optional blockIds array filters which blocks are serialized. blocks: false and blockIds are mutually exclusive (PARSE_ERROR).
Response:
{
"type": "response", "requestId": "1", "cmd": "READ_PAGES",
"results": [
{
"ok": true,
"version": 3,
"page": { "pageId": "AbcDef1234567890GhIj", "..." : "..." }
}
],
"snapshotSeq": 42
}When using tabs: true, the response also includes tabs:
{ "tabs": [{ "tabIndex": 0, "pageId": "AbcDef1234567890GhIj" }] }- Template IDs in explicit
pageIdsmode return per-entryTEMPLATE_PAGE. tabs: truemode can return both regular pages and templates (deduplicated in first-seen tab order).
CREATE_PAGES
Creates one or more regular pages.
Request:
{
"type": "command", "requestId": "1", "cmd": "CREATE_PAGES",
"returnPages": true,
"pages": [
null,
{
"icon": "📝",
"title": [{ "type": "text", "text": "New Page" }],
"subtitle": [],
"blocks": [{
"blockId": 0,
"items": [{ "type": "text", "style": "", "content": [{ "type": "text", "text": "Hello!" }] }]
}]
}
],
"images": { "photo.png": "data:image/png;base64,..." }
}nullentries create default blank pages (icon"📄", empty title/subtitle, one empty text block).returnPages: trueincludes the serialized page in each successful result.- Partial success is allowed — each entry succeeds or fails independently.
Response:
{
"type": "response", "requestId": "1", "cmd": "CREATE_PAGES",
"results": [
{ "ok": true, "pageId": "NewPage1234567890AbC", "version": 0, "page": { "..." : "..." } },
{ "ok": true, "pageId": "NewPage1234567890XyZ", "version": 0, "page": { "..." : "..." } }
]
}Errors (per entry): PARSE_ERROR, INVALID_ICON, NO_BLOCKS, NO_ITEMS, INVALID_BLOCK_ID, DUPLICATE_BLOCK_ID, INVALID_VAR_ID, DUPLICATE_VAR_ID, INVALID_STYLE, EMPTY_TEXT, SELF_LINK, IMAGE_NOT_FOUND, IMAGE_FILE_NOT_FOUND, IMAGE_AMBIGUOUS, INVALID_META_REF, INVALID_TEMPLATE_VALUE, INVALID_LINK_ORDER.
UPDATE_PAGES
Updates one or more regular pages. Supports full block replacement or surgical edits.
Full-replace request:
{
"type": "command", "requestId": "1", "cmd": "UPDATE_PAGES",
"pages": [{
"pageId": "AbcDef1234567890GhIj",
"readVersion": 3,
"icon": "🔥",
"blocks": [{ "blockId": 0, "items": [{ "type": "text", "style": "", "content": [{ "type": "text", "text": "New content" }] }] }]
}]
}Surgical request:
{
"type": "command", "requestId": "1", "cmd": "UPDATE_PAGES",
"pages": [{
"pageId": "AbcDef1234567890GhIj",
"readVersion": 3,
"updateBlocks": [{ "blockId": 0, "items": [{ "type": "text", "style": "", "content": [{ "type": "text", "text": "Edited" }] }] }],
"insertBlocks": [{ "blockId": 4, "linkOrder": "A.M.tt", "items": [{ "type": "pageLink", "pageId": "ZyxWvu9876543210TsRq" }] }],
"blockOrder": [0, 4]
}]
}Response:
{
"type": "response", "requestId": "1", "cmd": "UPDATE_PAGES",
"results": [{ "ok": true, "pageId": "AbcDef1234567890GhIj", "version": 4 }]
}readVersionis optional; omit to skip conflict checking.returnPages: trueadds serializedpageto successful entries.- Errors: All
CREATE_PAGESerrors plusPAGE_NOT_FOUND,CONFLICT,TEMPLATE_PAGE,NO_UPDATES,BLOCK_NOT_FOUND,BLOCK_ALREADY_EXISTS,BLOCK_ORDER_MISMATCH,DUPLICATE_BLOCK_OP.
DELETE_PAGES
Deletes one or more regular pages.
Request:
{ "type": "command", "requestId": "1", "cmd": "DELETE_PAGES", "pageIds": ["AbcDef1234567890GhIj"] }Response:
{ "type": "response", "requestId": "1", "cmd": "DELETE_PAGES", "results": [{ "ok": true, "pageId": "AbcDef1234567890GhIj" }] }Errors (per entry): PAGE_NOT_FOUND, TEMPLATE_PAGE, LAST_PAGE.
QUERY
Searches and filters pages with sorting, pagination, and field projection.
Request:
{
"type": "command", "requestId": "1", "cmd": "QUERY",
"scope": "pages",
"search": { "text": "invoice", "sections": ["title", "blocks"] },
"fields": ["icon", "title", "counts", "outboundPageLinks"],
"sortBy": "updatedAt",
"sortDirection": "desc",
"maxResults": 20
}All parameters (optional):
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| pageIds | string[] | null | Restrict to specific pages (deduplicated, invalid IDs skipped) |
| scope | string | "pages" | "pages", "templates", or "all" |
| search | object | null | Text search or error search (see below) |
| fields | string[] | [] | Fields to include (see below) |
| sortBy | string | null | Sort mode (see below) |
| sortDirection | string | null | "asc" or "desc" |
| offset | number | 0 | Pagination offset |
| maxResults | number | null | Max results; must be a positive integer (null = unlimited) |
Search (mutually exclusive — exactly one of text, errors, or references):
- Text:
{ "text": "query", "caseSensitive": false, "sections": ["title", "subtitle", "blocks"] } - Errors:
{ "errors": "all" | "brokenPageLinks" | "brokenValues" } - References:
{ "references": { <target> }, "sections": ["subtitle", "blocks"] }
Note: text: "" and sections: [] match nothing. Error search ignores titles.
References search targets — exactly one of these five shapes inside references:
| Target | Shape | Matches |
|--------|-------|---------|
| imageId | { "imageId": "<64hex>" } or { "imageId": "<64hex>.<ext>" } | image items in blocks |
| varName | { "varName": "...", "prefix": false } | var definitions, V refs (live name lookup), PLCV refs, block linkOrder keys (prefix: true enables trailing-* match) |
| metaRef | { "metaRef": <MetaRefQuery> } | metaValue units in subtitles, block text items, and var formulas |
| pageLink | { "pageLink": { "pageId": "..." } } | standalone pageLink items and inline pageLink units |
| webLinkUrl | { "webLinkUrl": "..." } | webLink units (exact URL match) |
MetaRef query object — head is required, every other field is optional but must be valid for head:
| Field | Applies to heads | Notes |
|-------|------------------|-------|
| head | all | "CT", "CA", "UA", "V", "PLCV", or "M" |
| pageId | CA, UA, V, PLCV, M | 20-char page ID |
| blockId | CA, UA, PLCV, M | non-negative integer |
| varId | V | non-negative integer |
| type | M | one of the metaType codes (tp, tb, …) |
| derivation | PLCV | one of cnt, sum, avg, min, max |
| varName | PLCV | exact match (no prefix wildcard here) |
| format | CT, CA, UA | one of the time-format codes (a.ut, a.d, r.h, …). CT accepts absolute formats only |
A meta ref matches iff every non-null field on the query equals its parsed component. { "head": "M" } alone matches every M ref.
References sections — defaults to ["subtitle", "blocks"]. Titles are accepted but never produce matches. Empty sections array matches nothing.
Note: matchCount counts structural occurrences (one per matching unit, item, or linkOrder key), not distinct target IDs. Invalid input (unknown target kind, multiple targets, malformed IDs, head-incompatible or invalid component values) returns PARSE_ERROR.
Graph filters (arrays of page IDs, all AND together):
| Filter | Description |
|--------|-------------|
| linksToPage / notLinksToPage | Pages with/without outbound links to these targets |
| linkedFromPage / notLinkedFromPage | Pages linked/not linked from these source pages |
| reachesPage / notReachesPage | Pages that can/cannot transitively reach these targets |
| reachableFromPage / notReachableFromPage | Pages reachable/not reachable from these sources |
Count filters (non-negative integers, min cannot exceed max):
minOutboundPageLinks, maxOutboundPageLinks, minInboundPageLinks, maxInboundPageLinks, minInboundReferences, maxInboundReferences
outboundPageLinks/inboundPageLinksrefer to standalonepageLinkitems.inboundReferencesrefers to inline page-link units in text content.
Fields:
| Value | Result key(s) | Description |
|-------|---------------|-------------|
| "icon" | icon | Page icon emoji |
| "title" | title | Page title as plain text string |
| "subtitle" | subtitle | Page subtitle as plain text string |
| "blocks" | blocks | Array of block preview strings |
| "outboundPageLinks" | outboundPageLinks | Array of linked page IDs |
| "inboundPageLinks" | inboundPageLinks | Array of page IDs linking to this page |
| "inboundReferences" | inboundReferences | Array of page IDs referencing this page in text |
| "timestamps" | createdAt, updatedAt | Timestamps |
| "vars" | vars | Array of { id, name, value } |
| "counts" | counts | Page statistics |
Default fields is [], so base results contain only kind and pageId.
Note: title and subtitle here are plain text strings, not unit arrays (unlike READ_PAGES).
Sort modes: title, createdAt, updatedAt, outboundPageLinkCount, inboundPageLinkCount, pageReach, pageReachDistinct.
pageReachsorts by transitive reach and addspageReachto result entries. Regular pages ranked first; templates appended.pageReachDistinctdeduplicates — pages covered by a higher-reach page are excluded. IgnoressortDirection.
Response:
{
"type": "response", "requestId": "1", "cmd": "QUERY",
"ok": true,
"results": [{
"kind": "page",
"pageId": "AbcDef1234567890GhIj",
"icon": "📄",
"title": "Invoice 17",
"counts": { "..." : "..." },
"outboundPageLinks": ["ZyxWvu9876543210TsRq"],
"matches": [{ "section": "title", "matchCount": 1, "units": [{ "type": "text", "text": "Invoice 17" }] }]
}],
"totalResults": 1,
"truncated": false,
"snapshotSeq": 42
}matches is present when search was used. Match sections: "title", "subtitle", "block". Title/subtitle matches include units; block matches include a serialized block.
Templates Commands
Template commands mirror their page counterparts. Key differences:
- Template pages can contain
templateValueunits. - Regular page IDs return
NOT_TEMPLATE_PAGEerrors. - Template reads serialize
templateValueunits without resolvedvaluefields. - Default blank template icon is
"📋".
READ_TEMPLATES
Same as READ_PAGES (page-ID mode only, no tabs mode).
{ "type": "command", "requestId": "1", "cmd": "READ_TEMPLATES", "pageIds": ["TplPage1234567890AbC"] }CREATE_TEMPLATES
Same as CREATE_PAGES. Templates may freely use all three templateValue types.
{
"type": "command", "requestId": "1", "cmd": "CREATE_TEMPLATES",
"pages": [{
"icon": "📋",
"title": [{ "type": "templateValue", "valueType": "dateToday" }, { "type": "text", "text": " — Daily Note" }],
"subtitle": [],
"blocks": [{ "blockId": 0, "items": [{ "type": "text", "style": "#", "content": [{ "type": "text", "text": "Tasks" }] }] }]
}]
}UPDATE_TEMPLATES
Same structure and options as UPDATE_PAGES.
DELETE_TEMPLATES
Same as DELETE_PAGES, but no "last page" restriction (templates are separate from the required real-page set).
{ "type": "command", "requestId": "1", "cmd": "DELETE_TEMPLATES", "pageIds": ["TplPage1234567890AbC"] }CREATE_FROM_TEMPLATE
Creates a regular page from a template and inserts a page link into a source block.
Request:
{
"type": "command", "requestId": "1", "cmd": "CREATE_FROM_TEMPLATE",
"pageId": "AbcDef1234567890GhIj",
"blockId": 0,
"templateId": "TplPage1234567890AbC",
"insertAtTop": false,
"returnPage": true
}| Field | Type | Required | Description |
|-------|------|----------|-------------|
| pageId | string | Yes | Source page receiving the inserted link |
| blockId | number | Yes | Block within the source page |
| templateId | string | Yes | Template to clone |
| insertAtTop | boolean | No | Insert link at top of block (default: false) |
| returnPage | boolean | No | Include the created page in response (default: false) |
Template values are resolved at creation time:
nextNumber— Increments based on existing page links in the source block.nextDay— Advances one day from the most recent date in neighboring links.dateToday— Current date.
Response:
{
"type": "response", "requestId": "1", "cmd": "CREATE_FROM_TEMPLATE",
"ok": true,
"createdPageId": "NewPage1234567890AbC",
"version": 0,
"sourceVersion": 4,
"sourcePageId": "AbcDef1234567890GhIj",
"templateId": "TplPage1234567890AbC",
"page": { "..." : "..." }
}version— created page's version.sourceVersion— source page's version after link insertion.sourcePageId— the source page ID.templateId— the template page ID used.- The source block's
lastSelectedTemplateIdis updated totemplateId. - Errors:
PARSE_ERROR,PAGE_NOT_FOUND,BLOCK_NOT_FOUND,INVALID_TEMPLATE_ID.
Items Commands
These commands add or remove items from individual blocks without replacing the entire page.
Anchor + Offset
All push/pop commands use anchor and offset to specify position:
"top"anchor: insertion/removal index ismin(offset, itemCount)."bottom"anchor: insertion index ismax(itemCount - offset, 0); removal starts before the skipped tail.
Offsets are clamped to valid bounds automatically.
Examples: top + 0 = before first item. bottom + 0 = after last item. bottom + 1 = before last item.
PUSH_PAGE_ITEMS
Inserts items into a block on a regular page.
Request:
{
"type": "command", "requestId": "1", "cmd": "PUSH_PAGE_ITEMS",
"operations": [{
"pageId": "AbcDef1234567890GhIj",
"blockId": 0,
"anchor": "bottom",
"offset": 0,
"readVersion": 4,
"items": [{ "type": "text", "style": "[ ]", "content": [{ "type": "text", "text": "New task" }] }]
}]
}Response (per operation):
{
"ok": true, "pageId": "AbcDef1234567890GhIj", "version": 5,
"insertedAt": 3, "totalItemCount": 4,
"didReorderPageLinks": false,
"block": { "blockId": 0, "..." : "..." }
}didReorderPageLinksreports whether automatic link-order sorting changed page-link positions after the mutation.- Operations execute in array order; later operations observe earlier version bumps.
- Errors:
PARSE_ERROR,TEMPLATE_PAGE,PAGE_NOT_FOUND,BLOCK_NOT_FOUND,CONFLICT,NO_ITEMS,DUPLICATE_VAR_ID, plus item/unit validation errors.
PUSH_TEMPLATE_ITEMS
Identical to PUSH_PAGE_ITEMS but targets template pages. Regular page IDs return NOT_TEMPLATE_PAGE.
POP_PAGE_ITEMS
Removes items from a block on a regular page.
Request:
{
"type": "command", "requestId": "1", "cmd": "POP_PAGE_ITEMS",
"operations": [{
"pageId": "AbcDef1234567890GhIj",
"blockId": 0,
"anchor": "bottom",
"offset": 0,
"count": 2,
"readVersion": 5,
"expectedItemType": "text"
}]
}Response (per operation):
{
"ok": true, "pageId": "AbcDef1234567890GhIj", "version": 6,
"removedFrom": 2, "removedCount": 2, "totalItemCount": 2,
"didReorderPageLinks": false,
"removedItems": [{ "type": "text", "style": "", "content": [{ "type": "text", "text": "Removed" }] }],
"block": { "blockId": 0, "..." : "..." }
}- If the computed range is empty, succeeds as a no-op (
removedCount: 0, unchanged version). - Removing all items from a block is rejected with
NO_REMAINING_ITEMS. expectedItemTypevalidates that all items in the removal range match ("text","var","image","pageLink").- Errors:
PARSE_ERROR,TEMPLATE_PAGE,PAGE_NOT_FOUND,BLOCK_NOT_FOUND,CONFLICT,NO_REMAINING_ITEMS,UNEXPECTED_ITEM_TYPE.
POP_TEMPLATE_ITEMS
Identical to POP_PAGE_ITEMS but targets template pages. Regular page IDs return NOT_TEMPLATE_PAGE.
Workspace Commands
READ_WORKSPACE
Returns the current workspace layout.
Request:
{ "type": "command", "requestId": "1", "cmd": "READ_WORKSPACE" }Response:
{
"type": "response", "requestId": "1", "cmd": "READ_WORKSPACE",
"ok": true,
"version": 2,
"workspace": {
"scrollLeftPx": 0,
"tabs": [{
"pageId": "AbcDef1234567890GhIj",
"path": [],
"displayWidthPx": 500,
"scrollTopPx": 120,
"isLinkedRefsExpanded": false
}, {
"pageId": "ZyxWvu9876543210TsRq",
"path": [{ "pageId": "AbcDef1234567890GhIj", "icon": "📄", "title": "Inbox" }],
"displayWidthPx": 650,
"scrollTopPx": 0,
"isLinkedRefsExpanded": true
}]
},
"snapshotSeq": 42
}pathentries are always resolved objects on read. Template tabs usepath: [].versionis the workspace version, not a page version.- Errors:
NO_PROJECT.
WRITE_WORKSPACE
Atomically replaces the workspace layout.
Request:
{
"type": "command", "requestId": "1", "cmd": "WRITE_WORKSPACE",
"readVersion": 2,
"workspace": {
"scrollLeftPx": 0,
"tabs": [
{ "pageId": "AbcDef1234567890GhIj", "path": [], "displayWidthPx": 500, "scrollTopPx": 0 },
{ "pageId": "ZyxWvu9876543210TsRq", "path": ["AbcDef1234567890GhIj"], "displayWidthPx": 650, "scrollTopPx": 0, "isLinkedRefsExpanded": true }
]
}
}Response:
{ "type": "response", "requestId": "1", "cmd": "WRITE_WORKSPACE", "ok": true, "version": 3 }Validation rules:
tabsmust be a non-empty array.scrollLeftPxandscrollTopPxmust be non-negative integers.displayWidthPxmust be an integer between 350 and 5000.- Every
pageIdandpathentry must resolve to an existing page or template. pathentries may be strings or{ pageId: string }objects.- Template tabs must have an empty path.
- Path must not end with the tab's own
pageId. isLinkedRefsExpandeddefaults tofalsewhen omitted.
Errors: PARSE_ERROR, NO_TABS, PAGE_NOT_FOUND, INVALID_TAB_PATH, CONFLICT, NO_PROJECT.
Discovery Commands
MAP
Builds a recursive page-link tree rooted at one page.
Request:
{
"type": "command", "requestId": "1", "cmd": "MAP",
"pageId": "AbcDef1234567890GhIj",
"limits": [12, 4, 1],
"subtitle": true,
"blockText": true
}| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| pageId | string | Yes | — | Root page ID |
| limits | number[] | null | No | null | Per-layer link budgets (each element must be a positive integer). null = auto mode (~200 page budget). [] = leaf only |
| subtitle | boolean | No | true | Include subtitle text |
| blockText | boolean | No | true | Include text preview from first text item (max 100 chars) |
Response:
{
"type": "response", "requestId": "1", "cmd": "MAP",
"ok": true,
"root": {
"pageId": "AbcDef1234567890GhIj",
"icon": "📄",
"title": "Inbox",
"subtitle": "Open items",
"blocks": [{
"blockId": 0,
"text": "Projects",
"templateId": "TplPage1234567890AbC",
"templateName": "📋 Daily Note",
"links": [{ "pageId": "ZyxWvu9876543210TsRq", "icon": "📄", "title": "Alpha" }],
"+": 5
}]
},
"snapshotSeq": 42
}- Only regular pages can be mapped; template pages return
TEMPLATE_PAGE. "+"indicates how many links were truncated at that block.- Recursion stops on exhausted limits, path cycles, or no valid child pages.
- Errors:
PAGE_NOT_FOUND,TEMPLATE_PAGE,PARSE_ERROR.
ANCESTORS
Walks UP the page-link tree from a target page, returning a recursive ancestor tree. The complement to MAP (which walks down).
Request:
{
"type": "command", "requestId": "1", "cmd": "ANCESTORS",
"pageId": "AbcDef1234567890GhIj",
"limits": [10, 5, 3],
"subtitle": true,
"blockText": true
}| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| pageId | string | Yes | — | Target page ID |
| limits | number[] | Yes | — | Per-layer parent budgets (each element must be a positive integer). [] = leaf only (no parent expansion). No auto-mode — limits is always required. |
| subtitle | boolean | No | true | Include subtitle text on page entries |
| blockText | boolean | No | true | Include text preview on viaBlocks entries |
Response:
{
"type": "response", "requestId": "1", "cmd": "ANCESTORS",
"ok": true,
"root": {
"pageId": "AbcDef1234567890GhIj",
"icon": "🔴",
"title": "Erling Haaland",
"subtitle": "#9 – 30 goals, 5 assists",
"parents": [
{
"pageId": "ZyxWvu9876543210TsRq",
"icon": "🏃",
"title": "Squad",
"subtitle": "Manchester City 2025-26",
"viaBlocks": [
{ "blockId": 0, "text": "All Players", "templateId": "TplPage1234567890AbC", "templateName": "📋 Player" }
],
"parents": [
{
"pageId": "Abcdef12345678901234",
"icon": "🩵",
"title": "Manchester City",
"viaBlocks": [{ "blockId": 3, "text": "Team Pages" }]
}
]
}
],
"+": 2
},
"snapshotSeq": 42
}- Only regular pages can be queried; template pages return
TEMPLATE_PAGE. viaBlockson each parent entry lists blocks in that parent containing a page-link item pointing to the child page.blockIdis the stable block identifier (not a positional index). Entries are ordered by block position within the parent page."+"indicates how many parent pages were truncated at that level.parentsis absent (not an empty array) when the page has no qualifying parents or is a leaf.- Recursion stops on exhausted limits, path cycles, or no qualifying parent pages.
- Template source pages and non-existent source pages are silently skipped.
- Errors:
PAGE_NOT_FOUND,TEMPLATE_PAGE,PARSE_ERROR.
ORIENTATION
Returns a high-level structural overview of the workspace.
Request:
{ "type": "command", "requestId": "1", "cmd": "ORIENTATION" }No parameters. Automatically chooses between two modes:
Hub mode — when the workspace has well-connected pages (hub coverage >= 60%):
{
"type": "response", "requestId": "1", "cmd": "ORIENTATION",
"ok": true,
"mode": "hubs",
"totalPages": 48,
"totalTemplatePages": 6,
"tabs": [{
"pageId": "AbcDef1234567890GhIj", "kind": "page", "icon": "📄",
"title": "Inbox", "subtitle": "Open items",
"path": [], "displayWidthPx": 500
}],
"templates": [{ "pageId": "TplPage1234567890AbC", "icon": "📋", "title": "Daily Note Template" }],
"hubs": [{
"pageId": "HubPage1234567890AbC", "icon": "📄", "title": "Projects",
"pageReach": 31, "pageReachPercent": 64.6,
"map": { "pageId": "HubPage1234567890AbC", "icon": "📄", "title": "Projects", "blocks": [] }
}],
"coveredPages": 31,
"coveragePercent": 64.6,
"snapshotSeq": 42
}Listing mode — when pages are mostly disconnected:
{
"type": "response", "requestId": "1", "cmd": "ORIENTATION",
"ok": true,
"mode": "listing",
"totalPages": 12,
"totalTemplatePages": 1,
"tabs": [],
"templates": [],
"pages": [{
"pageId": "AbcDef1234567890GhIj", "icon": "📄", "title": "Inbox",
"subtitle": "Open items",
"blocks": ["First block preview"],
"outboundPageLinks": [{ "pageId": "ZyxWvu9876543210TsRq", "title": "Alpha" }],
"inboundPageLinks": [],
"counts": { "outboundPageLinkCount": 1, "inboundPageLinkCount": 0, "pageReach": 4 }
}],
"snapshotSeq": 42
}Key details:
tabspreserves workspace order. Subtitles truncated to 100 chars.templatescapped at 30 entries, sorted by creation time.- Hub mode: up to 3 hubs selected greedily by reach. Coverage target 80%, minimum 60%.
- Listing mode: up to 200 pages. Detail tiers: < 30 pages (full detail), < 100 (subtitle), >= 100 (minimal).
Events Commands
SUBSCRIBE
Subscribes to real-time event notifications. Events are delivered as push messages on the same WebSocket connection.
Request:
{ "type": "command", "requestId": "1", "cmd": "SUBSCRIBE", "categories": ["pages", "project", "workspace", "files"] }Valid categories: "pages", "project", "workspace", "files".
Subscriptions are additive — calling SUBSCRIBE again adds to existing subscriptions.
Response:
{
"type": "response", "requestId": "1", "cmd": "SUBSCRIBE",
"ok": true,
"activeCategories": ["pages", "project", "workspace"],
"seq": 42
}activeCategoriesis the full set after the update.seqis the instance's current event sequence number — use it to detect gaps.
UNSUBSCRIBE
Removes event subscriptions for specified categories.
Request:
{ "type": "command", "requestId": "1", "cmd": "UNSUBSCRIBE", "categories": ["workspace"] }Response:
{ "type": "response", "requestId": "1", "cmd": "UNSUBSCRIBE", "ok": true, "activeCategories": ["pages", "project"] }Unsubscribing from a category you're not subscribed to is a no-op.
Event System
Subscribed clients receive event messages:
{
"type": "event",
"event": "pages_updated",
"seq": 43,
"instanceId": "desk-main",
"timestamp": 1712766300,
"...": "event-specific fields"
}All events include type, event, seq, instanceId, and timestamp (UNIX seconds) — with one exception: the eviction event (see Eviction Event) omits seq, instanceId, and timestamp because it is a server-generated signal, not an instance-scoped event.
Sequence Numbers and Gap Detection
Each instance maintains a monotonic seq counter. When you subscribe, the response includes the current seq. Compare it with seq on incoming events and with snapshotSeq from read commands to detect gaps.
Debounce behavior: High-frequency events are debounced before delivery:
- User page edits: 300ms trailing debounce, 1000ms max wait.
- Scroll events: 2000ms trailing debounce.
- API-originated mutations: delivered immediately after response.
Debounce merging can produce seq gaps — this is expected and does not mean events were lost.
Important: Events are mutation-backed, not output-backed. If page B's title changes and page A has a page link to B, page A's resolved pageLink.title changes but page A does not emit an event. Cached reads can go stale without a direct event for the dependent page.
Pages Events
All events include source ("user", "api", "system") and requestId (when source is "api").
pages_created
Includes the page's icon, serialized title, and source template ID (if created from a template).
{
"type": "event", "event": "pages_created",
"seq": 43, "instanceId": "desk-main", "timestamp": 1712766300,
"source": "api", "requestId": "req-1",
"pages": [{
"kind": "page",
"pageId": "NewPage1234567890AbC",
"icon": "📋",
"title": [{ "type": "text", "text": "Meeting: Sprint Review" }],
"sourceTemplateId": "TemplABCDEF123456780"
}]
}sourceTemplateId is always present: a page ID string when created from a template, null otherwise.
pages_updated
Every changed region includes before/after diffs — the serialized state before and after the mutation.
{
"type": "event", "event": "pages_updated",
"seq": 44, "instanceId": "desk-main", "timestamp": 1712766310,
"source": "user",
"pages": [{
"kind": "page", "pageId": "AbcDef1234567890GhIj",
"role": "direct",
"scope": ["icon", "blocks"],
"icon": { "before": "📄", "after": "📝" },
"blockChanges": [{
"blockId": 0, "op": "updated",
"before": { "blockId": 0, "items": ["...old..."] },
"after": { "blockId": 0, "items": ["...new..."] }
}]
}]
}Page entry fields:
role:"direct"(edited) or"cascade"(side-effect, e.g. backlink updated).scope: which regions changed —"icon","title","subtitle","blocks".icon:{ before, after }— present when"icon"is in scope.title:{ before, after }— present when"title"is in scope. Values are serialized title unit arrays.subtitle:{ before, after }— present when"subtitle"is in scope. Values are serialized subtitle unit arrays.blockChanges: per-block changes withbefore/afterdiffs:op: "created"→before: null,after: { serialized block }op: "deleted"→before: { serialized block },after: nullop: "updated"→before: { old block },after: { new block }op: "reordered"→before: { old block },after: { new block }
pages_deleted
Includes the page's icon and title at deletion time.
{
"type": "event", "event": "pages_deleted",
"seq": 45, "instanceId": "desk-main", "timestamp": 1712766320,
"source": "api", "requestId": "req-1",
"pages": [{
"kind": "page",
"pageId": "AbcDef1234567890GhIj",
"icon": "📝",
"title": [{ "type": "text", "text": "Old Notes" }]
}]
}Project Events
All project events include source and requestId (when triggered by an API command).
project_opened
{
"type": "event", "event": "project_opened",
"seq": 1, "instanceId": "desk-main", "timestamp": 1712766240,
"source": "api", "requestId": "req-1",
"state": "folder", "folder": "Work Notes", "demo": null
}project_closed
{
"type": "event", "event": "project_closed",
"seq": 50, "instanceId": "desk-main", "timestamp": 1712766400,
"source": "api", "requestId": "req-1"
}concurrent_access_detected
{
"type": "event", "event": "concurrent_access_detected",
"seq": 51, "instanceId": "desk-main", "timestamp": 1712766410,
"source": "system",
"detectedBy": "api_server"
}detectedBy: "workspace_poll", "api_server", or "both".
Workspace Events
All workspace events include source and requestId (when triggered by an API command).
workspace_tab_opened
{
"type": "event", "event": "workspace_tab_opened",
"seq": 49, "instanceId": "desk-main", "timestamp": 1712766450,
"source": "user",
"tabIndex": 1, "pageId": "ZyxWvu9876543210TsRq", "kind": "page"
}workspace_tab_closed
{
"type": "event", "event": "workspace_tab_closed",
"seq": 50, "instanceId": "desk-main", "timestamp": 1712766460,
"source": "user",
"tabIndex": 1, "pageId": "ZyxWvu9876543210TsRq", "kind": "page"
}workspace_tab_navigated
{
"type": "event", "event": "workspace_tab_navigated",
"seq": 51, "instanceId": "desk-main", "timestamp": 1712766470,
"source": "user",
"tabIndex": 0, "previousPageId": "AbcDef1234567890GhIj", "newPageId": "ZyxWvu9876543210TsRq", "kind": "page"
}workspace_tab_resized
{
"type": "event", "event": "workspace_tab_resized",
"seq": 52, "instanceId": "desk-main", "timestamp": 1712766480,
"source": "user",
"tabIndex": 0, "pageId": "AbcDef1234567890GhIj", "displayWidthPx": 640
}workspace_scrolled
{
"type": "event", "event": "workspace_scrolled",
"seq": 53, "instanceId": "desk-main", "timestamp": 1712766490,
"source": "user",
"tabIndex": 0, "pageId": "AbcDef1234567890GhIj", "scrollTopPx": 420
}workspace_changed
A coarse fallback, suppressed when granular workspace events fire. WRITE_WORKSPACE emits only this event.
{
"type": "event", "event": "workspace_changed",
"seq": 54, "instanceId": "desk-main", "timestamp": 1712766500,
"source": "user",
"workspaceVersion": 3
}Files Events
Subscribe with category files.
files_changed
Emitted for each change record produced by an active FILES_WATCH subscription.
{ "type": "event", "event": "files_changed",
"seq": 99, "instanceId": "desk-main", "timestamp": 1712766500,
"source": "system",
"subscriptionId": "fwatch_...",
"subpath": "inbox",
"recursive": true,
"changeType": "appeared",
"path": ["recipes", "curry.png"],
"movedFrom": null }subscriptionId— the subscription that produced the record.subpath— the watched folder, verbatim as passed toFILES_WATCH.recursive— whether the watch was started in recursive mode.changeType— one ofappeared,disappeared,modified,moved,unknown,errored.path— path components of the changed entry, relative tosubpath. Empty array when the subpath itself changed.movedFrom— the former path components whenchangeTypeismoved, otherwisenull.
See FILES_WATCH for the full list of sharp edges (unknown, errored, Windows cross-directory moves, partial-write timing, per-origin limits).
Eviction Event
Always delivered regardless of subscriptions. Sent when another tab connects with the same gb_id:
{
"type": "event", "event": "eviction",
"reason": "Another instance connected with the same ID"
}The evicted app disconnects and stops reconnecting.
Error Reference
Server-Level Errors
Arrive as type: "error" messages.
| Code | Description |
|------|-------------|
| INVALID_JSON | Message is not valid JSON |
| MISSING_REQUEST_ID | Command missing requestId or cmd |
| NO_INSTANCES | No GalaxyBrain instances connected |
| UNKNOWN_INSTANCE | Specified instance ID not found |
| INSTANCE_REQUIRED | Multiple instances connected; must specify instance |
| INSTANCE_DISCONNECTED | Instance disconnected while processing command |
| PROTOCOL_MISMATCH | Browser app protocol version mismatch |
| UNKNOWN_MESSAGE_TYPE | Unrecognized message type |
PROTOCOL_MISMATCH errors include additional fields:
serverProtocolVersion— the server's protocol version (number)clientProtocolVersion— the client's claimed protocol version (number or null)
Command-Level Errors
Arrive inside response messages as { ok: false, error, message }.
General:
| Code | Description |
|------|-------------|
| NO_PROJECT | Project-level command sent with no project open |
| CONFLICT | readVersion mismatch |
| INTERNAL_ERROR | Unexpected exception |
Parse and validation:
| Code | Description |
|------|-------------|
| PARSE_ERROR | Invalid payload shape, field types, or unrecognized command name |
| INVALID_ICON | Icon is not a valid emoji |
| INVALID_STYLE | Unsupported text item style |
| NO_BLOCKS | Page has zero blocks |
| NO_ITEMS | Block or push payload has zero items |
| INVALID_BLOCK_ID | Invalid block ID |
| INVALID_VAR_ID | Invalid variable ID |
| DUPLICATE_BLOCK_ID | Block ID duplicated within a page |
| DUPLICATE_VAR_ID | Var ID duplicated within a page or collides with existing |
| INVALID_LINK_ORDER | linkOrder string does not match grammar |
| INVALID_TITLE_UNIT | Title used unsupported unit type or styled text |
| INVALID_FORMULA_UNIT | Formula used unsupported unit type or styled text |
| EMPTY_TEXT | Text or URL field is empty |
| INVALID_META_REF | Meta reference string is malformed |
| SELF_LINK | Page link points to the page being written |
| INVALID_TEMPLATE_ID | Template ID invalid, missing, or is a regular page |
| INVALID_TEMPLATE_VALUE | Template value type invalid or unavailable |
| NO_UPDATES | Surgical update has no actual changes |
| DUPLICATE_BLOCK_OP | Same block in multiple surgical operations |
| BLOCK_ORDER_MISMATCH | blockOrder doesn't match final block set |
Images:
| Code | Description |
|------|-------------|
| IMAGE_NOT_FOUND | imageId is malformed |
| IMAGE_FILE_NOT_FOUND | imageFile key missing from images map |
| IMAGE_AMBIGUOUS | Image item has both imageId and imageFile |
Page and template targeting:
| Code | Description |
|------|-------------|
| PAGE_NOT_FOUND | Page or template not found |
| TEMPLATE_PAGE | Regular-page command sent to a template |
| NOT_TEMPLATE_PAGE | Template command sent to a regular page |
| BLOCK_NOT_FOUND | Block ID doesn't exist on target page |
| BLOCK_ALREADY_EXISTS | Inserted block ID already exists |
| LAST_PAGE | Cannot delete the last remaining real page |
| NO_REMAINING_ITEMS | Pop would remove every item from a block |
| UNEXPECTED_ITEM_TYPE | expectedItemType mismatch |
Workspace:
| Code | Description |
|------|-------------|
| NO_TABS | Empty or missing tabs array |
| INVALID_TAB_PATH | Tab path structurally invalid |
Folder and demo lifecycle:
| Code | Description |
|------|-------------|
| FOLDER_NOT_FOUND | Recent-folder ID doesn't exist |
| PERMISSION_REQUIRED | Browser requires user gesture for folder access |
| FOLDER_UNAVAILABLE | Saved folder handle no longer accessible |
| FOLDER_LOAD_FAILED | Folder could not be loaded as a project |
| DEMO_LOAD_FAILED | Demo could not be loaded |
| OFFLINE | Command unavailable in offline mode |
File watching:
| Code | Description |
|------|-------------|
| UNSUPPORTED_BROWSER | Browser does not expose FileSystemObserver |
Recommended Client Flow
- Connect to
ws://localhost:<port>. - Send
LIST_FOLDERSto see available folders and demos. - If you expect multiple app tabs, include
instanceon every command. - Open a project with
OPEN_FOLDERorOPEN_DEMO. - Get your
