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.
Open it from a component's right-click menu on the canvas — Edit Events. Its left rail has two tabs: Events, the component's own events, and Message, the handlers that run when a message arrives.
Events
Each event in the list carries a muted line saying what it hands a script 1.0.8
— row, event, context.point — with the full text on hover and in the event's parameters card
as In a script: …. It is written as the identifier you actually type, which is not uniform: a few
payload keys are bound as bare names ahead of an event script, and the rest stay on context.
| Bare name | Comes from |
|---|---|
value | Input changes |
error | A failed action, Form submitError |
row | Table rowClick |
selectedRows · selectedRowIds | Table selectionChange |
event 1.0.8 | Scheduler eventClick, eventChange, eventDoubleClick |
slot 1.0.8 | Scheduler slotSelect |
Everything else is reached through context — context.point on a chart click, context.file on
an upload, context.submit on a form. An event with no payload line simply has not had its payload
written down yet.
event and slot are now bareChanged in 1.0.8 Before 1.0.8 a Scheduler script had to write context.event.id,
while a Table script wrote row.id, and a bare event.id threw a ReferenceError. Both spellings
work now. slotSelect also carries resourceId and time; those are deliberately not bare (a
script declaring its own time would break), so reach them as slot.time or context.time.
Available on every component
| Event | Label | Fires on |
|---|---|---|
click | Click | Single click |
dblclick | Double click | Double click |
contextmenu | Right click | Right click — see the note below |
mouseenter | Mouse enter | Pointer enters |
mouseleave | Mouse leave | Pointer leaves |
contextmenu does not fire on the Designer canvasRight-click is the Designer's own gesture: on the canvas it opens the component's context menu
(Edit Events, Arrange, Copy, Delete …), and the event is consumed there. Your configured
contextmenu actions run at runtime only — in preview and in LiveView.
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 Run script action.
- 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 | row — the clicked row |
rowDoubleClick | The row |
selectionChange | selectedRows and selectedRowIds |
rowEdit | The edited row |
rowDelete | The row to delete |
rowReorder | rowIds, movedRowId, fromIndex, toIndex |
cellClick · cellDoubleClick | Per column — see events on a part |
Form
| Event | Payload |
|---|---|
submitSuccess | context.submit — the submitted values |
submitError | error (bare), plus context.submit |
Upload
| Event | Payload |
|---|---|
fileReceived | context.file and context.files |
Other widgets
Charts (dataClick, dataDblClick with context.point, legendSelect), the Scheduler, Tree,
Tag Browser, File Manager, Map, Video Player, the menus, and the QR/Barcode widgets and scanner have
events of their own. Each widget's page in the component reference
lists them.
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. On a 3D Viewer you can also right-click the object on the canvas and pick Edit Events for "<part>".
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
The Add action menu is split into four groups 1.0.8:
| Group | Actions |
|---|---|
| Logic & data | Run script · Set tag · Set variable · Refresh binding |
| Screens & layout | Navigate · Popover · Panel · Dock |
| Feedback & messaging | Notification · Send message |
| User session | Profile · Sign in · Sign out |
Picking Notification adds two cards at once: one that runs On success and one that runs
On error, whose message defaults to {{error.message}}.
Most actions have a Require confirmation before … checkbox. Ticked, the operator is asked first; leave the message blank for a sensible default (Write these tags?, Sign out?). Cancelling stops the whole chain, not just that action.
Run 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
Set tag
Write one or more tags. The writes sit in a Tag writes table — + Add tag on its title row adds one — with a Mode per row: Set, Toggle or Pulse (write, then restore after a duration, 200 ms by default).
Set tag → Tag writes:
Line1/Filler/Motor1/SpeedSetpoint Set {{self.props.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. Tick Require confirmation before writing, and set a write level so only sufficiently privileged operators can reach it.
Set variable
Set a variable by key. The Scope decides who sees it:
| Scope | Behaviour |
|---|---|
| Client — tab-local, instant | Screen-local state no other session sees. Read it as client.<key>. |
| Gateway — shared (system.vars) | A gateway variable of this project, shared by every session, script and pipeline in it — or by every project, if the name is global. |
Mode is Set or Toggle. Use client variables for selections, filters and UI state; for anything shared, use a gateway variable or an internal tag.
Refresh binding
Re-evaluate bindings on demand — a refresh button, or after a write that changes what a table should show. The target is This widget, Every binding in this view, or Specific components. A query-bound Table re-fetches its pages too 1.0.8.
Navigate
Switch to view — another view, optionally with parameters — or Open URL, optionally in a new tab.
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.
Popover
Open, Toggle or Close a view as a popover anchored to This widget or the Cursor, on a chosen side (Auto (most room) by default). Close and Toggle act on The most recent popover or This popover (self).
Panel / Dock
Panel opens, closes or toggles a panel. Dock toggles, collapses or expands a docked region — Header, Footer, Left sidebar or Right sidebar.
Notification
Show the operator a message. When: Always, On success (previous action) or On error (previous action). Kind: Simple message (toast) with a severity of Info, Success, Warning or Error, or Custom view to show a view as the notification.
Send message Changed in 1.0.8
Send a message on the message bus. Widgets with a matching handler receive it — and, depending on the scope, so do other views, other browsers and pipelines.
| Field | What it does |
|---|---|
| Subject | A dotted name, e.g. line1.motor.start. Letters, digits, _ and - per segment; wildcards are for receiving only. |
| Scope | Who can hear it — see below. |
| Mode | Network scopes only: Send — don't wait, or Request — wait for a reply. |
| Timeout | For a request. 5000 ms by default, 100–60000. |
| Reply to | For a request: a client variable that receives the reply, so a widget bound to client.<name> shows it. The reply is also the action's result (last). |
| Payload | Key/value fields, + Add field on the table's title row. A value can be literal, a {{token}}, or a property. |
| Scope | Reaches |
|---|---|
| All projects (network) — the default | Every project on the server: other browsers, and pipelines listening on the subject |
| This project (network) | This project's browsers and pipelines only |
| This browser tab only | This tab |
| This view only | This view |
| A specific component | One component, picked as Target |
A network message is limited to 256 KB of payload, and a chain of messages that triggers further messages is cut off after 8 hops, so two handlers answering each other cannot loop forever.
→ Message bus · Messages from scripts
In 1.0.7 a Send message action had a free-text Type and stayed inside the browser. On upgrade, views are migrated when they are first opened:
- The type becomes the subject, with every character a subject cannot hold turned into
_:Motor Start→Motor_Start. Handlers are renamed the same way, so a pair that matched still matches. - The scopes Whole session and This page (tab) — and no scope at all — become This browser tab only, which is what they always did.
{{messageType}}in a handler's actions still works; it is the same value as{{subject}}.
Script code is not rewritten. A script that sends with a type containing a space, : or /, or
with scope: "session" / "page", needs editing — and context.message.send(...) with no
scope now broadcasts to every project instead of staying in the tab. See
context.message and
What changed from 1.0.7.
On message
On the Event Manager's Message tab, type a subject pattern and Add message handler. The handler is listed as On message "<pattern>" and takes actions like any event.
| Pattern | Matches |
|---|---|
line1.motor.start | Exactly that subject |
line1.*.start | * is one segment |
line1.> | > is one or more trailing segments, and must come last |
Its actions can use {{payload}}, {{subject}}, {{senderId}} and {{source}}.
Profile / Sign in / Sign out
LiveView only, and inert in the Designer preview. Sign in and Sign out trigger the LiveView runtime login and logout, for projects bound to a User Database. Profile opens the signed-in operator's own account — photo, display name and password — and asks the viewer to sign in when nobody is.
Chaining actions
Actions on one event run in order:
Button "Apply recipe" → click
1. Run script validate the entered values
2. Set tag write the setpoint
3. Notification "Recipe applied"
4. Refresh binding refresh the recipe table
Keep chains short. Once the logic outgrows a few steps, move it into a single Run 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 Notification does not count as "the author reported this", so the built-in failure message can still appear if an earlier Set tag or Run script failed.
Patterns
Drill-down from a table
Table.rowClick → Navigate("equipment-detail", { tagPath: {{row.path}} })
Guarded start
Button.click → Set tag Line1/Conv1/StartCmd = true
with Require confirmation 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 → Set variable client "line" = {{value}}
→ Send message "dashboard.filter" scope: This view only { line: {{value}} }
Server-side work from a button
Button.click → Run 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.