Skip to content

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.

A custom function is a page under the code/ directory:

  • the page title is the call name (a page titled total is called as total(...));
  • 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" };
}

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.

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.

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.

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.

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.

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.