Skip to content
View as Markdown

Writing Muxit scripts ​

A reference for AI assistants. It is self-contained: you can write a correct script from this page alone. Read How a script runs first and finish with the checklist.

A script is automation: a measurement run, a sweep, a monitoring loop, a data logger, an alarm. It uses devices that already have connectors, by name — connector("psu") is the connector defined in workspace/connectors/psu.js. If a device has no connector yet, write that first (Writing Connectors).

The deliverable ​

  • One file, workspace/scripts/<name>.js. Sub-folders are fine (workspace/scripts/tests/iv-curve.js).
  • Plain top-level JavaScript. No export, no import, no main() — the file body is the program. A top-level return value; ends the script and hands value back to whoever ran it.
  • Scripts in workspace/startup/ (top level only, alphabetical order) start automatically when the server starts, after connectors are up. Auto-start needs a Maker licence or higher.
  • There are no script parameters. Put settings in constants at the top of the file, read them from a connector (a Virtual store a dashboard can edit), or ask the operator with ask.*.

You need the connector names and their properties ​

A script can only use names the connector actually has; anything else throws ReferenceError: Connector 'psu' has no property or method 'voltag'. Do not guess names. Ask the user for the connector file, or for the property and action list shown when the connector is expanded in Muxit's Connectors panel. If you are connected to Muxit over MCP, call list_connectors / get_connector_schema.

Know which names are properties (read with dev.x, write with dev.x = v) and which are actions (call with dev.x(…)). Reading an action without parentheses gives you a function, not a value — a camera's snapshot, for example, is an action: cam.snapshot().

How a script runs ​

js
const psu = connector("psu");

psu.voltage = 5;                 // write: blocks until the device has it
psu.output = true;
delay(500);                      // let it settle
const v = psu.measured_voltage;  // read: a plain value, no await
log.info(`Measured ${v} V`);
return { v };                    // optional result
  1. Everything is synchronous. Property reads, writes, action calls, delay, ai and ask block the script until they finish and return plain values. Do not write await; it is unnecessary (it does no harm on a value, but await dev.x = 5 is a syntax error).
  2. Loops must call delay(ms). A loop without one spins a CPU core until stopped. delay caps at 300 000 ms (5 minutes) per call.
  3. There are no timers. setTimeout, setInterval and requestAnimationFrame do not exist. Use a while loop with delay.
  4. Stop is a hard kill. When the user presses Stop, the engine is interrupted wherever it is: code after the loop does not run and finally blocks do not run. Put setup before the loop, leave the hardware in a safe state at every iteration, and never rely on cleanup code at the end. A script that should run until stopped is simply while (true) { …; delay(ms); } — there is no script.running flag.
  5. An uncaught error ends the script, logged with its line number. Wrap device calls you expect to fail in try / catch.
  6. Different scripts run concurrently; the same script cannot be started twice. The Free tier allows three running scripts at once.

Device access ​

js
const dev = connector("name");     // device("name") is an alias

connector() throws if the connector does not exist or is disabled.

Reading dev.prop returns the latest polled value. Muxit polls every readable property the device backend declares every 200 ms, so a read costs no I/O and can be up to about 200 ms old. Properties the connector computes itself are recalculated on every read.

Writing dev.prop = value sends the value to the device and waits for the write to complete. The value you read back immediately afterwards is still the previous polled value; to confirm a setting, delay(300) and read again (or read a measured property after the device has settled). Assigning null, undefined, NaN or [] does nothing. A property the connector file defines with its own setter is the exception: the write is queued (at most one per 100 ms) and an error in it only reaches the server log.

Actions are called with positional arguments in the documented order:

js
dev.reset();                        // no arguments
psu.save(3);                        // one argument
wh.notify("Run done", "12 points", "success");   // several
dev.moveTo({ x: 10, y: 20 });       // or one object with named arguments

For a driver action, positional arguments are matched to the declared argument names, and an object is passed through as named arguments. Do not pass a lone array — dev.load([1, 2, 3]) is treated exactly like dev.load(1, 2, 3). Wrap it: dev.load({ values: [1, 2, 3] }).

What comes back is converted to plain JavaScript: numbers, strings, booleans, null, arrays and objects.

Globals ​

GlobalWhat it does
connector(name), device(name)Device proxy, as above.
log.info(…), log.warn(…), log.error(…), log.debug(…)Log to the script output panel. Any number of arguments, joined with spaces; plain objects are JSON-formatted, arrays are not — JSON.stringify them yourself.
console.log, console.warn, console.errorAliases of log.info / log.warn / log.error. There is no console.info or console.debug.
delay(ms)Sleep, up to 300 000 ms. Cut short when the script is stopped.
timestamp()Current time as an ISO 8601 UTC string.
emit(event, data)Send an event to other scripts. Each call is delivered, even with the same data twice.
on(event, handler), waitFor(event, opts?)Receive events. See Events.
stream(connectorName, streamName, data)Push data to dashboard widgets subscribed to that stream (a terminal, a chart, a canvas).
say(text, opts?)Show text in the chat panel, spoken aloud when the user has script speech on. opts.emotion: "excited", "serious", "whisper", …
ask.confirm(msg, opts?), ask.choose(msg, choices, opts?), ask.text(msg, opts?)Ask the operator and wait for the answer. See Operator interaction.
ai(prompt, image?, options?)One LLM call, returns the answer as a string. See AI.
script.nameThis script's name: its path under scripts/ without .js (tests/iv-curve), or startup/<name>.

Standard JavaScript is available: Math, JSON, Date, Number, String, Array, Object, Map, Set, RegExp, Promise, Proxy, Reflect, globalThis, template strings, destructuring, classes and helper functions.

Not available: require, import, fetch, XMLHttpRequest, WebSocket, fs, process, Buffer, atob / btoa, TextEncoder, setTimeout, setInterval, eval, new Function, and every Node.js or browser API. Files go through a FileAccess connector; HTTP goes through a WebhookNotifier connector; scheduling is a loop with delay.

Events ​

Scripts can signal each other, and react to property changes, without polling.

js
emit("unit-done", { serial, pass });                // script A
on("unit-done", (u) => log.info(u.serial));         // script B: runs for every event
const u = waitFor("unit-done", { timeout: 60000 }); // or block for the next one
  • A bare name is another script's emit(). "state:<connector>.<property>" fires when that property's value changes; an unchanged value fires nothing. "connector:<connector>.<event>" fires on every connector-config emit(), JS-driver __emitEvent, and every matching line of a LineText property with emit:, repeats included: use it for barcode scans. "dashboard:<name>" fires each time a dashboard widget with config.emit: "<name>" is used; its data is { value, user, widget }. A trailing * matches by prefix ("state:psu.*"); meta.event is the actual name.
  • waitFor options: timeout (ms), default (returned on timeout; without it the call throws an error with code === "EVENT_TIMEOUT"), where (filter function). After the first waitFor(name), later events of that name are buffered until taken, so a loop around waitFor loses nothing while it is busy.
  • Events are delivered only while the script waits, in delay(), waitFor() or ask.*. A loop that never calls either never runs a handler.
  • A script that ends its top-level code with on() handlers registered stays running until stopped. Don't add a while (true) loop just to keep it alive.
  • Never use on() for safety: a handler runs at the next wait, not instantly. Limits belong in the connector's safety block.

Files ​

The sandbox has no file system. Files go through a connector using the built-in FileAccess driver, sandboxed to a folder of the workspace (by default workspace/data/). A new workspace ships one named fileaccess — confirm the name with the user; if they have none, they add a connector { driver: "FileAccess", config: { basePath: "data" } } first.

js
const fa = connector("fileaccess");
const f = fa.openText("runs/iv-curve.csv");   // path relative to workspace/data/

f.write("time,voltage_v,current_a\n");          // overwrite (creates folders)
f.append([timestamp(), 5.0, 0.103]);           // array → one CSV row, quoted as needed
f.append("a plain line\n");                    // strings are written as-is
const text = f.read();                         // whole file as a string
const lines = text.split("\n");
f.exists;                                       // property: true / false
f.size;                                         // property: bytes
f.rename("archive/iv-curve.csv");              // moves the file; the handle follows
f.delete();                                     // false if it was already gone

const tsv = fa.openText("out.tsv", "\t");      // another delimiter for array rows
const { files, dirs } = fa.list("runs");       // one folder, not recursive
fa.mkdir("runs/2026");

const img = fa.openBinary("snapshots/part.jpg");
img.write(connector("webcam").snapshot());     // binary handles read and write base64

Paths are always relative to the sandbox folder: .. that escapes it is refused, and a leading / is not an absolute path ("/tmp/x.csv" lands in data/tmp/x.csv). Files are limited to 10 MB by default. Prefer read().split("\n") over readLines(), and avoid info(): both return .NET objects rather than plain JavaScript ones.

Operator interaction ​

js
if (!ask.confirm("Output will switch on at 12 V. Continue?", { timeout: 60000, default: false })) {
  return "cancelled";
}
const port  = ask.choose("Which fixture?", ["A", "B", "C"], { default: "A", timeout: 120000 });
const label = ask.text("Batch label?", { timeout: 300000 });
  • The script waits until a dashboard user answers.
  • timeout is in milliseconds and at least 1000. Without it the script waits indefinitely.
  • On timeout the script gets default; with no default it throws PROMPT_TIMEOUT.
  • requireObserver: true pauses the countdown while no dashboard is open.

AI ​

js
const verdict = ai("Reply PASS or FAIL only: is 85 °C acceptable for a motor rated 90 °C?");
const seen = ai("Is the part seated in the fixture? Answer yes or no.", connector("webcam").snapshot());

ai() returns a string (up to about 1024 tokens) and has no memory between calls and no tools. The second argument is a base64 JPEG, or an array of up to 8 images (strings or { image, label }). It uses the AI provider configured in Muxit: text works on every tier with a local model and needs Maker on the Muxit-managed service; any call with an image needs Maker. When the tier does not allow it, ai() throws — wrap it in try / catch if the script must keep going.

Safety ​

Writes and actions a script makes go through Muxit's safety gate, using the safety level the user has chosen and — on the Lab tier — the connector's safety block. (Connectors on drivers with no hardware to protect — Virtual, FileAccess, Webcam, Onvif, WebhookNotifier — skip the gate.)

  • A blocked operation — out of range, or over a rate limit — throws an error with code SAFETY_BLOCKED and a message such as limit.outOfRange; the hardware never sees it.
  • An operation that needs confirmation pauses the script until someone approves it in a dashboard. A denial, or no answer within 30 seconds of a dashboard being open, throws (user.denied); with no dashboard open the script waits.
  • At the Observe level, ordinary writes are simulated without error and ordinary actions return null.

So do not catch and ignore safety errors in a loop; let the script stop, or stop it yourself.

Patterns ​

Sweep with CSV logging

js
// workspace/scripts/iv-curve.js — step the PSU, log V and I.
const psu = connector("psu");
const csv = connector("fileaccess").openText(`iv/${timestamp().replace(/[:.]/g, "-")}.csv`);

const START_V = 0, STOP_V = 5, STEP_V = 0.25, SETTLE_MS = 400;

csv.write("set_v,measured_v,measured_a\n");
psu.voltage = START_V;
psu.output = true;

for (let set = START_V; set <= STOP_V + 1e-9; set += STEP_V) {
  psu.voltage = Number(set.toFixed(3));
  delay(SETTLE_MS);                                   // settle + at least one poll cycle
  csv.append([set.toFixed(3), psu.measured_voltage, psu.measured_current]);
}

psu.output = false;                                   // a natural end; Stop would skip this
psu.voltage = 0;
return "done";

Monitor with an alarm and hysteresis

js
const sensor = connector("thermocouple");
const alarm  = connector("webhook");          // a WebhookNotifier connector
const LIMIT = 80, HYSTERESIS = 5;
let armed = true;

while (true) {
  const t = sensor.temperature;
  if (t === null || t === undefined) { log.warn("no reading"); delay(1000); continue; }
  if (armed && t > LIMIT) {
    alarm.notify("Over temperature", `${t.toFixed(1)} °C`, "error");
    armed = false;
  } else if (!armed && t < LIMIT - HYSTERESIS) {
    armed = true;
  }
  delay(1000);
}

Wait for a condition, with a timeout

js
function waitFor(check, timeoutMs, pollMs = 250) {
  const end = Date.now() + timeoutMs;
  while (Date.now() < end) {
    if (check()) return true;
    delay(pollMs);
  }
  return false;
}

const robot = connector("robot");
robot.moveTo({ x: 100, y: 50, z: 20 });
if (!waitFor(() => robot.idle, 30000)) throw new Error("move did not finish in 30 s");

Hand values to a dashboard: write them to a Virtual connector's properties (connector("store").status = "Running step 3/10"); widgets bound to store.status update at once.

Common mistakes ​

MistakeInstead
await dev.voltage, async function main(), main()Plain top-level code; reads return values directly.
setTimeout / setIntervalwhile (true) { …; delay(ms); }
require("fs"), fetch(…)A FileAccess connector; a WebhookNotifier connector.
Polling a value in a loop to notice that something happenedwaitFor("state:<connector>.<property>"), or emit() / waitFor() between scripts.
Cleanup after the loop or in finallySafe state at every iteration; stop naturally with break when cleanup matters.
Loop without delayAlways delay(ms) inside loops.
Reading a value right after writing it and expecting the new valuedelay(300) first, or read a measured property.
dev.snapshot for an actiondev.snapshot() — know which names are actions.
connector("files")Use the user's FileAccess connector name (fileaccess in a new workspace).
Passing an array as the only argumentWrap it in an object with the argument name.
Invented property or connector namesAsk for the connector's schema.
read_property(…), call_action(…) inside a scriptThose are AI tools, not script functions. Use the proxy.

Checklist ​

  • [ ] The file goes in workspace/scripts/ (or workspace/startup/ to auto-start) and is plain top-level code: no import, export, require or main().
  • [ ] Every connector, property and action name came from the user or a schema, and actions are called with ().
  • [ ] No await, no setTimeout / setInterval, no on() / emit(), no Node or browser APIs.
  • [ ] Every loop calls delay(ms).
  • [ ] Nothing important happens after the loop or in a finally block; the hardware is safe at every point the script could be stopped.
  • [ ] Setpoints are within limits the user confirmed; outputs are not switched on, and axes not moved, without the user asking for it.
  • [ ] Reads that must reflect a just-written value wait at least one poll cycle.
  • [ ] File access goes through the user's FileAccess connector with paths relative to its folder.
  • [ ] Anything you could not verify (connector names, units, safe ranges) is listed for the user.

Muxit — Hardware Orchestration Platform