Skip to main content

@modular/sdk

Build mods for Modular

One typed value, exported from src/mod.tsx. No manifest to keep in sync, no activation step, no second schema language — your TypeScript types are the contract.

A mod is one typed value. You call defineMod with metadata plus an array per contribution kind, and the host mounts them. There is no imperative activation step and no lifecycle to manage.

import { function command<TId extends string>(commandValue: CommandInput<TId>): Command<TId>
Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.
@categoryAuthoring
command
, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): Mod
The entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
} from '@modular/sdk';
export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Hello";
        readonly description: "A first mod";
    };
    readonly commands: readonly [Command<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Hello";
        readonly description: "A first mod";
    };
    readonly commands: readonly [Command<string>];
}): Mod
The entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
({
metadata: {
    readonly displayName: "Hello";
    readonly description: "A first mod";
}
metadata
: { displayName: "Hello"displayName: 'Hello', description: "A first mod"description: 'A first mod' },
commands: readonly [Command<string>]commands: [ command<string>(commandValue: CommandInput<string>): Command<string>
Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.
@categoryAuthoring
command
({
id: stringid: 'hello.greet', title: stringtitle: 'Greet', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { await ctx: CommandContextctx.ui: UiCapability
Notifications, dialogs, quick input, and progress.
ui
.UiCapability.notification: UiNotificationCapabilitynotification.UiNotificationCapability.info(request: UiNotificationRequest): Promise<UiActionResult>info({ UiNotificationRequest.message: stringmessage: 'Hello.' });
}, }), ], });

The host folds that value into a manifest at build time and into live wiring at runtime. The package ships with the host, so the version you build against is the version that runs you.

The guides

Organised by who is on the other end, not by what type a thing is. The reference half of this site already sorts by type, and better.

Two ways to read this SDK

Breadth and depth are different questions with different answers.

QuestionAnswer
What exists?The reference — every export, grouped by entry point
What is X, which I have not imported?modular mod docs <mod-id> --symbol X
What is X at this position in my code?modular mod language <mod-id> hover <file> --line <n> --column <n>
How does a process use the running workspace?CLI
Did the public surface change?packages/modular/sdk/api/sdk.api.md, committed

The generated reference and modular mod docs read the same TypeDoc output. modular mod language queries the installed source snapshot at one exact position.

Entry points

One package name over the authoring API and its runtime subpaths. See Entry points.

@modular/sdk/ui is the host's live React component library. The Components guide shows how it composes, and the generated UI reference contains every export.

For host resources, start with Terminal. It shows one complete path from a CLI command to a persistent Codex terminal and back to a structured response.