Skip to content

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.

HookWhen 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 state bag.
typescript
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.

typescript
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:

typescript
// 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.

typescript
// 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:

typescript
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:

typescript
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.

Proprietary software, used under the Stagewright Software Licence.