Hooks
The script reacts to events through hook functions registered at top level. All hooks live as global functions; calling them multiple times registers multiple handlers (they don't replace).
| Hook | When it fires |
|---|---|
onInit(fn) | Once at script load / engine ready; once again on every hot reload |
onCC(fn) | Every CC from any midi-in (use midi.input(name).on('cc', ...) to filter) |
onProgramChange(fn) | Every program change from any midi-in |
onTimer(ms, fn) | Every ms milliseconds; minimum 10 ms |
onRackChange(fn) | Active rack switches (manual or programmatic) |
onVariation(fn) | Variation switches within a rack |
onSongPart(fn) | Song-part advance (v2, not in v1) |
onShutdown(fn) | Engine is about to stop; last chance to flush state |
onInit(fn)
Runs once when the script is first parsed AND once on every hot reload. Use it to:
- Compute and cache widget handles.
- Push initial LED feedback to controllers.
- Restore state from the per-project KV store.
onInit(() => {
console.log('script ready');
refreshAllLeds();
});Top-level statements ALSO run at load time. onInit is for code that needs to assume widgets / midi devices are bound (some are not available before the engine has hydrated the project).
onCC / onProgramChange / per-device handlers
There are TWO ways to listen for CCs:
- Global:
onCC((m) => ...)fires for every CC from every midi-in. The event has adevicefield with the source name. - Per-device:
midi.input(name).on('cc', (m) => ...)fires only for that midi-in. Same event shape minus thedevicefield.
Prefer per-device unless your script genuinely treats every CC the same, it's clearer and a touch faster (no name comparison per event).
// Global: log everything for debugging.
onCC((m) => console.log(`${m.device}: cc${m.controller}=${m.value}`));
// Per-device: act on AKAI MIDI Mix CCs specifically.
midi.input('MIDI Mix').on('cc', (m) => { /* ... */ });onTimer(ms, fn)
onTimer(500, () => {
console.log(`tick, ${new Date().toISOString()}`);
});Timers fire on the message thread; the same ~50 ms tick budget applies. They survive across rack switches but are cancelled when the script reloads.
Return false (or call the supplied stop()) to cancel:
let n = 0;
onTimer(1000, ({ stop }) => {
console.log(`heartbeat ${++n}`);
if (n >= 10) stop();
});onRackChange(fn) and onVariation(fn)
Fire when the user (or another script call) switches the active rack or variation.
onRackChange((rack) => {
console.log(`switched to rack: ${rack.name}`);
refreshAllLeds();
});
onVariation((v) => {
// v.id, v.name, v.rackId
});Use onRackChange to:
- Re-emit LED feedback to controllers (LEDs reset on each rack).
- Reset any state your script holds that's rack-specific.
- Log into the diagnostics panel so future-you knows why a setting changed.
onSongPart(fn), v2
Not in v1
Songs / Song Parts / Setlists are advertised on the corporate page but not in code. The hook exists on the typings as a placeholder; the engine won't ever fire it in v1. See Roadmap.
// v2 only:
onSongPart((part) => {
widget('PART_LABEL').setLabel(part.name);
});onShutdown(fn)
Last call before the engine stops. Use it to flush LEDs to off, write final state to kv, or close hardware-specific connections.
onShutdown(() => {
// dark every LED so the controller doesn't sit lit between sessions
for (let i = 0; i < 24; i++) out.sendNoteOn(1, 1 + i, 0);
});Has a 100 ms hard timeout, if your shutdown work hasn't finished by then, the engine exits anyway. Keep it small.
Persistent state: kv
kv.get(key: string): unknown | null
kv.set(key: string, value: unknown): void
kv.delete(key: string): void
kv.keys(): string[]JSON-serialisable values only. Persists with the project file. 4 KB total cap per project, for richer state, build it into the project itself.
onInit(() => {
const lastTempo = kv.get('tempo') as number | null;
if (lastTempo != null) widget('BPM').set(lastTempo);
});
widget('BPM').on('change', (v) => kv.set('tempo', v));Next: Recipes & patterns.