Table Triggers
A table trigger reacts to a row event on a source table by running a
Lua fire(row, prev, event) that returns a list of operations — and
the engine applies each one to another table: append a point, update a
state row, delete a key. One trigger can write to many tables of any kind
at once — alarms, audit trails, derived events, materialized rollups. It
is the sibling of Calculated Fields: both drain a
table and run Lua per row, but a calculated field enriches the row in
place, while a trigger writes to other tables.
TableTrigger(vehicle_events) → TriggerRules(vehicle_events)
The node drains the source and does its own writes; there is no
TableSink. Definitions live as data in x_triggers, fetched fresh
every run — a save is a hot reload, no restart, no republish.
A definition is name + triggers + events + script
| part | meaning |
|---|---|
| name | a name for the rule |
| triggers | run only when one of these columns arrives (raw) or changes (a computed sibling); empty = every row |
| on events | run only on insert / update / delete; empty = all |
| script | the Lua below |
There is no single target: each op the script returns carries its own table, so one trigger fans out wherever it needs to.
Writing the script
Define fire(row, prev, event) and return an op, a list of ops, or
nil for nothing. Each op is a table with a verb and the table it acts on.
You may declare helper functions and constants alongside fire (a bare
body is wrapped for you):
function fire(row, prev, event)
if row.rpm ~= nil and row.rpm > 5000 then
return {
{ op = 'insert', table = 'alarm_events',
row = { entity = row.entity, ts = row.ts, level = 'high', msg = 'overrev' } },
{ op = 'update', table = 'vehicle_state',
set = { overrev = 1 }, where = { xid = row.entity } },
}
end
end
The three verbs
| verb | needs | becomes | typical table kind |
|---|---|---|---|
insert | row = {…} | INSERT INTO t (cols…) VALUES (…) | timeseries / rows |
update | set = {…}, where = {…} | UPDATE t SET … WHERE … | rows |
delete | where = {…} | DELETE FROM t WHERE … | rows / kv |
The engine resolves each verb by the target table's kind — a kv table
matches on where = { key = … } and a rows table on
where = { xid = … }, an insert into a timeseries appends a point.
The node itself stays kind-agnostic: it only builds valid SQL. where
clauses are AND-joined; nil cells in a row are dropped (sparse,
schema-on-write). Returning a single op instead of a one-element list is
accepted as a convenience. No op may target the trigger's own source
table (that would feed its own drain).
prev, event and state
prev, event and state behave exactly as in a calculated field:
-
previs this entity's previous row (its trigger columns), so a crossing fires once instead of on every row above the line:function fire(row, prev, event)if (prev.rpm or 0) <= 5000 and row.rpm > 5000 thenreturn { op = 'insert', table = 'alarms',row = { entity = row.entity, ts = row.ts, msg = 'crossed 5000' } }endend -
eventis"insert"(first sighting) /"update"/"delete".prevandeventadvance only forward in time, so a replay or the pipeline's own feed echo never fires a trigger twice. -
stateis a per-entity table that survives across runs (persisted inx_trigger_state).
The console
Under each table the sidebar has a Triggers section: New trigger on its menu, and each trigger an Edit / Delete menu (delete stops the trigger; rows it already wrote stay). The editor is a workspace tab: name, trigger columns, on-events, and the Lua editor.
Everything is plain SQL on x_triggers, so automation can do the same:
INSERT INTO x_triggers
(name, source_table, script, version, enabled, trigger_cols, on_events)
VALUES ('overrev', 'vehicle_events',
'return row.rpm and row.rpm > 5000 and { op=''insert'', table=''alarms'', row={entity=row.entity, ts=row.ts, msg=''overrev''} } or nil',
1, true, '["rpm"]', NULL);
A trigger needs a published pipeline whose TableTrigger names the source
table and whose TriggerRules targets it:
{
"version": 1, "name": "vehicle-triggers",
"nodes": [
{"id": "trig", "type": "TableTrigger", "config": {"table": "vehicle_events"}},
{"id": "rules", "type": "TriggerRules", "config": {"table": "vehicle_events"}}
],
"edges": [ {"from": "trig.rows", "to": "rules.in"} ]
}