Project file
The Tangle project file format in JSON or YAML - rooms, devices, scenes, timelines, dashboards, automations, variables, listeners and Sentinel monitoring.
A Tangle project is one file that holds everything tangle-core runs: rooms, devices, scenes, timelines, dashboards, automations and monitoring. The designer, the REST API, MCP and the tangle command all edit this file; you can also write it by hand. This page describes every section and field.
Format and loading
- The file is JSON (
.json) or YAML (.yaml,.yml). The format follows the extension. Changes made through the API are saved back in the same format. - Field names are camelCase. Unknown fields are refused.
- Ids (
id,deviceId,roomand other references) are lowercase: letters, digits,.,_and-, starting with a letter or digit, at most 128 characters. For exampleroom-a,room-a.projector-1. - Timestamps are milliseconds since the Unix epoch (UTC). Times of day are
"HH:MM"(24 h) in the core machine's local time. Weekdays aremon,tue,wed,thu,fri,sat,sun.
Core checks a project in two steps: first against the contract JSON Schema (Project), then the cross-references the schema cannot express. Every error names the place to fix. Check a file without running it:
tangle-core check --project show.yaml
error: project is invalid:
scene presentation action 2: unknown device light-3
Cross-reference checks:
- Ids are unique within rooms, devices, scenes, timelines, automations, alert rules, notification channels, maintenance windows and probes.
- A device's
roomexists. - Scene actions point at existing devices.
- Cues point at existing devices (action cues) or existing rooms and devices (intent cue targets).
- Automation triggers, conditions and actions point at existing devices, timelines and cues; a
scene.recallintent names an existing scene; a cue trigger has acueId. - Alert rules point at existing devices, rooms and notification channels; a
stalecondition hascapabilityandkey, ormetric. - Maintenance windows point at existing alert rules, devices and rooms, and have
start(once) orfromandto(weekly). - A probe's
roomexists.
The license also limits what a project may contain (devices, DMX universes, timelines). See Licensing.
Top level
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Project id. Also names the project's dashboard and monitoring folders. |
name | string | yes | Project name. |
contract | string | no | Contract version the project was written for, for example "0.2.43". |
rooms | array of Room | no | Rooms and areas. |
devices | array of Device | no | Controlled devices. |
scenes | array of Scene | no | Named sets of device actions. |
timelines | array of Timeline | no | Timecode shows and cue lists. |
dashboards | array of Dashboard | no | Control surfaces. |
automations | array of Automation | no | Step lists run by triggers, buttons, listeners or by hand. |
variables | array of project variables | no | Values core keeps for the whole project: name, optional label, description, default, persist (default true). See Automations. |
listeners | array of listeners | no | Ports other systems send messages to. See Listeners. |
maps | array of maps | no | Floor plans with devices placed on them. |
views | array of views | no | End-user views. See End-user views. |
lighting | object | no | Lighting engine: groups, presets, sequences and executors. |
cluster | object | no | How a cluster shares the work: mode (standby or shared), folder placement, node health limits. See Redundancy. |
units | object | no | system (metric or imperial) and per-quantity overrides for how values are shown. |
controls, controlColors | no | The default dashboard control style and the highlight colours of each style. | |
macros | array | no | Deprecated since contract 0.2.30: read and moved into automations when the project loads. |
alertRules | array of AlertRule | no | Monitoring rules. |
notificationChannels | array of NotificationChannel | no | Where alerts are sent. |
maintenanceWindows | array of MaintenanceWindow | no | Periods when notifications are muted. |
probes | array of Probe | no | Network checks for assets without a device module. |
Complete example
A meeting room with a projector and two lights, a scene, a short cue list, a dashboard, an automation and a monitoring rule:
{
"id": "demo",
"name": "Demo building",
"contract": "0.2.43",
"rooms": [{ "id": "room-a", "name": "Room A" }],
"devices": [
{ "id": "proj-1", "name": "Projector", "moduleId": "misc.pjlink.projector", "room": "room-a", "config": { "host": "10.0.0.20" } },
{ "id": "light-1", "name": "Ceiling light", "moduleId": "tangle.virtual-light-js", "room": "room-a" },
{ "id": "light-2", "name": "Wall light", "moduleId": "tangle.virtual-light-py", "room": "room-a", "tags": ["wall"] }
],
"scenes": [
{
"id": "presentation",
"name": "Presentation",
"actions": [
{ "deviceId": "proj-1", "capability": "tangle.power", "action": "on" },
{ "deviceId": "light-1", "capability": "tangle.level", "action": "set", "params": { "level": 0.3 } },
{ "deviceId": "light-2", "capability": "tangle.power", "action": "off" }
]
}
],
"timelines": [
{
"id": "welcome",
"name": "Welcome",
"mode": "cuelist",
"tracks": [
{
"id": "main",
"name": "Main",
"cues": [
{ "id": "c1", "name": "House to half", "atMs": 0, "payload": { "kind": "intent", "intent": { "name": "level.set", "target": { "kind": "room", "id": "room-a" }, "params": { "level": 0.5 } } } },
{ "id": "c2", "name": "Presentation", "atMs": 5000, "payload": { "kind": "intent", "intent": { "name": "scene.recall", "target": { "kind": "room", "id": "room-a" }, "params": { "scene": "presentation" } } } }
]
}
]
}
],
"dashboards": [
{
"id": "room-a",
"name": "Room A",
"access": { "mode": "open" },
"pages": [
{
"id": "main",
"name": "Main",
"widgets": [
{
"id": "light",
"type": "toggle",
"label": "Ceiling light",
"props": { "value": { "kind": "state", "device": "light-1", "capability": "tangle.power", "key": "on" } },
"action": { "kind": "intent", "name": "power.on", "target": { "kind": "device", "device": "light-1" } },
"offAction": { "kind": "intent", "name": "power.off", "target": { "kind": "device", "device": "light-1" } }
}
],
"layouts": { "tablet": [{ "widget": "light", "x": 0, "y": 0, "w": 3, "h": 2 }] }
}
]
}
],
"automations": [
{
"id": "evening-off",
"name": "Lights off at night",
"triggers": [{ "kind": "time", "at": "22:00" }],
"steps": [{ "kind": "intent", "intent": { "name": "power.off", "target": { "kind": "room", "id": "room-a" } } }]
}
],
"alertRules": [
{ "id": "offline", "name": "Device offline", "severity": "critical", "condition": { "kind": "status", "rooms": ["room-a"] }, "forSeconds": 30, "channels": ["techs"] }
],
"notificationChannels": [
{ "id": "techs", "name": "Technicians", "kind": "webhook", "settings": { "url": "https://ops.example.com/tangle" } }
],
"maintenanceWindows": [{ "id": "sunday", "name": "Sunday", "days": ["sun"], "from": "02:00", "to": "04:00" }],
"probes": [{ "id": "switch-1", "name": "Core switch", "kind": "tcp", "host": "10.0.0.2", "port": 22 }]
}
The same project start in YAML:
id: demo
name: Demo building
rooms:
- id: room-a
name: Room A
devices:
- id: proj-1
name: Projector
moduleId: misc.pjlink.projector
room: room-a
config:
host: 10.0.0.20
scenes:
- id: presentation
name: Presentation
actions:
- deviceId: proj-1
capability: tangle.power
action: "on"
Note: in YAML, quote values such as
"on","off","yes"and"no", or YAML reads them as booleans.
Rooms
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Room id. |
name | string | yes | Display name. |
tags | array of strings | no | Free labels. |
An intent with target { "kind": "room", "id": "room-a" } reaches every device whose room is room-a.
Devices
Each device is one module plus one configuration (contract DeviceInstance).
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Device id. |
name | string | yes | Display name. |
moduleId | id | yes | The module that drives it, for example misc.pjlink.projector. |
config | object | no | Settings, checked against the module's settings schema (configSchema) when changed through the API or tangle. |
room | id | no | Room it belongs to. |
tags | array of strings | no | Labels; intents can target a tag ({ "kind": "tag", "tag": "wall" }). |
node | id | no | Redundancy: the core node its hardware is attached to (serial port, USB DMX, LTC output). Only that node runs its module. Leave it out for networked devices. See Redundancy. |
runsOn | id | no | Shared mode: the node a networked device prefers. It moves to another node while this one is down. |
folder | string | no | Folder in the device list, /-separated (Stage/Truss 1). |
planned | boolean | no | true: a placeholder whose address may not be known yet. Core does not start it. |
parent | id | no | The device this one is a sub-device of (a light under its DMX output). |
setup | object | no | Progress of the device's setup assistant. Never holds secrets. |
- Secret settings (marked
x-secretin the module) are not stored in the project file: they go to the OS credential store when set through the designer, the API ortangle device add. Set one by hand withtangle device secret <device> <setting>. - A device whose module is not installed is accepted; its status shows the problem.
- DMX modules put the universe a device uses in a
universesetting; licensing counts distinct universes.
Every device reports a status: connecting, online, degraded, offline, timeout or error. See Devices.
Scenes
A scene is recalled with the scene.recall intent (params.scene). Only the actions for devices inside the intent's target run.
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Scene id. |
name | string | yes | Display name. |
actions | array of scene actions | yes | What to do. |
Scene action:
| Field | Type | Required | Description |
|---|---|---|---|
deviceId | id | yes | Device. |
capability | id | yes | Capability, for example tangle.power, tangle.level. |
action | string | yes | Action of that capability, for example on, set. |
params | object | no | Action parameters, for example { "level": 0.3 }. |
Timelines
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Timeline id. |
name | string | yes | Display name. |
mode | timecode | cuelist | yes | timecode: runs freely and fires cues at their time. cuelist: waits for GO at each cue. |
tracks | array of tracks | yes | Tracks of cues. |
clock | clock | no | Time source. Default internal. See Clocks. |
frameRate | 24 | 25 | 29.97df | 30 | 50 | 60 | no | Frame rate (a string). |
durationMs | number > 0 | no | Length. At the end the timeline stops, or starts again when loop is true. Without it, the timeline keeps playing after its last cue until stopped. |
loop | boolean | no | Start again at the end. |
timecodeOutputs | array | no | Devices that generate timecode following this timeline: [{ "deviceId", "offset" }]. offset is the timecode sent at position 0 (default 00:00:00:00). |
Tracks
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Track id (unique in its timeline). |
name | string | yes | Display name. |
cues | array of cues | yes | Cues. |
muted | boolean | no | A muted track fires no cues. |
Cues
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Cue id. |
atMs | number >= 0 | yes | Position from the timeline start, in ms. |
payload | object | yes | What the cue does (below). |
name | string | no | Display name. |
preRollMs | number >= 0 | no | Send this many ms early to absorb device latency. |
enabled | boolean | no | Default true. A disabled cue does not fire. |
Payloads:
kind | Fields | Description |
|---|---|---|
intent | intent: an intent request (name, target, params, priority) | Sends an intent, for example scene.recall. |
action | deviceId, capability, action, params | Runs one device action directly. |
Clocks
kind | Fields | Status |
|---|---|---|
internal | none | The default: core's own clock. |
timecode | deviceId (required), offset (timecode at position 0, default 00:00:00:00), freewheelMs (0 to 10000, default 500) | Chases a timecode input device (any device with the tangle.timecode capability: LTC, MTC, Art-Net timecode). |
ltc | input | Older form, not played: use timecode with an LTC input device. |
mtc | port | Older form, not played: use timecode with an MTC input device. |
ptp | deviceId (PTP clock reference device), domain (0 to 127, default 0, used when deviceId is not set) | Plays like internal, at the rate of the PTP grandmaster. |
wallclock | timezone (IANA zone, empty: the computer's zone) | The position is the time of day; restarts from 0 at midnight. |
Timecode values are HH:MM:SS:FF, with ; before the frames for drop-frame. See Timelines.
Dashboards
The project holds the published version of each dashboard. Drafts and older versions are kept by tangle-core outside the project file. See Dashboards.
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Dashboard id; opened at /d/<id>. |
name | string | yes | Display name. |
pages | array of pages | yes | At least one page. |
description | string | no | Description. |
version | integer >= 0 | no | Published version, increased by core on every publish (0 = never published). |
theme | light | dark | auto | no | Color theme. |
skin | console | lobby | no | Visual skin. Missing or unknown values render as console. |
access | object | no | Who may open it (below). Default { "mode": "open" }. |
roomParam | boolean | no | Room-relative: device references with room "$room" use the room the dashboard is opened with. |
room | id | no | Room used when none is given (room-relative dashboards). |
generated | object | no | Set by the generator: { "kind": "room" | "show" | "overview", "source" }. |
Access:
| Field | Description |
|---|---|
mode | open: anyone on the network. pin: a PIN unlocks it. panels: enrolled panels (API tokens) only. |
pin | Write-only, 4 to 12 digits. Core stores it as pinHash and never returns it. |
pinHash | Set by core. |
Page:
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Page id. |
name | string | yes | Display name. |
widgets | array of widgets | yes | In reading order (used to reflow when a screen class has no layout). |
layouts | object | yes | Grid positions per screen class: phone, tablet, panel, desktop (may be {}). |
Screen classes: phone below 600 px wide, tablet below 1024, panel below 1440, desktop above. A layout is a list of { "widget", "x", "y", "w", "h" } on a grid of 12 columns (4 on phone).
Widget:
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Widget id, unique on its page. |
type | string | yes | button, toggle, slider, meter, value, source, scenes, status, transport, cue, clock, label, image or group. |
label | string | no | Label. |
props | object | no | Property name to binding. Which properties a widget reads depends on its type. |
action | widget action | no | What it does when used. |
offAction | widget action | no | Toggles: sent when turning off (action is sent when turning on). |
confirm | none | confirm | hold | no | Critical actions: ask first, or require press-and-hold. |
parent | id | no | The group widget on the same page it belongs to. |
generated | object | no | Set by the generator: { "source", "removed" }. |
Bindings (props values):
kind | Fields | Value |
|---|---|---|
const | value | A fixed value. |
state | device, capability, key | A device state value, for example tangle.level / level. |
status | device | A device's status. |
timeline | timeline, key (status, positionMs, lastCue, nextCue) | Timeline state. |
system | key (connection, role, nodeId, license, time) | Core and connection information. |
A device reference is a device id, or a selector among a room's devices: { "room": "<room id>" | "$room", "capability", "tag", "index" } (index: which match in project order, default 0).
Widget actions:
kind | Fields | Description |
|---|---|---|
intent | name, target, params | Sends an intent. A param value "{{value}}" (or the older "$value") is replaced by the widget's value (slider level, selected input...); "Level {{value}}" fills it in as text. target is { "kind": "device", "device" }, { "kind": "room", "id" } (id may be "$room"), { "kind": "tag", "tag" } or { "kind": "all" }. |
timeline | timeline, op (play, pause, stop, go) | Drives a timeline. |
Automations
Run by tangle-core on the primary (or a standalone node). The Automations guide lists every trigger, condition and step kind with its fields.
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Automation id. |
name | string | yes | Display name. |
steps | array | no | Run in order. actions, the name before contract 0.2.30, is still read; core saves steps. |
triggers | array | no | Any one starts a run. Without triggers it runs only when something runs it (a button, a listener, another automation, the API). |
conditions | array | no | All must hold when triggered or run by hand (unless run with force). |
params | array | no | Inputs (name, label, default), read as {{name}}. |
description | string | no | Description. |
folder | string | no | Folder in the automation list. |
enabled | boolean | no | Default true. false: triggers do not start it; it still runs by hand and from other steps. |
mode | single | restart | queued | parallel | no | What a trigger does while a run is in progress. single (default): ignored. restart: cancels the run and starts again. queued: runs after it (at most 10 waiting). parallel: runs alongside (at most 10). |
continueOnError | boolean | no | Keep running the next steps when one fails (the run ends as partial). |
priority | integer 0-100 | no | Priority of the intents it sends. |
Trigger kinds: deviceState, deviceEvent, deviceStatus, time, timeline, variable, control, listener, startup, manual. Condition kinds: state, status, time, timeline, variable, control. Step kinds: intent, action, timeline, delay, if, repeat, while, waitUntil, set, control, automation, call, stop, comment, log; every step takes disabled.
Alert rules
Monitoring (Sentinel) is part of Tangle Pro and Ultimate. See Sentinel.
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Rule id. |
name | string | yes | Display name. |
severity | info | warning | critical | yes | Severity of its alerts. |
condition | object | yes | What to watch (below). |
description | string | no | Description. |
enabled | boolean | no | Default true. |
forSeconds | integer >= 0 | no | How long the condition must hold before the alert fires. Default 0. |
clearForSeconds | integer >= 0 | no | How long the condition must be false before the alert resolves. Default 0. |
schedule | object | no | Active only { "days", "from", "to" } (local time; from later than to spans midnight). |
channels | array of ids | no | Channels to notify. Empty or absent: every channel whose minSeverity allows it. |
repeatMinutes | integer >= 1 | no | Notify again while firing and not acknowledged. |
notifyOnResolve | boolean | no | Default true. |
message | string | no | Template with {name}, {subject}, {value}, {severity}. |
Conditions:
kind | Fields | Description |
|---|---|---|
status | devices, rooms, modules, statuses | Devices whose status is one of statuses (default offline and error). Devices are chosen by id, room or module; all devices when none is given. |
state | capability, key (required); devices, rooms, modules, above, below, equals, notEquals, clear | A device state value compared to a threshold. clear is the value that resolves the alert (hysteresis). |
metric | name (required); labels, above, below, equals, notEquals, clear | A custom or probe metric compared to a threshold, per label set. |
stale | seconds (required); devices, rooms, modules, capability, key, metric, labels | No update for seconds: a device state key (capability and key) or a metric (metric and labels). |
coreNode | on (down, unsynced, noPrimary); nodes | A core node is not heard, is out of step, or the cluster has no primary. nodes limits it to some node ids. |
Metric names use letters, digits, ., _ and -, for example room.temperature. Labels are string pairs, for example { "room": "server" }.
Notification channels
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Channel id. |
name | string | yes | Display name. |
kind | string | yes | email, sms, push, webhook, slack or teams. |
settings | object | yes | Depends on kind (below). |
enabled | boolean | no | Turn the channel off without deleting it. |
minSeverity | info | warning | critical | no | Lowest severity sent through this channel. |
quietHours | object | no | { "days", "from", "to", "includeCritical" }: non-critical notifications are dropped in this window; critical ones too when includeCritical is true. |
Secret settings (marked below) go to the OS credential store when set through the designer or the API; the project keeps ********.
kind | Settings |
|---|---|
email | host, from, to (list) required; port; security (starttls default on port 587, tls on 465, none on 25 for trusted networks only); username; password (secret). |
sms | provider (twilio), accountSid, from (E.164 number or messaging service id), to (list) required; authToken (secret). |
push | provider (ntfy or pushover) required. ntfy: server (default https://ntfy.sh), topic, token (secret). Pushover: userKey, appToken (secret). |
webhook | url required; signingKey (secret: signs the body, header X-Tangle-Signature: sha256=<hex HMAC>); headers. |
slack | webhookUrl (secret, required). |
teams | webhookUrl (secret, required): a Teams workflow (Power Automate) webhook. |
Maintenance windows
A maintenance window mutes notifications, once or every week.
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Window id. |
name | string | yes | Display name. |
start, end | timestamps | once | A single period. |
days, from, to | weekdays, HH:MM | weekly | A weekly period (local time). |
rules | array of ids | no | Alert rules it applies to. |
devices | array of ids | no | Devices it applies to. |
rooms | array of ids | no | Rooms it applies to. |
With no rules, devices or rooms, it applies to everything.
Probes
A probe checks a network asset that has no device module. It produces the metrics probe.up, probe.latencyMs and, for HTTP, probe.httpStatus, labelled probe=<id>.
| Field | Type | Required | Description |
|---|---|---|---|
id | id | yes | Probe id. |
name | string | yes | Display name. |
kind | tcp | http | yes | Check type. |
host, port | string, 1-65535 | tcp | Address to connect to. |
url | string | http | Address to request. |
expectStatus | array of status codes | no | HTTP only: accepted status codes. Default 200 to 399. |
room | id | no | Room it belongs to. |
enabled | boolean | no | Turn it off without deleting it. |
intervalSeconds | integer >= 5 | no | Default 30. |
timeoutMs | 100 to 60000 | no | Default 5000. |