Skip to main content

Macro Scripts

The Run Script macro step executes JavaScript in a sandboxed VM. Use it for logic that doesn't fit the other step types — computing a value, iterating dynamic collections, conditional branching beyond the built-in condition steps, or driving several devices from one place.

Authoring a script needs elevated access

Only an elevated (admin) user may add or edit a Run Script step. A non-elevated session — a kiosk or scene-builder login — can still build ordinary command/scene steps, but any attempt to create a Run Script step, or to re-type an existing step into one, is refused with not authorized. A script runs real code with the whole server in scope, so authoring one is an admin-only action. Give the operator an elevated role if they need to write scripts.

The execution surface

A script runs in an isolated context with these globals:

GlobalWhat it is
gemThe live server. Call gem.command(...), gem.macro(id, context), gem.getAttribute(target, id, name), gem.setAttribute(target, id, name, value, valueType), and read the in-memory collections gem.zones, gem.devices, gem.subsystems, gem.avZones, gem.avSources — each keyed by id, with the entity's current attributes merged onto the object. Read live state from these, not from a database query.
contextThe current macro context: one object shared by every step in this run. Read values placed by a Set Variable step or the current item of a For Each, and assign context.<name> = … to hand a value to the steps that follow (they see the same object).
signalAn AbortSignal that fires when the macro run is stopped.
console, setTimeout / setInterval (and their clear…), Promise, Buffer, URL / URLSearchParamsStandard utilities.
JSON, Math, Date, Array, Object, String, Number, Boolean, RegExp, Map, Set, parseInt, parseFloat, encodeURIComponent, …Standard JS built-ins.

process, require, and the filesystem are not in scope — the sandbox cannot reach them, so a script cannot read files or shell out.

Writing a script — wrap it in an async function

The script body is a plain script, not a module and not a function body. That means a top-level await or return is a syntax error and fails the step immediately (you'll see Illegal return statement or await is only valid in async functions… as the step error). Put your logic inside an async function that you call right away — an async IIFE — so you can freely await and return inside it:

(async () => {
// side effects — this is how a script changes the system
await gem.setAttribute('zone', 5, 'state', 'on', 'string');

// read live state from the in-memory collections
const zones = Object.values(gem.zones);
const active = zones.filter(z => z.state === 'on');

// hand a value to later steps, and/or produce a result for the run log
context.active_count = active.length;
return { count: active.length };
})()

What the step does with your script

  • Side effects are the point. A script affects the system by calling gem.command(...), gem.setAttribute(...), and the other gem.* helpers, and by writing onto context so later steps can read what it computed. Those happen as the script runs.
  • The result is recorded and sets the step status. The value the script produces is stored in the macro's run log (visible in the run history) and decides the step outcome: return an object shaped {error: 'message'} and the step is marked error — which stops the macro if it's set to stop on error; the run reports aborted if the macro was stopped mid-script.
  • Only a Promise result is captured. The step keeps a result only when the script's final expression is a Promise — an async IIFE (above), or a bare async call as the last line, e.g. gem.command({ device: 3, action: 'on' }) or Promise.resolve(value). A plain synchronous value (a number, a bare object literal) is not captured — the step result comes back empty. The side effects still happen regardless; only the recorded result differs.

Limits

  • 30-second hard timeout. A script that runs longer is aborted — including a runaway synchronous loop, so never write while (true). Keep scripts short.
  • Scripts honor the macro's abort signal, so a long-running async script exits promptly when the macro is stopped.
warning
gem.* and context.* are a frozen public API

Saved macros and user scripts are stored as JSON written by past versions. Changing the signature of a gem.* or context.* method silently breaks every stored script that uses it. New helpers are added; existing ones are never repurposed or removed. Prefer the documented gem.<helper> methods over raw database access.

See the Automation guides for building macros, and Architecture for how gem relates to the rest of the system.