Skip to main content
Version: Next

Context API Reference

context is the browser-local surface available to every script that runs in a screen: the view and its components, the live tag values already on screen, the operator's session, and the shell around the view — panels, toasts, popovers.

Its counterpart is system, which is everything that crosses the wire to the server. The split is the rule worth remembering:

context.*system.*
RunsIn the browserOn the gateway
CostsA property readA round trip
ReturnsA value, immediatelyA promise — await it
Authorised byNothing — it is UI stateThe server, on every call
Exists inBrowser scripts onlyBrowser and Python scripts
context is not a security boundary

Nothing on this page is a permission check. context.session.auth.hasRole("Supervisor") tells you what to show; it can never tell you what to allow, because the operator's browser is not where that decision can be trusted. Authorisation belongs in a Gateway script or in the server's own gates. → Client scripts are not a security boundary

Where context exists, and in which shape

There are two shapes, and one difference between them will bite you if you do not know it.

ScriptShapeself.props.x = 1
Binding transform, property scriptResolution contextself, root and nodes[id] are live component proxiesWrites the prop
Widget event action (on click, on change, …)Event context — flatter: self is {id, props, updateProps}Does nothing. Use self.updateProps({x: 1})

Everything else — tag, view, session, client, vars, message, ui, find — behaves the same in both.

The silent one

In an event script, assigning into self.props is a no-op: no error, no change, no clue. If a click handler "does nothing", check this first.

// ❌ in an event action — silently does nothing
context.self.props.label = "Running";
// ✅ everywhere
context.self.updateProps({ label: "Running" });

Every name below is also available bare inside a script — find("Tank") is the same as context.find("Tank"), and self, value, params, view, tag, ui and the rest follow the same rule. Use whichever reads better; the examples here mix both, as real scripts do.


The view and its components

context.selfThe component the script belongs to
context.propShorthand for context.self.props
context.rootThe view's root node
context.nodes[id]Every node in the view, keyed by component id
context.find(name)The node with this name, anywhere in the view — however deeply nested
context.findAll(name)Every node with this name, as an array. [] when there are none
context.valueThe incoming value, in a binding transform
context.manifestThe project manifest

A node

node.idIts component id
node.propsIts properties — readable, and writable in a resolution context
node.updateProps({...})Merge properties. The way to write in an event script
node.custom · node.paramsCustom props and view params carried on the node
node.children.<name>A named child

find searches by the name you gave the component in the Designer, not by its id, and it is the one to reach for: a path like root.children.panel.children.input breaks the moment somebody moves the input into a container.

Read one component, write another
const line = context.find("Dropdown_Line").props.value;
context.find("Label_Selected").updateProps({ text: `Line ${line}` });
Fan out over every component with the same name
context.findAll("StatusLamp").forEach((n) => n.updateProps({ colour: "grey" }));

The view itself

context.view.id · .name · .slug · .routingPathIdentity
context.view.width · .height · .enabledIts design box and state
context.view.switchView(viewId)Navigate to another view
context.paramsThe view's params. Same object as context.view.params
context.setParam(key, value)Set one at runtime
context.customThe view's custom props
A master–detail screen
const row = context.find("Table_Orders").props.selectedRow;
if (!row) return;
context.setParam("orderId", row.id);
context.refreshBinding("view"); // re-run every binding with the new param

Re-running bindings

context.refreshBinding(target?)Re-evaluate bindings now. 'self' (default), 'view', or a component id

Queries refetch too, which is what makes it the right call after a script has changed something on the server that the screen is bound to.

await system.script.runOnGateway("orders.approve", { id });
context.refreshBinding("view"); // the table now shows the new state

Live data

context.tag

The runtime tag object already on the screen — no round trip, because the subscription that feeds the bindings has already delivered it.

const t = context.tag["Line1/Filler/Tank1/Level"];
t.value; // the live value
t.quality; // "Good" | "Bad" | "Uncertain" | "NotFound"
t.meta.writable; // the full static definition is joined on as .meta
t.meta.eng_high;
t.meta.unit;

context.tag and system.tag.read return the same shape, so a script can move between them without rewriting the code around it. The difference is cost and freshness:

context.tag[path]await system.tag.read(path)
CostNone — already in the browserA round trip
ValueWhatever the subscription last pushedRead at the moment you ask
Use itFor a tag the screen is already showingFor a tag the screen is not bound to, or when you need it fresh

A path the screen does not know returns an error string rather than a value, so check quality before acting either way.

Variables

context.clientClient variables — tab-local, read/write, shared across every view and script in this runtime
context.varsGateway variables, read-only: a polled mirror of the server-side set
Client variables — a selection every screen can see
context.client.selectedPlant = "Plant2";
// read back anywhere, including in a binding
// {{client.selectedPlant}}

Client variables are ephemeral and live only in that tab. They are not shared between operators, do not survive a refresh, and are not a security boundary — anything that matters belongs in a gateway variable or a tag.

To write a gateway variable, use system.vars.set, which is permission-gated. context.vars is deliberately read-only: a write that looked local but was not would be the worst of both.

const target = context.vars.dailyTarget; // read: free, local
await system.vars.set("dailyTarget", 4200); // write: gated, server-side

context.session

Who is looking at the screen, on what.

session.auth.isAuthenticated, .id, .username, .email, .firstName, .lastName, .roles, .hasRole(role)
session.device.type (desktop / tablet / mobile), .os, .browser, .userAgent
session.gateway.address, .locale, .timezone
session.theme.mode (light / dark), .themeColor, .allowThemeToggle, .allowThemeColorSelect

session.gateway and session.theme are writable: assigning to one overrides it for the rest of that operator's session, which is how a screen offers a timezone picker or a dark-mode switch.

A theme toggle
const s = context.session;
if (!s.theme.allowThemeToggle) return;
s.theme.mode = s.theme.mode === "dark" ? "light" : "dark";
Show a supervisor-only panel — presentation, not permission
context.find("Panel_Supervisor").updateProps({
visible: context.session.auth.hasRole("Supervisor"),
});

The second example hides a panel. It does not stop anybody reaching what is behind it; the tag write levels and the project's access policy do that, server-side.


context.message

Component-to-component messaging inside the browser.

context.message.send(type, payload?, opts?)Fire every component with a matching On message handler
context.message.onMessage(type, cb)Subscribe imperatively. Returns an unsubscribe function
opts.scopeReaches
session (default)Every view in this tab
viewThis view only
pageThis page
componentOne component, named by opts.target
A filter bar telling the rest of the screen
context.message.send(
"filterChanged",
{ line: context.find("Dropdown_Line").props.value,
shift: context.find("Dropdown_Shift").props.value },
{ scope: "view" },
);

```javascript title="The chart's On message handler for filterChanged" context.setParam("line", payload.line); context.setParam("shift", payload.shift); context.refreshBinding("self");


Prefer a **declarative** On message handler on the receiving component over
`onMessage`. The subscription is owned by the component, is de-duplicated per type, and is torn down
when the widget unmounts — so it cannot leak, and the returned unsubscribe function is only there
for cancelling early. Imperative subscriptions are also unavailable to sandboxed scripts, where
handlers stay declarative by design.

:::note[Not the same as `system.message.send`]
`context.message` is **browser-local**: it never leaves the tab, and the server never sees it.
[`system.message.send`](./system-api.md#systemmessage) is the opposite direction — the gateway
pushing down to browsers. Reaching for the wrong one is the usual reason a handler never fires.
:::

---

## context.ui

The shell around the view. All of it is local to the browser, which is why it lives on `context` and
not on `system`.

### Panels and docks

| | |
|---|---|
| `ui.openPanel(idOrName?, opts?)` | Open a panel. Omit the name to act on the panel the script is in |
| `ui.closePanel(idOrName?)` | Close it |
| `ui.togglePanel(idOrName?, opts?)` | Toggle it. `opts.params` are used when it opens |
| `ui.toggleDock('left' \| 'right' \| 'header' \| 'footer')` | Collapse or expand a docked region |

```javascript
context.ui.openPanel("OrderDetail", { params: { orderId: row.id } });

Notifications

ui.notify({ message, title?, severity?, duration? })A simple toast. Returns its id
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

severity is info (the default), success, warning or error.

context.ui.notify({ message: "Setpoint applied", severity: "success" });
context.ui.notify({ title: "Write rejected", message: e.message, severity: "error", duration: 8000 });

Popovers

ui.openPopover(view?, opts?)Open an anchored popover hosting a View — by id, slug, name, or folder path ("Dashboards/Plant1/Detail"). Returns its instance id
ui.closePopover(id?)Close one. Omit the id to close the popover the script is in, falling back to the most recent
ui.togglePopover(view?, opts?)Close if open, open otherwise
OptionMeaning
paramsView params, as values
anchorTowidget (default) or pointer
sideauto (default) · top · right · bottom · left — a preference the host may flip for room
alignstart · center · end
sideOffsetDistance from the anchor, in px
width · heightContent box in px. Unset uses the View's own design size, clamped to the viewport
showArrowDraw the arrow back at the anchor. Unset = shown
dismissibleClose on outside click or Escape. Unset = yes
groupMutual-exclusion key — opening closes any live popover in the same group
A detail popover from a table row
context.ui.openPopover("Orders/Detail", {
params: { orderId: row.id },
side: "right",
width: 420,
group: "row-detail", // opening another row's popover closes this one
});
A popover has to point at something

Only a DOM event knows where the click was. A script running in a widget event gets the firing element's box; anywhere else it falls back to the last click, then to 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 is a sequence of actions, and a script in one can see what happened before it.

context.lastThe previous script or set-tag action's value
context.okWhether it succeeded
context.errorIts failure, when it did not
context.resultsEvery prior action's value, in order
context.outcomesEvery prior action's { type, ok, value, error } — successes and failures
context.signalAn AbortSignal, aborted when the widget unmounts or the action times out
context.storeThe Designer store. An escape hatch; prefer a named API

Notify, navigate, panel and dock actions do not count as steps — only the ones that produce a value do, which keeps last meaning what you expect.

Report what the previous action actually did
if (!context.ok) {
context.ui.notify({ message: `Write failed: ${context.error?.message}`, severity: "error" });
return;
}
context.ui.notify({ message: `Wrote ${context.last}`, severity: "success" });
Summarise a whole chain
const failed = context.outcomes.filter((o) => !o.ok);
context.ui.notify({
message: failed.length
? `${failed.length} of ${context.outcomes.length} steps failed`
: "All steps completed",
severity: failed.length ? "warning" : "success",
});

Pass context.signal to anything that can outlive the click:

const res = await fetch(url, { signal: context.signal });

Worked example — a setpoint form

Everything on this page, in the shape a real screen uses it.

On click: validate locally, write server-side, report either way
// 1. Read the form — local, free, no await.
const raw = context.find("Input_Setpoint").props.value;
const sp = Number(raw);

// 2. Validate against the tag's OWN limits, which the screen already has.
const tag = context.tag["Line1/Filler/SpeedSP"];
if (!Number.isFinite(sp) || sp < tag.meta.eng_low || sp > tag.meta.eng_high) {
context.ui.notify({
title: "Out of range",
message: `Setpoint must be between ${tag.meta.eng_low} and ${tag.meta.eng_high} ${tag.meta.unit}`,
severity: "error",
});
return;
}

// 3. The write is the one thing that crosses the wire — and the one thing gated.
try {
await system.tag.write("Line1/Filler/SpeedSP", sp);
} catch (e) {
context.ui.notify({ title: "Write rejected", message: e.message, severity: "error" });
return;
}

// 4. Tell the rest of the screen, and leave a trace the operator can see.
context.client.lastSetpoint = sp;
context.message.send("setpointChanged", { value: sp }, { scope: "view" });
context.ui.notify({ message: `Setpoint set to ${sp} ${tag.meta.unit}`, severity: "success" });

Note what is not in it: no permission check. The operator's write level decides whether that write lands, server-side, and the catch is how the screen finds out.

Next

Client scripts · System API reference