Everything the object passed to your plugin can do, by area. The member list and the signatures are checked against the TwigApi type by a test, so this page cannot fall behind the code.
The plugin. Twig imports the module and calls its default export with an API object of its own: at launch while it is on, each time it is switched on, and again when the marketplace listing changes what it runs (a re-approve, a new URL). Whatever it returns is called when the plugin is switched off or removed, and before each of those restarts.
twig
TwigApiThis plugin’s own API object; disabling the plugin undoes everything registered through it.
Returns
void | (() => void)
It can run more than once in a session, so keep no state outside it that a second run would double. Every add returns a function that undoes it, and on an unsubscribe, but you do not have to keep them: Twig undoes everything a plugin registered before it stops or starts again. Return a cleanup for what you made yourself, such as a timer or a window listener.
The page the screen is showing: a page, a day, or the page a zoomed block belongs to.
Undefined on screens that are not a page — All pages, Tasks, Settings, and a plugin view of something else. On a view that says page: true it is the page that view is showing. Pair it with on(‘route’) to follow the user around.
jspages.current
const page = twig.pages.current()
if (!page) return
Every block that references the page, by [[name]], #tag or alias, and the non-empty blocks of any page that names it in its own properties. Each block once, in no particular order.
name
string
Returns
Block[]Empty when no page has that name.
jspages.backlinks
const n = twig.pages.backlinks('Ana Ruiz').length
twig.notice(`${n} blocks mention Ana`)
A new array of the live records, built by a walk over the whole graph: fine on a click, not on every keystroke. Read the records; change them only through the API.
The block’s words as the app draws them on one line, the way a task row shows them: the priority as its small letter, a reference or a tag as its name, code as code. No brackets, backticks, [#A], date lines or properties. A reference to a journal day reads short, as a row says a day: Sep 23, with the year only when it is another (Sep 23, 2025).
id
string
drop?
(ref: string) => booleanCalled with each reference’s page name as written (Sep 23rd, 2026, not the short form it reads as). One it returns true for is left out, with the space after it — say, the page your view is already showing. Kept when it is all the words the block has.
Returns
HTMLElement
A new <span> on each call, styled by the app’s own classes. A reference in it is words, not a link, so it can sit inside a link of your own; a macro is left out. Empty once the block is deleted.
jsblocks.words
const a = document.createElement('a')
a.href = `#/block/${block.id}`
a.append(twig.blocks.words(block.id))
A slash command, listed under / by name. run receives the block the cursor is in. From a plugin the marketplace files under Actions, the same command is also in the Actions menu on a page’s own screen (not on Today, a day’s screen, a whiteboard or a zoomed block), and run then receives the page’s first block, or '' when the page is empty.
name
stringWhat the / list shows, and what the Actions menu shows.
run
(ctx: { blockId: string }) => void
Returns
() => void
Keyed by exact name: a second plugin registering the same name replaces the first.
Renders {{name args}} wherever it appears in a block. The way a plugin puts something inside the outline.
name
string
render
(args: string, blockId: string) => HTMLElementGets everything after the name, as written, and the id of the block it is in.
Returns
() => void
The name is lower-cased when stored, so {{WordCount}} and {{wordcount}} are one macro. The element is rebuilt when the macro’s own text changes or the block leaves edit mode — not when other lines of the block change. Give the element a dispose function and Twig calls it when the element is discarded.
jsmacros.add
twig.macros.add('shout', (args) => {
const el = document.createElement('strong')
el.textContent = args.toUpperCase()
return el
})
A screen of its own at #/x/<slug>. Anything after the slug in the hash arrives as params, already decoded. A view that reads params[0] as a page name says so with page: true, and a page’s own screen then offers it as a tab beside Outline, opening #/x/<slug>/<page name>. There is no tab on Today, a day’s screen, a whiteboard or a zoomed block.
slug
stringOne case, like a macro name: #/x/Board and #/x/board are one view.
view
{ label: string; page?: true; render(host: HTMLElement, params: string[]): void | (() => void) }page: true arrived in 0.17.0; before it, no view had a tab on a page.
Returns
() => void
The screen is rebuilt when the params change, and the cleanup you return runs first. Keep state in the hash if it should survive that.
A short line at the bottom of the screen: the way to say “done”, or that something happened out of view.
Gone after three seconds. Two can be up at once; a third replaces the oldest rather than queueing behind them. Trimmed and cut at 160 characters; an empty one shows nothing.
What pages.list, pages.get, pages.create and pages.current return. A journal day is a page too, and so is a whiteboard.
id
stringStable. blocks.children(page.id) is its top-level blocks.
name
stringAs written, case kept.
nameLower
stringThe name in lowercase, for comparing.
kind
'page' | 'journal' | 'whiteboard'A journal is a day.
day?
numberA journal’s day key, such as 20260918.
favorite?
trueStarred.
aliases?
string[]Kept for builds before 0.43.0; read props.alias instead.
type?
PageTypeWhat the page is, set from its menu (0.35.0): page, tag, person, location, book, website, organization, event, artist, album, track, movie or tv-show (organization and event since 0.56.0, the last five since 0.90.0). Absent, it is inferred.
props?
Record<string, string>The page’s own properties (0.43.0): a type’s fields, alias, tags, anything imported. Keys lowercase, values as written. Absent on a page from before 0.43.0 not yet moved over.
propsAt?
Record<string, number>When each property last changed. For sync; a plugin has no use for it.
createdAt
numberMilliseconds since the epoch.
updatedAt
numberMilliseconds since the epoch, when last written.
These are the live records: read them, and change a page only through the API.
What blocks.get, blocks.all, pages.backlinks and the inserts return. Everything after updatedAt is read out of content, so it changes when the text does.
id
stringStable, and what ((id)) references.
pageId
stringThe page it is on.
parentId
string | nullThe block it is under, or null at the top of the page.
order
numberSorts it among its siblings. Compare it; do not compute with it — blocks.children is the order.
content
stringThe text as written, markers, dates and properties included.
collapsed?
trueFolded.
createdAt
numberMilliseconds since the epoch.
updatedAt
numberMilliseconds since the epoch, when last written.
refs
string[]The page names it references — [[name]], #tag, and tags:: and alias:: values — first-seen case, each once.
tags
string[]The refs written as tags.
blockRefs
string[]The block ids it references with ((id)).
marker?
MarkerTODO, DOING, DONE, LATER, NOW, WAITING or CANCELED, from the first line.
priority?
PriorityA, B or C, written [#A].
heading?
numberThe level of a # heading.
scheduled?
numberThe SCHEDULED: day key.
deadline?
numberThe DEADLINE: day key.
properties?
Record<string, string>The block’s own key:: value lines.
These are the live records: read them, and change a block only through the API.
A field in your own panel or view: a plugin cannot add a section to Settings. The Reading time example is one.
Storage
localStorage, under a key only your plugin uses, prefixed with its name. It stays on the device: not synced, not in a backup.
Network
fetch, as on any page, within CORS. Say what leaves the device in the marketplace’s Network line.
Reads and writes
The graph is held in memory and written behind you, so reads are synchronous and cheap. Writes go through the same path the editor uses: references are re-derived, undo records a step, and other devices get the change on the next sync.
Rendering
A plugin never sees React. A macro returns an element; a panel or a view is handed a host element to fill. The app’s design tokens are in scope on anything you make — write color: var(--ink) or background: var(--panel) and both color schemes are handled for you. A block’s words, drawn the way the app draws them, are blocks.words. Styles you need are yours to inject: a single <style> element added once, from a string, is the pattern the shipped plugins use.
The app’s class names are its own and can change in any release, except these eight, which a plugin may put on what it makes. The examples use nothing else.
Class
What it draws
chip
Plugin or macro output: a small, quiet label that is not a link.
meta
A line of small print in the app’s voice, in --ink-3.
muted
A quiet sentence: a hint, or an empty state.
eyebrow
A label in mono capitals over a section or a column.
num
A number in the mono.
check
The app’s checkbox, on an <input type="checkbox">.