Skip to main content
Version: 1.0.8

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 context — self, 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 this project's own variables and the global ones
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

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.

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 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.scopeReachesWhere
all (default)Every project: pipelines and every open browserNetwork
projectThis project's pipelines and browsersNetwork
tabEvery handler in this browser tabLocal
viewThis view onlyLocal
componentOne component, named by opts.targetLocal

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.

A filter bar telling the rest of the screen
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

A send with no scope now broadcasts to every project

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 delivered

Both 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 fails

The 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

A type with a space, : or / is refused

Subjects 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
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 });

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.

EventThe script reads
Table Row clickrow
Table Selection changeselectedRows, selectedRowIds
Scheduler Event click, Event change (drag/resize), Event double click 1.0.8event — { 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.8slot — { resourceId, resourceLabel, resourcePath, start, end, startISO, endISO, time, timeISO, durationMinutes }
Form Submit successcontext.submit
Form Submit errorerror, and context.submit
Upload File receivedcontext.file, context.files
Chart Data click, Data double clickcontext.point
Scanner eventscontext.scan or context.camera
Barcode Encode error, Encode recoveredcontext.barcode

The Event Manager shows the same line under each event that carries a payload, with the full text on hover. 1.0.8

Scheduler — Event change: save the moved block
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.8

A 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.

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