Custom functions
A custom function is a small piece of TypeScript you write as an ordinary page, that
becomes callable from any bullet like total(pipeline) and renders a live, structured
result. Functions run in a sandbox with no network or filesystem access by default and
only the knowledge-base capabilities you explicitly call, so a function you paste from
somewhere else, or one an agent writes, can’t do anything surprising.
Writing one
Section titled “Writing one”A custom function is a page under the code/ directory:
- the page title is the call name (a page titled
totalis called astotal(...)); - the first code cell node holds the TypeScript source;
- the first plain bullet is the function’s description, shown in the slash menu and the argument UI.
The source default-exports a function, sync or async, that returns a SubUI tree (see below). Subspace transpiles it with esbuild before it ever runs.
export default async function total(table: string) { const cell = await table.cell(table, "B1"); return { type: "stat", value: cell, label: "Total" };}Calling a function
Section titled “Calling a function”Type the function’s name followed by ( in a bullet, total(, and the introspected
signature appears as dashed argument-placeholder chips for each parameter. Typing [[
inside an argument opens the same page autocomplete used everywhere else in the outliner.
Arguments accept:
| Syntax | Resolves to |
|---|---|
[[Page Title]] |
The page’s title, as a string |
"a string" |
A string literal |
42, 3.14 |
A number |
true, false |
A boolean |
Once the call is filled in, ⌘↵, the same universal activation
shortcut every element uses, converts the bullet into a live function element. You can
also reach a function through the slash menu: typing
/ lists registered functions with their description and a type badge.
The sandbox
Section titled “The sandbox”Each run executes in a fresh QuickJS-WASM instance (asyncified, so await works
naturally inside sandboxed code), torn down after the call returns. Limits are enforced
per run: 32MB of memory, a 1MiB stack, and a 2 second wall-clock budget. The sandbox
starts with zero ambient authority, no globals for the filesystem, network, or process,
so everything a function can do is exposed explicitly through the host API.
Host API
Section titled “Host API”The host API is entirely async and capped at 64 calls per run:
| Call | Does |
|---|---|
kb.page(title) |
Reads a page by title |
kb.query({ dir }) |
Lists pages in a directory |
kb.backlinks(title) |
Lists pages linking to a given page |
kb.label(page, key) |
Reads a labeled node’s value off a page |
table.cell(name, 'A1') |
Reads a cell off a named table, the same addressing tables use |
http.get(url) |
Fetches an external URL |
kb.append(page, text) |
The one write capability: appends a bullet as an ordinary audited command |
kb.append is the only way a function can leave a durable trace; everything else it
returns is a live, recomputed view rather than a stored value.
SubUI: the render target
Section titled “SubUI: the render target”A function never returns HTML. It returns a small, Zod-validated JSON tree, SubUI, that a shared React renderer turns into UI. Because the schema is closed, a function can’t inject a script tag or break the surrounding page’s layout no matter what it computes.
| Node type | Shape |
|---|---|
text |
Plain text |
stat |
{ value, label?, tone? }, a labeled number or short figure |
badge |
A small pill of text |
row / col |
Layout containers |
table |
{ columns, rows } |
link |
{ target }, a page title, rendered as a knowledge-base link |
tone (on stat and badge) is one of default, ok, attention, urgent, or
link, giving the renderer a consistent color vocabulary across every function’s output.
Liveness
Section titled “Liveness”A function’s rendered output is a live view, not a stored snapshot. Subspace tracks
everything a run reads, labels, table cells, page contents, and re-runs and re-renders
the function in place whenever any of it changes, even when the change comes from another
window or another user. Nothing about the output is persisted; if you want a function’s
result to stick around as knowledge-base content, call kb.append explicitly from inside
the function.
Functions as agent tools
Section titled “Functions as agent tools”Every custom function doubles as a tool an agent can call: the same sandboxed page, the same host API, the same signature introspection. See plugin tools for how a function’s page becomes an entry in an agent’s tool allowlist.