Scripts
JavaScript nodes: read the live data, change it, control nodes and devices, draw graphics.
Scripts (JavaScript)#
Scripts run on JavaScriptCore (the macOS system JS engine). That is currently the only language, the old Lua/Python popup entries were placeholders and have been removed.
A Script node transforms the frame, written with bare names: Reads see the current data, writes become the node's output (and drive devices/nodes via writeback). Typos on reads throw with the full path. Unset fields pass through unchanged.
system.beep = CDJ1.beat.bar; // beep every bar
value.inChorus = (CDJ1.phrase.current == "CHORUS") ? 1 : 0;
if (CDJ1.beat.currentBpm > 130) { CDJ1.select.id = 2; }
CDJ1.status.isQuantizeOn = 1; // device write via CDJ1's deck
source.* and output.* still exist for the one case needing both sides of a key, original vs. changed: metadata.title = source.metadata.title + " [LIVE]";
The inline script box is fine for one-liners; for real work press Editor ⤢ (under the Script label in the inspector), or just double-click the Script node: And the Patch swaps for a full-size editor styled like the other panels:
- Line numbers in the left gutter.
- Syntax coloring (comments, strings, numbers, keywords,
source/output/g), refreshed shortly after you stop typing. - Live validation: ~0.6 s after the last keystroke the script is compiled AND test-run against the node's current live frame, so garbage, ReferenceErrors and
source.*typos all show as the red error in the footer (✓ runs on live data when clean). Your node keeps running its last good code while you type. - Tab completion: Press Tab mid-word to complete against this node's actual data keys (
source.beat.…), theg.*helpers and JS basics; the candidates appear in the footer. - ▶ Run (top right) applies the code and test-fires the node once, same as the node's ▶ button.
- Close (bottom right) applies the code and returns to the Patch. Changes are otherwise not live while you type.
system.beep beeps (edge-triggered). A Script fed by a trigger runs once per fire; fed by anything else it runs every tick.
Node parameters in the data stream#
Node names are identifiers: Spaces and symbols are stripped automatically (type "CDJ 1" → the name becomes CDJ1). The Description field (in the Node section of every node) is free text, when set, the canvas shows it instead of the name; the name stays the address. New nodes get unique lowercase numbered names (device1, router1, matrix1, source1, group1, …) rename them to whatever address you like (CDJ1, MainDeck). One reserved corner: a BARE all-lowercase data-namespace word (device, value, select, …) can't be a node name, the trailing number in the defaults is what keeps them safe, so keep a digit or a capital in yours.
As a frame passes through a node, all of that node's settings ride along as <Name>.<setting> keys, so any downstream Live Data monitor shows the upstream chain's state, and scripts read and control those nodes with plain dot syntax:
var active = source.CDJ1.select.id; // read an upstream node's state
output.CDJ1.select.id = 2; // control any node by NAME
output.MainRouter.router.active = "Input 2";
output.FXScript.node.bypassed = true; // bypass/stop work too
Addressing is by node name. Works on every setting you see in the inspector, for every node in the Patch, from any script (a cue's included), whether or not the data passed through that node. Reading works the same way: source.FXScript.node.bypassed gives that node's current setting.
output.OSC.node.bypassed = true; // bypass the OSC node while the cue runs
output.Resolume.node.stopped = true; // stop a node
output.MainRouter.router.active = "Input 2";
A script inside a cue only runs while the cue is active, and a node keeps the last value it was given. To put it back when the cue ends, use a Trigger on cue.active State Off feeding a script that writes output.OSC.node.bypassed = false;. Overriding a <Name>.* row in the Live Data monitor drives the node the same way. Nested data also uses plain dots now: source.mixer.ch1.fader, source.cue.1.color.
parent and child (name-free addressing)#
Two aliases spare you hard-coding names in scripts:
parent= whichever identity node OWNS the incoming data (the Device, or the last Router/Matrix it passed through, delays/triggers in between don't count). Read and write:var bar = source.parent.beat.bar; // same as source.device1.beat.bar output.parent.led.play = 1; // press Play on the routed deck output.parent.select.id = 2; // retarget the feeding Device node
child= the node(s) wired directly after this script, WRITE-ONLY (data flows downstream, so there is nothing to read). A write fans out to every direct child; settings a child doesn't have are ignored:output.child.gen.fps = "30"; // configure the MTC node after me output.child.node.bypassed = true; // bypass whatever I feed
Scripts that should survive rewiring use parent/child; scripts that target one specific node use its name.
Writing device settings from a script#
Changing a util.* (DJM utility) or setting.* (CDJ player setting) key in a script writes it to the hardware:
output.util.digitalLevel = 1; // on the mixer's frame
output.setting.quantize = 1; // on a deck's frame (deck = device.number)
Only keys the script changes (vs. its input) are sent. The write repeats every 2 s until the device's polled state confirms it, then goes quiet, so a script that keeps setting a value effectively enforces it. The same applies to a Live-Data override on a util.*/setting.* key: overriding it pushes it to the device. The output.setting / output.util / output.led / output.status / output.playback sections always exist (pre-created), so the assignments above work even before the device has reported those keys.
Panel states too: Setting a led.* / status.* key drives the deck until it matches, settings-backed toggles (status.isMasterTempoOn, status.isQuantizeOn, status.isAutoCueOn, status.isVinylModeOn) ride the settings write; the rest press the real button only while the deck disagrees (max one press per 2 s):
output.led.play = 1; // deck not playing? → press Play/Pause
output.led.sync = 1; // sync off? → press Sync
output.led.loop = 0; // looping? → press Reloop/Exit
output.status.isQuantizeOn = 1; // via the settings write
Transport also works by name via playback.state, accepted values: "playing", "paused", "cued" (other states aren't commandable):
output.playback.state = "paused"; // playing? → press Play/Pause
One-way states: led.cue and led.master only act on 1 (a deck can't un-cue or hand off master by button). led.onAir isn't controllable (that's the mixer fader). A persistent script value or override enforces the state, pausing the deck by hand gets re-pressed within 2 s until you remove the line or revert the override.
Any device, from any script, a cue's included. Name the Device node (its title in the Patch) in front of the section. output.<Device>.setting / .util / .led / .status / .playback always exist, whatever the script is wired to. Reading source.<Device>.… works too: a key the script's own input doesn't carry is filled in from that Device node's live data.
var periodMs = 600; // each on AND each off phase
var on = Math.floor(Date.now() / periodMs) % 2 === 0;
output.cdj2.setting.jogRingBrightness = on ? 2 : 0; // 0-2
output.cdj2.setting.jogLcdBrightness = on ? 5 : 1; // 1-5 (no "off")
if (source.cdj2.status.isQuantizeOn) { /* … */ }
A script inside a cue runs only while the cue is active. Outside it, its input passes through untouched and nothing is written. A Trigger-fired script still runs, e.g. one fired on cue.active State Off to put a setting back. The same goes for a Schedule item's graph (item.active). A device setting keeps the last value the script sent, so write the "normal" value back from a State-Off trigger if you want it restored.
Mind the setting. part: output.cdj2.jogRingBrightness (without it) is a setting of the cdj2 node, not of the player. Values outside a setting's range are clamped. Player settings are written over the network and take a moment to land, so very fast blinking (well under half a second) will skip steps.
Drawing to a Graphic node#
A Script can draw on a Graphic node wired after it, using the g helper. Coordinates are canvas pixels (top-left origin); colors are "#RRGGBB".
g.clear("#101014");
g.rect(4, 4, source.beat.beatInBar * 40, 24, "#33aaff");
g.circle(180, 60, 20 + source.beat.currentBpm % 20, "#ff3399");
g.text(8, 110, source.metadata.title, "#ffffff");
Helpers: g.clear(color), g.rect(x,y,w,h,color), g.line(x1,y1,x2,y2,color), g.circle(x,y,r,color), g.text(x,y,string,color), g.waveform(x,y,w,h,mode,zoom) (cached waveform, mode "detail"/"overview"), g.artwork(x,y,w,h) (the track's cover art, when cached). Wire Script → Graphic; the Graphic node renders the commands live.
Double-click a Graphic node to open a full-size live viewer in place of the Patch (letterboxed at the render resolution, refreshed every engine tick); Close returns to the Patch.
Graphic node settings:
- Render Width / Height: The real output resolution the script draws for (e.g. 1280×720). Coordinates in
g.*are in these pixels. - Node Width / Height: How big the node's preview is on the canvas (or drag the node's bottom-right corner). The render is shown scaled to fit.
- Preview FPS: Caps how often the in-patch preview redraws (the output runs faster).
- Output: Off, Screen (opens a resizable output window, letterboxed at full render resolution, at Output FPS), or NDI (sends an NDI source named NDI Source Name ).
Only Keys the Script Names (faster scripts)#
A Script node normally receives the whole incoming frame (hundreds of keys for a deck) and hands a whole frame back, every engine tick, even when the script reads one key. Switch Only Keys the Script Names on (Script section of the node) and the script receives only the keys its text names, and only the keys it writes are changed; everything else passes through untouched. It is off by default.
What counts as "named": every dotted name in the script, bare CDJ1.beat.bar, source.…, output.…, parent.… (the deck the frame belongs to) and bracket keys x["3.inMs"]. A named group (source.CDJ1.beat) brings every key under it.
Leave it off for a script that
- Builds key names while it runs (
get("playback." + x),source[name],for (k in source)) (those keys would silently be missing, or - Deletes keys to filter what goes downstream) deleted keys would stay.
Good candidates: scripts that read a few fixed keys and write value.*, setting.* or a child gate (e.g. an "only during a drop" gate, a jog-ring blink).
