Skip to content

Macros and Triggers

Automate your AV space with macros (command sequences) and triggers (automatic conditions).

Macros

Macros are named sequences of actions and the easiest way to automate without writing code. If you have used Crestron macros or Extron presets, this works the same way but with more flexibility.

Use the search box at the top of the macro list to filter by name.

Creating a Macro

  1. Click Macros in the sidebar
  2. Click New Macro
  3. Give it a descriptive name (e.g., system_on, select_laptop, shutdown_all)
  4. Add steps using the + button:
Step TypeDescriptionExample
Device CommandSend a command to a deviceprojector_main -> power_on
Group CommandSend a command to all devices in a group at onceprojectors -> power_on
DelayWait N seconds between stepsWait 15 seconds for projector warmup
Wait UntilPause until a state value matches a conditionWait until the projector reports it is warm
Set VariableSet a user variablevar.room_active = true
Emit EventFire a custom event on the event busroom.shutdown_complete
Run MacroExecute another macro as a sub-routineRun select_hdmi1
ConditionalIf/else branching based on stateIf projector is already on, skip power-on
Navigate PanelSend every panel to a specific page or overlayReturn all panels to home at end of meeting

The Device Command step uses smart dropdowns: after selecting a device, the command dropdown only shows commands defined by that device’s driver, with parameter fields that match the driver’s command definition. No guessing at command syntax.

The Group Command step works the same way but targets a device group instead of a single device. All devices in the group receive the command concurrently. Only commands shared by every device in the group are shown, and its parameters use the same smart inputs as a single-device command: a Yes/No selector for boolean parameters, a dropdown for enumerated values, and the $ toggle for dynamic values. You pick from the driver’s choices instead of typing raw values. Offline devices are skipped automatically. Create and manage device groups from the Groups tab in the Devices view.

The Emit Event step can attach optional payload fields that travel with the event. Each field has a name and a typed value (text, number, or true/false). Scripts receive them in the payload argument of an @on_event handler, and a macro triggered by the event can read each field with $trigger.<field>.

The Set Variable step stores values with the variable’s declared type: a string variable keeps text like 0 or true exactly as typed. When the target is a device or system state key instead of a variable, a type selector chooses whether the value is stored as text, a number, or true/false.

Reorder steps by dragging the grip handle on the left side of each step. Toggle Stop on Error in the macro header to halt execution if any step fails (by default, macros continue through errors).

Step Templates: Click the Templates dropdown in the step toolbar to insert pre-built multi-step patterns: Volume Ramp, Source Switch, and Power Sequence. Templates add several coordinated steps at once and are a fast way to build common sequences.

Copy and Paste Steps: Right-click a step (or use the step menu) to copy it. Paste into the same macro or a different one. This works across macros, so you can reuse steps without rebuilding them.

Plugin Actions: Installed plugins can add their own step types. They appear at the bottom of the Add Step menu under Plugin Actions, grouped by plugin name. Examples: an Audio Player plugin contributes Play Sound; an MQTT bridge plugin contributes Publish Message. Each plugin action shows the plugin’s parameter form, supports dynamic $var.foo values just like built-in steps, and can be reordered, copied, and used inside conditionals like any other step. If you remove a plugin that a macro is using, the affected steps show a “Missing plugin” warning until you reinstall it or delete the steps.

A Typical System-On Macro

Here is a real-world example of a system_on macro for a conference room:

StepTypeDetails
1Set Variablevar.room_active = true
2Device Commandscreen_1 -> lower
3Device Commandprojector_main -> power_on
4Delay20 seconds (projector warmup)
5Device Commandprojector_main -> set_input, input: hdmi1
6Device Commandswitcher_1 -> set_route, input: 1, output: 1
7Device Commanddsp_1 -> set_level, channel: program, level: -20
8Device Commanddsp_1 -> unmute, channel: program

Conditional Steps

Use the Conditional step type to add if/else logic to a macro without writing a script.

A conditional step checks a state value and runs one set of steps if the condition is true, and optionally a different set if false. For example: “If the projector is already on, skip the power-on and warmup delay.”

  1. Add a Conditional step
  2. Set the If condition: pick a state key, operator, and value. Available operators:
OperatorAliasesDescription
eqequals, ==Equal to
nenot_equals, !=Not equal to
gtgreater_than, >Greater than
ltless_than, <Less than
gtegreater_or_equal, >=Greater than or equal to
lteless_or_equal, <=Less than or equal to
truthyValue is truthy (non-empty, non-zero, non-null)
falsyValue is falsy (empty, zero, null, false)
  1. Add steps to the Then block (runs when condition is true)
  2. Optionally add steps to the Else block (runs when condition is false)

The value field adapts to the key you chose: pick a boolean variable and you get a true / false dropdown instead of a text box, so there is no guessing at how to spell the value. This same condition editor is used by Skip If guards, Wait Until steps, and trigger guard conditions, so the true/false dropdown appears in all of them.

Comparisons automatically handle type differences between device state and your condition value. A device that reports volume as "-12.5" (text) will correctly compare against a numeric threshold like -20. Similarly, "true" (text) matches true (boolean). Text equality also ignores case, so a condition value of On matches a device that reports on. You don’t need to worry about the internal type or exact casing — just enter the value you expect.

eq/ne against a value that has not been reported yet (a device that has not polled, or a key that does not exist) never match: power != "on" will not fire while power is still unknown, so an automation cannot act on state a device has not sent. To branch on a missing or empty value, use falsy.

Conditionals can be nested (a conditional inside a conditional) up to 5 levels deep. Macros that call other macros via “Run Macro” steps can nest up to 10 levels deep. For most rooms, one level of each is enough.

Wait Until

A Wait Until step pauses the macro until a state value matches a condition, then continues. This replaces the common pattern of guessing a fixed Delay for a projector warmup or a display input change. The macro resumes the instant the device reports ready, and has a timeout so a stuck device does not strand the macro forever.

Fields:

  • Wait until — a condition (same picker as conditional steps): state key, operator, value.
  • Timeout — seconds to wait before giving up. Check Never time out to wait indefinitely.
  • If timeout — when a timeout fires, either Fail the macro (default) or Continue anyway. Failing is the safer default for hardware waits, since the next step is usually something that depends on the device being ready.

Example: a projector warmup that no longer guesses a delay.

StepTypeDetails
1Device Commandprojector_main -> power_on
2Wait Untildevice.projector_main.power_state equals "on", timeout 60s, fail on timeout
3Device Commandprojector_main -> set_input, input: hdmi1

Other common uses: wait for a display to confirm an input change before routing audio (short timeout, continue anyway so audio always follows), wait for a device to come back online after a power cycle, or wait for a user to press a confirm button on the panel. For the last case, set a user variable from the button binding and use Never time out so the macro waits as long as the user needs. A waiting macro still respects cancel groups and explicit cancellation, so “Never time out” is always breakable from outside.

A Navigate Panel step tells every connected panel to switch to a specific page. The target can be:

  • A regular page ID, like home or meeting_controls
  • An overlay or sidebar page ID — the panel pushes it onto the overlay stack
  • $back — phone-style back: closes the topmost overlay if one is open, otherwise returns to the previously-visited page. No-op if there’s nothing to go back to. Page history resets on idle timeout.
  • $dismiss — closes the topmost overlay only. No fallback to page history.

Every panel showing this project follows. Common uses:

  • A “End meeting” macro that resets the system and sends panels back to home
  • A recovery macro triggered by a fault that opens an error_overlay
  • A schedule-driven macro that opens a welcome page in the morning

For switching modes within a single page (lecture vs. presentation, video conferencing vs. local source), this is usually the wrong tool. Prefer setting a variable with Set Variable and binding element visibility to that variable with the Shows > Visible when… card in the UI Builder. That pattern doesn’t push every panel to a new page, survives panel reconnects, and keeps the mode state in one place that scripts and other macros can read.

Skip If Guards

Every step has an optional Skip this step if… guard in the Guards section at the bottom of the step editor. When enabled, the step is silently skipped if the condition is true.

This is simpler than a full conditional block when you just want to skip one step. For example: skip the “power on” command if device.projector.power already equals "on".

Device Command steps also have a Skip if device is offline checkbox. When checked, the step is silently skipped instead of failing if the device is disconnected. This is useful for macros that control optional equipment that may not always be present.

Dynamic Parameters

Macro step parameters are normally static values set at design time. Dynamic parameters let a step use whatever value a variable holds at runtime.

Click the $ button next to any parameter field to switch between static and dynamic mode. In dynamic mode, you pick a state key (like var.target_volume), and the macro reads the current value when it runs.

For example, a volume slider on the touch panel writes to var.volume_level. A macro step can use $var.volume_level as the level parameter for a DSP set_volume command. The same macro works for any volume, determined by the slider position.

Dynamic references also work on the Set Variable step’s value field. This lets you copy one variable to another at runtime.

The same $ picker now appears on UI Builder bindings, so a touch panel button or slider can read a variable or device state the same way a macro step does. See UI Builder for the binding-side picker.

Reading the trigger that fired the macro

When a macro is run by a trigger, it can read what fired it through the Trigger event group in the dynamic-value picker:

  • For an event trigger: $trigger.event (the event name) and $trigger.data / $trigger.raw (the payload, for example the bytes a device sent back).
  • For a state change trigger: $trigger.new_value, $trigger.old_value, and $trigger.key (which state key changed).

For example, a trigger that fires when a device sends an unsolicited message can run a macro that stores the received text with a Set Variable step set to $trigger.data, or branches on it in an If step (condition key trigger.data). This makes “when this arrives, do that” automations possible without writing a script.

These references only resolve when the macro is run by a trigger. If you run the same macro directly (from a button, the API, or another macro), $trigger.* reads as empty.

Cancel Groups

When two macros should not run at the same time (like System On and System Off), assign them to the same cancel group. When a macro with a cancel group starts, any other running macro in the same group is cancelled first.

Set the cancel group in the macro header, next to the macro ID. Give both macros the same group name (e.g., system_power). Now if someone presses System Off while System On is still running, the system-on macro stops immediately and system-off takes over.

You can also cancel a running macro manually by clicking the Cancel button in the macro header while it’s running.

Side effects of a cancelled macro. Cancellation is not transactional. Any steps the cancelled macro already executed stay in effect: a Set Variable write is still in the state store, a Device Command or Group Command is already on the wire to the equipment, and an Emit Event payload has already fired on the event bus. None of those are rolled back. The two steps that unwind cleanly are Delay and Wait Until: if cancellation hits during either, the wait is abandoned with no residual change. Sub-macros invoked with Run Macro are cancelled at their current step, so partial effects from inside the sub-macro persist the same way. Plan paired macros (System On / System Off, for example) so that being interrupted partway through is still recoverable by the macro that takes over. A common pattern is to set a “target state” variable as the very first step of each, so whichever one finishes last decides the final system state.

Overlap and Cooldown

A macro can guard against being run too often, or being run again while it’s still busy, right from its header, next to the cancel group.

  • Overlap decides what happens when the macro is fired again while a previous run is still going. Allow (the default) lets them run at the same time. Skip ignores the new run until the current one finishes. Queue holds the new run and starts it once the current one is done.
  • Cooldown sets a minimum number of seconds between starts. A start that arrives inside the window is dropped. Leave it at 0 for no cooldown.

The important part is where these apply. A trigger has its own overlap and cooldown settings, but those only cover fires from that one trigger. The macro’s own overlap and cooldown are enforced on the macro itself, so they hold no matter what starts it: a schedule, a panel button, a script loop, an automation, the AI assistant, or another macro. That closes the gap where a macro you carefully protected on its trigger could still be hammered from a script or a button.

If both a trigger and its macro carry a guard, both apply and the stricter one wins. Adding a guard to a macro can only ever tighten its behavior, never loosen a trigger’s, so it’s safe to set a sensible macro-level default and still tune individual triggers. (Because it protects against unthrottled re-runs, a macro’s overlap defaults to Allow to preserve existing behavior, while a trigger’s defaults to Skip.)

Progress in Panel

When a macro is running, the panel provides two forms of feedback:

Button busy state (automatic). If a button triggers a macro and that macro is currently running, the button automatically shows a pulsing animation and blocks re-presses. This prevents users from accidentally triggering the same macro twice. No configuration needed.

Progress label (opt-in). You can bind a label’s text to “Macro Progress” in its Shows > Text card. Select the macro to track and set the idle text (what shows when the macro is not running). While the macro runs, the label shows the current step’s description. Add descriptions to each macro step in the step editor to control what users see (e.g., “Powering on projector”, “Waiting for warmup”). If no description is set, the system generates one from the action type.

Variables in Macros

The Set Variable step picks its target from the Variable Picker (a searchable dropdown that shows every variable with its current value, and a Create New Variable option inline) rather than a free-text key, so there is nothing to misspell. Its value field then matches the variable’s type: a boolean variable gets a true / false dropdown, and the $ toggle lets you copy another variable, device state, or system value in. Variables let macros share state. For example, the system_on macro sets var.room_active to true, and UI buttons use that variable for feedback.

Macro Dependencies

The macro detail panel shows a dependency tree listing which macros call this macro and which macros this macro calls (via “Run Macro” steps). If you create a circular dependency (macro A calls B, B calls A), the IDE warns you at save time.

Testing Macros

Click Test to execute the macro immediately. A progress indicator shows which step is running, with live status updates. Conditional steps show whether the condition evaluated to true or false, and group command steps show per-device success/fail icons. A Last Run summary shows the timestamp, duration, step results, and any errors from the most recent execution.

Convert to Script

Click Convert to Script to generate a Python script from the macro. A preview shows the generated code in a diff-like view before committing. This is useful when you need loops, error handling with retries, external API calls, or complex data processing that macros cannot express. The generated script is fully functional and includes all the same steps with proper await calls.

Triggers

Triggers automatically execute macros based on conditions. Click the Triggers tab in the macro editor to add triggers to any macro.

Trigger TypeFires WhenExample
ScheduleCron expression matches0 22 * * 1-5 (10 PM weekdays)
State ChangeA state key matches a conditiondevice.projector.power equals "on"
EventAn event fires on the busui.press.btn_panic
StartupSystem startsRun initialization macro

The schedule trigger includes a visual cron builder with two modes: a field-by-field editor (separate inputs for minute, hour, day, month, weekday with labels) and a raw expression editor. A dropdown of common examples (“Every weekday at 8am”, “First Monday of month”, “Every 15 minutes during business hours”) lets you start from a template.

Fire Now: Click the Fire Now button on any trigger to manually execute it immediately, without waiting for the condition to be met. Useful for testing.

Condition Preview: State change triggers show a live preview: “If this condition were evaluated now, it would be: TRUE/FALSE” with the current state values displayed.

Event Autocomplete: The event pattern field in event triggers suggests events from your configured devices, macros, and scripts as you type.

Trigger safety features prevent runaway automation:

  • Debounce. Prevent rapid re-firing (configurable seconds).
  • Delay + re-check. Wait, then verify the condition is still true before executing.
  • Cooldown. Minimum interval between executions.
  • Guard conditions. Additional state conditions that must all be true. Supports the same operators as conditional steps (eq, ne, gt, lt, gte, lte, truthy, falsy) and their aliases.
  • Overlap policy. Choose what happens when a trigger fires while its macro is still running: skip (ignore the new fire, the default), queue (run it once the current one finishes), or allow (run them concurrently).
  • Stop on error. Set stop_on_error: true on a macro to halt execution if any step fails (default is to continue).

Example: A “projector auto-off” trigger watches device.projector_main.power for "on", with a guard condition that var.room_active equals false. This shuts down a projector that someone turned on manually without using the panel, but only if the room is not in active use.

Scripts

For logic that macros cannot express, write Python scripts using the Code view in the Programmer IDE. Scripts are stored in the active project’s scripts/ folder (projects/default/scripts/) as standard .py files.

Use the search box at the top of the script list to filter by file name.

The Script Editor

  • File tree on the left showing project scripts (files with load errors show a red icon)
  • Monaco editor with Python syntax highlighting and autocomplete
  • Autocomplete for the OpenAVC API with return types, parameter types, and one-line examples
  • Save to write changes, Run to hot-reload without restarting (Ctrl+Shift+R for keyboard shortcut)
  • Console panel showing script output and full error tracebacks with clickable line numbers

Quick Example

from openavc import on_event, devices, state, log, delay
@on_event("ui.press.btn_system_on")
async def system_on(event, payload):
log.info("System ON triggered")
state.set("var.room_active", True)
await devices.send("projector_main", "power_on")
await devices.send("screen_1", "lower")
# Wait for projector warmup
await delay(20)
await devices.send("projector_main", "set_input", {"input": "hdmi1"})
await devices.send("switcher_1", "set_route", {"input": 1, "output": 1})
await devices.send("dsp_1", "set_level", {"channel": "program", "level": -20})
log.info("System ON complete")

When to Use Scripts Instead of Macros

Use a Macro WhenUse a Script When
Sequences of commands with delaysNeed loops (repeat N times, while condition)
Simple if/else branching (conditional steps)Need complex multi-condition logic trees
Skipping steps based on stateNeed try/except error handling with retries
Quick one-off actionsNeed to call external APIs or do math
Non-programmers will maintain itNeed string manipulation or data parsing

Most rooms can be built entirely with macros, conditionals, and bindings. Scripts are there when you need them.

Script Templates

Click Templates to insert boilerplate for common patterns:

  • Button handler (event listener with device command)
  • State change handler (react to a device state change)
  • Device control (send commands with error handling)
  • Periodic timer (recurring status checks)
  • System on/off (full room startup/shutdown sequence)
  • Scheduled task (cron-driven operations)
  • Device monitor (watch device state and react)
  • Custom event handler (listen for custom events)
  • Variable watcher (react to variable changes)

See the Scripting Guide for the complete API reference including all available functions, decorators, and patterns.

See Also