Skip to content

Page metadata

Every page carries a metadata column, an arbitrary nested JSON object, alongside its bullets. Metadata is how a page becomes more than an outline: a directory turns into a board, a page turns into a terminal entry point, a person becomes a CRM record, all without a separate schema or table, just JSON on the page itself.

Metadata is hidden by default so it doesn’t clutter the outline. Press ⌘⇧M (or Ctrl+Shift+M) on any open page to toggle a panel labeled “metadata · queryable by custom functions”, a raw JSON text editor pre-filled with the page’s current metadata, pretty-printed. Edit the JSON directly and click save to commit it.

  1. Toggle the panel

    ⌘⇧M opens (or closes) the metadata panel on the current page.

  2. Edit the JSON

    The panel is a plain textarea containing JSON.stringify(page.metadata, null, 2). Edit any key, add new ones, or remove them, the object can be nested arbitrarily deep.

  3. Save

    Click save to send a setPageMetadata command. Invalid JSON is rejected client-side (the editor flags it and refuses to save) rather than partially applied.

Metadata is stored directly on kb.pages.metadata (jsonb, defaulting to {}), so it commits in the same transaction as every other page write and is versioned alongside the page like any other field. There is no separate metadata table: one JSON column per page, queried straight from Postgres with jsonb operators when a custom function or table formula needs it.

Several built-in features are just conventions on top of this one field. Subspace looks for a handful of well-known keys and renders different UI when it finds them:

Key Effect
fs.path Marks the page as a terminal entry point: one click drops a terminal node rooted at that filesystem path.
file.fileId Marks the page as file-owned: its body renders the referenced file or blob instead of plain bullets.
crm.* Renders the CRM header block (name, company, stage chips) at the top of a person or company page; see CRM.
meeting.* Drives the meeting status banner and links the page to its calendar event; see Meetings.
board.statusField, board.columns Configures a directory page’s board view: which metadata field lanes cards, and the default column order.
triage.onIngest Opts a page into project-relevance ingest triage, with an optional brief and context steering what counts as relevant.
status (or a custom path like crm.stage) The default field a directory’s board view lanes child pages by.

None of these keys are reserved at the schema level, they are simply the keys each feature’s rendering code looks for. Anything else you put in metadata is inert to the built-in UI but fully queryable by your own custom functions and plugins.

Because metadata is a normal jsonb column, a custom function can read any page’s metadata by id or by resolved [[Title]] reference and branch on arbitrary nested paths, the same way the board view resolves a dotted path like crm.stage into a lane. Combine it with a page’s labels when you want a value that lives visibly in the outline instead of hidden in JSON.