Views
A view renders a live table or board over a set of pages without any custom code: you describe a data source and a shape, and Subspace’s shipped, code-reviewed view components do the rendering. Because a view is described by data, not by a component you write, a plugin can ship a view with zero JavaScript, just a page.
Declarative view specs
Section titled “Declarative view specs”A view-spec element lives in a page like any other element, and its configuration is
its page content, so it rides OKF export/import and
multiplayer editing the same as everything else. Its shape:
type ViewSpec = { type: 'view-spec'; source: { dir: string }; view: 'table' | 'board'; fields?: string[]; // metadata dot-paths, e.g. 'crm.company' groupBy?: string; // board lanes; misses land in '(none)' filter?: { path: string; eq: unknown }[];};Data comes from kb.dir.pages { slug }, every page under the given directory that
carries metadata. Because it’s a live query rather than a snapshot, any
command that touches a page in that directory refreshes
the view over the pages topic, no manual reload.
tablerenders a title column plus one column per entry infields.boardrenders one lane per distinct value ofgroupBy(pages missing that field land in a trailing(none)lane); cards show the page title plus whicheverfieldsare non-empty on that page.
filter narrows the source to pages where a metadata path equals a given value, so a
view can, for example, show only people with crm.kind equal to "customer" out of a
directory that also holds companies and deals.
Saved live queries
Section titled “Saved live queries”A query element is the more capable sibling of view-spec: five views instead of two,
richer sources, and comparison predicates rather than a single equality filter.
{ type: 'query'; source: { dir: 'projects' }; // exactly one source, see below where: [{ field: 'status', op: 'neq', value: 'archived' }]; sort: [{ field: 'due', dir: 'asc' }]; // up to 5 group: 'status'; // lanes for board, sections for list fields: ['status', 'due']; // up to 50 view: 'table' | 'list' | 'board' | 'card' | 'calendar'; limit: 100; // default 100, capped at 500}Sources — exactly one of { dir } (returns pages), { type }, { tag },
{ checkbox: { checked } } (all return nodes), or { search: { q, kinds } }. On a search
source, kinds narrows the result to the indexed corpora you name, any of node,
memory, file, and mail. Omit it to search everything. Naming any kind also drops
page-title matches, which have no corpus of their own and so cannot be asked for.
Predicates — eq, neq, lt, lte, gt, gte, contains, exists. Comparisons
are type-gated: a numeric predicate only matches rows whose stored value is a number, so
a number kept as a string will not match gt: 5. Date values accept the macros
{{today}}, {{today+3d}}, and {{today-2w}}, which is how a saved query stays live
against the current date.
Fields you can filter, sort, or group on are page metadata (dotted paths like
crm.stage work), key:: labels, typed
node properties, and the built-ins id, pageId, title, slug, kind, tags — plus
nodeId and text on node sources. Built-ins win over same-named metadata. Note that
created_at and updated_at are not available.
Results are ACL-filtered before the limit is applied, so a page you cannot read never consumes a result slot. The element re-runs on every knowledge-base write, and a board card dragged to another lane writes the new group value back through the ordinary audited command path.
Creating and editing: type /query on any bullet to insert a starter query (open
checkboxes as a table), then use the frame’s edit action to change the source, view,
fields, grouping, sort, filters, and limit as a form — applied as one ordinary
setElement command, so it versions and multiplays like any other edit. The element is
still plain OKF (a ```subspace:query fenced block),
so agents and the SDK write the same JSON the form does. The built-in Todos page at
/p/todos is a shipped { checkbox: { checked: false } } list query. When a result set
fills the limit (or the server’s 500 cap, 50 for search), the element shows
showing first N — more may match so a truncated set is never mistaken for a complete
one.
Two sharp edges worth knowing. Calendar uses the declared dateField when set (the
edit form exposes it for the calendar view); without one it falls back to the first entry
in fields that any row can parse as a date, and with neither it renders
calendar needs a date field in fields. And a search source is answered by
hybrid search rather than the query compiler, so its rows carry
only kind, pageId, title, slug, rank, and text — filtering one on page metadata
matches nothing — its limit is capped at 50 regardless of limit, where and sort are
applied in the browser, and the memory kind comes back as mem.
The Kanban board
Section titled “The Kanban board”Two places in Subspace expose a purpose-built Kanban board on top of the same
drag-to-move interaction, rather than a generic view-spec:
Project and directory pages. Every directory page (other than the workspace root)
shows list | board tabs next to its title. Board mode lanes the directory’s child
pages by a status field, metadata.status by default, configurable per directory via
{ board: { statusField: 'stage' | 'crm.stage' | …, columns: [...] } } on the directory
page’s own metadata. Columns default to todo / doing / done; any status value seen
in the data that isn’t in the configured column list gets its own extra lane, and pages
with no status land in a trailing (none) lane. Cards show the page title and its
last-updated time; clicking one opens the page. Dragging a card to a different lane
issues an ordinary setPageMetadata command, the same audited write path as any other
metadata edit, and dropping on (none) clears the field.
The task queue. The task queue has its own list | board tabs,
where the board’s columns are its four sections, Needs confirmation, Up next, Scheduled,
Done, rather than an arbitrary status field. Cards show the task’s title and metadata
(kind, due date, run status, or completion time). Only the Done column accepts a
drop: dropping a card there is the one server-side resolution action, routed through the
same audited ops.tasks.resolve path the Confirm button or a completed checkbox use
(a needs-confirmation card resolves as confirm, an up-next item resolves as done).
Scheduled and Done cards are informational and can’t be dragged.
ESM viewers from plugins
Section titled “ESM viewers from plugins”When a table or board isn’t expressive enough, a plugin can ship a full ESM viewer instead: a compiled JavaScript bundle registered against a viewer id, loaded through an import map at runtime, and rendered wherever a page references it. This is how richer, domain-specific surfaces (a metric chart, a sweep board) get built without forking core view rendering. See views and surfaces for how a plugin registers one.