Docs menu
Reference

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, room and other references) are lowercase: letters, digits, ., _ and -, starting with a letter or digit, at most 128 characters. For example room-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 are mon, 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 room exists.
  • 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.recall intent names an existing scene; a cue trigger has a cueId.
  • Alert rules point at existing devices, rooms and notification channels; a stale condition has capability and key, or metric.
  • Maintenance windows point at existing alert rules, devices and rooms, and have start (once) or from and to (weekly).
  • A probe's room exists.

The license also limits what a project may contain (devices, DMX universes, timelines). See Licensing.

Top level

FieldTypeRequiredDescription
ididyesProject id. Also names the project's dashboard and monitoring folders.
namestringyesProject name.
contractstringnoContract version the project was written for, for example "0.2.43".
roomsarray of RoomnoRooms and areas.
devicesarray of DevicenoControlled devices.
scenesarray of ScenenoNamed sets of device actions.
timelinesarray of TimelinenoTimecode shows and cue lists.
dashboardsarray of DashboardnoControl surfaces.
automationsarray of AutomationnoStep lists run by triggers, buttons, listeners or by hand.
variablesarray of project variablesnoValues core keeps for the whole project: name, optional label, description, default, persist (default true). See Automations.
listenersarray of listenersnoPorts other systems send messages to. See Listeners.
mapsarray of mapsnoFloor plans with devices placed on them.
viewsarray of viewsnoEnd-user views. See End-user views.
lightingobjectnoLighting engine: groups, presets, sequences and executors.
clusterobjectnoHow a cluster shares the work: mode (standby or shared), folder placement, node health limits. See Redundancy.
unitsobjectnosystem (metric or imperial) and per-quantity overrides for how values are shown.
controls, controlColorsnoThe default dashboard control style and the highlight colours of each style.
macrosarraynoDeprecated since contract 0.2.30: read and moved into automations when the project loads.
alertRulesarray of AlertRulenoMonitoring rules.
notificationChannelsarray of NotificationChannelnoWhere alerts are sent.
maintenanceWindowsarray of MaintenanceWindownoPeriods when notifications are muted.
probesarray of ProbenoNetwork 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

FieldTypeRequiredDescription
ididyesRoom id.
namestringyesDisplay name.
tagsarray of stringsnoFree 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).

FieldTypeRequiredDescription
ididyesDevice id.
namestringyesDisplay name.
moduleIdidyesThe module that drives it, for example misc.pjlink.projector.
configobjectnoSettings, checked against the module's settings schema (configSchema) when changed through the API or tangle.
roomidnoRoom it belongs to.
tagsarray of stringsnoLabels; intents can target a tag ({ "kind": "tag", "tag": "wall" }).
nodeidnoRedundancy: 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.
runsOnidnoShared mode: the node a networked device prefers. It moves to another node while this one is down.
folderstringnoFolder in the device list, /-separated (Stage/Truss 1).
plannedbooleannotrue: a placeholder whose address may not be known yet. Core does not start it.
parentidnoThe device this one is a sub-device of (a light under its DMX output).
setupobjectnoProgress of the device's setup assistant. Never holds secrets.
  • Secret settings (marked x-secret in the module) are not stored in the project file: they go to the OS credential store when set through the designer, the API or tangle device add. Set one by hand with tangle 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 universe setting; 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.

FieldTypeRequiredDescription
ididyesScene id.
namestringyesDisplay name.
actionsarray of scene actionsyesWhat to do.

Scene action:

FieldTypeRequiredDescription
deviceIdidyesDevice.
capabilityidyesCapability, for example tangle.power, tangle.level.
actionstringyesAction of that capability, for example on, set.
paramsobjectnoAction parameters, for example { "level": 0.3 }.

Timelines

FieldTypeRequiredDescription
ididyesTimeline id.
namestringyesDisplay name.
modetimecode | cuelistyestimecode: runs freely and fires cues at their time. cuelist: waits for GO at each cue.
tracksarray of tracksyesTracks of cues.
clockclocknoTime source. Default internal. See Clocks.
frameRate24 | 25 | 29.97df | 30 | 50 | 60noFrame rate (a string).
durationMsnumber > 0noLength. 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.
loopbooleannoStart again at the end.
timecodeOutputsarraynoDevices that generate timecode following this timeline: [{ "deviceId", "offset" }]. offset is the timecode sent at position 0 (default 00:00:00:00).

Tracks

FieldTypeRequiredDescription
ididyesTrack id (unique in its timeline).
namestringyesDisplay name.
cuesarray of cuesyesCues.
mutedbooleannoA muted track fires no cues.

Cues

FieldTypeRequiredDescription
ididyesCue id.
atMsnumber >= 0yesPosition from the timeline start, in ms.
payloadobjectyesWhat the cue does (below).
namestringnoDisplay name.
preRollMsnumber >= 0noSend this many ms early to absorb device latency.
enabledbooleannoDefault true. A disabled cue does not fire.

Payloads:

kindFieldsDescription
intentintent: an intent request (name, target, params, priority)Sends an intent, for example scene.recall.
actiondeviceId, capability, action, paramsRuns one device action directly.

Clocks

kindFieldsStatus
internalnoneThe default: core's own clock.
timecodedeviceId (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).
ltcinputOlder form, not played: use timecode with an LTC input device.
mtcportOlder form, not played: use timecode with an MTC input device.
ptpdeviceId (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.
wallclocktimezone (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.

FieldTypeRequiredDescription
ididyesDashboard id; opened at /d/<id>.
namestringyesDisplay name.
pagesarray of pagesyesAt least one page.
descriptionstringnoDescription.
versioninteger >= 0noPublished version, increased by core on every publish (0 = never published).
themelight | dark | autonoColor theme.
skinconsole | lobbynoVisual skin. Missing or unknown values render as console.
accessobjectnoWho may open it (below). Default { "mode": "open" }.
roomParambooleannoRoom-relative: device references with room "$room" use the room the dashboard is opened with.
roomidnoRoom used when none is given (room-relative dashboards).
generatedobjectnoSet by the generator: { "kind": "room" | "show" | "overview", "source" }.

Access:

FieldDescription
modeopen: anyone on the network. pin: a PIN unlocks it. panels: enrolled panels (API tokens) only.
pinWrite-only, 4 to 12 digits. Core stores it as pinHash and never returns it.
pinHashSet by core.

Page:

FieldTypeRequiredDescription
ididyesPage id.
namestringyesDisplay name.
widgetsarray of widgetsyesIn reading order (used to reflow when a screen class has no layout).
layoutsobjectyesGrid 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:

FieldTypeRequiredDescription
ididyesWidget id, unique on its page.
typestringyesbutton, toggle, slider, meter, value, source, scenes, status, transport, cue, clock, label, image or group.
labelstringnoLabel.
propsobjectnoProperty name to binding. Which properties a widget reads depends on its type.
actionwidget actionnoWhat it does when used.
offActionwidget actionnoToggles: sent when turning off (action is sent when turning on).
confirmnone | confirm | holdnoCritical actions: ask first, or require press-and-hold.
parentidnoThe group widget on the same page it belongs to.
generatedobjectnoSet by the generator: { "source", "removed" }.

Bindings (props values):

kindFieldsValue
constvalueA fixed value.
statedevice, capability, keyA device state value, for example tangle.level / level.
statusdeviceA device's status.
timelinetimeline, key (status, positionMs, lastCue, nextCue)Timeline state.
systemkey (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:

kindFieldsDescription
intentname, target, paramsSends 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" }.
timelinetimeline, 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.

FieldTypeRequiredDescription
ididyesAutomation id.
namestringyesDisplay name.
stepsarraynoRun in order. actions, the name before contract 0.2.30, is still read; core saves steps.
triggersarraynoAny one starts a run. Without triggers it runs only when something runs it (a button, a listener, another automation, the API).
conditionsarraynoAll must hold when triggered or run by hand (unless run with force).
paramsarraynoInputs (name, label, default), read as {{name}}.
descriptionstringnoDescription.
folderstringnoFolder in the automation list.
enabledbooleannoDefault true. false: triggers do not start it; it still runs by hand and from other steps.
modesingle | restart | queued | parallelnoWhat 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).
continueOnErrorbooleannoKeep running the next steps when one fails (the run ends as partial).
priorityinteger 0-100noPriority 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.

FieldTypeRequiredDescription
ididyesRule id.
namestringyesDisplay name.
severityinfo | warning | criticalyesSeverity of its alerts.
conditionobjectyesWhat to watch (below).
descriptionstringnoDescription.
enabledbooleannoDefault true.
forSecondsinteger >= 0noHow long the condition must hold before the alert fires. Default 0.
clearForSecondsinteger >= 0noHow long the condition must be false before the alert resolves. Default 0.
scheduleobjectnoActive only { "days", "from", "to" } (local time; from later than to spans midnight).
channelsarray of idsnoChannels to notify. Empty or absent: every channel whose minSeverity allows it.
repeatMinutesinteger >= 1noNotify again while firing and not acknowledged.
notifyOnResolvebooleannoDefault true.
messagestringnoTemplate with {name}, {subject}, {value}, {severity}.

Conditions:

kindFieldsDescription
statusdevices, rooms, modules, statusesDevices whose status is one of statuses (default offline and error). Devices are chosen by id, room or module; all devices when none is given.
statecapability, key (required); devices, rooms, modules, above, below, equals, notEquals, clearA device state value compared to a threshold. clear is the value that resolves the alert (hysteresis).
metricname (required); labels, above, below, equals, notEquals, clearA custom or probe metric compared to a threshold, per label set.
staleseconds (required); devices, rooms, modules, capability, key, metric, labelsNo update for seconds: a device state key (capability and key) or a metric (metric and labels).
coreNodeon (down, unsynced, noPrimary); nodesA 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

FieldTypeRequiredDescription
ididyesChannel id.
namestringyesDisplay name.
kindstringyesemail, sms, push, webhook, slack or teams.
settingsobjectyesDepends on kind (below).
enabledbooleannoTurn the channel off without deleting it.
minSeverityinfo | warning | criticalnoLowest severity sent through this channel.
quietHoursobjectno{ "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 ********.

kindSettings
emailhost, from, to (list) required; port; security (starttls default on port 587, tls on 465, none on 25 for trusted networks only); username; password (secret).
smsprovider (twilio), accountSid, from (E.164 number or messaging service id), to (list) required; authToken (secret).
pushprovider (ntfy or pushover) required. ntfy: server (default https://ntfy.sh), topic, token (secret). Pushover: userKey, appToken (secret).
webhookurl required; signingKey (secret: signs the body, header X-Tangle-Signature: sha256=<hex HMAC>); headers.
slackwebhookUrl (secret, required).
teamswebhookUrl (secret, required): a Teams workflow (Power Automate) webhook.

Maintenance windows

A maintenance window mutes notifications, once or every week.

FieldTypeRequiredDescription
ididyesWindow id.
namestringyesDisplay name.
start, endtimestampsonceA single period.
days, from, toweekdays, HH:MMweeklyA weekly period (local time).
rulesarray of idsnoAlert rules it applies to.
devicesarray of idsnoDevices it applies to.
roomsarray of idsnoRooms 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>.

FieldTypeRequiredDescription
ididyesProbe id.
namestringyesDisplay name.
kindtcp | httpyesCheck type.
host, portstring, 1-65535tcpAddress to connect to.
urlstringhttpAddress to request.
expectStatusarray of status codesnoHTTP only: accepted status codes. Default 200 to 399.
roomidnoRoom it belongs to.
enabledbooleannoTurn it off without deleting it.
intervalSecondsinteger >= 5noDefault 30.
timeoutMs100 to 60000noDefault 5000.
  • tangle.lock next to the project: module versions written by tangle module lock, made active when tangle-core starts. See CLI.
  • Secrets, dashboard drafts and history, and alert history live in the Tangle data folder, not in the project. See CLI.