Skip to main content

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.
@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: "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>];
}): 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: "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.
@categoryAuthoring
command
({
id: stringid: 'copy.selection', title: stringtitle: 'Copy the selection', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const
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;
} | null
selection
= 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;
} | null
Current 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;
} | null
selection
=== 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

ModularActionDescriptorDtoid, 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.
@exampleConfirm the mod's command reached the action catalog ```ts check({ name: 'my-mod.registered', run: async ctx => { const action = await ctx.describeAction('command:my-mod.hello'); ctx.assert(action !== null, 'my-mod.hello did not register'); }, }); ```@categoryAuthoring
check
, 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: "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>];
}): 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: "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.
@exampleConfirm the mod's command reached the action catalog ```ts check({ name: 'my-mod.registered', run: async ctx => { const action = await ctx.describeAction('command:my-mod.hello'); ctx.assert(action !== null, 'my-mod.hello did not register'); }, }); ```@categoryAuthoring
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): void
Fails the check with `message` when `condition` is false.
assert
(const actions: readonly ModularActionDescriptorDto[]actions.ReadonlyArray<T>.length: number
Gets 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): void
Fails 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.