Skip to main content
Version: Next

Client Scripts

Client scripts are JavaScript that runs in the operator's browser. They are for screen logic: validating input, computing display values, coordinating components, deciding what to show.

They are used as event actions and as project-level script resources.

The system API in the browser

The same surface as everywhere else, with every call returning a promise:

const r = await system.tag.read("Line1/Filler/Motor1/Speed");
if (r.quality !== "Good") {
context.ui.notify({ message: "Speed unavailable", severity: "warning" });
return;
}
await system.tag.write("Line1/Filler/Motor1/SpeedSetpoint", Math.round(r.value * 0.9));

System API reference

Screen context

Client scripts additionally get context, the screen-local surface. What follows is the tour; the per-member detail, the two context shapes and worked examples are in the Context API reference.

The screen

context.selfThis component — .id, .props, .updateProps({...}). context.prop is shorthand for self.props.
context.root · context.nodesThe view's root node, and every node in the view keyed by id.
context.find(name) · context.findAll(name)Find a node anywhere in the view by name; findAll returns an array.
context.view.id, .name, and .switchView(viewId).
context.params · context.setParam(key, value)The view's params, and a runtime setter.
context.project · context.customProject manifest data, and the view's custom props.
context.refreshBinding(target?)Re-evaluate bindings now'self' (default), 'view', or a component id. Queries refetch too.

Live data

context.tag["Plant/Line1/Temp"]The runtime tag object — .value, .quality, and the full static definition joined on as .meta (.meta.writable, .data_type, .address, .eng_low, .enable_history).
context.clientClient variables — tab-local read/write state shared across every view and client script in this runtime. context.client.selectedPlant = "Plant2", read back as {{client.selectedPlant}}. Ephemeral, and not a security boundary.
context.varsGateway variables, read-only — a polled mirror of the server-side set. To write one, use system.vars.set(...), which is permission-gated.

The operator's session

context.session describes who is looking at the screen, on what:

session.auth.isAuthenticated, .username, .roles, and .hasRole(role).
session.device.type (desktop / tablet / mobile), .os, .browser.
session.gateway.address, .locale, .timezone.
session.theme.mode (light / dark), .themeColor, and whether the operator may change either.

Writing to session.gateway.timezone or session.theme overrides the value for the rest of that operator's session — which is how a screen offers a timezone or a theme switch.

session.auth is for presentation, not permission

Reading hasRole("Supervisor") to decide what to show is fine. Reading it to decide what to allow is not — see below.

Talking to the rest of the screen

context.message.send(type, payload?, opts?)Fire every component with a matching On message handler. opts.scope is session (the whole tab, the default), view, page, or component with opts.target.
context.message.onMessage(type, cb)Subscribe imperatively; returns an unsubscribe. It is owned by this component and de-duplicated per type, so it cannot leak — calling the returned function is only for cancelling early.

context.message is local to the browser. To push from the server down to browsers, use system.message.send — a different direction and a different scope.

Panels, notifications and popovers

context.ui controls the shell around the view. It lives on the context rather than on system.* because all of it is local to the browser.

ui.openPanel(idOrName, opts?) · ui.closePanel() · ui.togglePanel()Omit the name and it acts on the panel the script is running in.
ui.toggleDock('left' | 'right' | 'header' | 'footer')Collapse or expand a docked region.
ui.notify({ message, title?, severity? })A simple toast.
ui.notify(slotName, payload?, opts?)Render an author-defined notification View instead. Returns its instance id, or undefined when no such slot is configured.
ui.dismissNotification(id?)Omit the id to dismiss the notification the script is running inside.
ui.openPopover(view, opts?) · ui.closePopover(id?) · ui.togglePopover(view, opts?)An anchored popover hosting a View, referenced by id, slug, name or folder path.

A popover has to be positioned, and only a DOM event knows where. A script running in a widget event gets the firing element's box; anywhere else it falls back to the last click, then the centre of the viewport. That is deliberate — a popover raised by a tag change has no on-screen cause to point at.

Inside an event chain

An event action's script gets a few more things, because an event is a sequence of actions:

context.last · context.ok · context.errorThe previous script or set-tag action's value, whether it succeeded, and its failure. Notify, navigate, panel and dock actions do not count.
context.results · context.outcomesEvery prior action's value, and every prior action's { type, ok, value, error } — successes and failures — in order.
context.signalAn AbortSignal. Pass it to fetch({ signal }); it aborts when the widget unmounts or the action times out.
context.storeThe designer store, for advanced use.

Patterns

Validate before writing

const sp = Number(context.find("Input_Setpoint").props.value);
if (!Number.isFinite(sp) || sp < 0 || sp > 3000) {
context.ui.notify({ message: "Setpoint must be between 0 and 3000 RPM", severity: "error" });
return;
}
await system.tag.write("Line1/Filler/SpeedSP", sp);
context.ui.notify({ message: "Setpoint applied", severity: "success" });

Confirm consequential actions

Confirmation is not something a script asks for — it is an option on the action. Tick Confirm on a Set tag or Set variable action and give it a message, and the operator is asked before the write happens.

That is deliberate: a confirmation a script raises could be skipped by a script, and the point of one is that it cannot be.

Coordinate components

context.message.send("filterChanged", { line: context.find("Dropdown_Line").props.value });

Every component with an On message handler for filterChanged fires. Narrow the reach with { scope: "view" } when the message means something only on this screen.

Hand off to the gateway

const batchId = context.find("Table_Batches").props.selectedRow?.id;
const res = await system.script.runOnGateway(
"batches.releaseBatch",
{ batchId },
{ idempotencyKey: `release-${batchId}` },
);
if (!res.ok) {
context.ui.notify({ message: res.error, severity: "error" });
return;
}
context.ui.notify({ message: "Batch released", severity: "success" });

Client scripts are not a security boundary

Everything a client script does is subject to server-side authorization:

  • Tag writes are gated by the tag's writable flag and write level.
  • Data access is gated by the project's access policy and the operator's runtime roles.
  • Capability scopes gate the system API.

Never put an authorization decision in a client script. "Only supervisors may release a batch" belongs in a Gateway script, where the operator cannot skip it. Hiding the button is good usability; it is not the control.

Async and errors

Every system call returns a promise. Forgetting await is the most common client-script bug — the script continues with a promise object instead of a value, and the failure is silent.

// ❌ writes "[object Promise]"
const r = system.tag.read("Line1/Tank1/Level");

// ✅
const r = await system.tag.read("Line1/Tank1/Level");

Handle failures explicitly:

try {
await system.tag.write("Line1/Filler/SpeedSP", sp);
context.ui.notify({ message: "Applied", severity: "success" });
} catch (e) {
context.ui.notify({ message: `Write rejected: ${e.message}`, severity: "error" });
}

A rejected write is normal — the tag may be read-only, or the operator's level may be below its write level. Say so, rather than leaving the screen silently unchanged.

Performance

  • Client scripts run on the operator's panel, which is usually far weaker than your laptop.
  • Batch tag reads (readAll) rather than looping.
  • Do not poll in a script. Bind, and let the subscription push.
  • Move heavy computation to the gateway; the browser's job is rendering.

Debugging

  • The browser developer console shows script errors and console.log output.
  • context.ui.notify is useful for tracing a flow an operator will actually see.
  • If a write silently does nothing, check the tag's writable flag and write level first — that is the answer far more often than a script bug.

Next

Context API reference · The script sandbox