Actions
An action is a governed entry point into the host. Your mod calls one instead of reaching for the underlying capability directly, and the call is scoped, permissioned, and traceable.
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.command, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): ModThe 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.defineMod } from '@modular/sdk'; export defaultdefineMod<{ readonly metadata: { readonly displayName: "Copy"; readonly description: "Copies the selection"; }; readonly commands: readonly [Command<string>]; }>(definition: { readonly metadata: { readonly displayName: "Copy"; readonly description: "Copies the selection"; }; readonly commands: readonly [Command<string>]; }): ModThe 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.defineMod({metadata: { readonly displayName: "Copy"; readonly description: "Copies the selection"; }metadata: { displayName: "Copy"displayName: 'Copy', description: "Copies the selection"description: 'Copies the selection' }, 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.command({ id: stringid: 'copy.selection', title: stringtitle: 'Copy the selection', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { constconst selection: { workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "lexical"; startBlock: number; endBlock: number; startLineNumber?: number | undefined; endLineNumber?: number | undefined; } | { workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "monaco"; startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number; } | nullselection = ctx: CommandContextctx.ContextBase.state<"editor.selection">(name: "editor.selection"): State<{ workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "lexical"; startBlock: number; endBlock: number; startLineNumber?: number | undefined; endLineNumber?: number | undefined; } | { workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "monaco"; startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number; } | null> (+1 overload)Acquire a live state handle. Passing your own `state(...)` declaration gives an {@link OwnedState } you can write with `set`. Passing a built-in name gives a read-only {@link State } : host state changes only through actions.state('editor.selection').State<{ workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "lexical"; startBlock: number; endBlock: number; startLineNumber?: number | undefined; endLineNumber?: number | undefined; } | { ...; } | null>.value: { workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "lexical"; startBlock: number; endBlock: number; startLineNumber?: number | undefined; endLineNumber?: number | undefined; } | { workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "monaco"; startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number; } | nullCurrent value. Reading it inside a reactive body subscribes to it.value; if (const selection: { workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "lexical"; startBlock: number; endBlock: number; startLineNumber?: number | undefined; endLineNumber?: number | undefined; } | { workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "monaco"; startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number; } | nullselection === null) { return; } await ctx: CommandContextctx.ContextBase.action<"clipboard.writeText">(id: "clipboard.writeText", input: ClipboardWriteTextInput, options?: HostActionClientRunOptions): Promise<ClipboardWriteTextResult>Dispatch an action. This is the only way a mod changes the app. Takes a host action id or a bare command id. Inside an event listener, prefer `occurrence.action(...)` so the causal link survives.action('clipboard.writeText', { ClipboardWriteTextInput.text: stringtext:const selection: { workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "lexical"; startBlock: number; endBlock: number; startLineNumber?: number | undefined; endLineNumber?: number | undefined; } | { workspaceId: string; docId: string; text: string; preview: string; charCount: number; wordCount: number; capturedAt: string; origin: "monaco"; startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number; }selection.text: stringtext }); }, }), ], });
The action id is checked against the host's map, and so is the input shape that goes with it.
The descriptor
ModularActionDescriptorDto
— id, owner, title, input, parameters, result, defaultScope,
effects, stability.
That member list is the argument for this whole category:
owner— the action belongs to someone, and it is usually not you.defaultScope— how far the call reaches by default.effects— what it changes, declared up front rather than discovered.stability— whether you may depend on it.input/parameters/result— a typed call signature across a boundary.
Discovery and audit
import { function check<TName extends string>(checkValue: CheckInput<TName>): CheckDeclaration<TName>Declare a self-verification the host runs before it activates the mod. Checks are a gate, not a report. They run in order once everything else in the generation is staged. The first one to throw or fail an assert stops the run, the staged contributions roll back, and the generation is quarantined with the failure message as evidence. So a check that fails keeps a broken mod off the user's machine rather than letting it half-load. Use one to confirm the mod's own contributions really landed, for example that a command it depends on is registered. This is not the place for unit tests of your logic. It answers "did I wire myself up correctly here", which only the running host can tell you.check, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): ModThe 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.defineMod } from '@modular/sdk'; export defaultdefineMod<{ readonly metadata: { readonly displayName: "Audit"; readonly description: "Watches what the app does"; }; readonly checks: readonly [CheckDeclaration<string>]; }>(definition: { readonly metadata: { readonly displayName: "Audit"; readonly description: "Watches what the app does"; }; readonly checks: readonly [CheckDeclaration<string>]; }): ModThe 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.defineMod({metadata: { readonly displayName: "Audit"; readonly description: "Watches what the app does"; }metadata: { displayName: "Audit"displayName: 'Audit', description: "Watches what the app does"description: 'Watches what the app does' }, checks: readonly [CheckDeclaration<string>]checks: [ check<string>(checkValue: CheckInput<string>): CheckDeclaration<string>Declare a self-verification the host runs before it activates the mod. Checks are a gate, not a report. They run in order once everything else in the generation is staged. The first one to throw or fail an assert stops the run, the staged contributions roll back, and the generation is quarantined with the failure message as evidence. So a check that fails keeps a broken mod off the user's machine rather than letting it half-load. Use one to confirm the mod's own contributions really landed, for example that a command it depends on is registered. This is not the place for unit tests of your logic. It answers "did I wire myself up correctly here", which only the running host can tell you.check({ name: stringname: 'audit.actions-available', run: (ctx: CheckContext) => Promise<void>run: async ctx: CheckContextctx => { const const actions: readonly ModularActionDescriptorDto[]actions = await ctx: CheckContextctx.CheckContext.listActions(): Promise<readonly ModularActionDescriptorDto[]>Every action the host currently offers, this mod's included.listActions(); const const clipboard: ModularActionDescriptorDto | nullclipboard = await ctx: CheckContextctx.CheckContext.describeAction(id: string): Promise<ModularActionDescriptorDto | null>One action's descriptor, or `null` when nothing is registered under `id`.describeAction('clipboard.writeText'); ctx: CheckContextctx.CheckContext.assert(condition: boolean, message: string): voidFails the check with `message` when `condition` is false.assert(const actions: readonly ModularActionDescriptorDto[]actions.ReadonlyArray<T>.length: numberGets the length of the array. This is a number one higher than the highest element defined in an array.length > 0, 'no actions are registered'); ctx: CheckContextctx.CheckContext.assert(condition: boolean, message: string): voidFails the check with `message` when `condition` is false.assert(const clipboard: ModularActionDescriptorDto | nullclipboard !== null, 'clipboard.writeText is missing'); }, }), ], });
The trace surface is unusual and worth noticing: a mod can watch actions complete, including ones it did not invoke. That is an interconnection primitive, not a debugging aid.
The action map
ModularActionMap is the set of
governed ids the host publishes — ai.session.create, ai.session.send,
ai.session.fork, ai.session.archive, and the rest of that family.
To intercept one rather than call it, see Hooks.