Skip to main content

State

What a mod remembers, across restarts.

State has its own entry point, and state is called the way every other declaration is:

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';
import { const state: StateFactory
Declare a piece of state your mod owns and persists. The value type is read off `TValue` by the authoring compiler, so the host can validate every write. `initial` is checked the same way at declaration time. What you get back is a declaration, not a value you can read. Pass it to `ctx.state(declaration)` inside a handler to get the handle with `.value` and `.set()`.
@exampleCount how often a command ran ```ts const runCount = state<number>({ id: 'my-mod.runCount', scope: 'workspace', initial: 0, }); command({ id: 'my-mod.run', title: 'Run', run: async ctx => { const count = ctx.state(runCount); await count.set(count.value + 1); }, }); ```@categoryState
state
} from '@modular/sdk/state';
const const greetCount: StateDeclaration<number>greetCount = state<number>(input: StateDeclarationInput<number>): StateDeclaration<number>
Declare a piece of state your mod owns and persists. The value type is read off `TValue` by the authoring compiler, so the host can validate every write. `initial` is checked the same way at declaration time. What you get back is a declaration, not a value you can read. Pass it to `ctx.state(declaration)` inside a handler to get the handle with `.value` and `.set()`.
@exampleCount how often a command ran ```ts const runCount = state<number>({ id: 'my-mod.runCount', scope: 'workspace', initial: 0, }); command({ id: 'my-mod.run', title: 'Run', run: async ctx => { const count = ctx.state(runCount); await count.set(count.value + 1); }, }); ```@categoryState
state
<number>({
StateDeclarationInput<number>.id: string
Identifier, unique within the mod.
id
: 'hello.greetCount',
StateDeclarationInput<number>.scope: "workspace" | "profile"
Whether the value persists per workspace or per profile.
scope
: 'workspace',
StateDeclarationInput<number>.initial: number
Value used the first time this state is registered.
initial
: 0,
}); export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Hello";
        readonly description: "A first mod";
    };
    readonly state: readonly [StateDeclaration<number>];
    readonly commands: readonly [Command<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Hello";
        readonly description: "A first mod";
    };
    readonly state: readonly [StateDeclaration<number>];
    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' },
state: readonly [StateDeclaration<number>]state: [const greetCount: StateDeclaration<number>greetCount], 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 => { const const count: OwnedState<number>count = ctx: CommandContextctx.ContextBase.state<number>(declaration: StateDeclaration<number>): OwnedState<number> (+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
(const greetCount: StateDeclaration<number>greetCount);
await const count: OwnedState<number>count.OwnedState<number>.set(value: number, options?: StateSetOptions): Promise<void>
Write a new value and wait for the host to commit it. The value is validated against the declaration's type first, so a write that does not match rejects instead of storing bad data.
set
(const count: OwnedState<number>count.State<number>.value: number
Current value. Reading it inside a reactive body subscribes to it.
value
+ 1);
}, }), ], });

What state(...) returns is a declaration, not a readable value. A handler passes it to ctx.state(...) to get the handle carrying .value and .set(). Only state your own mod declared is writable; built-in state is read-only. The trusted local workspace context CLI can list, read, and watch registered state, but it cannot write it.

What ships in @modular/sdk/state

Nine exports, and the names describe the model well enough to navigate:

ExportKind
statethe declaration factory you call
Statethe state handle
OwnedStatestate this mod owns
StateFactorywhat state is
StateDeclarationa declared slot
StateDeclarationInputwhat you pass to declare one
StateSetOptionsoptions on write
stateSchemaNamefunction
stateStabilityfunction

Declared state lands in the state key on your mod, and handlers reach the live value through ctx.state.

Why this lives in Start and not Connections

Declared state is the mod's durable memory. Other mods do not gain write access to it. The local CLI is an observation lane for the user, not a second state owner, so this page stays beside Contexts and the ctx.state surface that exposes it.