Signals & Sorcery has an extensible plugin system that lets you build custom input generators for the Loop Workstation. Plugins can generate MIDI patterns, manage audio samples, create generative audio textures, or combine all three.
Each plugin gets its own accordion section in the workstation UI and a scoped PluginHost API for interacting with tracks, MIDI, audio, and more. Plugins never access the audio engine directly; all interaction goes through the PluginHost, which enforces ownership scoping, capability gating, and track limits.
The fastest way to build a plugin is to clone the template into your plugins folder and run the build:
# macOS: see Install a Plugin for Windows/Linux pathscd ~/Library/Application\ Support/signals-and-sorcery/plugins/
git clone https://github.com/shiehn/sas-plugin-template.git @my-org/my-plugin
cd @my-org/my-plugin
npminstall&&npm run build
Restart Signals & Sorcery; your plugin appears in the workstation. Edit the source, rebuild, and iterate.
Plugin Template on GitHubopen in new window: fully commented hello-world plugin with examples of track creation, MIDI writing, and all common patterns. It builds against plugin SDK 3.x.
Every plugin implements the GeneratorPlugin interface. The host calls these methods during the plugin lifecycle.
Method
Signature
Description
activate
(host: PluginHost) => Promise<void>
Called when the plugin is activated. Receives the scoped PluginHost instance. Initialize state, load saved data, subscribe to events here.
deactivate
() => Promise<void>
Called when the plugin is deactivated (5-second timeout). Clean up listeners, save state, release resources.
getUIComponent
() => ComponentType<PluginUIProps>
Return the React component to render in the accordion section.
getSettingsSchema
() => PluginSettingsSchema | null
Return a JSON schema for auto-rendered settings, or null for no settings UI.
onSceneChanged
(sceneId: string | null) => Promise<void>
Optional. Called when the active scene changes. Reload scene-specific state here.
onContextChanged
(context: MusicalContext) => void
Optional. Called when musical context changes (BPM, key, chords). Update UI or recalculate patterns.
getSkills
() => PluginSkill[]
Optional. Declare actions agents can call. Each is registered as a tool named plugin:<pluginId>:<id>. PluginAction is an alias of PluginSkill.
getAgentSkills
() => PluginAgentSkill[]
Optional (SDK 3.20.0). Contribute agent skills: markdown know-how telling agents when and how to use your actions. Called at activation; hosts without plugin skill support ignore it. See Agent Actions and Skills.
Callback to open the scene selector. Null if not applicable
onOpenContract
(() => void) | null
Callback to open the contract/chords section
onExpandSelf
(() => void) | null
Callback to expand this plugin's own accordion section
isExpanded
boolean
Whether this plugin's accordion section is open. The panel stays mounted while collapsed, so watch this prop (not mount/unmount) to know the user is looking at it
All methods below are available on the host object your plugin receives in activate() and via PluginUIProps.host. Methods marked with ownership require the track to be owned by the calling plugin. Methods marked Optional can be missing on older hosts: check typeof host.method === 'function' before calling them.
Change the Surge XT preset, picked from a category matched to the track's MIDI pitch range. excludeNames removes presets from the pool; options.description enables description-based matching where the host supports it. Returns { presetName, presetCategory }. Ownership.
duplicateTrack
(trackId: string) => Promise<PluginTrackHandle>
Clone an owned track: copies MIDI data and role. A Surge XT copy gets a different preset; a custom instrument copy keeps the source's state. Ownership.
Optional. Render only this track, offline, from its own scene (its length and time signature) to a temporary WAV; returns { path, bpm, durationMs }. Throws a retryable ENGINE_ERROR while another render holds the engine. See exportTrackAudio. Ownership.
Same as the two above, in the plugin's own VST3/AU state format. Use for third-party instruments whose patches the default format does not preserve. Ownership.
Optional (SDK 3.22.0). The engine's verdict on whether the plugin really loaded the last state you wrote: verified, not_applied (errorCode: 'STATE_NOT_APPLIED'), timeout (default 45 s) or unsupported. Never rejects. → All uses it to re-apply a part once and name a part that still missed. See awaitStateApplied. Ownership.
getTrackPlugins
(trackId: string) => Promise<PluginSynthInfo[]>
List all plugins loaded on a track. Returns { index, name, type, enabled }[]. Ownership.
Per-track FX are 3rd-party VST3/AU inserts on the track's plugin chain, placed before Volume & Pan. All methods are optional: feature-gate on typeof host.getTrackExternalFx === 'function'.
Method
Signature
Description
getAvailableFx
() => Promise<InstrumentDescriptor[]>
Scanned FX (non-instrument) plugins on this machine, for an FX picker.
Copy a source track's whole FX chain onto an owned track. Partial success is normal: plugins missing from this machine land in externalMissing. Ownership (dest only).
Each plugin can have one mix bus per scene: volume, mute, solo and an FX chain on the sum of its tracks. All methods are Optional and scoped to this plugin. Most panels use the usePanelBus(host, activeSceneId) hook with the PanelMasterStrip component instead of calling them directly; see Panel Mix Bus.
Method
Signature
Description
getPanelBusState
(sceneId: string) => Promise<PanelBusState>
{ engaged, volume, muted, soloed, fx }. When the bus is engaged, reading also routes the panel's not-yet-routed tracks into it.
Full context with chord progression and concurrent track MIDI data. Use excludeTrackId to omit the current track; opts.pinTrackDbIds always includes those reference tracks in full.
Generate with native tool calling, for agent loops. Pass a model role, LLM_MODEL.BEST or LLM_MODEL.LIGHTWEIGHT, as model; the host maps it to the current model.
isLLMAvailable
() => Promise<boolean>
Check if LLM service is available (user authenticated, gateway reachable).
The two dialog methods require the fileDialog capability in the manifest. downloadFile requires the download's host to be listed in network.allowedHosts.