Plugin Lifecycle — How Plugins Register, Subscribe, and Reload
This is an explanation doc. It explains how the plugin system works conceptually, not how to write a specific plugin.
Workspace bus plugins
Section titled “Workspace bus plugins”Startup loading
Section titled “Startup loading”On container start, src/index.ts runs loadWorkspacePlugins():
- Scans
workspace/plugins/for.tsand.jsfiles - Dynamically imports each file via
await import(filePath) - Checks that the default export satisfies the
Plugininterface (name,install,uninstall) - Calls
plugin.install(bus)on each valid plugin
The install order is non-deterministic (filesystem scan order). Plugins should not depend on each other’s install order.
What happens in install()
Section titled “What happens in install()”install(bus) is where a plugin wires itself to the bus:
install(bus: EventBus): void { // Subscribe to inbound messages bus.subscribe("message.inbound.discord.#", this.name, this.handleInbound.bind(this));
// Start HTTP server for webhooks this.server = Bun.serve({ port: 8082, fetch: this.handleWebhook.bind(this), });}After install() returns, the plugin is live. It receives messages and can publish responses.
Graceful shutdown
Section titled “Graceful shutdown”On SIGTERM or SIGINT, src/index.ts calls plugin.uninstall() on each installed plugin in reverse order. Plugins should close HTTP servers, cancel timers, and release any resources here.
uninstall(): void { this.server?.stop(); clearInterval(this.pollInterval);}Hot reload is not supported
Section titled “Hot reload is not supported”There is no hot-reload for workspace bus plugins. To pick up a new or modified plugin:
docker restart workstaceanThe restart is fast (seconds) and is the intended workflow for plugin development.
SchedulerPlugin lifecycle
Section titled “SchedulerPlugin lifecycle”The SchedulerPlugin has its own internal lifecycle for timers:
- On
install(), it scansdata/crons/for YAML files and creates Node.js timers for each enabled schedule - On
command.scheduleactionadd— creates a timer immediately and writes the YAML file - On
command.scheduleactionremove— cancels the timer and deletes the YAML file - On
command.scheduleactionpause/resume— cancels/recreates the timer; updatesenabledin the YAML - On
uninstall()— cancels all active timers
Missed fire recovery: On startup, after loading all schedules, the plugin checks each lastFired timestamp. If a schedule was due between lastFired and now:
- Missed by ≤ 24 hours → fires immediately once
- Missed by > 24 hours → skipped; next regular fire applies
Plugin discovery failure modes
Section titled “Plugin discovery failure modes”If a workspace plugin file fails to import (syntax error, missing dependency), loadWorkspacePlugins() logs the error and skips that plugin. Other plugins continue to load. The server starts regardless.
If a plugin’s install() throws, the error is caught and logged. The plugin is not installed, but the server continues.
This means a broken plugin in workspace/plugins/ never prevents the server from starting — you can always connect and debug.