Technical reference
What this does: the facts IT and support ask for. Paths, ports, formats, limits and timings. You do not need this page to run a show.
- Desktop app: Tauri 2. The interface is Svelte in the system webview. The engine is Rust.
- Rust owns: LTC on the audio device, MTC on its own timing thread, Art-Net and MIDI Clock, OSC to and from the desk, the sessions host and peer, the project store, backups, cloud sync, uploads and downloads.
- The webview owns: the interface, audio playback of tracks and lanes, and the video and image players.
- Two clocks: the audio engine’s and the timecode engine’s. They are reconciled continuously. Playback sync in Settings → Audio Devices is the trim.
Files on disk
Section titled “Files on disk”Everything below lives under ~/Library/Application Support/ShowLink Cues/. The folder keeps the old product name on purpose: renaming it would strand every existing install.
| Path | Holds | Kept |
|---|---|---|
shows/<id>.json | One project per file. Saved on every change, through a temp file and a rename. | Until you delete the project |
shows/<id>.json.bak, .bak.1–.bak.3 | The previous saves, rotated on every save | 4 |
backups/<id>/<yyyy-mm-dd-HHMM>.json.gz | A snapshot on save when the newest is an hour old. Local time. A second one in the same minute gets -2, -3. A restore snapshots the current state first. | The newest 24, plus the first of each of the last 14 days. 38 at most. |
media/store/<sha256>.<ext> | Cues’ own copies, one per distinct content | Until moved to Trash |
media/dropped/ | Landing spot for dropped files, moved into the store at once | |
media/trash/<date>/ + manifest.json | Deleted media | 30 days. Purged at launch and every 24 h. |
media/sha-cache.json | Path to sha256, size and modified time, so files are not hashed twice | Rebuilt if deleted |
sessions/ | This Mac’s session certificate and key, the device book (devices.json), this Mac’s label (label.txt) | |
sessions/media/ | Media a session peer got from a host, named by sha256 | Never touched by Reclaim space |
sessions/peer-cache/<host fingerprint>/ | On a peer: the host’s last show. A rejoin needs only the changes. | |
cloud-uploads/ | upload-resume.json (open multipart uploads), upload-ledger.json (bytes sent per day), upload-settings.json | Ledger: 14 days |
waveforms/<track_id>.json | Track waveform cache | Rebuilt if deleted |
cache/peaks/ · cache/thumbs/ | Lane waveforms and video filmstrips | Rebuilt if deleted. Never synced, never backed up. |
logs/cues.YYYY-MM-DD.log | The log, one file per UTC day | 7 files |
crashes/<time>.txt | One report per crash | Until you delete them |
config.json | The grandMA2 plugin server: allowlist, port, bind address |
Outside that folder:
| Path | Holds |
|---|---|
~/Library/Application Support/helm-cues/cloud-media/<project>/<file id>.<ext> | Media downloaded from Helm Cloud. The Media Library shows the real name. |
~/Library/Application Support/com.elabs.helm.cues/licence.json | The licence key, its last check, this Mac’s device ID and the signed entitlements |
~/Library/Application Support/com.elabs.helm.cues/updates.json | The previous build and the What’s new notes |
~/.helm/cues-control.json | The Stream Deck socket’s port and token. Mode 0600, rewritten on every launch. |
/tmp/helm_*.txt | The desk file bridge, when grandMA3 onPC runs on this Mac. See Desk OSC commands. |
macOS keychain, helm-cues-cloud | The Helm Cloud token |
| The webview’s local storage | App settings, workspaces (helm-cues:workspaces, versioned JSON), onboarding progress, per-track sync records, the daily upload limit |
Name the risk: licence.json holds the full licence key. Treat a copy of that folder like the key itself. A support bundle never includes it.
Files you picked from elsewhere, such as ~/Music or a USB drive, are used in place. Cues never moves or trashes them.
File formats
Section titled “File formats”Project file
Section titled “Project file”- One JSON document per project:
id,name,artist,frame_rate,team_roles,tracks,created_at,updated_at,cloud_project_id,cloud_version,versions,metadata. - Tracks carry their stacks, cues and audio settings.
metadatacarries the media lanes, the running orders, the media list and the cloud upload mode. - Waveform peaks are never written into it.
- The cloud holds the same project, one record per track plus the show-level fields. See Cloud sync.
Show package (.helmpkg)
Section titled “Show package (.helmpkg)”- A ZIP:
manifest.json,show.jsonandmedia/, stored uncompressed. - The manifest lists each media file’s sha256, size and original name.
- Export writes a
.partfile, then renames it. - Import checks every sha256 and size. One mismatch stops the import and removes what it added.
- A package with the same project id replaces the open one after a backup, or comes in as a copy with a new id.
- File → Export Show Package… (⇧⌘S) and Import Show Package… See Backups and show packages.
Helm CSV
Section titled “Helm CSV”- File → Import / Export data… (⌘I), or Export → Track → Helm CSV… (⌘E).
- Header:
Timecode,Label,CueNumber,StackName,Sector,Command,TargetCue,FadeTime,DelayTime,Notes. - Name the risk: import reads timecode, label, cue number, target, command, fade and notes. StackName, Sector and DelayTime are dropped on import.
Other imports
Section titled “Other imports”File → Import Show (grandMA3 / Reaper)… reads a grandMA3 timecode XML, a Reaper .rpp or a Reaper marker CSV. See Import from grandMA3 and Reaper. QLab is not implemented.
Show-file compatibility
Section titled “Show-file compatibility”Until 0.3.0, every save also writes the older fields 0.2.12 needs. A project saved by today’s build still opens in 0.2.12 and later.
- Name the risk: an older build knows nothing of newer features. Edit a project in the newest build you have.
- Reinstall previous version warns:
Shows saved by <current> may not open in <previous>.See Updates. - At 0.3.0 the rule ends.
Timecode timings
Section titled “Timecode timings”LTC, MTC and Art-Net each run their own frame clock. The player’s heard position steers it; it never sets the labels. Operator rules: Timecode.
The feed
Section titled “The feed”| Item | Value |
|---|---|
| Rate | Live: 10 a second. Build: about 30 a second (once per 1/30 s of track time) |
| Stamp | Each position carries the page’s performance.now() when it was read |
| Ageing | Age = (arrival − stamp) − the lowest (arrival − stamp) of the last 5–10 s. A position held behind the main thread is moved on by its age |
| Settling flag | Set while the player’s heard time is a guess: inside the 2 s start band, or before the element clock moves off the play or seek point |
| Silence | 3 s with no position: the output pauses until the next one (was 1 s) |
The clock
Section titled “The clock”| Item | Value |
|---|---|
| Labels | A frame counter: +1 a frame (LTC, Art-Net), +2 a cycle (MTC) |
| Start | Start cues the position. The clock rolls on the first settled position that moves at up to 1.5 × real speed |
| Slew | Rate change = error ÷ 1 s, at most ±1 % |
| Relock | Error over 1.5 frames on two positions within a frame of each other, or over 0.5 s on one |
| Seek while settling | A settling position 0.5 s or more from the clock cues there |
| Stall | The position not moving (±2 ms) for 0.25 s: pause; it never steers |
| Below zero | Silent on every output; starts on 00:00:00:00 |
| Hours | Wrap at 24 |
| Rates refused | 50, 59.94, 60 on LTC, MTC and Art-Net |
| Item | Value |
|---|---|
| Quarter frames | 4 a frame: 10.0 ms at 25, 8.34 ms at 29.97, 8.33 ms at 30, 10.4 ms at 24 |
| Cycle | 8 pieces, 2 frames, every piece the label of the frame piece 0 starts. Piece 0 on its frame’s start; nibbles low first; rate in piece 7 bits 1–2 |
| Full frame | On a cue (start, seek, scrub while cued); before piece 0 after a roll or relock; on a rate change; every 1 s while held |
| Thread | Own thread, USER_INTERACTIVE QoS. Sleeps to 1.5 ms before each message, then spins |
| Late wake | Over 2 quarter frames late: no burst. The next cycle starts on the next frame |
| Port | USB MIDI moves each message by up to 1 ms |
| Item | Value |
|---|---|
| Bit length | sample rate ÷ (fps × 80), fractional: 1601.6 samples a frame at 29.97 fps and 48 kHz. Whole-sample bits ran 29.97 as 30.0, and 30 fps at 44.1 kHz 2 % fast |
| Bit layout | The shared Helm encoder (Helm DJ’s): drop flag bit 10, BGF0 bit 43 at 25 fps, polarity bit 27 over bits 0–63, sync word bits 64–79 |
| Level | ±0.8 (−1.9 dBFS) square wave |
| Timing | Each buffer is stamped with callback time + the stream’s output latency (capped at 250 ms) |
| Slew | The next frame’s bits run up to 1 % short or long. A frame in progress is never cut |
| Relock | Silence to the new label’s exact frame boundary, then the frame |
| Late callback | The device plays a gap; the stream resumes where it was and is slewed or relocked back |
Art-Net
Section titled “Art-Net”One ArtTimeCode packet a frame on its frame’s start, from the same clock. Over 2 frames late: no burst. Held: the frame repeats each frame period.
Measured
Section titled “Measured”| Where | Result |
|---|---|
| Field log, 0.2.17, 22–24 Oct 2026 | Quarter frames 0.8–1.9 ms late on average, 4.3 ms worst. 271 of 282 MTC plays sent a full frame mid-run, typically 0.7–0.9 s after the music started and 0.30–0.38 s back. A 1.2 s main-thread stall (session hosting, desk hello) stopped MTC, which came back with two full frames 11 ms apart and 1 s of timecode apart |
| Tests, 0.2.18 | 10 simulated minutes per rate (MTC: 23.976, 24, 25, 29.97 DF and NDF, 30; LTC at 25 and 29.97 DF): every label in order, piece 0 / frame start within 1 ms of the player, quarter-frame spacing within ±1 % |
| Real thread, 0.2.18 | A loaded test run, the show lock held while a multi-MB show serialises, and the feed stalled 300 ms: no relock, no extra full frame, piece 0 within 2.8 ms, worst quarter frame 6.3 ms late |
| Project save | A 4.2 MB project: 72 ms to serialise on the main thread in a dev build. The timing threads never take the show lock |
The log line
Section titled “The log line”Every 10 s while MTC runs: MTC timing stats: <n> quarter frames, <x>% success rate, avg delay: <ms>, max delay: <ms>, full frames <n>, relocks <n>, late skips <n>, max error <ms>. Also MTC: relocked to <s> (app <ms> from the clock) and MTC: app time standing at <s> — quarter frames paused.
Network
Section titled “Network”| Port | Protocol | Direction | What |
|---|---|---|---|
| 8000 | UDP / OSC | Cues → desk | The desk’s Helm IN row. Change it in Settings → Helm Sessions → Desk → Change ports… → Desk input port. |
| 8001 | UDP / OSC | desk → Cues | The desk’s Helm OUT row. Fixed. Open only while this Mac hosts a session. |
| 8005 | UDP broadcast | both | The session beacon, every 2 s. Every Cues listens to list the sessions on the network. |
| 6454 | UDP / Art-Net | out, in | Art-Net timecode out, when on. In, only when Art-Net is the chase source. |
| 5355–5362 | UDP / QUIC | peers → host | Sessions. The host binds the first free port in the range. |
| 5353 | mDNS, Bonjour _helm._udp | Cues → LAN | The session advert Helm Monitor finds. Sent by macOS; Cues opens no socket for it. |
| 8765 | TCP / HTTP | MA2 → Cues | The grandMA2 plugin server. Starts only when MA2 sync runs. |
| 47811–47818 | WebSocket, localhost only | Stream Deck → Cues | Control socket. 8 ports tried. |
| 443 | HTTPS | Cues → internet | app.helm.show: projects, files, entitlements. helm.show: licence checks and updates. *.r2.cloudflarestorage.com: media uploads and downloads, by signed link. |
- The beacon is a broadcast. It goes to each interface’s subnet and to 255.255.255.255. It does not cross a router. A switch that filters broadcast hides it. Join by address… needs no beacon.
- Two interfaces? The beacon leaves each one carrying that interface’s own address. Settings → Network → Primary Network Interface narrows it to one.
- Sessions use TLS 1.3 over QUIC with a pinned certificate per host. A device is known by its certificate fingerprint: 26 characters.
- The show network needs no internet. The desk bridge and sessions are LAN only.
Firewall
Section titled “Firewall”On the Mac: allow incoming connections for Helm Cues in the macOS firewall. macOS asks the first time a session is hosted.
Between devices:
| Allow | On | For |
|---|---|---|
| UDP 8001 in | The hosting Mac | Desks talking to Cues |
| UDP 5355–5362 in | The hosting Mac | Session peers: another Cues, Helm Monitor, Helm DJ, Helm Clock |
| UDP 8000 in | Each grandMA3 | Cues talking to the desk. Or the Desk input port set in Cues. |
| UDP 8005 broadcast | The show subnet | The session list |
| UDP 5353 multicast | The show subnet | Helm Monitor’s session list |
| UDP 6454 | The Art-Net subnet | Only when Art-Net timecode is on, out or in |
| TCP 8765 in | The Cues Mac | Only while grandMA2 sync runs |
| TCP 443 out | app.helm.show, helm.show, *.r2.cloudflarestorage.com | Cloud sync, licence, updates, media |
Stream Deck socket
Section titled “Stream Deck socket”- WebSocket on
127.0.0.1, port 47811. Taken? The next one, up to 47818. - A fresh 32-character token every launch. Cues writes the port, the token and its process ID to
~/.helm/cues-control.json, mode 0600. - The first message must carry the token within 3 s, or the socket closes.
- A connection with an
Originheader is refused. No web page can reach it. - Nothing off this Mac can reach it. Nothing to open in a firewall.
- Log lines start
control-surface:.
Desk OSC commands
Section titled “Desk OSC commands”For programmers writing their own grandMA3 macros. The Helm plugin’s playhead macros and the cue Commands of Desk GO drives Cues and Cues command… send some of these. Any macro can send any of them.
Send one from a macro
Section titled “Send one from a macro”SendOSC <line> "/Helm/start,s,<track>|<mark>"<line>is the Helm OUT row’s number in Menu → In & Out → OSC.<mark>is a cue label (any case), a desk cue number,start, or<seq>:<cue>: the desk cue on that sequence, by its name (05 - Main:3) or, from older builds, its number. No commas or|inside it.<track>is the track number or name. Leave out<track>|to stay on the loaded track. A named track loads even with Follow the desk’s track load off: the command names it.<seq>:<cue>finds the track by its sequence when the track number on the desk is out of date.- Joined desks only. Cues ignores a desk that has not joined the hosted session. It logs one line per desk per minute.
- Cues answers in the Global var
HELM_SHOW_ACK: one line, up to 160 characters. Live shows it in its header.
SendOSC 2 "/Helm/start,s,verse"SendOSC 2 "/Helm/start,s,3|chorus"SendOSC 2 "/Helm/goto,s,Blue|05 - Main:3"SendOSC 2 "/Helm/load,s,3"SendOSC 2 "/Helm/stop,s,now"Desk to Cues
Section titled “Desk to Cues”| Address | Payload | Cues does |
|---|---|---|
/Helm/load | Track number or name | Loads it in Live, no play. Needs Follow the desk’s track load on, Desk control on, nothing playing. |
/Helm/start | <mark>, or <track>|<mark>. Empty is start. | Loads the track if named, jumps Live to the mark and plays. Needs Live armed and Desk control on. |
/Helm/goto | As /Helm/start | Loads the track if named, jumps Live to the mark and stays parked. Pauses Live if it was playing. Needs Live armed and Desk control on. |
/Helm/pause | Any | Pauses Live. Needs Live armed and Desk control on. |
/Helm/resume | Any | Plays on from the pause. Needs Live armed and Desk control on. |
/Helm/stop | Any | Stops at the track top. Needs Live armed and Desk control on. |
/Helm/transport | play, pause, stop, rew, ff, prev, next | Drives Build’s player when Build is on screen. Otherwise Live, with Desk control on. |
/Helm/select | Track number, digits only | Build selects it. Live loads it, no play, with Desk control on. |
/Helm/track | Track name | Build selects it. Live loads it with Desk control on. |
/Helm/autosync | on or off | Sets this desk’s auto-sync |
/Helm/edit | <seq name>|<cue>|<seconds> | Moves the matching cue to that time. Sent when a Helm event moves on the desk. Plugin 0.16.4 names the sequence; older ones number it. |
/Helm/pull | all, track:<num>, list or sessions | Sends the show, one track, the track list alone (the packed track vars with the push handshake, no sequences: plugin 0.16.15’s pickers) or the list of sessions on the network |
/Helm/hello | MA3|<version>|<session>@<host>|<tile>|<sync version>|<ask> | Records the desk. Answers in HELM_LINK only a desk’s first hello of Cues’ run, a hello naming another session or plugin version than its last, and one with ask (plugin 0.16.12: Connect, Setup Desk, a press on a desk that is not LINKED). On a join it sends the cue times the desk missed (The live link). An empty session is a leave: the desk is forgotten, no answer. <sync version> is the desk’s HELM_SYNC_VERSION, empty when it holds none. Plugin 0.16.2; older plugins send four fields. |
/Helm/edited | <seq name>|<cue>|ok|miss|err|<detail>|<timecode name> | The plugin’s answer to each HelmEditEvent, HelmRetargetEvent, HelmAddEvent, HelmDelEvent and (0.16.6) HelmCue. <cue> is the number Cues addressed the event or cue by. miss or err: the track (found by the timecode name, else the sequence name) needs Import on that desk at once, logged every time. Nothing is resent. Plugin 0.16.4; 0.16.2 sends a sequence number and no timecode. HelmCue sends no timecode. OSC only. Details: How the desk finds an event, HelmCue. |
/Helm/diag | cues|<seq>|<k>/<n>|<cue>:<name>|…, cues|<seq>|missing, event|<tc>|<seq>|<k>/<n>|<PROP>=<value>|…, event|<tc>|<seq>|none|<why>, token|<wanted>|<key>=<read back>|set|fallback|refused|unverified | Logs each line at INFO as MA3 diag <desk>: …. A complete cues list is compared with Cues’ names; each difference is logged and marks the track Import needed on that desk. Plugin 0.16.6. OSC only. Details: Desk report. |
/Helm/hello and /Helm/pull with sessions are taken from any desk. Everything else needs a joined desk: one whose last hello named this session, allowed, not since dropped. A desk is dropped when three sends to it fail in a row (MA3 desk <ip> dropped from the session: …); its next hello rejoins it. No clock ages a desk out.
- Echo guard. A
/Helm/startor/Helm/gotothat lands within 0.5 s of where Live already plays that track is ignored and logged: it is Live’s own timecode firing a desk cue that calls back. - The same command twice within 750 ms is dropped. The macros send OSC and the file bridge together.
Cues to the desk
Section titled “Cues to the desk”Cues sends grandMA3 commands to the Helm IN port as /cmd. A batch goes out 5 ms apart, so the desk’s queue drops none.
| Global var | Holds |
|---|---|
HELM_LINK | joined, pending, denied or nosession. Cues’ answer to a hello it answers, and the only var a joined desk’s answer carries (plus HELM_SESSION when the desk’s lacks the host). Also on Allow and an approval flip that changes the desk’s verdict; on Remove and stop hosting only to a plugin before 0.16.13 (0.16.13 gets forget). The plugin writes nolink itself after an Import that failed. |
HELM_SESSION, HELM_AUTOSYNC, HELM_CUES_TRACK | Sent with a desk’s first joined hello of a session, then only when they change. |
HELM_SHOW_ACK | Cues’ answer to a show command |
HELM_CFG_BACKUP_OFFSET | Settings → Desk import → Backup offset. Sent with a desk’s first joined hello, at the head of every push, and to every joined desk when it changes. 0 = no backups. |
HELM_CFG_SET_BPM is never sent (Cues 0.2.18, plugin 0.16.11): TRACK_SETUP line 2 is always Master <HELM_CFG_BPM_MASTER> At BPM $CURRENT_TRACK_BPM, and Cues never runs rebuild_setup.
Live BPM to the desk (Settings → Desk import, off by default, metadata.deskLiveBpm): when the Live player, or Build’s while Build is in front, plays across a tempo marker of the track’s beat grid that changes the BPM, Cues sends each joined desk taking auto-sync one line, Master <m> At BPM <bpm>, the BPM to one decimal, logged at INFO as MA3 live BPM → <desk>: …. <m> is the master in the desk’s master|<m>|… diag line (Setup Desk, diag) since Cues started, else BPM master (3.1). A forward step longer than 1.5 s, a seek, track select, play and stop send nothing; one marker sends once per 250 ms.
By name. After an object exists, every command addresses it by name: Sequence "01 - Main", Page "Intro", Timecode "Intro", Macro "HELM …". Names drop . , ; " \ $ & * ? ^ { | } ~ (MA3 manual, Label keyword) and turn ' into ’.
A push (the Import path), per track:
| Line | Does |
|---|---|
Lua "if HelmEnsure then HelmEnsure('Page',<n>,'<track>','<old names>') end" | The page named after the track; else one under an old name, relabelled; else a new one at page <n>, or the next free number above |
Lua "if HelmEnsure then HelmEnsure('Sequence',<n>,'<prefix> - <stack>','<old names>') end" | The same for each visible stack’s sequence, created at Starting Sequence # + its place |
Lua "if HelmCue then HelmCue([==[<seq>]==],<n>,[==[<name>]==],<fade>,<delay>,[==[<note>]==]) end" | Each desk cue, by name: found, created only when missing, named, timed (nil = leave the desk’s own), noted (plugin 0.16.13; '' clears). See HelmCue. |
Lua "if HelmAssign then HelmAssign('<seq>','<page>',<exec>) end" | The sequence on its executor of the page found by name |
SetGlobalVariable "HELM_TRACK_<i>_SEQ_<j>" <n>, …_LABEL "<name>", …_WAS "<old names>" | The plugin’s build data: names to find, numbers only to create |
HelmEnsure and HelmAssign are plugin 0.16.4 globals, HelmCue 0.16.6. Pool numbers appear in nothing else. No Move, never a store over another object.
HelmCue
Section titled “HelmCue”Plugin 0.16.6. One line per desk cue, on Import and live. It replaces Store + Label + Set … "CueFade" / "CueDelay": on the 2026-10-22 desk test some cue 1s arrived unnamed, and nothing said so.
- The sequence named
<seq>, as it is now. None:err|no sequence named <seq> on the desk. - Its cue numbered
<n>, numbers compared as numbers. - Missing: created. First as an object (
GetChildClassmust readCue, thenAppendandSet('No'), checked; a mis-numbered one is deleted). ElseStore Sequence "<seq>" Cue <n> /NoConfirmation(MA3 manual, cue_store: “programmer values are not needed to store cues”). An existing cue is never stored over: Store merges the programmer into it. - The name:
Set('Name')on the cue (MA3 manual, lua_object_set), read back; elseLabel Sequence "<seq>" Cue <n> "<name>", read back. Empty: the desk’s name stays. - Fade, delay:
Set('CueFade')/Set('CueDelay')on the cue, read back; elseSet Sequence "<seq>" Cue <n> Property "CueFade" "<s>"(MA3 manual, keyword_set).nil: the desk’s own stays. - Note (plugin 0.16.13, the one argument added to this line):
Set('Note')on the cue, read back; elseSet Sequence "<seq>" Cue <n> Property "Note" "<note>".''clears it. A Cues that sends five arguments leaves the desk’s note. Cues sends the cue’s notes with quotes as’ ”,;as,, each line break as⏎(plugin 0.16.16 puts the break back; an older plugin shows the mark),\\and$as spaces, at most 160 characters.
| Answer | Detail |
|---|---|
ok | cue <n> '<name>', plus how: (new), (new by Store; object: <why>), (named by Label), (CueFade by command), note '<text>', note cleared, … by command, note did not land (reads '…') |
err | no sequence named …, cue <n> is not in <seq> after …, or cue <n> reads '<x>' after Name and Label; Cues has '<y>' |
GetChildClass, Append and Set('No') are not in the manual’s Object API list; the answer says which path ran.
| Var | Set by |
|---|---|
CURRENT_TRACK, CURRENT_TRACK_NUM, CURRENT_TRACK_BPM | Each track nav macro, in the plugin’s var_scope |
TRACK_<num>, TRACK_<num>_BPM, SHOW_NAME, SHOW_ARTIST, SHOW_TRACK_COUNT | Import, in the plugin’s var_scope |
Cues also runs Plugin "<tile>" "tick" on the desk: once per Cues run before its first live line (the plugin’s globals must exist), and after Allow, an approval flip that changes the desk’s verdict, or an auto-sync flip in Cues. Stop hosting and Remove (or Deny) send one Plugin "<tile>" "forget ended" / "forget removed" to plugin 0.16.13: the desk forgets the session, sends nothing back and opens no dialog. An older plugin gets HELM_LINK and a tick, as before. Plugin 0.16.12’s tick says no hello and repaints only what changed. "trackchanged" relabels HELM TRACK when Cues’ track changes. "hello" (plugin 0.16.2) runs only for Check MA3 connection…‘s round trip. None opens a menu. Cues never runs "import": the operator does. Every poke logs MA3 → <desk>: Plugin "<verb>" (<why>).
What Cues sends a desk, and when. Nothing on a clock: no heartbeat, no periodic poke, nothing as hosting starts but the same-Mac port release (Cues 0.2.18).
| Sends | Trigger |
|---|---|
| The push | The desk’s Import (a pull), Push now, Desk import → Apply |
Live lines (HelmEditEvent, HelmCue, …) | An edit in Cues, a drag, a desk joining after it missed edits |
HELM_LINK (+ HELM_SESSION, HELM_AUTOSYNC, HELM_CUES_TRACK, HELM_CFG_BACKUP_OFFSET on a join) | A hello Cues answers (above) |
HELM_SESSIONS, HELM_SESSIONS_V | The desk’s Connect asks for the session list |
HELM_CUES_TRACK + trackchanged | Cues’ track changes (Build selection, Live load) |
HELM_AUTOSYNC (+ tick from Cues’ side) | An auto-sync flip, on the desk or in Cues |
HELM_LINK + tick | Allow, an approval flip that changes the verdict; Remove and stop hosting (HELM_SESSIONS cleared too) to a plugin before 0.16.13 |
Plugin "<tile>" "forget ended" / "forget removed" | Stop hosting / Remove or Deny, to plugin 0.16.13: one line, the desk forgets the session |
HELM_CFG_BACKUP_OFFSET | Desk import → Backup offset changed |
HELM_SHOW_ACK | Answer to a desk show command |
Master <m> At BPM <bpm> | Live BPM to the desk on, the playhead crosses a tempo change |
Off Sequence "<name>" | Panic |
Plugin … "tick" | Once per run before a desk’s first live line |
Plugin … "hello" | Check MA3 connection…, twice |
Set OSC Property "EnableOutput" false / true | Once as a session is hosted (same-Mac onPC port release) |
The live link
Section titled “The live link”Two paths to a desk.
- Live: one short line per change, by name. Joined desks taking auto-sync on plugin 0.16.6, as you edit:
| Edit | Lines |
|---|---|
| Time | Lua "HelmEditEvent('<tc>','<seq>','<cue>',<old s>,<new s>)". A Flash: one for its Off event, then one for its On. |
| Name, fade, delay, note | One HelmCue line with the desk cue’s name, fade, delay and note (its first cue’s). An emptied name or cleared fade or delay sends nothing; cleared notes send ''. |
| New cue | HelmCue (only a desk cue the sequence does not have), then Lua "HelmAddEvent('<tc>','<seq>','<cue>',<s>,'<token>')" |
| Deleted cue | Lua "HelmDelEvent('<tc>','<seq>','<cue>',<s>)", then Delete Sequence "<seq>" Cue <n> /NoConfirmation when no other cue fires it |
| Renumbered cue | Copy Sequence "<seq>" Cue <old> At Sequence "<seq>" Cue <new> /NoConfirmation, Lua "HelmRetargetEvent('<tc>','<seq>','<old>','<new>',<old s>,<new s>)", Delete Sequence "<seq>" Cue <old> /NoConfirmation. Onto a desk cue in use: the retarget only. A chain goes from the top; two cues trading numbers park one on a free 0.001 step first. |
| Command (Go → Flash …) | HelmDelEvent, then HelmAddEvent |
<tc> is the track’s timecode name, <seq> the sequence name, times are track-relative seconds with 3 decimals. The plugin resolves each name to the object as it is now (its number at that moment) and compares event destinations against it. The MA3 manual has no command to renumber a cue (“There is no command or keyword to renumber cues”, cue_renumber); Move is documented for objects with a macro example only (keyword_move), so a renumber copies (cue_copy, /NoConfirmation: ok_noconfirmation) and deletes (keyword_delete). Nothing else goes live: no Command, appearance, macro, poke or push.
- Import: everything. Only when the operator asks: Import Tracks / Import Track… on the desk (it pulls), or Push now. Desk import’s Apply sends nothing.
Import needed. Recorded per desk and track in the show (metadata.ma3Desks, importNeeded) when the desk cannot follow live:
| Cause | Live lines for the track |
|---|---|
| A fader level or Desk GO of a cue; a stack recoloured or in a new place; a new Executor Start or track number | Still go |
miss or err from the plugin (marked at once, every one logged) | Still go |
A stack or the track renamed, the prefix changed, a new track start (names; stored as numbers) | Stop for that desk |
| The desk holds a sync version Cues did not send it | Still go |
| A new Starting Sequence #, page or timecode pool; Desk import’s Apply | No mark: numbers only say where a missing object is created |
Only a push the desk asked for clears it, for the tracks it carried, once the plugin builds them. Push now does not.
Plugin older than 0.16.8. It has no HelmCue (before 0.16.4 it also finds sequences by number), so a new cue’s HelmAddEvent would miss its cue: no live line goes to it. The hello logs plugin 0.16.6 needed, one toast, and the chip reads MA3 · plugin 0.16.6 needed.
A desk that was away. An edit a desk did not get (not joined, auto-sync off, no session) marks the track behind for it (dirty) and, in memory, where the desk still has each cue. On its join or auto-sync resume it gets the lines a live edit would have sent, from there. Behind with nothing in memory (Cues restarted): the track needs Import. A desk with no Cues data, or new to this show, gets nothing until its Import.
Pushes go in parts of 40 commands, each answered by the desk. A push the desk asked for (Import) waits up to 1.5 s for each answer. A push Cues starts itself (Push now) waits 1.5 s once; with no answer it waits 0.15 s after each part from then on, less when an answer comes. A whole show of 3000 commands takes under 30 s. Plugin 0.16.3–0.16.11 answered these from a 3 s check; 0.16.12 has no timer and answers only when it runs. The log says MA3 push <id>: no acks from <desk> — pacing by time.
Live drag. Each step (about 10 a second, latest wins; a group steps every 6 ms per cue, at least 100 ms) sends HelmEditEvent from where the last step left the event on that desk. The drop’s save (update_cue, shift_cues) starts from there. A drag that ends without a save moves the events back. A desk’s own /Helm/edit is not sent back to it.
Log. One INFO line per send per desk: MA3 live → 10.0.0.20 "Intro": 3 line(s) — "01 - Main" cue 5 12.000→12.500; "01 - Main" cue 3→3.5; …, the first 6 changes, then … and N more. A miss: MA3 desk 10.0.0.20 miss: "01 - Main" cue 3 — no event of cue 3 near 8.332 s; 12 event(s) on the track: Sequence 110 Cue 1@1.000=1; … → Import needed on that desk. Drag steps log at debug. An edit a desk did not get: MA3 live: N cue(s) of track "Opener" not sent to 10.0.0.20 (…) — the times follow when it joins. A join: MA3 desk 10.0.0.20 joined: 1 track(s) missed edits → 2 line(s) of times. A new mark: MA3 desk 10.0.0.20: Import needed for 1 track(s) — <why>.
How the desk finds an event
Section titled “How the desk finds an event”Plugin 0.16.5. The plugin reads each event of the track’s timecode track for the sequence (every time range, every subtrack) and resolves what it fires to a cue: the CUEDESTINATION property as a handle, or as text in any form a 2.x desk gives (Sequence 110 Cue 3, Sequence 110.3 or .003, Sequence "01 - Main" Cue 3, an address ending Sequence 110.Cue 3, Cue 3, 3, the cue’s name), then the text through ObjectList and FromAddr. The cue’s number and sequence are compared with the sequence found by name. Numbers compare as numbers: 3 = 3.000.
| The track holds, of that cue | The plugin |
|---|---|
| One event | Takes it, whatever its time. A desk that missed an edit still follows the move. |
| Several (a repeat) | Takes the one nearest the old time Cues sent |
| None | Answers miss |
| Delete with no cue number | The nearest event of the sequence within 0.05 s only |
Plugin 0.16.4 compared the text with Sequence <no>.<cue> only, and required the old time within 0.05 s. On a 2.x desk it missed every event (2026-10-22).
The answer’s <detail>, at most 240 characters:
| Status | Detail |
|---|---|
ok | moved from <desk time> to <new> s, plus (Cues had <old>) when the desk held it more than 0.05 s from where Cues thought, and (nearest of N) for a repeat. A retarget adds ; now cue <n>. A delete: deleted the event at <t> s. |
miss | no sequence named <seq> on the desk, no timecode named <tc>, no Helm group <tc> in its timecode, no timecode track for <seq> in <tc>; tracks target: <TARGET as read>; …, or no event of cue <n> near <old> s; <N> event(s) on the track: <destination>@<time>=<cue>; …; TARGET=<as read>. The first three events, in time order. <destination> is CUEDESTINATION as read (cut at 40 characters); =<cue> is the cue it resolved to in this sequence, =? none. " reads '. |
The desk’s command line shows the same line. /tmp/helm_events.txt on the desk gets every event of that track and, once per session, every property of its first event.
Event token
Section titled “Event token”Plugin 0.16.6. The MA3 manual (Timecode > Events) says an event carries a token, “the actions an event triggers”, and Flash a status On or Off; it names no Lua property. The plugin sets the event’s CUEDESTINATION first, then the token, so a destination set later cannot put it back to the default. It tries, in order:
- Keys
TOKEN,Token,token. - For each key: the spellings (
Go+,Go +,GoPlus,Gofor Go+;Goto,GoTo,Go To; the others as named), the full keyword fromGetTokenName, then the keyword index fromGetTokenNameByIndex, as a number and as text. - After each,
Get(key)andGet(key, Enums.Roles.Edit / Display)(lua_object_get: a role “will make the returned value a text string”).
A read-back counts when its letters name the token (goplus = go+), when it is an index GetTokenNameByIndex names it by, or a short keyword GetTokenName expands to it. The winning key and form serve the rest of the import. Goto only after every key and form; nothing readable at all keeps the first spelling, counted as unverified. Plugin 0.16.5 compared the read-back with the exact text it sent and wrote Goto over anything else: Go cues imported as Goto (2026-10-22). A retarget keeps the event’s token.
Desk report
Section titled “Desk report”Plugin 0.16.6. The desk sends /Helm/diag lines after every Import (the tracks it built) and on Plugin "<tile>" "diag" (every track, plus the last import’s token lines). Each under 200 characters; | , ; " in a name become spaces.
| Line | Holds |
|---|---|
cues|<seq>|<k>/<n>|<cue>:<name>|… | Part k of n of the sequence’s cues, as the desk has them (cue 0 left out) |
cues|<seq>|missing | No sequence of that name |
event|<tc>|<seq>|<k>/<n>|<PROP>=<value>|… | The first event of that track’s timecode track for the first stack with cues: every property PropertyCount / PropertyName give, Get, plus the Edit role’s text in brackets where it differs |
event|<tc>|<seq>|none|<why> | No such event |
token|<wanted>|<key>=<read back>|<verdict> | Once per token per import: set, fallback (Goto), refused (Flash) or unverified |
Cues logs each as MA3 diag <desk>: <line>. It joins a sequence’s parts (30 s to arrive) and compares the list with what it means for that sequence: per desk cue number, the name of the first cue firing it, as a push names it. A cue Cues has no name for is not compared; desk cues Cues does not fire are left alone. Each difference is logged (MA3 diag 10.0.0.20: "01 - Main" desk cue 1 is "", Cues has "SET UP") and marks the track Import needed on that desk with the first one as the reason. A missing sequence does too. A token line other than set also logs a warning.
Desk cue Commands
Section titled “Desk cue Commands”Cues writes a desk cue’s Command only when it has something for it, and records what it wrote per stack. It clears only what it recorded. A Command typed on the desk is never touched.
| Cues has | Command written |
|---|---|
| A fader level | FaderMaster Sequence "05 - Main" At 100 Fade 2 |
| Desk GO drives Cues, Play from the cue | Lua "if HelmStart then HelmStart([==[Blue]==],[==[05 - Main]==],3) end" |
| Desk GO drives Cues, Line up on the cue | Lua "if HelmGoto then HelmGoto([==[Blue]==],[==[05 - Main]==],3) end" |
| Both | FaderMaster Sequence "05 - Main" At 100 Fade 2; Lua "if HelmStart then HelmStart([==[Blue]==],[==[05 - Main]==],3) end" |
- The call names the track, the stack’s sequence and the desk cue, by name. Names go in Lua long strings (
[==[…]==]): the Command already nests'…'around"…". HelmStart/HelmGotoare plugin globals. They send/Helm/start,s,Blue|05 - Main:3(or/Helm/goto) on the Helm OUT row the plugin finds by port, and append the same line to the show file bridge. Cues turns the sequence name back into the stack. Theifkeeps a desk without the plugin loaded quiet.- Sent as
Set Sequence "05 - Main" Cue 3 Property "Command" '<command>': a Command holding double quotes goes in single quotes, one quote style nested in the other.;chains the two commands (MA3 manual: Semicolon keyword). - Which desk cues: every desk cue of the track’s Main stack (the stack on its Starting Sequence #), or of every visible stack. Not a desk cue fired more than once in its stack, and not a Flash, On, Off, Kill or Top cue.
- Rewritten on every push and Import, never live: a renumbered, moved or deleted cue, a renamed stack or track, the switch off or a scope change. A change marks the track Import needed.
- Cues command… in the plugin (0.16.5) builds the same call for a track and a cue of its Main stack, and can write it into any cue the operator names:
Set Sequence "<name>" Cue <n> Property "Command" '<Command>; <call>', after the cue’s own Command, read back, then the cue object’s ownSetif the command line form did not take. Cues does not record it. On a desk cue Cues writes, Cues’ next push replaces it. The plugin writes nothing when it cannot read the cue’s Command.
Link timings
Section titled “Link timings”There are none. MA3 prints every command a desk runs on its command line, Helm’s own included, so neither side runs anything on a clock (Cues 0.2.18, plugin 0.16.12).
| What | Rule |
|---|---|
| Joined, in Cues | From the desk’s hello until it says hello with no session (Disconnect), three sends to it fail in a row, or Cues restarts. lastHeard (Devices, the MA3 chip, ma3_connected_desks) is when anything last arrived from it. |
| Hello, from the desk | A press (any Helm tile or control macro), before an Import (not when one went in the last 10 s), Connect, Setup Desk, Disconnect. Never on a clock, never on Cues’ tick. |
| HELM CONNECT, on the desk | Changes only on an exchange: an answered hello, a completed Import (· <session>), an Import with no answer or a stall (· NO LINK), Disconnect, Cues’ Allow / Remove / stop hosting. |
| Cues quits | The desk finds out at its next Import or press. |
| Desk assigned to a peer | The peer gone: its commands are dropped and Devices reads Joined · driver gone. The desk is not told. |
| Session beacon | Every 2 s, Cues to Cues on UDP 8005. The desk does not hear it. |
- A 0.16.11 or older plugin with this Cues reads
HELM CONNECT · OFFLINE30 s after Cues starts: its watchdog waits for a heartbeat that no longer comes. 0.16.12 pins that old watchdog’s state so a timer left running in the desk’s Lua state stays silent.
Desk files
Section titled “Desk files”On the computer running grandMA3:
| File | Holds |
|---|---|
/tmp/helm_setup.txt | The Setup Desk log |
/tmp/helm_events.txt | One line per timecode event the plugin builds: wanted=<token> <key>=<read back>, FALLBACK Goto (…) or UNVERIFIED. Plugin 0.16.6: every token try (token try wanted=Go+ TOKEN=Go+ (string) read=…) and every /Helm/diag line (DIAG …). Plugin 0.16.5: each missed edit, with every event of that track (<destination> @<time> = <cue>) and, once per session, every property of one event. |
/tmp/helm_oscdump.txt | The desk’s OSC rows, before and after the plugin’s repair. Rewritten on every tile press, by Setup Desk and by diagnose. |
OSC rows look right but nothing flows? Run:
Plugin "Helm 0.16.6" "diagnose"It repairs empty OSC rows, writes /tmp/helm_oscdump.txt and shows the transport bridge state. "oscrestart" restarts the OSC engine.
On the Cues Mac, with onPC on the same Mac: onPC holds UDP 8001. Cues then reads the desk through /tmp/helm_hello.txt, helm_pull.txt, helm_transport.txt, helm_track.txt, helm_select.txt, helm_autosync.txt, helm_edit.txt and helm_show.txt, every 300 ms. The Desk card reads File bridge only · MA3 holds UDP 8001 on this Mac. A desk on another machine cannot reach this Cues then. The plugin writes no file for /Helm/edited: a missed edit on that onPC is not resent.
Cloud upload limits
Section titled “Cloud upload limits”Per Mac, unless it says the cloud.
Retries per file
Section titled “Retries per file”| Failure | Next try |
|---|---|
| 1st | 1 min |
| 2nd | 5 min |
| 3rd | 15 min |
| 4th | 60 min |
| 5th | Stops: Upload failed. Only Retry starts it again, with 5 more tries. |
- The count survives a relaunch. It is kept with the project’s media list on this Mac and never synced.
- Not a try: a pause, a cancel, a rate limit (waits at least 60 s), signed out, storage full.
- Signed out or storage full stops every upload of the project with one message.
- Inside one try, each request (a part, a link, start, finish) gets 5 attempts.
Pauses
Section titled “Pauses”One pause holds every upload.
| Cause | Pause | Lifted by |
|---|---|---|
| Cloud rate limit (HTTP 429) | Its Retry-After, at most 5 min. 60 s when it gives none. | Waiting it out |
| 10 network, stall or server failures in a row | 10 min | Resume |
| Daily limit. Default 10 GB. | Until local midnight | Resume sends anyway for the rest of the day |
- Only progress resets the failure count: bytes landed, a part confirmed, a file finished.
- The ledger counts every byte sent, failed tries included, per local day.
Large files and resume
Section titled “Large files and resume”| File | Sent as |
|---|---|
| Up to 100 MiB | One transfer |
| Over 100 MiB | 64 MiB parts. 3 transfers at once, across all uploads. |
| Over 100 MiB, link under 2 MB/s | 16 MiB parts on a new upload. One transfer at a time. |
- Stalled: a transfer still running after 60 s plus its size at 128 KiB/s. It counts as a network failure.
- Resume: open uploads are kept in
cloud-uploads/upload-resume.json. After a relaunch Cues asks the cloud which parts it holds and sends the rest.
Downloads
Section titled “Downloads”- Each file streams to
<name>.partbeside its final name, hashed as it lands. It is renamed only when its sha256 matches. - No bytes for 60 s: the download fails. Retry in the toast.
Cloud limits
Section titled “Cloud limits”| Limit | Value |
|---|---|
| Requests | 600 a minute per paired device. Over it, Cues waits as told, at most 60 s, and retries up to 3 times. |
| Server errors | 502, 503 and 504 retried after 1, 2 and 4 s |
| Upload attempts per file | Capped per UTC day: 20 starts, and part links scaled to the file’s size. Past it, that file waits until 00:00 UTC. The rest carry on. |
| Idle device | Unused for 90 days: signed out. Pair again. |
Session limits
Section titled “Session limits”Joining
Section titled “Joining”| Limit | Value |
|---|---|
| Members per host | 16. The 17th is told the session is full. |
| Devices waiting, not yet members | 4 at once, 2 per address. A newcomer past the cap closes the oldest waiting one. Members are never displaced. |
| Passcode | 6 digits |
| Passcode tries per address | 3 at once, then 1 a second |
| Passcode tries per host | 10 at once, then 4 a second, across all addresses |
| After a wrong code or a Deny | That device waits 1, 2, 4, 8, 16, then 30 s |
| Lockout | 10 wrong codes or Denies from one device: locked until the passcode changes |
| Code change | 20 wrong codes across all devices draw a new passcode. At most once every 5 min. |
Running
Section titled “Running”| What | Timing |
|---|---|
| Beacon | Every 2 s on UDP 8005 |
| Playhead to peers | 30 times a second rolling, twice a second parked |
Joined · quiet n s | Nothing from a member for over 3 s |
| LINK LOST on a peer | No packet for 1 s rolling, 3 s parked. The playhead freezes. |
| Desk assigned to a peer | ONLINE only while that peer was heard in the last 3 s |
Media over the LAN
Section titled “Media over the LAN”| Rule | Value |
|---|---|
| Files at once | 2 per peer, 6 across the host |
| Check | Size and sha256. A file the host lacks, or one that fails, comes from the cloud. That host is not asked for it again that session. |
| Slow link | The host resets a transfer still running after 60 s plus 1 s per MB |
| Where | sessions/media/ on the peer |
Licence cache
Section titled “Licence cache”- Where:
com.elabs.helm.cues/licence.json. See Files on disk. - Signed. The cloud signs each answer (Ed25519). Cues checks the signature on every read. A cache that fails the check is deleted.
- Offline: valid for 7 days from the last good check. Then free rules.
- Devices: a licence covers 3 devices by default.
- Versions: a licence covers releases up to its major version.
- In logs and bundles the key is cut to its first 8 characters, 4 for a short key.
In the log, each launch writes licence key=<first 8>… valid=… tier=… scopes=n checked_at=… and entitlements mode=… fetched_at=… verified=…. entitlements: none cached means no signed answer is on this Mac yet.
Logs and support bundle
Section titled “Logs and support bundle”- One file per UTC day,
logs/cues.YYYY-MM-DD.log, 7 kept. INFO and up. - Each launch logs
Helm Cues <version> (<build>, <os> <arch>) os=<version> pid=<pid> data=<folder>, the log folder, the licence and the cached entitlements. - Upload warnings come once a minute per file, with a count:
[cloud:upload] <file>: <kind> ×n in the last minute. - Upload links never reach the log. On launch Cues scrubs links from older logs and logs
[cloud] log scrub: n lines with presigned-URL values redacted in n files.
Support bundle
Section titled “Support bundle”Help → Save Support Bundle… writes helm-cues-support-YYYY-MM-DD-HH-MM-SS.zip, stamped in UTC.
| File | Holds |
|---|---|
report.txt | The Diagnostics report, as Copy report gives it |
device.json | Device ID, version and build, memory, licence summary, entitlements |
state.json | Timecode, clocks, session and MA3 bridge, as they stood |
settings.json | The Cues settings |
config.json | The grandMA2 plugin server: allowlist, port, flags |
devices.json | Every audio and MIDI input and output |
log-tail.txt | The last 200 log lines |
logs/ | Every log file kept |
crashes/ | Every crash report |
show/<id>.json | The open show |
- Never in it: the full licence key, the Helm Cloud token (it stays in the keychain), the Sessions certificate and key, the session passcode, media.
- Every file is redacted on the way in. Signed upload links read
X-Amz-Credential=<redacted>. - It does hold desk and peer IP addresses, device names, the masked email, settings and the open show with its track and cue names.
Crash reports
Section titled “Crash reports”crashes/<time>.txt holds the time, version, uptime, thread, message, location, a backtrace and the last 200 log lines.
Not built yet
Section titled “Not built yet”So nobody plans a show around it:
- Outputs beyond the desk bridge, timecode and MIDI Clock: no Art-Net DMX or sACN, no MIDI Show Control, no per-cue MIDI or OSC output, no OSC remote-control API, no MIDI-in GO.
- LTC in. The reader exists in the engine. Nothing in the interface starts it.
- Automatic failover. A standby takes over only when someone presses TAKE twice. See Sessions.
- Windows. Windows build not yet available.
- Crash upload. Crash files stay on the Mac until you send a bundle.
- Proxies or transcodes. A peer gets full-size media, from the host or the cloud.