Event Actions
An event action is what happens when an operator interacts with a component. Configure them in the Event Manager: pick an event, add one or more actions, and they run in order.
Events
Available on every component
| Event | Fires on |
|---|---|
click | Single click |
dblclick | Double click |
contextmenu | Right click — see the note below |
mouseenter | Pointer enters |
mouseleave | Pointer leaves |
contextmenu does not fire on the Designer canvasRight-click is the Designer's own gesture: on the canvas it opens the Event Manager for the
component under the cursor, and the event is consumed there. Your configured contextmenu actions
run at runtime only — in preview and in LiveView.
Test right-click in preview. Configuring it, right-clicking the canvas, seeing the Event Manager and concluding it is broken is the usual first encounter with this.
Two further things worth knowing:
- The browser's own menu still appears. The runtime fires your actions but does not suppress the
native context menu. If you are building a custom right-click menu, suppress it yourself in a
scriptaction. - It applies to every component, in every layout mode — the behaviour lives in the shared widget wrapper, not in individual widgets.
Input components
| Event | Fires on |
|---|---|
change | The value changed |
submit | Submitted |
focus | Received focus |
blur | Lost focus |
Table
| Event | Payload |
|---|---|
rowClick | The clicked row |
rowDoubleClick | The row |
selectionChange | The current selection |
rowEdit | The edited row |
rowDelete | The row to delete |
Form
| Event | Payload |
|---|---|
submitSuccess | The write result, under submit |
submitError | The error |
Upload
| Event | Payload |
|---|---|
fileReceived | File metadata, under file / files |
Events on a part of a widget New in 1.0.0
Some widgets have addressable parts, and a part can carry its own events. Wiring "click this 3D viewer" is rarely what you meant; "click the robot's eye" is.
| Widget | Its parts | Events they carry |
|---|---|---|
| 3D Viewer | Objects in the model | objectClick · objectDoubleClick |
| Table | Columns | cellClick · cellDoubleClick |
A wired part appears as an extra section in the Event Manager's list, below the widget's own events — not as a mode the dialog switches into, so opening it shows at a glance what the widget does and what each of its parts does. Everything else works unchanged: the action cards, chaining, and Only when… all behave on a part exactly as they do on the widget.
Adding one
Open the Event Manager and use the Add object… / Add column… field at the foot of the list.
A part is addressed by the same string a binding row targets — a 3D node name or index path, a Table column id — so a part never acquires a second name that could drift from the first.
The 3D Viewer publishes its node list, so the field offers them as suggestions. The Table does not publish a column list at runtime, so you type the column id — the same id used everywhere else in the Table's configuration. Typing is allowed either way: in the editor the model may not be loaded yet, and you may already know the name.
Removing a part removes every event on it.
Only address spaces you can enumerate at design time get sections. A query can produce thousands of
rows, so branching on a row stays a guard — rowClick with an Only when… on the row payload.
What a click resolves to
Both resolutions are worth knowing, because both fail silently when the address is wrong — the event simply never fires.
- 3D Viewer. A click reports the mesh the ray actually hit, which for a multi-primitive node is a
leaf like
Eye_L_2rather than theEye_Lyou wired. The hit is walked up to the deepest ancestor that has events on it, so wiring the name you see in the model tree works. - Table. The column comes from the cell's own column marker, not from a handler threaded through every cell — which is what keeps rows from re-rendering on every sort.
Actions
navigate
Go to another view, optionally with parameters.
navigate → view: "equipment-detail"
params: { tagPath: row.path }
The foundation of drill-down: an overview passes the selected equipment's path to a single parameterised detail view.
setTag
Write a value to a tag.
setTag → tag: "Line1/Filler/Motor1/SpeedSetpoint"
value: Slider.value
Subject to the tag's writable flag and write level,
enforced server-side. A modified client cannot bypass it.
A button that starts equipment should ask first. Pair setTag with a confirmation, and set a write
level so only sufficiently privileged operators can reach it.
setClientVar
Set a client-side variable — screen-local state that no other session sees.
setClientVar → name: "selectedLine"
value: Dropdown.value
Use it for selections, filters and UI state. For anything shared across sessions, use an internal tag; for anything shared across pipelines, use a variable.
script
Run a client script — JavaScript in the browser, with the full system API.
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", r.value * 0.9);
For anything transactional or long-running, call a Gateway script instead — it runs server-side, so a browser disconnect cannot strand an open transaction. → Gateway scripts
notify
Show the operator a message.
notify → message: "Setpoint applied"
severity: info | success | warning | error
sendMessage
Send a message to other components. Any component with a matching "on message" handler receives it.
sendMessage → type: "refreshDashboard"
payload: { line: "L1" }
This is local, in-browser messaging. To push from the gateway down to browsers — from a
pipeline or a Gateway script — use system.message.send, which reaches all sessions, one project,
or one user.
refreshBinding
Re-evaluate a query binding on demand — a refresh button, or after a write that changes what a table should show.
panel / dock
Open, close or toggle a panel or dock region — detail drawers, side panels, popovers.
login / logout
Trigger the LiveView runtime login or logout flow, for projects bound to a User Database.
Chaining actions
Actions on one event run in order:
Button "Apply recipe" → click
1. script validate the entered values
2. setTag write the setpoint
3. notify "Recipe applied"
4. refreshBinding refresh the recipe table
Keep chains short. Once the logic outgrows a few steps, move it into a single script action —
or a Gateway script — where it can be read as one piece.
Running an action conditionally
Every action carries an optional guard, edited from the Only when… chip in its header. It is a
comparison, not an expression: a source on the left, one of is / is not / is set / is empty,
and a value on the right.
Vertical Menu → Menu item click
1. Popover only when {{item.key}} is help
2. Sign out only when {{item.key}} is logout
This is what lets ONE handler serve several cases. A menu's item click is the clearest example — the event belongs to the whole widget, so without a guard every action would fire for every item — but the same applies to a table row, a chart point, or any chain that has to behave differently depending on a tag or a prior result.
The left side accepts anything a binding does: the event's own payload
({{item.key}}, {{row.status}}, {{submit.rows}}), a widget property (self.props.mode), a tag,
a view param, a variable, or a chain result (ok, error.message, results.0.id). The picker
offers exactly what applies to the event you are editing.
The right side is compared as typed unless you wrap it in {{ }}. That matters: error, ok,
last, results and outcomes are also the names of chain results, and a plain error here means
the word, not the previous action's error object.
Values are compared as text, so a tag holding 42 matches a typed 42. is set / is empty treat
an empty string, false, 0, null and undefined as empty — so a bound boolean tag behaves the
way you would expect rather than the way JavaScript would.
A guarded-out action is skipped completely: it records no result and raises no error, exactly as if it were not in the list for that particular firing.
Two consequences worth knowing:
- A later action's On success / On error condition refers to the last action that actually ran, not necessarily the one directly above it.
- A guarded-out
notifydoes not count as "the author reported this", so the built-in failure message can still appear if an earliersetTagorscriptfailed.
Patterns
Drill-down from a table
Table.rowClick → navigate("equipment-detail", { tagPath: row.path })
Guarded start
Button.click → setTag("Line1/Conv1/StartCmd", true)
with Confirm ticked: "Start conveyor 1?"
Confirmation is an option on the action, not something a script asks for. A confirmation a script raises could be skipped by a script, and the point of one is that it cannot be.
Filter that drives everything on the screen
Dropdown.change → setClientVar("line", value)
→ sendMessage("filterChanged", { line: value })
Server-side work from a button
Button.click → script:
const res = await system.script.runOnGateway("reports.generateShiftReport",
{ shift: "A" },
{ idempotencyKey: "shift-A-" + today });
context.ui.notify({ message: `Report ${res.id} generated`, severity: "success" });
The idempotency key means a double-click produces one report, not two.
Security
- Every action a screen can perform is still subject to server-side authorization. Client-side logic decides what to offer; the server decides what is allowed.
- Tag writes are gated by
writableand the write level. - Runtime operators are authorised by their runtime roles, independently of platform permissions.
- Consequential actions are recorded in the audit journal.
Design screens so operators do not see actions they cannot perform — but never rely on hiding a button as the control.