Commands & AI context
Two small contribution types: commands that show up in the command palette, and AI context providers that teach Undra’s agents about your extension’s data.
Both follow the same loadable-extension shape you already know from the overview: one extension.js, a manifest with the right capability, and an activate(ctx) that registers contributions. If you have not built one yet, read the ctx API first. Neither of these contributions needs an item type, so they are the lightest things you can ship.
Two separate capabilities
Commands need commands.registry. Context providers need contextProviders.registry. Declare whichever you use in manifest.capabilities. They are independent, declare one or both.
Commands
A command is a named action that appears in Undra’s command palette. The user opens the palette, types your command’s title, hits enter, and your handler runs. That is the whole model.
The shape
Register commands with ctx.registerCommands([...]). Each command is a CommandContribution:
type CommandContribution = {
id: string // unique, namespace it with your publisher
title: string // what the user sees in the palette
category: string // groups related commands in the palette
handler: (ctx: unknown) => void | Promise<void> // runs when invoked; may be async
}Field notes:
idis your stable handle. Namespace it (for examplecommunity.example.todo.archiveDone) so it never collides with another extension’s command.titleis the searchable label. Write it as an action: “Archive completed todos”, not “Todos”.categoryis a grouping label shown alongside the title (for example “Todo”). Keep it short and reuse the same category across your commands.handlerdoes the work. Return a promise if it is async. The argument is the host-provided invocation context, treat it as opaque, your real entry point to the workspace is thectxyou captured inactivate(closures work, see below).
A runnable example
This extension adds one palette command that creates a dated note. No item type, no editor, just an action.
export const manifest = {
id: 'community.example.dailynote',
version: '0.1.0',
displayName: 'Daily Note',
publisher: 'example',
capabilities: ['commands.registry'],
}
export function activate(ctx) {
ctx.registerCommands([
{
id: 'community.example.dailynote.create',
title: 'Create today\'s daily note',
category: 'Daily Note',
handler: async function () {
const today = new Date().toISOString().slice(0, 10) // YYYY-MM-DD
await ctx.workspace.create({
type: 'note',
title: 'Daily ' + today,
content: '# ' + today + '\n\n',
})
},
},
])
return function deactivate() {}
}Drop it at <workspace>/.undra/extensions/dailynote/extension.js, restart Undra, open the command palette, and type “daily”. Your command is there. Selecting it creates a fresh dated note.
The handler closes over activate’s ctx
The handler reaches the workspace through the ctx you captured in activate (the example calls ctx.workspace.create), not through the handler’s own argument. Capture anything you need (the workspace, the query API, your own state) in the closure. See the ctx API for workspace.create, workspace.update, and the read-only query surface.
AI context providers
A context provider is how your extension talks to Undra’s AI agents. When a chat or agent run is being prepared, Undra builds a prompt under a fixed token budget. Your provider gets called and can contribute two things into that budget: sticky facts (short, high-value lines the model should always know) and selected prompt lines (larger blocks, for example the body of a selected item). This is how you teach an agent about a custom item type or a piece of state it would otherwise have no idea exists.
Think of it as briefing the AI. If your extension stores data the model cannot see (a JSON body, an external sync, a computed status), a context provider is where you hand the model a one-line summary so its answers are grounded instead of guessed.
The shape
Register a provider with ctx.registry.registerContextProvider(manifest.id, contrib). The contribution is a ContextProviderContribution:
type ContextProviderContribution = {
id: string
order?: number // lower runs first; default 100
critical?: boolean // if true, a failure surfaces instead of being silently isolated
collect?: (ctx: unknown) => Promise<ContextProviderCollectResult | null | undefined>
collectSync?: (ctx: unknown) => ContextProviderCollectResult | null | undefined
}You provide collect, collectSync, or both:
collectSyncruns on the fast prompt-preview path. Use it for cheap, immediate sticky facts (no awaiting, no I/O). If the host is just previewing what the prompt will look like, this is what it calls.collectis the async path. Use it for heavier work: reading a selected item’s body, querying the workspace, hitting a cache. Returnnull(orundefined) when you have nothing to add, the host skips you cleanly.ordercontrols merge order across all providers (yours and Undra’s own). Lower numbers run first. Leave it at the default100unless you have a reason to sort earlier or later.criticaldefaults tofalse, which means if your provider throws, Undra isolates the failure and drops your slice rather than breaking the whole prompt. Set it totrueonly when missing your context is worse than a visible error.
What you return is a ContextProviderCollectResult:
type ContextProviderCollectResult = {
stickyFacts?: { tier: 'critical' | 'important' | 'supplemental'; text: string }[]
selectedPromptLines?: string[]
}stickyFactsare short lines the model keeps in view. Each has atier. When the budget is tight,criticalsurvives,importantis next, andsupplementalis dropped first. Spendcriticalsparingly, it is the most expensive real estate in the prompt.selectedPromptLinesare full prompt lines (for example the body of the item the user has selected). They are merged in registryorder. Use them for bulk content, not for the always-on summary.
Sticky facts cost budget
Every sticky fact competes for a fixed prompt budget with everything else (other extensions, Undra’s own context, the conversation). Keep each line tight and factual. One good critical fact beats five vague supplemental ones. Do not dump whole records here, that is what selectedPromptLines is for.
A runnable example
This pairs with a hypothetical “todo” item type (see item types) and teaches the AI how many todos are open, so an agent asked “what is on my plate” answers from real data instead of hallucinating.
export const manifest = {
id: 'community.example.todocontext',
version: '0.1.0',
displayName: 'Todo AI Context',
publisher: 'example',
capabilities: ['contextProviders.registry'],
}
export function activate(ctx) {
ctx.registry.registerContextProvider(manifest.id, {
id: 'community.example.todocontext.openCount',
order: 100,
// Async because we read the workspace. Returns null when there is nothing useful.
collect: async function () {
// ctx.query is the read-only workspace query surface (see "The ctx API").
const page = await ctx.query.queryMetadata({ itemType: 'todo', limit: 500 })
const open = page.rows.filter(function (r) { return !r.dueDate || r.dueDate >= todayIso() })
if (open.length === 0) return null
return {
stickyFacts: [
{
tier: 'important',
text: 'The user has ' + open.length + ' open todo item(s) of type "todo". '
+ 'Use the todo items, not notes, when asked about tasks.',
},
],
}
},
})
return function deactivate() {}
}
function todayIso() {
return new Date().toISOString().slice(0, 10)
}Drop it at <workspace>/.undra/extensions/todocontext/extension.js, restart Undra, then ask an agent about your tasks. Your sticky fact is now part of the prompt, so the model knows the count and which item type to trust.
Prefer collectSync for cheap facts
If your fact does not need any I/O (it is a constant, or something you already hold in a captured variable), put it in collectSync so it shows up on the fast preview path too. Provide collect only when you must await something. You can provide both, the host calls the right one for each path.
Where to go next
- The ctx API, the
workspaceandquerysurfaces your handlers and providers call into. - Item types, give your data a real type so context providers have something concrete to describe.
- Packaging & distribution, bundle and ship what you built here.