Scripting
Stagewright Script is the per-project show-logic language. One TypeScript file, full IntelliSense in Monaco, executed inside the engine on a sandboxed QuickJS runtime.
This is the meat of the docs. The five pages under this section cover the API in the order you'd actually learn it:
- Widgets API, how to read and write front-panel widgets from script.
- MIDI input & output, receive CCs / notes / program-change from controllers, send notes / CCs / SysEx out.
- Hooks, every event the script can react to:
onInit,onCC,onProgramChange,onTimer,onRackChange,onVariation,onSongPart. - Recipes & patterns, full-script examples pulled from real rigs.
A 30-second overview
// EVERY script gets these globals:
// widget(name) → WidgetHandle
// widgets() → WidgetHandle[]
// midi.input(name) → MidiInputHandle
// midi.output(name) → MidiOutputHandle
// onInit(fn) → run once at script load / engine ready
// onCC(fn), onProgramChange(fn), onTimer(ms, fn) → see hooks page
// onRackChange(fn), onVariation(fn), onSongPart(fn) → song-aware hooks
// console.log(...) → emits into the Diagnostics → Engine log
// kv.get(key), kv.set(key, value) → tiny key-value store, persists
const pianoVol = widget('PIANO_VOL');
pianoVol.on('change', (v) => {
console.log(`piano volume now ${v.toFixed(2)}`);
});
onInit(() => {
pianoVol.set(0.7);
});Types
The runtime ships ambient TypeScript declarations so Monaco's IntelliSense knows what widget(name) returns. Hover any symbol to see its signature; Cmd-click to jump to the type definition.
The full scripting surface is bundled with the app and exposed through Monaco's IntelliSense, so every symbol is documented as you type, no external setup required.
Limitations
- No
eval, no dynamicimport(), noFunctionconstructor. - No file system / network / process / fs / fetch / XMLHttpRequest. The script lives in a sealed sandbox; the engine is the only surface to the rest of the host.
- One script per project (v1). Multi-file authoring is on the roadmap; for now compose with TypeScript modules locally in your editor and copy/paste in.
- Tick budget. Each event handler has a ~50 ms soft budget; longer computations are interrupted.
onTimercallbacks fire on the message thread, so the same rule applies, don't block. - Per-script storage (
kv.*) is capped at 4 KB total per project. For richer state, hand-roll a structure inside the project file.
When NOT to use script
The MIDI bindings on a widget already handle "hardware controller moves this knob". You only need a script when the gesture is conditional, computed, or stateful:
- Press hardware Button A → set widget X to 1, set widget Y to 0 (radio-style switch across un-grouped widgets).
- LED feedback to the controller mirroring widget state (the AKAI MIDI Mix example).
- Tempo-relative timing (
onTimer(60_000 / bpm / 2, ...)). - Song-driven panel layouts (
onSongPart(part => widget('LABEL').setLabel(part.name))).
If your gesture is "knob moves parameter," no script needed, just bind the widget in the inspector.
Start with Widgets API →.