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>): 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,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)`.system } from '@modular/sdk'; export defaultdefineMod<{ 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>]; }): 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: "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)`.system.on<unknown>(input: SystemListenerInput<unknown>): SystemListener<unknown>on({ SystemListenerInput<TPayload = unknown>.id: stringIdentifier, unique within the mod.id: 'watcher.power', SystemListenerInput<TPayload = unknown>.title: stringLabel 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: stringWhich 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: LoggerYour 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
SystemSourceEvent —
source, 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
ModularEventMap —
editor.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.