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.
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/andexample/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.
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.
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.
| 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. |
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.
-shm/-wal sidecars.PRAGMA foreign_key_check on every
cartridge. If yours fails, the file is damaged: do not commit it over the copy you hold.ids are preserved from the authoring database. They are stable across republishes
and safe to use as cache keys. IDs are only meaningful within one Show.INTEGER 0/1.Both artifacts carry exactly one cartridge_meta row (§4.1):
cartridge_kind is 'project' or 'surface'.format_version is the specification version the cartridge was written against, e.g.
'25.0.1'. A Surface accepts any cartridge whose format_version has the same first
component as its own (25), and applies §10 to anything newer within it. A different first
component is a different format: refuse it and say so.Authoring migration history (grdb_migrations) is not carried. It describes the author’s
database, not the wire format.
project.db — the project cartridgecartridge_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.
<SURFACECODE>.db — the surface cartridgecartridge_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.
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"
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).
cartridge_meta — both artifacts, exactly one rowCREATE 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). |
media_manifest — both artifacts, one row per media fileCREATE 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).
project, project_daysCREATE 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.
surface_config, surface_location, surface_schedule_entryCREATE 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.
playlist, playlist_entry, directiveCREATE 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.
media_item, media_fileCREATE 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.
session, session_set, session_set_entryCREATE 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.
media_file_variant — the renditions offeredCREATE 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.
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"]
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.
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;
playlist_id of NULL means show nothing from now: clear to blank. (This differs from an
empty working set, §5.5, where nothing new is produced and the last frame stays.)timestamp > showNow among the playlist
entries and, on a Surface that runs the DemoStation mode, the demo_station entries. It is an
interrupt time (§5.9).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.
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.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.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.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.
A directive is a state change, not a window. For one entry, one type, at showNow:
project_days row where showNow BETWEEN start_time AND end_time.timestamp ≤ showNow.timestamp falls inside that day.on_screen is 1.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.
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.
position order; so do several active takeovers.alert directive ON at timestamp 0. The new cartridge
cuts at once (§5.9), on every day of the show and outside it (§5.4, §8.2). An alert is
self-providing, like a takeover (below), and it passes the orientation gate like any entry: an
alert meant for every device carries a file in both slots (§5.7). A consumer that predates the
alert type skips those rows (§10.4) and never shows it.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.
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).
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. |
end > start.session_set.duration × pageCount: each page gets the full duration.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.
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 |
demo_station slot and the picture-in-pictureA 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:
playlist entries carry only a playlist (or NULL, an authored blank) — never branding.demo_station entries carry branding only: background_item_id and an optional
overlay_item_id. They never carry a playlist.demo_station entry with a background means demo on; an all-NULL entry means demo off.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:
demo_station boundary that leaves the demo on changes only the branding. The item on screen
plays on.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.
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.
image/* you can decode as an image and any video/* you can decode as a video.
Do not hard-code an allow-list.codec (§7.6).media.not_playable.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. |
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.
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:
surface_location rows.label (else location_id), or take the only one.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.
<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.
Every manifest row and every rendition carries file_size and content_hash. Verify every file
before use:
file_size → reject as a truncated transfer; retry.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:
ETag beside the partial, and write it before the bytes it describes.Keep partials outside the directory your renderer resolves media from.
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).
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.
A Surface may download a rendition in place of the manifest’s default deliverable. The first one it can decode wins:
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.optimized when the file has one, else original.webOptimized.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:
codec. A video rendition without a codec is not known to be decodable.Play what you hold. The order above is what to fetch when you hold nothing for a file.
optimized, original) › wifiOptimized › webOptimized.
A browser Surface ranks webOptimized first.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:
demo_station slot has any entry with a background — the picture-in-picture plays the
same playlist in that orientation — and the demo branding in the orientation the Surface renders.
A Surface without the mode never draws the branding, so an item used only as demo branding — a
demo_station entry’s background or overlay that no playlist entry, backing, logo or wallpaper
names — is in none of its lanes.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.
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.
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
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.
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.
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.
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.
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:
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).
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.
If a table fails to decode, say so, distinctly from “this table has no rows”. They are different facts and lead to opposite investigations.
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).
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.
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
Artifacts
cartridge_meta.cartridge_kind, and refuses a different
format_version first component.(published_revision, generated_at),
and logs the refusal.Session boards (§5.15)
both as now / next then the schedule’s
pages, and falls to what the template offers with template.variant_unavailable.template_settings.vars through to the data document untouched; takes modifiers from
the device’s settings (TEMPLATE.md).variant.change when the variant changes, and nothing else.clock layout, else
the default template’s, over the wallpaper, in the ink its measurement of the wallpaper chooses.Compatibility
surface_location.orientation without a warning (§4.4).portrait, landscape) as malformed: skipped with
a warning, never read as a playlist entry (§4.4).Time
≥ evaluates the next content; 0 forces the next loop, whatever the show time, including before 1970 (negative milliseconds).What is on screen
playlist schedule as latest-timestamp-≤-now, whatever the orientation
it renders; treats a NULL playlist_id as show nothing.id; no playlist entry yet holds; one trace event per cause).Provisioning
location_id; re-picks when it leaves the cartridge; clears it when the address
changes.Media
codec, verifies each against its own hash and size, and renders and
prunes from the same decision.webOptimized first.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.
LOBBY3-A, for its reports.optimized rendition, so the master is its original; a device set to prefer WiFi
renditions takes its wifiOptimized HEVC rendition instead.A Surface at the venue on Day 1, from 11:59:55 PT.
| 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 |
| 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) |