marquee-cartridge-spec

Marquee Cartridge Specification — v25.0.1

The delivered-artifact contract for Marquee digital signage. Marquee Studio publishes a Show as self-contained SQLite cartridges. A Surface fetches them, resolves what belongs on screen from them, and renders. This document specifies those artifacts, and the rules for interpreting them, precisely enough to build a Surface on any platform.

Status

v25.0.1 — draft, greenfield. This version replaces the pre-v25 format in full. It is not backward compatible with cartridges from the legacy web Studio, which is a separate, retired product that continues to serve its existing shows unchanged. A v25.0.1 Surface reads v25.0.1 cartridges only.

From v25.0.1 onward the format evolves additively under the compatibility contract (§10), so a cartridge published this year opens in a Surface written next year.

Repository note. engine/ implements §5 and §8 in TypeScript and passes every scenario of the conformance suite; player/ and example/ run on it. Where this document leaves a choice to an implementation, the engine’s README lists the choice it makes. The reference player loads its show once: it keeps no cartridge or media between page loads and does not re-pull (§8.4, §8.5), which a deployed Surface must; and it draws session boards with the engine’s minimal one-page board, not with a session board template (§5.15) — the engine resolves the template, the player does not render one yet. This document is authoritative wherever they differ.

Relationship to the internal platform specification

The on-screen behavior in §5 and §8 restates the rules of Marquee’s internal platform specification for external implementers. The two change together, and this document is complete on its own: nothing here requires access to the Reference.


Client Implementer’s Guide

Audience: developers building a Marquee Surface — Apple, web, Android, Linux/embedded, set-top, signage SoC.

Scope: the delivered artifacts — the two SQLite files a Surface fetches and the media they name — and the rules for turning them into what is on screen. This is not the Marquee authoring schema. An authoring database has tables and columns a cartridge never carries, and a Surface must never assume it can read one.

Terms

Term Meaning
Show The authored event. Also called the project; the two words name one thing.
Studio The authoring application family (macOS, iPad, Web). The only producer of cartridges.
Surface Any device that conforms to this specification and renders a delivered cartridge.
Cartridge A delivered, self-contained, immutable SQLite artifact.

1. The shape of the system

A Show, identified by its projectCode (the show code, e.g. SHOW26), publishes a flat keyspace:

<base>/<projectCode>/project.db           ← the project cartridge (always present)
<base>/<projectCode>/<SURFACECODE>.db     ← one surface cartridge per surface config
<base>/<projectCode>/<file_name>          ← media bytes, flat, UUID-named

A Surface is configured with a three-level address:

Level Example Meaning
projectCode SHOW26 Which Show — finds the keyspace
surfaceCode LOBBY3 Which cartridge — names the .db
locationId LOBBY3-A Which physical installation — chosen at provisioning (§6)

A Surface with no surface code uses project.db alone and shows the Show’s wallpaper and date/time — the date and time drawn by the Show’s session board template’s clock layout, else the Surface’s default template’s (§5.15). A Surface with a surface code pulls both files: project.db is the project descriptor, and the surface cartridge carries the schedule and everything it plays.

The surface code PROJECT is reserved.


2. Byte-level facts

2.1 Identifying an artifact

Both artifacts carry exactly one cartridge_meta row (§4.1):

Authoring migration history (grdb_migrations) is not carried. It describes the author’s database, not the wire format.


3. Which tables are in which artifact

3.1 project.db — the project cartridge

cartridge_meta      project        project_days
media_item          media_file     media_file_variant     media_manifest

It carries the Show’s identity, timezone, days, wallpapers, the Show’s session board template (project.template_item_id and template_settings, §5.15 — for the project-only clock), and the media those need: the wallpapers’ files and the template’s package.

3.2 <SURFACECODE>.db — the surface cartridge

cartridge_meta      project        project_days
surface_config      surface_location       surface_schedule_entry
playlist            playlist_entry         directive
session             session_set            session_set_entry
media_item          media_file             media_file_variant     media_manifest

A surface cartridge also carries project and project_days, so a Surface can resolve time without opening project.db. Its project row has show_wallpaper_item_id and desktop_wallpaper_item_id forced to NULL: wallpapers live only in project.db, and a pointer to media the surface cartridge does not carry would fail foreign_key_check. Conversely, project.db has backing_item_id forced to NULL: the backing rides in each surface cartridge (§5.10).

Never present in either artifact (authoring-only): tag, tag_assignment, integration, media_optimization, grdb_migrations.

3.3 Entity relationships

erDiagram
    project ||--o{ project_days : "show days"
    surface_config ||--|{ surface_location : "installations"
    surface_config ||--o{ surface_schedule_entry : "schedule"
    surface_schedule_entry }o--o| playlist : "playlist_id"
    surface_schedule_entry }o--o| media_item : "demo branding"
    playlist ||--o{ playlist_entry : "ordered items"
    playlist_entry ||--o{ directive : "on/off over time"
    playlist_entry }o--o| media_item : "resource_type=media_item"
    playlist_entry }o--o| session_set : "resource_type=session_set"
    session_set ||--o{ session_set_entry : "scheduled sessions"
    session_set_entry }o--|| session : "session_id"
    session_set }o--o| media_item : "backing / logo"
    media_item }o--o| media_file : "portrait_file_id"
    media_item }o--o| media_file : "landscape_file_id"
    project }o--o| media_item : "default backing / template"
    session_set }o--o| media_item : "template"
    media_file ||--|| media_manifest : "one deliverable per file"
    media_file ||--|{ media_file_variant : "renditions offered"

4. Table reference

DDL below is the v25.0.1 wire format. Comments are normative.

Text columns hold UTF-8 without U+0000; a Loader treats a row containing one as malformed — skipped with a warning where a row with an unknown enumerated value is skipped (§10.4), refused in the tables that hold exactly one row (cartridge_meta, project, surface_config).

4.1 cartridge_meta — both artifacts, exactly one row

CREATE TABLE cartridge_meta (
  cartridge_kind     TEXT    NOT NULL,   -- 'project' | 'surface'
  format_version     TEXT    NOT NULL,   -- e.g. '25.0.1'
  project_code       TEXT    NOT NULL,   -- the show code; roots the media keyspace
  surface_id         TEXT,               -- the surface code; NULL in project.db
  published_revision INTEGER NOT NULL,   -- monotonic per artifact
  timezone           TEXT    NOT NULL,   -- IANA venue timezone
  generated_at       INTEGER NOT NULL    -- Unix ms the artifact was produced
);
Column Notes
cartridge_kind How to identify the file. surface_id is non-NULL exactly when this is 'surface'.
published_revision Monotonic per artifact: per surface config for a surface cartridge, per Show for project.db. Increments on every publish.
timezone The venue timezone. The single authority for every time decision (§8).

4.2 media_manifest — both artifacts, one row per media file

CREATE TABLE media_manifest (
  media_file_id         INTEGER NOT NULL UNIQUE REFERENCES media_file(id),
  deliverable_file_name TEXT    NOT NULL,   -- flat object key, §7.1
  content_hash          TEXT    NOT NULL,   -- 'sha256:…' of these bytes
  file_size             INTEGER NOT NULL,   -- bytes
  content_type          TEXT    NOT NULL
);

The complete list of bytes this artifact needs, with one default deliverable per media file: the optimized rendition where one exists, else the original. The manifest is the only place a deliverable name appears; media_file does not repeat it. A Surface that chooses a different rendition does so from media_file_variant (§7.6).

4.3 project, project_days

CREATE TABLE project (
  id                        INTEGER PRIMARY KEY,
  cloud_uid                 TEXT    NOT NULL,
  name                      TEXT    NOT NULL,
  project_code              TEXT    NOT NULL,
  timezone                  TEXT    NOT NULL,
  show_wallpaper_item_id    INTEGER REFERENCES media_item(id),
  desktop_wallpaper_item_id INTEGER REFERENCES media_item(id),
  backing_item_id           INTEGER REFERENCES media_item(id),   -- default backing, §5.10
  template_item_id          INTEGER REFERENCES media_item(id),   -- the session board template, §5.15
  template_settings         TEXT,     -- JSON { "vars": { name: string } }, §5.15
  created                   INTEGER NOT NULL,
  updated                   INTEGER NOT NULL
);

CREATE TABLE project_days (
  id         INTEGER PRIMARY KEY,
  day        TEXT    NOT NULL UNIQUE,   -- 'YYYY-MM-DD', venue-local
  start_time INTEGER NOT NULL,          -- Unix ms, venue-local 00:00:00.000
  end_time   INTEGER NOT NULL,          -- Unix ms, venue-local 23:59:59.999
  created    INTEGER NOT NULL,
  updated    INTEGER NOT NULL
);

project_days is not decoration. It defines the event window, scopes directive evaluation (§5.4), and anchors synthetic time (§8.2). Days are whole venue-local calendar days and may be non-contiguous — a Show can skip a day. Order by start_time. Studio guarantees at least one day.

4.4 surface_config, surface_location, surface_schedule_entry

CREATE TABLE surface_config (
  id                 INTEGER PRIMARY KEY,
  name               TEXT    NOT NULL,
  surface_id         TEXT    NOT NULL,   -- the surface code
  published_revision INTEGER NOT NULL,
  published_at       INTEGER NOT NULL,
  created            INTEGER NOT NULL,
  updated            INTEGER NOT NULL
);

CREATE TABLE surface_location (
  id          INTEGER PRIMARY KEY,
  config_id   INTEGER NOT NULL REFERENCES surface_config(id),
  location_id TEXT    NOT NULL UNIQUE,   -- globally unique real-world id
  label       TEXT,
  created     INTEGER NOT NULL,
  updated     INTEGER NOT NULL
);

CREATE TABLE surface_schedule_entry (
  id                 INTEGER PRIMARY KEY,
  config_id          INTEGER NOT NULL REFERENCES surface_config(id),
  slot               TEXT    NOT NULL,   -- 'playlist' | 'demo_station'
  timestamp          INTEGER NOT NULL,   -- most-recent <= now wins, per slot
  playlist_id        INTEGER REFERENCES playlist(id),     -- NULL on demo_station entries
  background_item_id INTEGER REFERENCES media_item(id),   -- demo branding (behind)
  overlay_item_id    INTEGER REFERENCES media_item(id),   -- demo branding (front)
  created            INTEGER NOT NULL,
  updated            INTEGER NOT NULL,
  CHECK (
    ( slot = 'playlist'
        AND background_item_id IS NULL AND overlay_item_id IS NULL )
    OR
    ( slot = 'demo_station'
        AND playlist_id IS NULL
        AND ( background_item_id IS NOT NULL OR overlay_item_id IS NULL ) )
  )
);

A surface cartridge carries exactly one surface_config row — its own — and at least one surface_location row. Every location shares the config’s cartridge.

A location identifies an installation. It carries no orientation: a device’s orientation is its own (§6). An earlier draft of this version carried the mount as surface_location.orientation; that column is retired, and a Loader ignores it, without a warning, in a cartridge that still has it.

A surface config has one schedule: its playlist entries, which every device of the config resolves, whatever its orientation (§5.1). A playlist carries both orientations already — each item holds a portrait file, a landscape file, or both (§4.6) — so a device plays the scheduled playlist with the files of the orientation it renders. Installations that need different content in each orientation are different surface configs. The demo_station entries are the DemoStation mode’s (§5.11).

An earlier draft of this version scheduled playlists per orientation, in the slots portrait and landscape. Those values are retired. A row that carries one is malformed, like a row holding a NUL byte (§4): skipped, with a warning naming the column. It is never read as a playlist entry, so a draft cartridge with no playlist row schedules nothing.

4.5 playlist, playlist_entry, directive

CREATE TABLE playlist (
  id              INTEGER PRIMARY KEY,
  name            TEXT    NOT NULL,
  created         INTEGER NOT NULL,
  updated         INTEGER NOT NULL
);

CREATE TABLE playlist_entry (
  id                   INTEGER PRIMARY KEY,
  playlist_id          INTEGER NOT NULL REFERENCES playlist(id),
  position             INTEGER NOT NULL,                  -- authored order
  resource_type        TEXT    NOT NULL,                  -- 'media_item' | 'session_set'
  media_item_id        INTEGER REFERENCES media_item(id),
  session_set_id       INTEGER REFERENCES session_set(id),
  start_time_portrait  REAL,
  end_time_portrait    REAL,
  start_time_landscape REAL,
  end_time_landscape   REAL,
  created              INTEGER NOT NULL,
  updated              INTEGER NOT NULL,
  CHECK (resource_type <> 'media_item'  OR media_item_id  IS NOT NULL),
  CHECK (resource_type <> 'session_set' OR session_set_id IS NOT NULL)
);

CREATE TABLE directive (
  id        INTEGER PRIMARY KEY,
  entry_id  INTEGER NOT NULL REFERENCES playlist_entry(id),
  type      TEXT    NOT NULL,   -- 'standard' | 'takeover' | 'alert'
  timestamp INTEGER NOT NULL,
  on_screen INTEGER NOT NULL,
  timezone  TEXT,               -- authoring context only; never evaluated
  created   INTEGER NOT NULL,
  updated   INTEGER NOT NULL
);

Studio-player runtime modifiers are not in the wire format. Studio’s operator-controlled player has live-mode controls (looping, pause on entry or completion, disabling an entry, shuffle, seamless video). They govern a human operator’s playback, not scheduled rules, so Studio strips them at publish. A Surface decides what is on screen from the schedule and directives only.

4.6 media_item, media_file

CREATE TABLE media_item (
  id                INTEGER PRIMARY KEY,
  name              TEXT    NOT NULL,
  portrait_file_id  INTEGER REFERENCES media_file(id),
  landscape_file_id INTEGER REFERENCES media_file(id),
  display_duration  REAL,                                 -- seconds, stills
  created           INTEGER NOT NULL,
  updated           INTEGER NOT NULL,
  CHECK (portrait_file_id IS NOT NULL OR landscape_file_id IS NOT NULL)
);

CREATE TABLE media_file (
  id                 INTEGER PRIMARY KEY,
  content_type       TEXT    NOT NULL,   -- MIME of the imported file
  codec              TEXT,               -- what the imported file needs to decode
  width              INTEGER,
  height             INTEGER,
  orientation        TEXT,               -- advisory
  aspect_ratio       REAL,               -- advisory
  intrinsic_duration REAL,               -- seconds, video
  created            INTEGER NOT NULL,
  updated            INTEGER NOT NULL
);

A media_item is the orientation-independent thing Studio schedules. It holds up to two media_files, one per orientation, and the Surface picks by the orientation it renders (§5.7). At least one is always present.

A media_file describes the imported asset. content_type is the MIME type of the file as imported; the types of its renditions live in media_file_variant. Deliverable names live only in media_manifest and media_file_variant.

4.7 session, session_set, session_set_entry

CREATE TABLE session (
  id          INTEGER PRIMARY KEY,
  name        TEXT    NOT NULL,
  abstract    TEXT,
  presenters  TEXT,               -- JSON array
  attributes  TEXT,               -- JSON array
  source_id   TEXT,
  source_type TEXT,
  source_name TEXT,
  created     INTEGER NOT NULL,
  updated     INTEGER NOT NULL
);

CREATE TABLE session_set (
  id                INTEGER PRIMARY KEY,
  name              TEXT    NOT NULL,
  duration          REAL    NOT NULL DEFAULT 8,              -- seconds per board page
  backing_item_id   INTEGER REFERENCES media_item(id),
  logo_item_id      INTEGER REFERENCES media_item(id),
  source_id         TEXT,
  source_name       TEXT,
  template_item_id    INTEGER REFERENCES media_item(id),  -- overrides the project's, §5.15
  template_settings   TEXT,                               -- for that override, §5.15
  created           INTEGER NOT NULL,
  updated           INTEGER NOT NULL
);

CREATE TABLE session_set_entry (
  id              INTEGER PRIMARY KEY,
  session_set_id  INTEGER NOT NULL REFERENCES session_set(id),
  session_id      INTEGER NOT NULL REFERENCES session(id),
  session_time_id TEXT,
  start_time      INTEGER NOT NULL,
  end_time        INTEGER NOT NULL,
  source_room_id  TEXT,
  room_name       TEXT,
  created         INTEGER NOT NULL,
  updated         INTEGER NOT NULL
);

A playlist entry with resource_type = 'session_set' puts a session board on screen: the room’s sessions as text, composited over a backing (§5.10). logo_item_id is part of the board’s content, placed by the board’s layout. Session boards are required on every Surface. The cartridge carries the content and the pointers; the layout is a session board template (§5.15): a package the Show, or one set, names, else the Surface’s built-in default template. Which of a template’s layouts a sign shows is the device’s own setting, never carried here.

render_modes and schedule_template, which earlier drafts of v25.0.1 carried on this table, are retired (§10.4): a Loader ignores them silently.

4.8 media_file_variant — the renditions offered

CREATE TABLE media_file_variant (
  id            INTEGER PRIMARY KEY,
  media_file_id INTEGER NOT NULL REFERENCES media_file(id),
  kind          TEXT    NOT NULL,   -- 'original' | 'optimized' | 'webOptimized' | 'wifiOptimized' | …
  file_name     TEXT    NOT NULL,   -- flat object key, §7.1
  content_type  TEXT    NOT NULL,
  codec         TEXT,               -- required for video
  width         INTEGER,
  height        INTEGER,
  file_size     INTEGER NOT NULL,
  content_hash  TEXT    NOT NULL,   -- 'sha256:…' of THESE bytes
  created       INTEGER NOT NULL,
  updated       INTEGER NOT NULL
);

Every media file has at least its original row. Ignore any kind you do not recognize.

kind What it is
original The file as imported.
optimized The venue master — visually lossless, Apple-native (HEVC video, HEIC stills).
webOptimized Browser-universal — H.264 up to 1080p, JPEG or PNG stills.
wifiOptimized A smaller Apple-native rung for a weak uplink. Video only; not every video has one.

codec names what a Surface must decode (HEVC, H.264, ProRes; HEIC, JPEG, PNG, WebP). It is the column to choose on: for video, content_type does not say whether a video/mp4 holds HEVC or H.264.


5. What is on screen

This is the part to get right. Everything else is plumbing.

A Surface runs a loop driven by the display (DisplayLink on Apple, requestAnimationFrame in a browser, the equivalent elsewhere). Each tick samples two clocks (§8.1) and passes the pair through the whole evaluation, so every decision in a tick sees the same instant. Nothing may depend on a tick arriving at a particular moment: a throttled or late tick simply catches up.

flowchart TD
    A["tick: showNow (synthetic, venue) + jump check"] --> C["the one schedule, slot 'playlist':<br/>latest timestamp ≤ showNow"]
    C --> D{"playlist_id NULL?"}
    D -->|yes| E["show nothing"]
    D -->|no| F["playlist_entry in position order"]
    F --> G["viability gates:<br/>orientation rendered (§5.1) → alert ON → day → directive ON"]
    G --> AL{"any alert ON?"}
    AL -->|yes| AS["working set = alert set"]
    AL -->|no| H{"any takeover ON?"}
    H -->|yes| I["working set = takeover set"]
    H -->|no| J["working set = standard set"]
    AS --> K["cursor: next position after the last shown"]
    I --> K
    J --> K
    K --> L["render marker: now + duration, or interrupt time"]

5.1 One schedule, and the orientation rendered

A surface config has one schedule, its playlist entries (§4.4), and every device of the config resolves it the same way (§5.2). What differs between devices is the orientation rendered: the device’s own (§6) — or, in a DemoStation’s picture-in-picture, the opposite one (§5.11) — never a value read from the cartridge. It decides which entries pass the orientation gate (§5.3), which file of each item plays (§5.7), the start and duration (§5.8), and the backing (§5.10). The schedule does not depend on it.

When the orientation rendered changes — the device rotates, or a demo starts or ends — the playlist does not. Cut immediately (§5.9), keep both rotation cursors (§5.6), and run the viability pass in the new orientation: entries whose slot is empty in it drop out, entries that were excluded join, and the rotation continues after the cursors with the new orientation’s files. The entry that was cut keeps its place as the last one shown, as after a show-clock jump (§8.3).

A Surface always has an orientation; if one is ever missing, skip the orientation gate (§5.3) and log the error.

5.2 Pick the schedule entry

The most recent playlist entry whose timestamp ≤ showNow wins. Entries are a timeline of changeovers, not windows: there is no end time.

SELECT * FROM surface_schedule_entry
 WHERE config_id = :config AND slot = 'playlist' AND timestamp <= :showNow
 ORDER BY timestamp DESC, id DESC LIMIT 1;

5.3 The viability pass

Run it each time the current content ends, always against the show clock. It is a sequence of gates, cheapest first; an entry that fails a gate is excluded and never reaches the next one.

  1. Orientation gate. For each playlist_entry of the active playlist, in position order: a media_item entry passes only if its item has a file in the slot for the orientation rendered (§5.1, §5.7). An empty slot is an intentional exclusion, so the entry’s directives are never evaluated — its alerts included. A session_set entry passes if the set is present.
  2. Alert gate. Find the entry’s governing alert directive over the whole timeline: an alert is never scoped to a day (§5.4). The entry joins the alert set if it is ON.
  3. Day gate. Once per pass, find the project_days row containing showNow (§5.4). It scopes the takeover and standard directives evaluated below to the current day. Outside every day, the whole timeline participates.
  4. Directive gate. Find the governing takeover and standard directives within the current day (§5.4). The entry joins the takeover set if its governing takeover directive is ON, and the standard set if its governing standard directive is ON. An entry may join any of the three sets.

An entry needs an ON directive to be viable. An entry with no directive of a type is not ON for that type, and an entry with no ON directive of any type never appears. Cartridges also deliver media used outside playlists (wallpapers, backings, branding); being delivered never makes anything viable.

5.4 Directives — the on/off timeline

A directive is a state change, not a window. For one entry, one type, at showNow:

  1. Find the project_days row where showNow BETWEEN start_time AND end_time.
  2. Take that entry’s directives of that type with timestamp ≤ showNow.
  3. If a day was found, keep only directives whose timestamp falls inside that day.
  4. The latest surviving directive governs. ON when its on_screen is 1.
  5. If showNow falls outside every day, skip step 3.
SELECT * FROM directive
 WHERE entry_id = :entry AND type = :type AND timestamp <= :showNow
   AND (:dayStart IS NULL OR timestamp BETWEEN :dayStart AND :dayEnd)
 ORDER BY timestamp DESC LIMIT 1;

Day scoping is what makes directives day-part: yesterday’s takeover cannot leak into today. directive.timezone is authoring context; never evaluate it.

An alert directive is never day-scoped: skip step 3 for it, on every day and outside every day. An alert switched ON governs until a later alert directive of that entry turns it OFF, so a single directive at timestamp 0 switches one on for the whole show.

5.5 The working set: alert beats takeover beats standard

If the alert set is non-empty, it IS the rotation. The takeover and standard sets are suppressed entirely. Otherwise, if the takeover set is non-empty, it IS the rotation, and the standard set is suppressed entirely — not appended, not interleaved.

5.6 Rotation: position cursors

Keep two cursors, each an authored position:

Cursor Tracks Reset when
standard cursor position of the last standard entry shown active playlist changes; cartridge replaced
takeover cursor position of the last takeover entry shown active playlist changes; cartridge replaced; a takeover period begins
alert cursor position of the last alert entry shown active playlist changes; cartridge replaced; an alert period begins

The next entry is the first entry in the working set whose position is greater than the cursor, wrapping to the first entry. With no cursor, start at the first entry.

5.7 Item → file, by orientation

portrait  → portrait_file_id     -- no fallback
landscape → landscape_file_id    -- no fallback

The slot is the author’s decision. There is no fallback to the other orientation.

The same rule applies to backings (an empty slot means no backing on that orientation) and to demo_station branding. Then choose which rendition of the file to play (§7.6).

5.8 Start and duration

Each entry resolves to a start and a duration, in seconds, for the orientation being rendered. They are the numbers Studio’s editor shows, so a Surface following this rule shows what the author saw.

  Rule
start start_time_<orientation>, else 0
duration end_time_<orientation> − start when the window has an end — for a still too, where it is the dwell. A video with no end: to the end of the clip. A still with no end: media_item.display_duration, else 8 s.

5.9 When content ends: the render marker and interrupts

Everything on screen — a still, a video, a session board, a composite — is governed by one render marker: a single timestamp on the show clock (§8.1). The render loop checks it on every tick, and any tick where showNow ≥ marker evaluates the next content. That check is the only place the next content is evaluated.

Each render item the viability pass produces carries a next-render hint:

Hint Marker
A duration (§5.8) showNow + duration, read when the content’s first frame is on screen
A time that timestamp

The viability pass gives a time hint when a known interrupt comes before the item’s natural end — the next activation of a higher set (below) is substituted for the duration: under the standard set, the next takeover or alert activation; under the takeover set, the next alert activation. Only entries that passed the orientation gate count, so a takeover or an alert authored for the other orientation never interrupts. An item otherwise always finishes its time, and nothing interrupts an alert item.

Setting the marker to 0 forces the next render loop to evaluate, whatever the show time, including before 1970 (negative milliseconds). Use it for a show-clock jump, a video completing its window, a skip after a load failure, a change of active playlist, a rotation, a mode change, and a newly committed cartridge.

For video, the hint is a watchdog — the window’s duration when the window has an end, else intrinsic_duration (the clip’s full length, even when trimmed at the start), else 300 s, plus 10 s grace — or the interrupt time, whichever is earlier; the media completing its window sets the marker to 0. One clip that never completes cannot park the rotation.

Interrupts — the only things that end content early:

Interrupt Effect
The working set changes to alert, from standard or takeover Cut immediately to the alert set’s first entry
The working set changes from standard to takeover Cut immediately to the takeover set’s first entry
A schedule boundary changes the active playlist (§5.2) Cut; resolve the new playlist; reset cursors
The device rotates (§5.1) Cut; keep the cursors; continue in the new orientation
The Surface’s mode changes: a DemoStation’s demo starts or ends (§5.11) Cut; the rotation moves into the picture-in-picture or back, and keeps its cursors
The show clock jumps (§8.3) Marker to 0; re-evaluate everything; cut
A new cartridge is committed Reset all state; cut

Not interrupts — these apply at the next viability pass, after the current content finishes naturally:

Compute interruptAt during the viability pass: while the working set is standard, it is the earliest future takeover or alert directive turning ON for an entry of the active playlist; while it is takeover, the earliest future alert directive turning ON. The schedule boundary is checked separately in the render loop (showNow ≥ next boundary); a changed playlist, or a demo starting or ending, sets the marker to 0.

The standard → takeover transition always cuts and starts the takeover set at its first entry, even when the entry on screen is also in the new takeover set. A transition to the alert set does the same with the alert set’s first entry.

Never disarm. Every path out of an evaluation — success, skip, load failure, empty set — leaves the marker armed: 0 to skip to the next entry, or showNow + 2 s for an empty set.

5.10 Composition: backing, content, overlay

Content is layered, bottom to top:

Layer Holds
Backing Media behind content — supports transparent content
Content The entry’s media, or a session board (text and logo)
Demo overlay In demo mode only: demo_station branding in front (§5.11)

A session board is text: it composites over its backing to complete what is on screen.

The backing is set at the project level, with an override for session boards. A backing is a media_item, resolved to a file by orientation (§5.7), and may be a still or a video.

Content Backing
Session board session_set.backing_item_id if set → project.backing_item_id → none
Media item project.backing_item_id → none

5.11 The demo_station slot and the picture-in-picture

A surface’s schedule has two kinds of entry: playlist entries (playlist slot) and DemoStation entries (demo_station slot). demo_station is resolved the same “latest ≤ now” way, in parallel with playlist, and its active entry alone tells a Surface which mode it is in. The CHECK constraint encodes the rules:

During a demo, a DemoStation gives its screen to the demo, framed by the branding: the background behind everything, the overlay in front. The schedule keeps playing in a picture-in-picture, and the picture-in-picture is the Surface’s player itself, moved into a smaller frame:

Identify a demo entry by its slot. How a demo is laid out — where the picture-in-picture sits, what fills the rest of the screen — is a Surface implementation choice. A Surface that does not implement demo mode ignores the slot entirely.

5.12 Content types

media_file.content_type and media_file_variant.content_type are MIME types. Studio imports image/png, image/jpeg, image/webp, image/heic, video/mp4, video/quicktime, and video/x-m4v, and produces renditions in the types of §4.8. A session board template is one application/zip file (§5.15), never decoded as media.

5.13 Failure handling: skip and warn


5.14 Where the rules leave a choice

The rules above are complete for what they state. Where they are silent, the reference engine had to choose. A conforming client makes the same choices, because the conformance traces (§11) depend on them.

Situation Rule
Two directives, schedule entries or playlist entries share a timestamp or position The higher id is later. Rotation cursors compare (position, id).
No playlist entry has started yet Hold, as for an empty working set (set.empty), and play from the first changeover.
What an interruption records One event per cause. A jump records jump only, and a new cartridge cartridge.commit only: what the schedule changes as a result — the playlist, the mode — is applied silently. A boundary that starts or ends a demo records mode.change, and a playlist change at the same boundary resets the cursors silently. A rotation and a boundary in the same tick are two causes, and each records its cut. A cut is recorded only when an entry is on screen, and names it.
An authored blank Re-evaluated every 2 s like a hold, without repeating the blank event.
A rotation; a new cartridge Applied at the next tick; the cut carries that tick’s show time.
Which failures count toward set.all_failed Load failures before or after the first frame, missing first frames, and entries that are not image or video media. The streak ends when an item finishes its time: naturally, at its watchdog, on completion, or when a takeover or an alert cuts it. It also ends when the playlist or the orientation rendered changes, and at a new cartridge.
A takeover or an alert due while the next item is still loading, or before a late first frame’s natural end It still cuts at its time (§5.9). The loading item is abandoned; a late item’s marker is armed at the activation instead.
The preview clock before its first command Reads what a Surface would show now, and runs.
No orientation from the host The gate is skipped (§5.1), and each entry plays its landscape file, else its portrait one; backings and demo branding resolve in landscape, as Automatic reads a display that is not taller than it is wide (§6). A DemoStation’s picture-in-picture skips the gate too.

5.15 Session board templates

A session board template is a package — a zip of template.json, a page, its layouts, stylesheets, fonts and a small engine — that a Surface renders a board with in a web view, from one data document per render that the Surface’s engine produces. The package format, the document and the host’s duties are specified in TEMPLATE.md; this section says how a template reaches a Surface and which one a board uses.

Columns (project and session_set, §4.3, §4.7):

Column Meaning
template_item_id The media_item holding the package: one media_file of type application/zip, in either slot.
template_settings JSON { "vars": { name: string } } — the Show’s values for the variables the template declares, passed through to the data document as they are. Anything else is malformed and ignored, with a value.malformed warning.

Resolution, the backing’s shape (§5.10):

template = session_set.template_item_id ?? project.template_item_id ?? the Surface's built-in default template

The settings follow the pointer that applied: a set with its own template uses its own template_settings; a set without one uses the Show’s template with the Show’s settings. A set cannot restyle the Show’s template — one pointer, one settings object, no merging.

A dangling pointer is a warning, not a refusal. A pointer to an item the cartridge does not carry, or carries with no file in the manifest, gives reference.dangling on that column and reads as null, so the next step of the resolution applies. A Surface whose package fails to extract or to load at render time does the same: skip to the next template, say so, and never render nothing.

The package is media, taken as named. It is delivered through the manifest like any file, verified like any file (§7.2), never decoded as media and never renditioned (§7.5), and wanted on every lane (§7.7): a device of either orientation renders the board with it. application/zip is never playable (§5.13): a package found in a playlist is skipped loudly.

The board variant is the device’s. A template advertises the layouts it offers (now-next, schedule, or both; TEMPLATE.md). Which of them a sign shows — now / next, the schedule, or both — is a setting of the device, beside its orientation (§6), never authored into a cartridge. both is one board: the now / next page first, then the schedule’s pages, so the board has 1 + the schedule’s page count pages and lasts duration × pages (§5.8) like any board. A variant the template does not offer falls to the one it does, with template.variant_unavailable noted; both on a one-layout template is that layout. A template’s style switches (its modifiers, TEMPLATE.md) are the device’s settings too, read off the package it is rendering.

When the variant changes (the operator sets it): a session board on screen is cut (variant.change, §5.9’s closed set extended) and the rotation continues after its cursors; anything else on screen plays on, and the next board is chosen with the new variant.

The default template. Every Surface carries one, so a Show that names none still has a board (INV-16’s spirit: nothing authored is ever shown as nothing). The reference template is published with this specification (TEMPLATE.md).

The project-only clock. A Surface with no surface code (§3) shows the Show’s wallpaper with the date and time over it, drawn by a template’s clock layout (TEMPLATE.md §2, §7):

clock = the Show's template, when it offers a clock layout ?? the Surface's default template

So project.db carries project.template_item_id, project.template_settings and the package the pointer names, exactly as a surface cartridge does (a dangling pointer is the same warning, and reads as null). The clock is pushed on the venue minute in the venue zone; its ink is measured from the wallpaper where the clock layout’s textRegion says its text falls (no wallpaper is black), and it is drawn only once that reading and the wallpaper are both in. The variant and the board’s cuts do not apply: a clock has one layout and one page.


6. Provisioning

A surface cartridge describes one or more installations of the same surface config: its surface_location rows. They all play the same cartridge; each is tracked by its location_id. A location says where a device is — for its status reports, and for the operator choosing among installations — and nothing about how it is mounted.

On first run:

  1. Read the cartridge’s surface_location rows.
  2. Pick one — present the list by label (else location_id), or take the only one.
  3. Persist the chosen location_id.

On later runs, keep the stored location_id only while the cartridge still lists it. If it is gone, re-pick. Clear the stored location_id whenever the configured projectCode or surfaceCode changes.

A device’s orientation is its own. A Surface renders the orientation set on the device:

A change — the operator’s, or the driven display turning — takes effect immediately (§5.1). It keeps the schedule: the device plays the same playlist, from where it was, with the other orientation’s files. A publish never changes the orientation: a client does not read one from the cartridge.


7. Media

7.1 Addressing

<mediaBase>/<projectCode>/<file_name>

Flat, per Show. file_name is the manifest’s deliverable_file_name, or the file_name of the rendition you chose (§7.6); renditions live in the same keyspace. Names are UUID-unique and immutable — a re-encode mints a new name — so presence by name is a sufficient “already have it” check. Cache on name.

Prune your cache against what you play plus what you are still fetching: the manifests of the cartridges you hold — or, fetching by lane (§7.7), the files of the lanes you render — each file replaced by the rendition you chose, plus any rendition still downloading.

7.2 Integrity

Every manifest row and every rendition carries file_size and content_hash. Verify every file before use:

  1. Downloaded length differs from file_size → reject as a truncated transfer; retry.
  2. SHA-256 differs from content_hash → reject as wrong bytes at that address; retrying will not help.

Report the two failures with different messages. Verify a rendition against its own hash and size, never its parent file’s.

A missing or rejected file degrades the media count; it never fails the bootstrap. Remember what failed and retry on later passes.

The origin answers ranged GETs (206, Accept-Ranges: bytes, Content-Range, ETag). If you resume:

  1. Store the origin’s ETag beside the partial, and write it before the bytes it describes.
  2. No validator, or a mismatched one → discard and start clean. Never append to bytes whose identity you cannot prove.

Keep partials outside the directory your renderer resolves media from.

7.4 Renderer limits

media_file.width / height and the rendition’s own dimensions are intrinsic pixels. Assets larger than your renderer can texture are legitimate: downscale them (§5.13).

7.5 Template packages

Session board template packages are delivered through the manifest like media, but they are not decoded as media and never choose among renditions. Take them exactly as the manifest names them. Their use is specified in §5.15.

7.6 Choosing a rendition

A Surface may download a rendition in place of the manifest’s default deliverable. The first one it can decode wins:

  1. wifiOptimized, if the device is set to prefer it. A weak uplink is a fact about where a device is installed, not about the Show, so this is a device setting.
  2. The venue master: optimized when the file has one, else original.
  3. webOptimized.
  4. original, as a last resort.

A browser Surface puts webOptimized first, and treats HEVC as undecodable unless it has probed the platform. H.264, JPEG, PNG, and WebP are the browser-safe set.

Rules that make the order safe:

Play what you hold. The order above is what to fetch when you hold nothing for a file.

7.7 Fetching by lane

A cartridge lists every file its surface config can play, in both orientations. A lane is one orientation over the config’s one schedule: the file in that orientation’s slot of each item (§5.7), and the backings in that orientation (§5.10). A Surface may fetch only the files of the lanes it renders:

After an orientation change, fetch the new lane’s files. Until a file is held, an entry that needs it is skipped when its turn comes, like any missing file (§5.13), and plays once the file arrives.

Fetching everything the manifest lists is conforming too. A server that mirrors a Show for other Surfaces fetches everything.


8. Time

8.1 One timeline

Every time-based decision runs on the show clock (§8.2): the schedule, directives, day scoping, session board now/next, and the render marker (§5.9).

The device’s monotonic clock — CACurrentMediaTime or equivalent on Apple, performance.now() in a browser — has one job: detecting show-clock jumps (§8.3). It never decides content and never times it.

All timestamp, start_time, and end_time values are absolute Unix ms, so comparisons are timezone-free. The venue timezone (cartridge_meta.timezone) matters for the day projection below and for presenting times. The device’s own timezone is never used.

8.2 The show clock

A Surface always behaves as if it is at the venue.

showNow(realNow):
  eventStart = the earliest project_days.start_time
  eventEnd   = the latest   project_days.end_time
  if eventStart ≤ realNow ≤ eventEnd:
      return realNow                                   # real venue time
  # outside the event: synthetic time
  tod       = time-of-day of realNow in the venue timezone
  synthetic = Day 1's date at tod, in the venue timezone
  if synthetic is outside [Day 1 start_time, Day 1 end_time]:
      synthetic = Day 1 start_time
  return synthetic

8.3 Show-clock jumps

A jump is a show-clock move that real elapsed time cannot explain. Each tick, compare the show clock’s movement with the monotonic clock’s movement since the previous tick. A move backward by any amount (midnight outside the event window; real time crossing the event end onto Day 1), or forward by more than the elapsed real time beyond 250 ms of noise (a wall-clock correction), is a jump. Every jump sets the render marker to 0, so the next render loop re-evaluates schedule, viability, and working set, and cuts (§5.9). Keep the rotation cursors unless the active playlist changed.

Because days are whole venue-local days, the Day 1 clamp (§8.2) cannot engage, and a Surface’s show clock never holds still.

8.4 Freshness: when to re-pull

Cloud (static bucket): conditional GET on each cartridge. Store the ETag, send If-None-Match, treat 304 as unchanged.

LAN server: a status document at <base>/<projectCode>/<fileName>/status:

{ "projectCode": "SHOW26", "surfaceId": "LOBBY3", "isProjectOnly": false,
  "generatedAt": 1789340889665, "publishedRevision": 3, "mediaFileCount": 98 }

Poll every 30–60 s. Compare for inequality, never for order, and re-pull when either token differs: generatedAt is a wall clock, not monotonic across machines.

Refuse an older cartridge. Before committing a pulled cartridge over the one you hold, compare (published_revision, generated_at) lexicographically and keep the current one if the new one is strictly older. Log the refusal.

8.5 Bootstrap

sequenceDiagram
    participant S as Surface
    participant O as Origin (LAN or cloud)
    participant C as Local cache

    S->>C: load committed cartridges (if any) and render immediately
    Note over S,C: never wait on the network to show something

    S->>O: GET /<project>/project.db   (If-None-Match)
    O-->>S: 200 + ETag | 304
    S->>O: GET /<project>/<SURFACE>.db (If-None-Match)
    O-->>S: 200 + ETag | 304

    Note over S: check SQLite magic, cartridge_kind, format_version
    Note over S: refuse if (revision, generatedAt) is older (§8.4)
    S->>C: atomic replace, then open read-write, FKs ON

    S->>S: provision the location (§6)
    S->>S: choose files by lane (§7.7) and renditions (§7.6)
    loop each file not held
        S->>O: GET /<project>/<file_name>
        O-->>S: bytes
        S->>S: size and hash check (§7.2)
        S->>C: atomic move into media cache
    end
    S->>S: prune cache (§7.1)
    S->>S: evaluate (§5) and render

Never replace a good cartridge with an unvalidated body. A captive portal answers 200 with an HTML login page. Check the 16-byte SQLite magic (SQLite format 3\0) and read cartridge_meta before the swap — a zero-byte file is a valid empty database, so “it opens” is not enough.


9. Branding

A cartridge carries no brand of its own. A Show’s typefaces, palette and text colours are the session board template’s: they travel inside the package (§5.15, and the template format’s brand and fonts/), and the template draws with them. Anything else on screen is media the Show authored.

Earlier drafts of v25.0.1 delivered a style book here — project.brand_style, brand_style_item_id, the same pair on session_set, and media_item.brand_member, with every typeface as a media item. Those columns are retired (§10.4): a cartridge that still carries them loads, and they are never read.


10. The compatibility contract

A record that reads a delivered artifact is a WIRE FORMAT, not a schema.

The DDL in §4 is the v25.0.1 baseline. Every baseline column is present in every v25 cartridge. From here, cartridges are read by Surfaces older and newer than they are, so:

10.1 Producers may only add

New columns are added nullable, or NOT NULL with a DEFAULT. Tables and columns are never removed, renamed, reordered, or retyped. Enumerated values are never repurposed.

The baseline was revised once, in 2026-10, before any Surface outside the reference clients read the format: session_set.render_modes and session_set.schedule_template were removed (the layout became the template’s, §5.15) and the four template columns were added. The DDL in §4 is the revised baseline; the removed columns are retired (§10.4).

10.2 Consumers must tolerate absence

Decode every column added after the baseline as optional-with-default. Probe PRAGMA table_info(<table>) once per opened cartridge and build row mappers from what is there. A strict decode of a missing column fails the whole table, and the Surface reports “no content” for a Show whose rows are all present.

10.3 Never let a read failure look like empty

If a table fails to decode, say so, distinctly from “this table has no rows”. They are different facts and lead to opposite investigations.

10.4 Ignore what you do not understand

Unknown tables, columns, and values of resource_type, slot, type, and kind are skipped, not treated as errors. A column retired from the baseline (surface_location.orientation, §4.4; session_set.render_modes and schedule_template, §4.7; the style book’s brand_style, brand_style_item_id and brand_member, §9) is ignored as well, and silently: it is not unknown. Nor is a retired value — portrait or landscape in surface_schedule_entry.slot (§4.4) — but a row that carries one cannot be read: it is malformed, skipped with a warning (§4).

10.5 A new table must be safe to ignore

A Surface that has never heard of a new table still plays the cartridge correctly. An absent table is not a table that failed to decode.


11. Conformance

11.1 Conformance suite

The Marquee Conformance Suite lives in conformance/: fixture cartridges, and for each scenario its setup, clock, simulated-host behaviour, and the expected trace of on-screen transitions with stable reason codes. A Surface conforms when its trace matches for every scenario. The scenarios cover basic rotation; takeover start (cut) and end (natural finish); standard directives turning off mid-item; cursor resume past an excluded entry; overlapping takeovers; synthetic time from a device in another timezone; show-clock jumps; multi-page session boards interrupted by a takeover; video completion, load failure, and the watchdog; schedule changeover; day scoping; Studio preview; empty orientation slots and a landscape file in a portrait slot; all entries failing without disarming; the first-frame timeout; late callbacks from replaced items; same-playlist boundaries and authored blanks; one schedule played in both orientations, with a takeover authored for one of them; a device rotating mid-show; a DemoStation’s picture-in-picture as a demo starts and ends; schedule rows on retired slots, where the scenario also lists the warnings the Loader must give; and alerts — cutting a takeover, never scoped to a day, and behind the orientation gate.

The reference engine in engine/ passes every scenario:

node conformance/run.mjs --engine engine/dist/node.js

11.2 Checklist

Artifacts

Session boards (§5.15)

Compatibility

Time

What is on screen

Provisioning

Media


12. Worked example

A test Show, trimmed and with identifiers replaced.

cartridge_meta   cartridge_kind 'surface', format_version '25.0.1',
                 project_code SHOW26, surface_id LOBBY3,
                 timezone America/Los_Angeles
project_days     1 row: 2026-09-15, 00:00:00.000–23:59:59.999 PT
surface_location 1 row: LOBBY3-A
surface_schedule_entry  1 row: slot 'playlist', Day 1 00:00 PT → playlist "Editor parity"
playlist_entry   12 rows
directive        3 rows, all on entry 5 (position 3):
                   standard ON  08:00 PT
                   takeover ON  12:00 PT
                   takeover OFF 12:30 PT

A Surface in New York, the week before the Show, at 19:30 ET.

  1. Show clock — real time is before the event, so the Surface simulates Day 1 at the venue’s current time of day: 16:30 PT on 2026-09-15 (§8.2).
  2. Orientation — the Surface drives a portrait display and is set to Automatic, so it renders portrait (§6). It takes the only location, LOBBY3-A, for its reports.
  3. Schedule — the playlist entry at 00:00 is the latest ≤ 16:30 → “Editor parity”. A landscape device of the same config resolves the same entry; only the files it plays differ.
  4. Viability — only entry 5 has directives. Its governing standard directive (08:00) is ON; its governing takeover directive (12:30) is OFF. The standard set is {entry 5}. The other 11 entries have no ON directive and never appear on a Surface — even though Studio’s operator player plays all 12. (Studio warns about this at publish.)
  5. Rotation — the takeover set is empty, so standard governs, and entry 5 repeats.
  6. File — entry 5’s item has both orientations; portrait resolves to a 10 s H.264 portrait clip. It has no optimized rendition, so the master is its original; a device set to prefer WiFi renditions takes its wifiOptimized HEVC rendition instead.
  7. Duration — no trim, so the clip plays to its end: 10 s.

A Surface at the venue on Day 1, from 11:59:55 PT.


13. Glossary

Term Meaning
Show The authored event; the same thing as the project
Surface Any device that conforms to this specification
cartridge A delivered, self-contained, immutable SQLite artifact
project cartridge project.db — Show identity, days, wallpapers
surface cartridge <SURFACECODE>.db — one surface config’s schedule and content
show code / projectCode The Show’s public identifier and media keyspace root
surface code Names the surface cartridge; PROJECT is reserved
location One physical installation of a surface config, by location_id; carries no orientation (§6)
session board template The package a Surface renders a board with (§5.15, TEMPLATE.md); the Show’s, a set’s, or the Surface’s built-in default
board variant Which of a template’s layouts a sign shows: now / next, the schedule, or both — the device’s setting (§5.15)
lane One orientation over the one schedule: the files a device rendering it plays. A Surface renders its own orientation’s lane, a DemoStation’s picture-in-picture the opposite one (§5.1, §7.7)
slot playlist | demo_station, the two kinds of schedule entry (§4.4). An item’s portrait_file_id and landscape_file_id are its orientation slots (§5.7)
orientation rendered The orientation whose files play: the device’s own, or the opposite one in a DemoStation’s picture-in-picture (§5.1)
picture-in-picture Where a DemoStation’s player plays during a demo: the same playlist, in the opposite orientation (§5.11)
directive A timestamped on/off state change for one playlist entry, of type standard, takeover or alert
takeover A directive type that, while ON for any entry, replaces the standard rotation
alert A directive type that, while ON for any entry, replaces both the takeover and the standard rotation; never scoped to a day
working set The alert set if non-empty, else the takeover set if non-empty, else the standard set
cursor The authored position of the last entry shown, per set
show clock Synthetic venue time used for every decision and for the render marker
monotonic clock The device’s never-stepping clock, used only to detect show-clock jumps
interrupt One of the closed set of events that end content before its duration (§5.9)
backing / overlay Media composited behind / in front of content
rendition One encoding of a media file — original, optimized, web, WiFi
tier A rendition’s quality class: master › wifiOptimized › webOptimized
deliverable The manifest’s default file for a media file
wire format A record that reads a delivered artifact; additive changes only

14. Changes from the pre-v25 format

Area Pre-v25 v25.0.1
Producers Swift Studio and legacy web Studio Studio only; the legacy web Studio is a separate product
Names screen_config, screen_location, screen_schedule_entry, screen_id, screen code surface_*, surface_id, surface code
Identification Project cartridge identified by the absence of cartridge_meta; grdb_migrations carried cartridge_meta in both, with cartridge_kind and format_version; no grdb_migrations
Media homes Primary plus cloud_media_base_url fallback One home
Integrity Hash and size optional; hash-less files admitted and counted Hash and size required on every file and rendition
Deliverable names In media_file and the manifest In the manifest and renditions only
Renditions Optional table Always present
Locations May be empty At least one
Schedule One per orientation: portrait and landscape slots, each scheduling its own playlists One schedule per surface config (playlist): a device plays it with the files of its orientation (§5.1)
Location orientation screen_location.orientation, the mount, adopted by a Surface Not carried: a device renders its own orientation (§6)
Studio runtime modifiers Carried, not honoured Not carried
Server bookkeeping last_checked_at, last_pulled_revision, revision, archived carried Not carried
Viability Entry must have an ON directive (implicit) Stated: an ON directive is required
Rotation Last entry by identity; restart at top when missing Position cursors per set
Takeover with no playable media Falls back to standard Takeover owns the screen; entries with an empty slot for this orientation are excluded
Orientation Empty slot falls back to the other orientation No fallback: an empty slot excludes the entry; the slot’s file plays as authored
Durations Not specified One render marker on synthetic time, armed at first frame from the render item’s hint
Interrupts Not specified Closed set; standard → takeover and any change to the alert set cut immediately
Alerts Not specified alert directives outrank takeovers, are never day-scoped, and cut at their activation (§5.3 – §5.6, §5.9)
Synthetic time Optional; operator’s wall clock projected onto Day 1 Required; venue time of day on Day 1
Session boards Optional Required
Session board layout Built in per platform; session_set.render_modes chose the modes A session board template (§5.15) — the Show’s, a set’s, or the built-in default — rendered from a data document; which layouts a sign shows is the device’s board variant; render_modes and schedule_template removed (2026-10)
Project-only clock Drawn by each platform The clock layout of the Show’s template (carried in project.db), else the default template’s (§5.15, 2026-10)
DemoStation entries May carry a PIP playlist Branding only; the picture-in-picture is the Surface’s player, moved: the same playlist in the opposite orientation, its rotation continuing (§5.11)
Composition Backing and overlay on playlists and media items Project default backing (project.backing_item_id, new), session board override; playlist and item backing/overlay removed
Branding v5/v6 columns, undocumented The session board template’s (§9, §5.15); the style book columns of earlier v25 drafts retired (2026-10)