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.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'; import { const state: StateFactoryDeclare 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()`.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()`.state<number>({ StateDeclarationInput<number>.id: stringIdentifier, 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: numberValue used the first time this state is registered.initial: 0, }); export defaultdefineMod<{ 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>]; }): 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: "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.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: numberCurrent 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:
| Export | Kind |
|---|---|
state | the declaration factory you call |
State | the state handle |
OwnedState | state this mod owns |
StateFactory | what state is |
StateDeclaration | a declared slot |
StateDeclarationInput | what you pass to declare one |
StateSetOptions | options on write |
stateSchemaName | function |
stateStability | function |
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.