Skip to content

Commands

Commands are named actions available through ctx.commands. A plugin can run them directly, and the host can expose them through menus, shortcuts, or a command palette.

Keep each command focused on one action. Ask for confirmation or collect input before running a command rather than putting interactive UI inside run.

Plugins must request registry.command to access ctx.commands:

{
"recall": {
"permissions": ["registry.command"]
}
}
type ArchiveNotePayload = {
noteId: string;
};
const registration = await ctx.commands.register<ArchiveNotePayload, boolean>({
id: "focus-tools.note.archive",
name: "Archive note",
description: "Moves a note into the archive.",
async run(payload) {
if (!payload || typeof payload.noteId !== "string") {
throw new Error("A note id is required.");
}
return archiveNote(payload.noteId);
},
});

Command definitions contain:

FieldTypeDescription
idstringStable id unique within the runtime. Prefix plugin-owned ids to avoid collisions.
namestringHuman-readable label for menus, palettes, and diagnostics.
descriptionstring?Optional explanation of the action.
run(payload) => resultAction executed when the command is invoked. It may return a promise.

Recall supplies owner and registeredAt from the active plugin session; a plugin does not provide those fields.

Duplicate ids are rejected. The registry does not validate a command’s payload, so validate unknown input inside run before reading it.

Pass the command id and its payload to run:

const archived = await ctx.commands.run<ArchiveNotePayload, boolean>(
"focus-tools.note.archive",
{ noteId: "note_123" },
);

run rejects when the id is not registered or when the command throws. Handle errors at the point where you can show useful feedback or recover safely.

Use get for one id and list for a snapshot of the registry:

const archiveCommand = await ctx.commands.get("focus-tools.note.archive");
const allCommands = await ctx.commands.list();

Treat returned command definitions as read-only.

register returns a disposable handle. Dispose it when the plugin unloads so the command does not outlive the plugin:

await registration.dispose();

ctx.commands.unregister(id) also removes a command by id. Prefer the registration handle for plugin-owned cleanup because it represents that exact registration. Dispose subscriptions separately when they are no longer needed.

Subscribe to registry changes with on:

const subscription = await ctx.commands.on("listChange", ({ commands }) => {
console.log(`${commands.length} commands available`);
});
await subscription.dispose();
EventPayload
register{ command } for the new command.
unregister{ command } for the removed command.
listChange{ commands } containing the current command snapshot.
MethodPurpose
register(command)Register a command and return its disposable handle.
unregister(id)Remove a command by id.
get(id)Read one registered command.
list()Read a snapshot of all commands.
run(id, payload?)Invoke a command and await its result.
on(event, listener)Subscribe to command registry events.