Skip to content

/docs · CueLab · Deep

Designing cue-list data that survives a re-org

A cue list is a data model first and a UI second. Treat it like one.


Most cue-list software treats the cue list as a UI artifact: a spreadsheet-style table where each row is a cue, each column is a property, and the user types text into cells. That’s fine until you want to re-order the show, branch the script, share cues with another operator, or migrate to a different tool.

A cue list designed as a data model first — where the UI is the projection, not the source of truth — survives all of those operations cleanly. Here’s the schema that works.

The minimum viable cue

interface Cue {
  id: string;              // stable, never changes
  index: number;           // display position; can change
  type: CueType;           // 'audio' | 'video' | 'lighting' | 'scene' | 'note'
  label: string;           // human-readable name
  description?: string;    // longer note for the operator
  duration?: number;       // seconds, optional
  trigger: TriggerSpec;    // when to fire
  payload: unknown;        // type-specific data
}

Five fields earn their place:

  • id: a UUID or short string. Stable across renames, re-orders, edits. References between cues use the id, never the index.
  • index: the display ordering. Changes constantly. Never treat it as identity.
  • type: the kind of cue. Used by the UI to route to the right renderer.
  • trigger: when the cue fires. See below.
  • payload: anything type-specific. Audio cues carry file paths and levels; lighting cues carry channel/value pairs.

Triggers as a sum type

A cue can fire on:

type TriggerSpec =
  | { kind: 'manual' }                                  // operator hits GO
  | { kind: 'after'; previousId: string; delay: number } // chained
  | { kind: 'absolute'; t: number }                      // at clock time t
  | { kind: 'timecode'; t: number }                       // at SMPTE/MIDI time
  | { kind: 'event'; eventName: string };                 // external signal

Most apps reduce all triggers to “manual” because the UI doesn’t support the rest. Once you encode triggers as a proper sum type, you can build chained sequences, time-locked cues, and event-driven cues without changing the UI for each.

Why this matters: the re-org problem

The most common failure mode in cue-list tools is the re-org. You decide to move scene 3 to after scene 5. In a UI-first tool, you drag the row down. In a data-first tool, you swap the indices.

Now: what about all the references? If cue 17 (“fade out music at the end of scene 3”) references cue 12 by index, your move just broke it. If it references it by id, the move is a no-op for the chain — cue 17 still fires after cue 12, regardless of where they ended up in display order.

This is the kind of thing that gets you bitten 10 minutes before showtime. Building the schema around stable ids removes a whole class of incident.

Versioning

Every cue list needs a version. Adding a new trigger kind shouldn’t break old files; removing one shouldn’t silently corrupt them. The pragmatic pattern:

{
  "schemaVersion": 2,
  "cues": [ ... ],
  "metadata": { ... }
}

And in your loader:

function loadCueList(json: unknown): CueList {
  const v = (json as { schemaVersion?: number }).schemaVersion ?? 1;
  if (v === 1) return migrateV1ToV2(json as CueListV1);
  if (v === 2) return json as CueListV2;
  throw new Error(`Unknown cue list schema version: ${v}`);
}

The 10 minutes you spend on migration now save you 10 hours of “we can’t open the file from last show” later.

What goes in metadata

Things that aren’t cues but matter for the show:

  • Show name, date, operator(s)
  • Default audio device IDs (so the cue list opens cleanly on a different machine)
  • Global rules (e.g. “all cues fade in 200ms unless otherwise specified”)
  • Tags / categories used in this show

Keep payload-level data out of metadata. If it’s per-cue, it lives in the cue.

Why nobody does this

Most cue-list tools are spreadsheet plus a Go button. They work because shows are mostly linear, fail modes are operator-driven, and the user base is small enough that a robust schema isn’t a competitive advantage.

CueLab’s position is that this is upside-down. The schema is the product; the UI is the projection. Once the data model is solid, you can build the Go button five different ways — for a streamer, for a podcast producer, for a live event team — without rewriting the foundation.

This is the dull, important work that makes the difference between a tool you use for one show and one you use for ten years.