Skip to main content

System

The host's own signals, exposed to your mod.

system is a value, not a factory:

import { 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
,
const system: Readonly<{
    on<TPayload = unknown>(input: SystemListenerInput<TPayload>): SystemListener<TPayload>;
}>
Declare a listener on a host event source. `system.on` builds a value you list in `defineMod`; it does not subscribe by itself. The host owns the subscription for as long as the mod is active. Two neighbours to keep straight. For in-editor facts such as selection changes and committed actions, `ctx.on('editor.selectionChanged', listener)` is typed and simpler. For a subscription whose lifetime you manage yourself, call `ctx.system.on(request, listener)` and dispose it when you are done. Source and event ids come from the host, not from a fixed list in the SDK. Discover them with `ctx.system.listSources()` and `ctx.system.describeSource(source)`.
@categoryState
system
} from '@modular/sdk';
export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Watcher";
        readonly description: "Watches a host source";
    };
    readonly system: readonly [SystemListener<unknown>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Watcher";
        readonly description: "Watches a host source";
    };
    readonly system: readonly [SystemListener<unknown>];
}): 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: "Watcher";
    readonly description: "Watches a host source";
}
metadata
: { displayName: "Watcher"displayName: 'Watcher', description: "Watches a host source"description: 'Watches a host source' },
system: readonly [SystemListener<unknown>]system: [
const system: Readonly<{
    on<TPayload = unknown>(input: SystemListenerInput<TPayload>): SystemListener<TPayload>;
}>
Declare a listener on a host event source. `system.on` builds a value you list in `defineMod`; it does not subscribe by itself. The host owns the subscription for as long as the mod is active. Two neighbours to keep straight. For in-editor facts such as selection changes and committed actions, `ctx.on('editor.selectionChanged', listener)` is typed and simpler. For a subscription whose lifetime you manage yourself, call `ctx.system.on(request, listener)` and dispose it when you are done. Source and event ids come from the host, not from a fixed list in the SDK. Discover them with `ctx.system.listSources()` and `ctx.system.describeSource(source)`.
@categoryState
system
.on<unknown>(input: SystemListenerInput<unknown>): SystemListener<unknown>on({
SystemListenerInput<TPayload = unknown>.id: string
Identifier, unique within the mod.
id
: 'watcher.power',
SystemListenerInput<TPayload = unknown>.title: string
Label shown where the host lists active subscriptions.
title
: 'Power state',
SystemListenerInput<unknown>.source: `system.${string}`
Which host source to listen to, for example `'system.power'`.
source
: 'system.power',
SystemListenerInput<TPayload = unknown>.event: string
Which of that source's events to listen for.
event
: 'changed',
SystemListenerInput<unknown>.handle: SystemListenerHandler<unknown>
Runs on every occurrence.
handle
: async (occurrence: SystemSourceEvent<unknown>occurrence, ctx: SystemListenerContextctx) => {
ctx: SystemListenerContextctx.ContextBase.logger: Logger
Your mod's log channel.
logger
.Logger.info(message: string, ...args: readonly unknown[]): voidinfo(`${occurrence: SystemSourceEvent<unknown>occurrence.SystemSourceEvent<unknown>.source: `system.${string}`source} emitted at ${occurrence: SystemSourceEvent<unknown>occurrence.SystemSourceEvent<TPayload = unknown>.emittedAt: stringemittedAt}`);
}, }), ], });

system.on builds a value you list in defineMod; it does not subscribe by itself. Source and event ids are host-owned — discover them at runtime with ctx.system.listSources().

The handler receives a SystemSourceEventsource, event, payload, scope, emittedAt. system.on takes a payload type parameter, but the system slot is typed SystemListener<unknown>, so a listener declared with one does not fit. Narrow the payload inside the handler instead.

Its shape is Readonly<{ on }> — one member. You subscribe; you do not publish.

The asymmetry is the point

Compare it to events, where your mod both declares and emits:

events    your mod declares  →  others subscribe
system    the host declares  →  your mod subscribes

Same mechanism, opposite direction. That is why both live under Connections: one is the outbound half of talking to the rest of the system, the other is the inbound half.

Reach it from a handler

ctx.system exposes the same surface inside a running handler, so a handler does not need the module-level import.

What the host publishes

The event ids the host owns are listed in ModularEventMapeditor.selectionChanged, host.action.didCommit, multiplayer.presenceChanged.

host.action.didCommit is the one to notice: it is the completed-action signal that pairs with hooks, which fire before an action commits.