Docs menu
Guides

Automations

Step lists that run on triggers, from buttons, listeners or by hand - with conditions, control flow, variables, expressions, library functions, run modes and a run log.

An automation is a named list of steps. It runs when one of its triggers fires and all of its conditions hold, and also whenever something runs it: a dashboard button, a listener rule, another automation, the API or MCP. Automations are stored in the project (project.automations) and run inside tangle-core, on the primary node of a cluster or on a standalone node.

Since contract 0.2.30, automations also hold what macros used to (params, steps without triggers): macros in older projects are moved into automations when the project loads.

Example

At 08:00 on weekdays, if the projector is online, recall the presentation scene in Room A:

{
  "id": "morning",
  "name": "Morning",
  "triggers": [{ "kind": "time", "at": "08:00", "days": ["mon", "tue", "wed", "thu", "fri"] }],
  "conditions": [{ "kind": "status", "deviceId": "projector-1", "status": "online" }],
  "steps": [
    {
      "kind": "intent",
      "intent": { "name": "scene.recall", "target": { "kind": "room", "id": "room-a" }, "params": { "scene": "presentation" } }
    }
  ],
  "mode": "single"
}

Automation fields:

FieldRequiredMeaning
id, nameyesIdentifier and display name.
stepsnoWhat it does, in order. actions, the name before contract 0.2.30, is still read.
triggersnoAny one starts a run. Without triggers it runs only when something runs it.
conditionsnoAll must hold for the run to go ahead. None: always runs.
paramsnoInputs, read as {{name}} in steps: name, optional label and default.
modenoWhat a trigger does while a run is in progress. Default single.
continueOnErrornoKeep running the next steps after one fails. The run ends as partial.
priorityno0 to 100, applied to the intents the automation sends. Higher wins on conflict. Default 50, the same as timelines.
enablednoDefault true. false: its triggers do not start it, but it still runs by hand, from buttons and from other steps.
foldernoFolder in the automation list, for example Lobby/Evening.
descriptionnoFree text.

Editing in the designer

Right-click Automations in the project tree and choose New automation, or open one from the tree.

The editor has the step table on the left: one row per step with its number, an On checkbox (a step turned off is skipped, with the steps inside it), its type, target, value and what it waits for. + Add step opens the step picker, grouped as Device, Flow, Data, Run and Notes. Keyboard: Insert adds a step after the selected row, Alt+↑/↓ moves it, Space turns it off, Ctrl+D duplicates and Ctrl+Z undoes.

On the right:

  • Step N edits the selected step.
  • Automation holds the name, id, description, folder, Params, Run when (any of) (triggers), Only if (all of) (conditions) and Behaviour: the mode ("When it starts again while running"), Continue after a failed step and Priority (0–100).
  • Runs has Run now (with Ignore the conditions when there are some), Stop, and the run log, which updates live.

The line above the table sums the automation up, for example "10:00 every day → Power on Gallery, then wait until done (at most 2 min), Media player: transport play". Click Create automation, then Save for later changes. Runs use the saved version: save first to try your changes.

Tutorial: a gallery installation builds two automations step by step.

Triggers (Run when)

KindIn the designerFieldsFires when
timeTime of dayat (HH:MM, 24 h), optional daysThe node's local clock reaches that minute. Every day when days is absent.
deviceStateDevice state changesdeviceId, capability, key, optional to, above, below, unitA state value changes. With to, above or below, only when the new value starts matching. unit converts the thresholds (above: 26, unit: "°C").
deviceEventDevice event (button…)deviceId, nameThe device emits the event, for example button.pressed.
deviceStatusDevice goes online/offlinedeviceId, statusThe device's status becomes this one (online, offline, timeout, degraded, error, connecting).
timelineTimeline starts/stops/reaches a cuetimelineId, on, cueId for on: "cue"started, stopped (end reached or stop) or cue (reaches cueId).
variableA project variable changesoptional variable, to, above, belowA project variable changes. Without variable: any of them. The run gets {{variable}}, {{value}} and {{previous}}.
controlA dashboard control is clicked or changesoptional controlId, on, to, above, belowSomeone operates a dashboard control, or a step or the API changes its value. on: change (default), click, press, release, hold or any. A button fires on click. The run gets {{control}}, {{value}}, {{previous}} and {{event}}.
listenerA listener message matchesoptional listener, ruleA rule of an inbound listener matches a message. The run gets the rule's captures as {{name}}. See Listeners.
startupCore startsnoneThis node starts running automations: core starts, or a backup is promoted to primary.
manualOnly by handnoneNever fires by itself. Marks an automation meant to be run by hand. Any automation can be run by hand.

Weekdays are mon, tue, wed, thu, fri, sat, sun. In the designer each trigger has a Triggers on checkbox to switch it off without deleting it.

Note: A minute that passes while core is down, or while the node is a backup, is not replayed. A time trigger at 08:00 does not fire at 08:05 because core restarted at 08:03.

Conditions (Only if)

KindFieldsHolds when
statedeviceId, capability, key, optional equals, above, below, unitThe current value passes every test given.
statusdeviceId, statusThe device's current status is this one.
timeoptional after, before, daysLocal time is in the window. after later than before spans midnight (for example 19:00 to 07:00).
timelinetimelineId, statusThe timeline's status is stopped, playing, paused or waiting.
variablevariable, optional equals, notEquals, above, belowA project variable, param or run variable passes every test. A missing one is null.
controlcontrolId, optional property, equals, notEquals, above, belowA dashboard control's value (or another property: label, visible, enabled, pressed or a prop name) passes every test.

A state that core has not seen yet counts as not holding. A run whose conditions fail is logged as skipped with reason conditions not met. Conditions are also checked when the automation is run by hand, from a button or the API (unless forced), but not when another automation's step runs it.

Steps

KindIn the designerFieldsWhat it does
intentIntentintent, optional wait, timeoutMs, onTimeoutSends an intent (scene recall, power, level and so on) to a device, a room, devices with a tag, or everything.
actionDevice actiondeviceId, capability, action, optional params, wait, timeoutMs, onTimeoutSends one device action.
ifIfconditions, optional match (all or any), steps, otherwiseRuns steps when the conditions hold, otherwise when they do not.
repeatRepeatcount, stepsRuns the steps count times. {{index}} counts from 0.
whileWhileconditions, optional match, steps, maxIterations (default 1000)Runs the steps again while the conditions hold, checked before each pass. Fails with loop limit reached past the limit.
delayWaitmsWaits, up to 86400000 ms (one day).
waitUntilWait for a conditionconditions, optional match, timeoutMs (default 30000), onTimeoutWaits until the conditions hold. At the timeout: continue (default) or fail.
stopStopoptional result (ok or fail), reasonEnds the run here, completed or failed.
setSet a variable, Expressionvariable, value or addGives a variable a value, or adds to it (a missing one counts as 0). The designer's Expression is a set whose value is an expression.
controlSet a controlcontrolId, optional value, add, label, visible, enabled, propsSets a dashboard control on every screen showing it. null clears an override.
automationRun an automationautomationId, optional params, wait (default true)Runs another automation, inside this run (wait: true, its failure fails the step) or as its own run (wait: false). Calls go at most 8 deep and an automation cannot call itself.
callFunctionfunction, optional args, into, timeoutMs, onErrorCalls a library function and keeps its result in into. See Function libraries.
timelineTimelinetimelineId, op, optional positionMs, cueTransport: play, pause, stop, seek or go. cue jumps to a cue by id or {{name}}. See Timelines.
commentCommenttextA note in the list. Does nothing.
logLogmessage, optional level (info, warning, error), deviceIdWrites a line in the run's log and the core log, and in the device's log when deviceId is given.

Every step takes disabled: true to be skipped (the designer's On checkbox).

Intents and device actions are sent with source automation and the automation's id, so the intent engine's priorities and holds apply. A delay in one automation never holds up another.

Waiting until devices are done

By default an intent or device action step waits until every device accepted it (wait: "sent"), then the next step runs. Tick Wait until the device reports it is done (wait: "done") to wait until each device reports that it finished: a projector is on, not warming up; a screen is down. The step waits at most timeoutMs (default 120000, At most in the designer), then goes on (onTimeout: "continue", the default) or fails the run ("fail").

{
  "kind": "intent",
  "intent": { "name": "power.on", "target": { "kind": "room", "id": "gallery" } },
  "wait": "done",
  "timeoutMs": 120000
}

Failures

When a step is rejected or fails, the run ends as failed, and later steps are not run. An action sent to an offline device fails at once. With continueOnError: true (Continue after a failed step), the remaining steps run and the run ends as partial.

The run records the failed step by its position (step, for example 2 or 3.1 inside an if) and a reason, for example power.on partial: right-projector device offline.

Variables and expressions

Variables

Three kinds of values are read as {{name}} in step values, templates and expressions:

  • Params of the automation, filled by whatever runs it. A dashboard control passes its value as the param named value. A listener rule passes its captures.
  • Run variables: any name a set or call step writes that is not a project variable. They live for one run.
  • Project variables, declared under Variables in the project tree (project.variables): values core keeps for the whole project, for example houseMode = "show" or visitors = 12. Everyone sees them: dashboards show and change them, the API reads and writes them, and variable triggers start automations when they change. They are kept across restarts unless Keep is unticked (persist: false).

Triggers add their own values: {{value}} and {{previous}} for variable and control triggers, captures for listener triggers, {{index}} inside a repeat.

A value that is only "{{name}}" keeps the variable's type (a number stays a number). "Room {{room}} is ready" fills it in as text.

Expressions

Wherever a step takes a value, it can take an expression instead, computed when the step runs. In the designer, click ƒx next to a value.

{ "kind": "set", "variable": "level", "value": { "$expr": "clamp({{level}} * 1.2, 0, 100)" } }

An expression has:

  • {{name}} for params and variables, numbers, "strings", true, false, null.
  • + - * / %, == != < <= > >=, && || !, a ? b : c and parentheses.
  • state(device, capability, key) to read device state.
  • Built-in functions: abs, min, max, round, floor, ceil, clamp, len, upper, lower, str, num, now.
  • Dashboard controls: control.value(id), control.label(id), control.visible(id), control.enabled(id), control.pressed(id), control.min(id), control.max(id), control.prop(id, name). control.click(id), control.press(id), control.release(id) and control.hold(id) are true when this run was started by that event of that control, and control.event(id) gives the event name.
  • Device playheads: device.timecode(id, playhead?), device.position(id, playhead?), device.duration(id, playhead?) and device.running(id, playhead?), for devices with the tangle.playhead capability (media servers, players). Without a playhead name they read the main one.
  • Library functions such as math.scale(...) or time.format(now()).

Function libraries

Function libraries add named functions to expressions and to the Function step (call), written namespace.name(args). Core comes with five libraries:

LibraryFunctions
mathpow, sqrt, exp, log, log10, sin, cos, tan, asin, acos, atan, atan2, hypot, degrees, radians, sign, pi, mod, scale, lerp, clamp01, roundTo, dbToGain, gainToDb
texttrim, pad, replace, split, join, contains, startsWith, endsWith, indexOf, slice, repeat, format, hex, fromHex
timeformat, parse, add, diff, ms, duration, hour, minute, weekday, date
listat, first, last, sum, avg, min, max, sort, reverse, unique, includes, slice, range
colorhex, rgb, hsv, fromHsv, kelvin, mix, toRgbw, brightness

For example math.scale(128, 0, 255, 0, 100) is 50.2, time.add(now(), "1h30m") is in an hour and a half, and color.kelvin(3200) is the colour of warm white light.

Modules can add libraries too. The Image processing library (tangle.image.library, namespace image: info, resize, thumbnail, crop, rotate, flip, grayscale, adjust, compose, convert, averageColor, dominantColors, pixel) needs the IMAGE entitlement of Pro and Ultimate licenses; the free designer and demo mode can call every library. Install it from Settings > Modules.

Only pure functions (same arguments, same result, no side effects) can be used inside expressions and conditions. Any function can be used in a Function step, which waits for it (default 5 s, at most 120 s) and fails the run on an error, or goes on with {{error}} set when On error is continue.

GET /api/v1/functions lists every function with its signature, and POST /api/v1/functions/{name}/call tries one. Writing a library module is covered in Module SDK.

Run modes

mode decides what happens when a trigger fires during a run:

ModeBehaviour
singleIgnore the new trigger. The ignored run is logged as skipped (already running).
restartCancel the run in progress (logged as cancelled, restarted) and start again.
queuedRun again after the current run. Up to 10 runs can wait; more are skipped (queue full).
parallelRun alongside the current run, up to 10 at once; more are skipped (too many runs).

Loop guard

An automation can trigger itself, for example by changing the state that its trigger watches. Core starts at most 10 runs of one automation per second. Further runs are skipped with reason rate_limited and a warning is logged. The show and other automations keep running.

Timing

From the triggering event to the first step takes under 20 ms on the primary. Time triggers fire within a second of the minute.

Running an automation by hand

Running by hand needs a token with the control scope. Conditions are checked unless you force the run.

REST:

curl -X POST http://127.0.0.1:7340/api/v1/automations/morning/run \
  -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
  -d '{"force": true, "params": {}}'

force: true skips the conditions; params fills the automation's params. The answer is the run, with state started or skipped. POST /api/v1/automations/{id}/stop cancels its runs in progress.

WebSocket (for example from a dashboard button):

{ "v": 1, "id": "7", "type": "automation.run", "automationId": "close-room" }

The acknowledgement carries the run (runId, state). The run's trigger is recorded as manual.

MCP: run_automation.

A manual run is refused with 409 not_primary on a backup node.

Run history

Core keeps the last 50 runs of each automation in memory (they are not kept across restarts).

  • Designer: the Runs tab of the automation.
  • REST: GET /api/v1/automations/{id}/runs, newest first.
  • MCP: get_automation_runs.
  • Events: subscribe to automation.run:<id> (or automation.run:*). Core publishes an event when a run starts and when it ends.

Each run looks like this:

{
  "runId": "4f0c6d1e-...",
  "automationId": "morning",
  "state": "completed",
  "trigger": "time 08:00",
  "params": {},
  "log": [],
  "startedAt": 1760356800012,
  "endedAt": 1760356800190
}

Run states: started, completed, partial, failed, skipped, cancelled. reason explains a failed, partial, skipped or cancelled run, and step names the step in progress or the one that failed. log holds the lines of log steps (at most 100). trigger describes what started it, for example manual, startup, deviceEvent keypad-1 button.pressed or timeline show cue c4.

Automations and redundancy

Only the primary node (or a standalone node) runs automations, also in shared mode. A backup stays idle and starts running them when it is promoted, which also fires its startup triggers. Runs in progress on a failed primary are not resumed. When a node steps down to backup, its runs in progress are cancelled. See Redundancy.

More examples

Recall a scene when a keypad button is pressed:

{
  "id": "present",
  "name": "Presentation button",
  "triggers": [{ "kind": "deviceEvent", "deviceId": "keypad-1", "name": "button.pressed" }],
  "steps": [
    { "kind": "intent", "intent": { "name": "scene.recall", "target": { "kind": "room", "id": "room-a" }, "params": { "scene": "presentation" } } }
  ]
}

Start the attract loop at night only, when the show timeline has stopped:

{
  "id": "attract",
  "name": "Attract loop after the show",
  "triggers": [{ "kind": "timeline", "timelineId": "show", "on": "stopped" }],
  "conditions": [{ "kind": "time", "after": "19:00", "before": "23:00" }],
  "steps": [
    { "kind": "delay", "ms": 5000 },
    { "kind": "timeline", "timelineId": "attract", "op": "play" }
  ]
}

Count visitors with a door contact on an Ethernet I/O module, and switch the house to "full" at 120:

{
  "id": "count-visitors",
  "name": "Count visitors",
  "triggers": [{ "kind": "deviceEvent", "deviceId": "door-sensor", "name": "input.changed" }],
  "mode": "queued",
  "steps": [
    { "kind": "set", "variable": "visitors", "add": 1 },
    {
      "kind": "if",
      "conditions": [{ "kind": "variable", "variable": "visitors", "above": 119 }],
      "steps": [{ "kind": "set", "variable": "houseMode", "value": "full" }]
    }
  ]
}

Set a room level from a dashboard slider (0 to 100) with a param. level.set takes a level from 0 to 1:

{
  "id": "set-level",
  "name": "Set the room level",
  "params": [{ "name": "value", "label": "Level (%)", "default": 50 }],
  "steps": [
    {
      "kind": "intent",
      "intent": { "name": "level.set", "target": { "kind": "room", "id": "room-a" }, "params": { "level": { "$expr": "clamp({{value}} / 100, 0, 1)" } } }
    }
  ]
}

Shut a room down by hand from a dashboard, continuing past devices that do not answer:

{
  "id": "close-room",
  "name": "Close room",
  "triggers": [{ "kind": "manual" }],
  "steps": [
    { "kind": "timeline", "timelineId": "show", "op": "stop" },
    { "kind": "intent", "intent": { "name": "power.off", "target": { "kind": "room", "id": "room-a" } } }
  ],
  "continueOnError": true,
  "mode": "single"
}