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.
Opening the metadata panel
Section titled “Opening the metadata panel”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.
-
Toggle the panel
⌘⇧Mopens (or closes) the metadata panel on the current page. -
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. -
Save
Click save to send a
setPageMetadatacommand. Invalid JSON is rejected client-side (the editor flags it and refuses to save) rather than partially applied.
Where it lives
Section titled “Where it lives”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.
How metadata drives behavior
Section titled “How metadata drives behavior”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.
Querying metadata
Section titled “Querying metadata”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.