Concepts
The building blocks of a Tangle project - rooms, devices, modules, capabilities, state, intents, scenes, timelines, automations, variables, listeners, dashboards and core nodes.
This page explains the words used across Tangle and how the pieces relate. Every part of Tangle (core, modules, dashboards, the designer, the API) shares one set of definitions, the Tangle contract, written as JSON Schema. The current contract version is 0.2.43.
Project
A project is everything core needs to run one site or one show, in one file (JSON or YAML). It holds:
| Section | Contents |
|---|---|
id, name | Required. The id is used in folder names and dashboard storage. |
rooms | Places that group devices |
devices | Configured devices |
scenes | Sets of device actions recalled together |
timelines | Shows: tracks of timed cues, or cue lists |
automations | Step lists run by triggers, buttons, listeners or by hand |
variables | Values core keeps for the whole project |
listeners | Ports other systems send their own messages to |
dashboards | Published control surfaces |
maps, views | Floor plans with devices, and the menus end users open |
lighting | Lighting engine: groups, presets, sequences, executors |
cluster, units, controls | How a cluster shares the work, how values are shown, the default control style |
alertRules, notificationChannels, maintenanceWindows, probes | Sentinel monitoring |
The smallest valid project:
{ "id": "my-show", "name": "My show" }
Core keeps a library of projects on its computer (Open or new project… in the designer, tangle-core projects on the command line) and runs one at a time. It loads the project at start, checks it against the schema and checks references (a scene that names a missing device is an error). Changes made through the designer, dashboards, REST or MCP are checked the same way, applied live, written back to the file and recorded in audit.log. Passwords and keys never go in the file. See Project file.
Identifiers
Ids of rooms, devices, scenes, timelines, cues, dashboards and modules are lowercase: letters, digits, ., _ and -, starting with a letter or digit, up to 128 characters. room-a.projector-1 is valid; Room A is not. Times are integer milliseconds since the Unix epoch (UTC).
Rooms
A room groups devices by place: a meeting room, a gallery, the stage. A room has an id, a name and optional tags. A device belongs to at most one room. Rooms are what you target to control a whole space ("turn the Hall off"), and what room dashboards are generated from.
Devices
A device is one controlled thing in the project: a projector, a media server, a DSP, a lighting device, a computer. A device is one module plus one configuration:
{
"id": "hall-projector",
"name": "Hall projector",
"moduleId": "misc.pjlink.projector",
"room": "hall",
"tags": ["projectors"],
"config": { "host": "10.0.0.21", "port": 4352 }
}
| Field | Meaning |
|---|---|
moduleId | The module that drives it |
config | Settings, checked against the module's settings schema. Secret settings (passwords) are stored in the OS credential store, not here. |
room | Optional room id |
tags | Optional labels, used as intent targets (projectors, stage-left) |
folder | Optional folder in the device list (Stage/Truss 1) |
planned | Optional. A device not on site yet: kept in the project, not started |
node | With redundancy: the core node the device's hardware is attached to (serial port, USB DMX, LTC output). Omit for networked devices. |
Status
Every device reports a connection status:
| Status | Meaning |
|---|---|
connecting | First connection in progress |
online | Connected and answering |
degraded | Answering, but reporting a problem |
offline | Unreachable, refused or disconnected |
timeout | Reachable or connected, but not answering in time |
error | Needs the operator, for example a bad password or a missing setting |
See Devices.
Modules
A device module is the driver for a kind of device. It runs as its own process, started and supervised by core, and talks to core over a simple JSON protocol on standard input and output. A crash affects only that module: core restarts it with a backoff. Core also limits every action to 15 seconds.
Module ids follow <manufacturer or misc>.<product or protocol>.<category>[.<version>], for example misc.pjlink.projector, qsc.core.dsp or dataton.watchout.mediaserver.v7. Categories include timecode, projector, display, mediaserver, engine, player, computer, io, dsp, lighting, console, switcher, control, generic, gateway, network, microphone, camera, codec, presentation, avoip, routing, amplifier, building, power and scheduling, plus listener and library for modules that open listeners or add functions.
Each module ships a manifest (tangle-module.json) that declares:
- the runtime: Node.js, Python or a native executable;
- the capabilities its devices support, standard and custom;
- the settings schema, which the designer turns into a form;
- the permissions it needs (network, serial, USB, MIDI, file system);
- optionally, network discovery, ready-made dashboard controls and layouts, setup assistants for the device side, documentation links and several connections (LAN and RS-232, say).
Modules are installed from the designer's Driver library, or with tangle module add, and written with the module SDK or, without code, with the device creator. See the device catalog for the official modules.
Capabilities
A capability is a reusable set of state keys and actions. Capabilities let core, scenes and dashboards control any device the same way, whatever its protocol: every device with tangle.power turns on with the same action.
Standard capabilities:
| Capability | State | Actions |
|---|---|---|
tangle.power | on (boolean) | on, off, toggle |
tangle.level | level (0 to 1) | set (level, optional fadeMs) |
tangle.mute | muted (boolean) | mute, unmute, toggle |
tangle.input | input (string) | select (input) |
tangle.transport | status (stopped, playing, paused), positionMs, clip | play, pause, stop, seek (positionMs), load (clip) |
tangle.preset | current (string) | recall (preset) |
tangle.timecode | timecode, fps, running, signal | start, stop, locate (timecode) |
tangle.playhead | timecode, positionMs, durationMs, running, rate, fps, playheads | none: where a media player or server is in time, one playhead per timeline for media servers. Timelines can chase it. |
tangle.preview | streamUrl, snapshotUrl, available | none: a picture of what the device shows, for Camera controls |
tangle.setup | needed, configurator, reason | none: the device answers but is not set up for Tangle yet; the designer offers its setup assistant |
A module can add custom capabilities under its own prefix. The PJLink module adds pjlink.lamp (lamp hours), pjlink.status (power including warming and cooling) and pjlink.health (fan, lamp, temperature, cover, filter errors).
State values and action parameters are typed: boolean, number (with optional min, max, step and unit), string, enum or JSON. The designer uses these types to build its pickers, for example a number field limited to 0 to 1 for tangle.level set.
State
State is what a device reports right now, per capability: tangle.power on: true, tangle.level level: 0.8, pjlink.lamp hours: 1790. Modules push state as it changes or as they poll. Core keeps the latest state of every device and sends changes to clients. Dashboards display state through bindings; automations trigger on it and test it in conditions.
Actions
An action is one concrete command for one device: device, capability, action name and parameters.
{ "deviceId": "hall-projector", "capability": "tangle.input", "action": "select", "params": { "input": "31" } }
Scenes are lists of actions. Timeline cues and automations can run an action directly or send an intent.
Events
Core publishes events when something changes. Each event has a topic <type>:<subject id>, and clients subscribe to the topics they need (wildcards such as device.state:* are allowed).
| Event | Topic | When |
|---|---|---|
device.state | device.state:<deviceId> | A state value changed |
device.status | device.status:<deviceId> | A device's status changed |
device.event | device.event:<deviceId> | A device reported something, for example a button press or an alarm |
intent.status | intent.status:<intentId> | An intent progressed |
timeline.state | timeline.state:<timelineId> | A timeline played, paused, moved or reached a cue |
node.heartbeat | node.heartbeat:<nodeId> | A core node reported its health |
project.changed | project.changed:<projectId> | The project was changed (with a revision number and the list of changes) |
module.status, module.health | module.status:<moduleId> | A module started, crashed or restarted; resource use and latency every 10 s |
device.action | device.action:<deviceId> | An action was sent, is in progress, done or failed |
device.log | device.log:<deviceId> | A device log entry was added |
automation.run | automation.run:<automationId> | An automation run started or ended |
variable.changed | variable.changed:<name> | A project variable changed |
control.changed, control.event | control.changed:<controlId>, control.event:<controlId> | A dashboard control's value or properties changed, or it was clicked, pressed, released or held |
listener.message | listener.message:<listenerId> | A listener received a message |
dashboard.draft | dashboard.draft:<dashboardId> | A dashboard draft was saved (other designer windows refresh) |
sentinel.alert | sentinel.alert:<ruleId> | An alert changed or a notification was sent |
license.status | license.status:core | The license changed, or a session paused or resumed |
See WebSocket.
Intents
An intent says what should happen, not how: "power on the Hall", "recall Presentation in room A". Core resolves the target to devices, turns the intent into actions for the devices that support it, and reports the outcome. Dashboards, timelines, automations and API clients all control devices through intents.
{
"name": "scene.recall",
"target": { "kind": "room", "id": "hall" },
"params": { "scene": "presentation" },
"priority": 30
}
Intent names
| Name | Does |
|---|---|
<capability>.<action> | A standard capability's action on every targeted device that has it: power.on, power.off, level.set, input.select, transport.play, preset.recall. mute.on and mute.off are short for mute.mute and mute.unmute. |
scene.recall | The actions of the scene in params.scene, for the targeted devices only |
action | One raw action: params.capability, params.action, params.params |
release | Drops priority holds on the targeted devices |
Devices without the capability are skipped and reported, not treated as errors.
Target kinds
| Kind | Example | Covers |
|---|---|---|
device | { "kind": "device", "id": "hall-projector" } | One device |
room | { "kind": "room", "id": "hall" } | Every device in the room |
tag | { "kind": "tag", "tag": "projectors" } | Every device with the tag |
all | { "kind": "all" } | Every device in the project |
Priority and holds
Each intent has a priority from 0 to 100. When two intents act on the same device state and overlap, the higher priority wins and the other reports the device as overridden. Defaults depend on the source: system 90, timeline, schedule and automation 50, everything else (users, dashboards, API) 30.
- The
holdMsparameter keeps the intent's priority on the state it set for that long. - An intent with priority 90 or more holds the state until a
releaseintent.
An intent can also carry at (run at a time) and expiresAt.
Intent status
An intent goes through accepted, scheduled, running, then ends completed, partial, failed, rejected or cancelled. Each device gets an outcome: ok, failed, overridden or skipped. A room intent where one of three devices is offline ends partial and names the offline device.
Scenes
A scene is a named set of device actions recalled together: "Presentation" turns the projector on, selects HDMI 1 and dims the lights.
{
"id": "presentation",
"name": "Presentation",
"actions": [
{ "deviceId": "hall-projector", "capability": "tangle.power", "action": "on" },
{ "deviceId": "hall-projector", "capability": "tangle.input", "action": "select", "params": { "input": "31" } }
]
}
Recall a scene with the scene.recall intent. Only the actions for devices inside the intent's target run, so one scene can be recalled for a whole site or for one room.
Timelines
A timeline is a show: tracks of cues, each cue at a position in milliseconds. A cue sends an intent or runs a device action, can fire early by a pre-roll to absorb device latency, and can be disabled.
| Setting | Values |
|---|---|
mode | timecode: runs freely from play. cuelist: waits for GO at each cue. |
clock | internal, timecode to chase a timecode input device (LTC, MTC, Art-Net timecode), wallclock to follow the time of day, or ptp to run at a PTP grandmaster's rate |
frameRate | 24, 25, 29.97df, 30, 50, 60 |
timecodeOutputs | Timecode output devices that follow the timeline |
durationMs, loop | Length and looping |
Transport operations are play, pause, stop, seek and go. A timeline's status is stopped, playing, paused or waiting (a cue list waiting for GO, or a chasing timeline waiting for timecode).
Note: The older
ltcandmtcclock forms are not played: use atimecodeclock with an input device.
See Timelines.
Automations
An automation is a named list of steps, run when one of its triggers fires and its conditions hold, or whenever something runs it: a dashboard button, a listener, another automation, the API.
- Triggers (any one starts a run): time of day, device state, device event, device status, timeline (started, stopped, cue reached), project variable, dashboard control, listener message, core start, only by hand.
- Conditions (all must hold): device state, device status, time window, timeline status, variable, dashboard control.
- Steps (in order): intent, device action (optionally waiting until the device reports it is done), timeline, wait, wait for a condition, if, repeat, while, set a variable, set a control, run another automation, call a function, stop, comment, log.
- Params pass values in (a slider's level), and mode decides what a trigger does while a run is in progress:
single(ignore),restart,queuedorparallel.
Automations run on the primary core only. Macros, from older projects, are automations now. See Automations.
Variables and expressions
A project variable is a value core keeps for the whole project, such as houseMode = "show" or visitors = 12. Automations, listeners, dashboards and the API read and set it, and automations can start when it changes. Steps, bindings and labels read variables and params as {{name}}, and compute values with expressions such as clamp({{level}} * 1.2, 0, 100). See Automations.
Listeners
A listener opens a port (WebSocket, HTTP, TCP, UDP, OSC) or a serial line for other systems, which send their own messages. Rules match each message and set variables, run steps and automations, and answer. See Listeners.
Dashboards
A dashboard is a control surface: pages of controls (buttons, toggles, sliders, keypads, scene lists, status, transport, GO, cameras, maps…) laid out on a grid per screen size. Controls read device state, variables, other controls or expressions through bindings, and send intents, timeline commands, run automations or set variables. Each dashboard has a skin (Console or Lobby), a control style (Classic, Studio, Skeuomorphism or Relief), a theme (light, dark or auto) and an access mode (anyone, PIN, or enrolled panels). Screens open it at /d/<id>. See Dashboards.
Core nodes and clusters
A core node is one running tangle-core. Each node has an id (default core-1) and a role:
| Role | Meaning |
|---|---|
standalone | A single core, no redundancy |
primary | In a cluster, the node that drives devices, runs automations and Sentinel |
backup | In a cluster, computes the same show in lockstep, ready to take over |
For redundancy, two or more nodes run the same project and exchange heartbeats over port 7341. Every node plays timelines on a shared cluster clock; only the primary drives devices. If the primary stops, the next node takes over within a second at the same point in the show. In shared mode, devices are spread across the healthy nodes instead, while the primary still runs timelines and automations. Each failover increases an epoch counter, and the highest epoch wins, so two nodes never both stay primary. Backups compare a log of the cues they computed with the primary's and resync if they differ. With three or more nodes, a node is primary only while it reaches a majority.
See Redundancy.