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 this project's own variables and the global ones |
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
In a LiveView the mirror holds only the gateway variables the project's views and client scripts
name — a vars.<name> binding, vars["<name>"] or system.vars.get("<name>") — and it works for
operators and anonymous visitors of a public project. Changed in 1.0.8 Before
1.0.8 a vars.<name> binding stayed empty for anyone not signed in to the Studio.
→ system.vars
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 Changed in 1.0.8
Messaging by dotted subject (line1.motor.start) — inside the browser, or over the
message bus to pipelines and every other open screen.
context.message.send(subject, payload?, opts?) | Send. Every handler whose pattern matches receives it |
context.message.request(subject, payload?, opts?) | Network only: send and wait for the first reply — a pipeline's Message Reply |
context.message.onMessage(pattern, cb) | Listen imperatively. cb(payload, meta); returns an unsubscribe function |
opts.scope | Reaches | Where |
|---|---|---|
all (default) | Every project: pipelines and every open browser | Network |
project | This project's pipelines and browsers | Network |
tab | Every handler in this browser tab | Local |
view | This view only | Local |
component | One component, named by opts.target | Local |
A network send returns a promise; a local one returns nothing. request takes opts.timeoutMs
(default 5000, 100–60000) and rejects on a timeout, when nobody is listening, or when the pipeline
rejects it — with the caught error on err.details when its Message Reply was fed by a Catch node.
A receiving pattern may use * for one segment and > for the rest: line1.*.start, line1.>.
meta carries subject (the exact one that matched), scope, senderId, source (kind is
pipeline, designer or script for a network message) and hops.
context.message.send(
"filter.changed",
{ 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 filter.changed"
context.setParam("line", payload.line);
context.setParam("shift", payload.shift);
context.refreshBinding("self");
```javascript title="Ask a pipeline"
const s = await context.message.request("motor.status", { id: "m1" }, { scope: "project" });
context.find("Label_State").updateProps({ text: s.state });
Inside an On message handler, payload, subject, senderId, source and messageHops are
bare names. messageType is kept as another name for subject, so a handler written for 1.0.7
still reads a value.
Prefer a declarative On message handler on the receiving component over
onMessage. The subscription is owned by the component, is de-duplicated per pattern, 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.
Views are migrated when they are opened; script code is not rewritten. Four things in a 1.0.7 script break — check each one after upgrading. → What changed
context.message.send("reload", p) stayed in the tab in 1.0.7. From 1.0.8 the default scope is
all: it goes to every project on the server, to every open browser listening on reload, and
to every pipeline with a Message In on it. Pass { scope: "tab" } or { scope: "view" } for a
message that means something only on this screen.
scope: "session" and "page" are no longer deliveredBoth meant "this tab" in 1.0.7. A script passing either is not delivered — the browser console logs
a warning and nothing fires. Use "tab". (Send message actions with those scopes are migrated to
This browser tab only automatically.)
system.message.send(..., {scope: "user"}) now failsThe bus cannot deliver to a single user, so the call raises (Python) or rejects (JavaScript) with
an error saying so. It is deliberately not widened to project, which would show a message meant
for one person to everyone. Send with {scope: "project"} and put the user in the payload for the
receiver to check. → system.message
: or / is refusedSubjects are letters, digits, _ and - between dots. system.message.send(...) and a network
context.message.send(...) reject "may only contain letters, digits, '_' and '-'"; a local send
or an onMessage only logs that warning to the console. A migrated handler for Motor Start now
listens on Motor_Start — send that, or a dotted subject at both ends.
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 |
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 });
What an event hands the script
Some events carry data — the clicked row, the dragged scheduler block, the scanned code. Seven
payload keys are bound as bare names ahead of an event script: value, error, row,
selectedRows, selectedRowIds, event and slot. Everything else stays on context.
| Event | The script reads |
|---|---|
| Table Row click | row |
| Table Selection change | selectedRows, selectedRowIds |
| Scheduler Event click, Event change (drag/resize), Event double click 1.0.8 | event — { id, resourceId, label, start, end, category }, times in epoch ms. On a change, start and end are the new ones |
| Scheduler Empty slot select 1.0.8 | slot — { resourceId, resourceLabel, resourcePath, start, end, startISO, endISO, time, timeISO, durationMinutes } |
| Form Submit success | context.submit |
| Form Submit error | error, and context.submit |
| Upload File received | context.file, context.files |
| Chart Data click, Data double click | context.point |
| Scanner events | context.scan or context.camera |
| Barcode Encode error, Encode recovered | context.barcode |
The Event Manager shows the same line under each event that carries a payload, with the full text on hover. 1.0.8
await system.db.runNamedQuery("move_job", {
id: event.id,
start: new Date(event.start).toISOString(),
end: new Date(event.end).toISOString(),
});
event is bare from 1.0.8A Scheduler script had to write context.event before 1.0.8 — a bare event was a
ReferenceError, while a Table's row was bare. Both forms work now. slot.resourceId and
slot.time are deliberately not bare names of their own; read them through slot.
A bare name is declared ahead of every event script, so a script that declares its own const event or const slot is a syntax error. Rename the local.
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.