Getting started
Write, load, and hot-reload your first Sulfur plugin
Plugin layout
A plugin is a directory inside the server’s plugins/ folder containing a plugin.toml manifest
and an entrypoint script:
Directories without a plugin.toml are treated as containers and scanned recursively (up to 8
levels deep), so you can group plugins into folders:
A directory with a plugin.toml is loaded as a plugin and never recursed into.
A minimal plugin
Drop the directory into plugins/ and start the server (or just save the file while the server is
running — the hot-reload watcher picks it up). The server logs:
The runtime selector
The runtime manifest field routes the plugin to its execution lane:
runtime = "starlark"— the entrypoint.starfile is executed once by the embedded Starlark interpreter. Hooks run inline on the tick goroutine (fast, sandboxed, deterministic).runtime = "python"— the entrypoint.pyfile is executed once by embedded CPython if the server was built with-tags python. Hooks are dispatched off-tick on a bounded worker pool. On a default build the plugin is skipped with a log line:plugin "name": python runtime not built in this binary; skipping.- Any other value is rejected loudly:
unknown runtime "..." (want "starlark" or "python").
Load semantics
- The module body runs exactly once. Everything you want the server to know must be captured
during that run (via
register,declare_mob,set_recipe_matcher, …). After load, module globals are frozen. - Loading is tolerant per plugin. The operator scan (
plugins/) skips a plugin that fails to load — bad manifest, Starlark error, unknown capability — logging the reason loudly, and continues with the rest. One broken plugin never takes down the others. - Errors are loud, never silent. An unknown event name, capability string, goal flag, base type, skill trigger, mechanic kind, or a typo’d entrypoint fails at load with a precise message. Sulfur’s plugin API has a strict “load loudly or skip” discipline: nothing silently no-ops.
Builtins available to every Starlark plugin
Mob plugins additionally receive the declaration builtins declare_mob, goal, skill,
mechanic, targeter, and condition — see declare_mob and
Skills.
Starlark has no open, no network, no import, and no way to reach the filesystem. If a plugin
needs data (like the recipe table), the host parses it in Go and hands it in as a frozen value.
Hot reload
While the server runs, an fsnotify watcher observes plugins/ and every plugin subdirectory. On
any .star/.toml write, create, remove, or rename:
- Events are coalesced behind a 150 ms debounce (one editor save = one rebuild).
- A fresh plugin manager is built off the tick goroutine by re-scanning the whole directory.
- On success, the new manager is handed to the tick loop over a swap channel and installed between ticks — dispatch never observes a half-updated hook table.
- On failure (e.g. a file saved mid-edit with a syntax error), the rebuild is discarded and the last-good plugin set keeps running.
New plugin directories created at runtime are picked up automatically.

