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.* | |
|---|---|---|
| Runs | In the browser | On the gateway |
| Costs | A property read | A round trip |
| Returns | A value, immediately | A promise — await it |
| Authorised by | Nothing — it is UI state | The server, on every call |
| Exists in | Browser scripts only | Browser and Python scripts |
context is not a security boundaryNothing 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.
| Script | Shape | self.props.x = 1 |
|---|---|---|
| Binding transform, property script | Resolution context — self, root and nodes[id] are live component proxies | Writes 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.
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.self | The component the script belongs to |
context.prop | Shorthand for context.self.props |
context.root | The 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.value | The incoming value, in a binding transform |
context.manifest | The project manifest |
A node
node.id | Its component id |
node.props | Its properties — readable, and writable in a resolution context |
node.updateProps({...}) | Merge properties. The way to write in an event script |
node.custom · node.params | Custom 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.
const line = context.find("Dropdown_Line").props.value;
context.find("Label_Selected").updateProps({ text: `Line ${line}` });
context.findAll("StatusLamp").forEach((n) => n.updateProps({ colour: "grey" }));
The view itself
context.view.id · .name · .slug · .routingPath | Identity |
context.view.width · .height · .enabled | Its design box and state |
context.view.switchView(viewId) | Navigate to another view |
context.params | The view's params. Same object as context.view.params |
context.setParam(key, value) | Set one at runtime |
context.custom | The view's custom props |
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) | |
|---|---|---|
| Cost | None — already in the browser | A round trip |
| Value | Whatever the subscription last pushed | Read at the moment you ask |
| Use it | For a tag the screen is already showing | For 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.client | Client variables — tab-local, read/write, shared across every view and script in this runtime |
context.vars | Gateway variables, read-only: a polled mirror of the server-side set |
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.
const s = context.session;
if (!s.theme.allowThemeToggle) return;
s.theme.mode = s.theme.mode === "dark" ? "light" : "dark";
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.scope | Reaches |
|---|---|
session (default) | Every view in this tab |
view | This view only |
page | This page |
component | One component, named by opts.target |
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 |
| Option | Meaning |
|---|---|
params | View params, as values |
anchorTo | widget (default) or pointer |
side | auto (default) · top · right · bottom · left — a preference the host may flip for room |
align | start · center · end |
sideOffset | Distance from the anchor, in px |
width · height | Content box in px. Unset uses the View's own design size, clamped to the viewport |
showArrow | Draw the arrow back at the anchor. Unset = shown |
dismissible | Close on outside click or Escape. Unset = yes |
group | Mutual-exclusion key — opening closes any live popover in the same group |
context.ui.openPopover("Orders/Detail", {
params: { orderId: row.id },
side: "right",
width: 420,
group: "row-detail", // opening another row's popover closes this one
});
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.last | The previous script or set-tag action's value |
context.ok | Whether it succeeded |
context.error | Its failure, when it did not |
context.results | Every prior action's value, in order |
context.outcomes | Every prior action's { type, ok, value, error } — successes and failures |
context.signal | An AbortSignal, aborted when the widget unmounts or the action times out |
context.store | The 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.
if (!context.ok) {
context.ui.notify({ message: `Write failed: ${context.error?.message}`, severity: "error" });
return;
}
context.ui.notify({ message: `Wrote ${context.last}`, severity: "success" });
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.
// 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.