# WhileTrue Trigger Mode for Conditional & Expression Script Triggers — Design **Date:** 2026-05-18 **Status:** Approved (brainstorming) — implementation to follow ## Context Template script triggers of type `Conditional` and `Expression` currently fire *once* when their condition first holds: - **Expression** is edge-triggered — the script runs once per `false→true` transition of the boolean expression. - **Conditional** runs the script on each change of the monitored attribute for which the `{operator, threshold}` comparison is true. There is no way to keep a script running *while* a condition remains true. This design adds a second mode, **WhileTrue**, alongside the existing behavior (referred to as **OnTrue**). ### Decisions taken during brainstorming - **WhileTrue is a timer re-fire cadence.** On the `false→true` edge the script fires immediately; while the condition stays true it re-fires on a periodic timer; on the `true→false` edge the timer stops. It re-fires even if no attribute updates arrive (e.g. a static condition). - **The re-fire interval is the script's existing `MinTimeBetweenRuns`** — no new configuration field. One value serves as both the WhileTrue cadence and the general per-script throttle. - **OnTrue is unchanged.** A trigger config with no `mode` field parses as `OnTrue`, so every existing deployed template behaves identically. - **Alarms are out of scope** — alarm triggers are already level-based ("active while true"). ## Design ### 1. Trigger model & storage `Conditional` and `Expression` trigger `TriggerConfiguration` JSON gains an optional `mode` field: | Trigger | JSON shape | |-------------|---------------------------------------------------------| | Conditional | `{ attributeName, operator, threshold, mode? }` | | Expression | `{ expression, mode? }` | `mode` is `"OnTrue"` (default; absent ⇒ `OnTrue`) or `"WhileTrue"`. Entity types are unchanged — `TemplateScript.TriggerConfiguration` stays `string?`. ### 2. Runtime evaluation (`ScriptActor`) The internal trigger-config records gain a `Mode`: ```csharp internal enum TriggerMode { OnTrue, WhileTrue } internal record ConditionalTriggerConfig( string AttributeName, string Operator, double Threshold, TriggerMode Mode) : ScriptTriggerConfig; internal record ExpressionTriggerConfig(string Expression, TriggerMode Mode) : ScriptTriggerConfig; ``` **OnTrue** — current behavior, unchanged: - Conditional: fire on each monitored-attribute change for which the comparison is true. - Expression: fire once per `false→true` transition. **WhileTrue** — the actor tracks the condition's current truth state: - **`false→true` edge:** fire once immediately (via `TrySpawnExecution`, which still respects `MinTimeBetweenRuns` against any prior run), then start a periodic Akka timer with both initial delay and interval set to `MinTimeBetweenRuns`. - **Each timer tick:** spawn an execution **directly** — the timer interval is itself the cadence, so ticks bypass the `MinTimeBetweenRuns` skip-check (which could otherwise drop a tick to sub-millisecond timing jitter). - **`true→false` edge:** cancel the timer. - The state transitions naturally re-arm: a later `true` after a `false` starts a fresh cycle. Truth-state sources: - Conditional depends only on the monitored attribute, so it is re-evaluated on each change of that attribute (unchanged gate). - Expression is re-evaluated on every attribute change (unchanged). A throwing/non-bool expression is treated as `false` (existing behavior); for WhileTrue that cleanly stops the timer. **Edge case — WhileTrue with no `MinTimeBetweenRuns`:** a periodic timer has no interval to use, so the trigger degrades to a single edge fire and logs a warning. Deployment-time validation of this combination is a deliberate non-goal of this change. New surface in `ScriptActor`: a `TriggerMode` on the two config records, one `WhileTrueTick` internal message, and start/stop timer helpers (the actor already implements `IWithTimers` for Interval triggers). Timer key `"whiletrue-trigger"`. ### 3. Editors & codec - **`ScriptTriggerConfigCodec`:** `ScriptTriggerModel` gains a `Mode` (`ScriptTriggerMode` enum, default `OnTrue`). `Parse` reads `mode` for the Conditional and Expression kinds; `Serialize` writes it for those kinds. Absent/unrecognized `mode` ⇒ `OnTrue`. - **`ScriptTriggerEditor.razor`:** the Conditional and Expression panels gain a mode `