Hooks
A script reacts to lifecycle events through hook functions registered at top level. There are three: onInit, onActivate, and onDeactivate. Each holds a single handler, so registering the same hook twice keeps only the last one.
| Hook | When it fires |
|---|---|
onInit(fn) | Once after the script is evaluated, and again on every hot reload |
onActivate(fn) | After a rack becomes active, including the first activation on load. fn receives the rack's display name |
onDeactivate(fn) | Before a rack is torn down on a switch. fn receives the rack's display name |
Reacting to MIDI, widget changes, OSC, and timers is not done through top-level hooks. Those are covered below and on the MIDI and OSC pages.
onInit(fn)
Runs once after the script is first parsed, and once again on every hot reload (Save). Use it for setup that assumes the engine has hydrated the project (widgets and MIDI devices bound):
- Compute and cache widget handles.
- Push initial LED feedback to controllers.
- Restore values from the persistent
statebag.
onInit(() => {
console.log('script ready');
refreshAllLeds();
});Top-level statements also run at load time. Reach for onInit when the code needs devices and widgets to already exist, which is not guaranteed before the engine has hydrated the project.
onActivate(fn) and onDeactivate(fn)
Fire when the engine switches the active rack, whether the user did it or a script call did. onActivate fires after the new rack is live (including the first one when the project loads), onDeactivate fires before the outgoing rack is torn down. Both receive the rack's display name.
onActivate((rackName) => {
console.log('activated rack:', rackName);
refreshAllLeds(); // LEDs reset on each rack, re-emit them here
});
onDeactivate((rackName) => {
console.log('leaving rack:', rackName);
// stash anything rack-specific before it goes away
});Use onActivate to re-emit LED feedback to controllers (LEDs reset on each rack), reset rack-specific state your script holds, or log into the diagnostics panel so future-you knows why a setting changed.
Reacting to MIDI
There is no global MIDI hook. Listen per device with midi.input(name).on(...), which fires only for that input:
// Act on AKAI MIDI Mix CCs specifically.
midi.input('MIDI Mix').on('cc', (m) => {
console.log(`cc ${m.controller} = ${m.value}`);
});See the MIDI page for the full message shapes and MIDI out.
Timers
Timers are the browser-shaped setTimeout / setInterval (not a hook). They fire on the message thread and are cancelled when the script reloads.
// Every second until cleared.
const id = setInterval(() => {
console.log('tick');
}, 1000);
// Later: clearInterval(id);For timed sequences inside an async function, await sleep(ms) reads more cleanly than nesting timeouts:
onInit(async () => {
widget('CUE').set(1);
await sleep(250);
widget('CUE').set(0);
});Persistent state: state
state is a plain object that survives hot reloads. The engine snapshots whatever you assign to it (JSON-serialisable values only: plain objects, arrays, numbers, strings, booleans, null) before re-evaluating your script, and restores it before the new source runs. Functions, Date, Map, and the like are dropped on serialisation.
Initialise lazily so a fresh project also works:
state.tempo ??= 120;
onInit(() => {
widget('BPM').set(state.tempo);
});
widget('BPM').on('change', (v) => {
state.tempo = v;
});Songs and song parts
Songs, Song Parts, and Setlists are on the roadmap for v2. There is no song-part hook in v1, and the engine will not fire one. When it lands, this page will document it.
Next: Recipes & patterns.