# Expression Trigger for Template Scripts and Alarms — Design **Date:** 2026-05-16 **Status:** Approved (brainstorming) — implementation plan to follow ## Context Template scripts and template alarms can only be triggered by single-attribute conditions. Scripts support `Interval`, `ValueChange`, `Conditional` (`{attributeName, operator, threshold}` — one attribute, numeric compare), and `Call`. Alarms support `ValueMatch`, `RangeViolation`, `RateOfChange`, and `HiLo` — all single-attribute. There is no way to trigger on a relationship between *multiple* attributes (e.g. "speed is high *and* mode is Run"). This design adds an **Expression trigger**: a user-supplied read-only boolean C# expression, evaluated whenever an instance attribute updates, that fires the script / activates the alarm when it returns true. It generalizes the existing single-attribute `Conditional` trigger. ### Decisions taken during brainstorming - The trigger is a **read-only boolean expression** — no `External`/`Database`/ `Notify`/`CallScript` side effects. It must be cheap and safe to run on every attribute update. - **Scripts fire edge-triggered** — once per `false→true` transition. - **Alarms are level-based** — active while the expression is true, clear when false (consistent with all existing alarm trigger types). - **Evaluation approach B** — compile against a *restricted read-only globals type*, so read-only is enforced, not merely conventional. Reuses the existing Roslyn compilation pipeline. ## Design ### 1. Trigger model & storage - **Scripts:** `TemplateScript.TriggerType` (`string?`) gains the value `"Expression"`. `TriggerConfiguration` JSON is `{ "expression": "" }`. - **Alarms:** `AlarmTriggerType` enum gains a member `Expression`. `TriggerConfiguration` JSON is the same `{ "expression": "" }`. - The expression is a bare C# boolean expression (no `return` keyword — Roslyn scripting returns the trailing expression's value), e.g. `Attributes["Speed"] > 1000 && (string)Attributes["Mode"] == "Run"`. - Entity types unchanged: both `TriggerConfiguration` fields stay `string?`. Adding the `AlarmTriggerType` member touches three switch sites: `AlarmActor.ParseEvalConfig`, `AlarmActor.HandleAttributeValueChanged`, `AlarmTriggerConfigCodec`. ### 2. Runtime evaluation - **`TriggerExpressionGlobals`** (new, `ScadaLink.SiteRuntime`) — a read-only globals type exposing only `Attributes["X"]`, `Children["C"].Attributes["X"]`, and `Parent.Attributes["X"]`, backed by an in-memory snapshot dictionary. No side-effecting APIs. A missing attribute reads as `null` (never throws). - The expression is compiled once via the existing Roslyn pipeline (same forbidden-API trust checks) against `TriggerExpressionGlobals`; the compiled delegate is cached on the actor. - **Attribute snapshot:** `ScriptActor` and `AlarmActor` already receive every `AttributeValueChanged`. Each keeps a local `Dictionary` snapshot — seeded from the instance's initial attribute set at startup, then updated on each change. The expression evaluates against the snapshot — no `Ask` back to the `InstanceActor`; cheap and re-entrancy-free. - **On each `AttributeValueChanged`:** update snapshot → run cached expression → `bool`. - **Script (edge):** track the previous result; on `false→true`, run the script (spawn `ScriptExecutionActor`, as the other triggers do). - **Alarm (level):** the `bool` feeds the existing binary Normal↔Active state machine — raise on `→Active`, clear on `→Normal`. - Cost per attribute update: one cached-delegate call + one bool compare. ### 3. Editors & analysis - **`ScriptTriggerEditor`:** add `Expression` to `ScriptTriggerKind` and `ScriptTriggerConfigCodec` (round-trips `{ expression }`). - **`AlarmTriggerEditor`:** add an `Expression` case to its trigger `@switch`. - Both render the same **expression panel**: a compact `MonacoEditor` (~120 px) with C# syntax, `Attributes["..."]` completion driven by the template's attribute metadata (self / children / parent), and live compile diagnostics. A one-line hint summarizes what fires. - **Analysis:** reuse the existing `Template` analysis kind — completion and diagnostics work with no new analyzer code. Editor completion is slightly permissive (also shows `Instance`/`External`), but the runtime's restricted `TriggerExpressionGlobals` is what enforces read-only. A dedicated strict analysis kind is a possible later refinement, out of scope here. ### 4. Error handling & validation - **Pre-deployment:** extend `ValidationService` to compile-check expression triggers (against `TriggerExpressionGlobals`); compile errors block deployment and surface like other validation errors. Unknown `Attributes["..."]` keys are flagged as the existing trigger-reference validation does. - **Runtime — expression throws:** caught; treated as `false` for that update; a script-error event is written to the site event log. The actor never crashes. - **Non-bool result:** treated as `false` and logged. - **Missing attribute:** reads as `null` (handled in `TriggerExpressionGlobals`). - **Blank expression:** the trigger is inert; validation emits a warning. ### 5. Testing & verification - **Unit:** codec round-trip for script and alarm `{ expression }`; expression compile (valid + invalid). - **Runtime:** deploy an instance with an expression-triggered script and alarm; drive attribute updates (bound Test Run / CLI); confirm the script fires only on `false→true` and the alarm raises/clears with the expression. - **UI:** the expression panel in both editors; save → reopen round-trip. ## Implementation tasks - #25 — Implement expression trigger model + codecs - #26 — Implement runtime expression evaluation (blocked by #25) - #27 — Add expression panel to the trigger editors (blocked by #25) - #28 — Validate expression triggers pre-deployment (blocked by #25, #26)