# Flow Debugger — Script Entry Point (design) **Date:** 2026-09-08 **Status:** approved (brainstorm 2026-09-08), implementation plan follows **Supersedes:** the *Value-change trigger* entry of `2026-09-05-flow-debugger-design.md` §4/§5.3 as the page's second entry point. §5.3's trace arm and the `FlowDebugRunValueChangeCommand` wire record are unchanged. ## 1. What changes The Flow Debugger's second entry point selects **site → instance → script** instead of site → instance → attribute. - A script whose trigger is **ValueChange or Conditional** names one monitored attribute in its trigger configuration. The debugger writes that attribute (the operator supplies only the new value) and waits for the run the change spawns — exactly today's value-change behaviour, with the attribute derived from the script rather than picked. - Every other script (**Call, Interval, Expression, or no trigger**) is invoked **directly** through the traced routed-call path that `Route.To(instance).Call(script, args)` already uses. The Inputs card shows a parameter form generated from the script's own `ParameterDefinitions`, the same way the inbound-API method entry does. The decision on trigger kinds was taken in the brainstorm: Conditional joins ValueChange on the write side because both name a single attribute; Expression joins the call side because it names none, and a direct call is the only honest single-step run of it. The attribute-level CLI command `flow-debug run-value-change` stays as the escape hatch for firing an Expression trigger through a real attribute write. ## 2. Script resolution Both the page and the runner resolve an instance's scripts through the existing `IFlatteningPipeline.FlattenAndValidateAsync(instanceId, validateScripts: false)` (DeploymentManager, already referenced by both CentralUI and ManagementService — Create Instance's binding preview uses the same pipeline). `FlattenedConfiguration.Scripts` gives `ResolvedScript` rows carrying the **canonical name** (so composed-module scripts appear as `Module.Script`, the name the site's `InstanceActor` keys on), the effective `TriggerType`/`TriggerConfiguration`, and the effective `ParameterDefinitions`/`ExecutionTimeoutSeconds` after inheritance and instance overrides. The runner resolves the script itself and never trusts the client's claim of trigger kind: the branch below is taken on the server-side `ResolvedScript`. A new pure Commons helper, `ScriptTriggerAttribute.TryRead(triggerType, configJson, out attribute)` (`Commons/Types/Scripts/`), returns the monitored attribute for `ValueChange`/`Conditional` (keys `attributeName` then `attribute`, matching `ScriptTriggerConfigCodec` and SiteRuntime's `TriggerRouting.TryReadAttributeName`) and false for every other kind. `FlowDebugService.IsValueChangeOn` is rewritten on top of it. SiteRuntime's private copy is left alone — it also serves alarms and fail-open routing, a different contract. **Design vs deployed.** The site runs the last deployed snapshot. A script that exists in design but was never deployed fails at the site with its existing `Script 'X' not found on instance 'Y'` error, which the debugger surfaces verbatim; the page does not try to detect staleness (the Instances page already shows it). ## 3. Wire and command changes (additive) ```csharp /// Flow Debugger Live run of a site script by name (design 2026-09-08). public record FlowDebugRunScriptCommand( int InstanceId, string ScriptName, // canonical name from the flattened configuration JsonElement? Parameters, // call kinds: flat JSON object validated against the script's ParameterDefinitions string? Value, // write kinds: the new value for the trigger's attribute int WaitSeconds = 45); // write kinds: NO_TRIGGER_WITHIN_WINDOW window ``` - Registered in `ManagementEndpoints`' command type list beside the other two run commands. - `ManagementActor.GetRequiredRoles`: `DeployerOnly`, same as `FlowDebugRunMethodCommand` / `FlowDebugRunValueChangeCommand`. Replay roles unchanged. - `FlowDebugRunValueChangeCommand` is **kept** (CLI `run-value-change`, additive-only contracts). `FlowDebugRunResult` is unchanged. - No proto change: the direct call rides the existing `RouteToCallRequest.Trace` / `RouteToCallResponse.TraceEvents` fields. ## 4. Runner (`FlowDebugRunner.RunScriptAsync`) 1. `EnsureActiveNode`; load instance (`NOT_FOUND`); `EnforceSiteScope(user, instance.SiteId)`; load site. 2. Flatten; find `ResolvedScript` by canonical name, case-insensitive (`NOT_FOUND` with the message `Script 'X' is not defined on instance 'Y'.`). 3. **Write kinds** (`ScriptTriggerAttribute.TryRead` succeeds): `Value` is required (`VALIDATION_FAILED`); call the shared private helper `WriteAndPollAsync(sp, user, instance, site, attribute, value, waitSeconds, ct)` — the body of today's `RunValueChangeAsync` from the traced `RouteToSetAttributesAsync` onward, extracted unchanged. `RunValueChangeAsync` becomes a thin caller of the same helper so both commands share one code path and one set of tests. 4. **Call kinds**: `ParameterValidator.Validate(cmd.Parameters, script.ParameterDefinitions, resolveRef)` with `SchemaRefResolver.BuildAsync` over the shared-schema repository, exactly as `RunMethodAsync` (`VALIDATION_FAILED` on failure; null definitions + null parameters is valid and sends no parameters). Then: - `root = Guid.NewGuid()`; `timeout = script.ExecutionTimeoutSeconds ?? SiteRuntime default 30 s` (the runner has no site options — it uses a `DefaultScriptTimeoutSeconds = 30` constant, documented as mirroring `SiteRuntimeOptions.ScriptExecutionTimeoutSeconds`). - `IInstanceRouter.RouteToCallAsync(site.SiteIdentifier, new RouteToCallRequest(correlationId, instance.UniqueName, script.CanonicalName, validation.Parameters, now, ParentExecutionId: root, Trace: true), linked token cancelled after timeout + RunGrace)`. A cancellation is reported as `TIMEOUT` like the method path. - Synthesize the **root row**: `AuditChannel.ScriptTrace` / `AuditKind.RouteCall`, status by `response.Success`, actor = operator, target = `{instance}.{script}`, `executionId = root`, `sourceSiteId`/`sourceInstanceId`, `durationMs`, `requestSummary` = the parameters JSON, `responseSummary` = the return value serialized with `ScriptTraceExtra.JsonOptions`, `errorMessage`, `extra = ScriptTraceExtra { Source = "flow-debugger" }`. Written through `WriteCentralAsync`; the returned copy is `redactor.Apply`'d (review follow-up rule). - The site's `response.TraceEvents` are pushed through a `FlowTraceCollector(redactor:)` so the inline copy is redacted and capped (arch-review F1), then `Complete()`d. - Result: `[redactedRoot] + collector events`, in **emission order** (F5: no re-sort across clocks), `Completed = response.Success`, `ErrorCode = response.Success ? null : "CALL_FAILED"`, `Truncated = collector.Truncated`. Not using `RouteHelper` here is deliberate: it would emit its own always-on `RouteCall` row with no operator actor, giving the graph two roots. Calling `IInstanceRouter` directly mirrors how the write path calls `RouteToSetAttributesAsync` and keeps one synthesized, operator-attributed root. `FlowGraphBuilder` already treats the first row of the root execution as the chain root; a `RouteCall`-kind root with `ScriptRun` children parented to it renders Central → Site exactly like the routed leg of a method run. The plan pins this with a builder test. ## 5. Central UI **`IFlowDebugService`** (UI-internal, not a wire contract): ```csharp sealed record FlowScriptOption(string CanonicalName, string? TriggerType, string? TriggerAttribute, string? ParameterDefinitions, bool IsWriteKind); Task> GetInstanceScriptsAsync(int instanceId, CancellationToken ct = default); Task PredictScriptAsync(int instanceId, string scriptName, CancellationToken ct = default); Task RunScriptAsync(int instanceId, string scriptName, JsonElement? parameters, string? value, CancellationToken ct = default); ``` `PredictValueChangeAsync` / `RunValueChangeAsync` are removed from the UI service (their only caller was the page); `PredictScriptAsync` delegates to the value-change prediction for write kinds (with the trigger's attribute) and, for call kinds, builds a `RouteCall` root on the Central lane (`{instance}.{script}`) followed by `FlowShapePredictor.PredictSiteScript`. `RunScriptAsync`'s Ask timeout is `RunAskTimeoutFor(script.ExecutionTimeoutSeconds ?? 30)` for call kinds and `ValueChangeAskTimeoutFor(45)` for write kinds. The script list is cached per instance for the life of the scoped service (one flatten per selection). **Page (`FlowDebugger.razor(.cs)`).** - Entry button label: **Site script** (`data-test="flow-entry-script"`, replacing `flow-entry-value-change`; `EntryKind.ValueChange` → `EntryKind.Script`). - Pickers: Site, Instance, **Script** (`data-test="flow-script-select"`); each option renders `Name (ValueChange: Level)` / `(Conditional: Level)` / `(Call)` / `(Interval)` / `(Expression)` / `(no trigger)`. - Inputs card: write kinds → the existing single field, labelled *New value for ``* with the attribute read-only (`flow-input-value`); call kinds → the shared `ParameterValueForm` bound to the option's `ParameterDefinitions` (`data-test="flow-script-params"`), or the note *This script takes no parameters* when the definitions are empty. `ParameterValueForm` hands back `Dictionary`, serialized to a `JsonElement` for the command. - `CanRun`: script chosen and (write kind → any value entered; call kind → form has no parse errors). - Query parameters: `?instanceId=` and `?script=`; `?attribute=` is removed (nothing outside the page links with it). Inputs persistence keys become `flow-debugger.script.{instanceId}.{script}`. - `FlowStepDetail`'s `SiteDbId`/`InstanceDbId` pass-through keys on `EntryKind.Script`. - The status strip, refresh loop, Retry/Discard and Replay are untouched. ## 6. CLI `flow-debug run-script --instance-id --script [--params ''] [--value ] [--wait-seconds 45]` → `FlowDebugRunScriptCommand`, same client timeout rule as `run-method` (`--timeout-seconds`, default 120) because the server may run a call script up to its execution timeout + grace. `run-value-change` stays, its help text noting it is the attribute-level path (fires Expression triggers) and that `run-script` is the script-level one. ## 7. Security and audit Unchanged posture: Deployer-only, active node only, site scope enforced from the instance before any site traffic, the root row is written centrally against the operator's name whether the call succeeded or not, inline trace copies are redacted and capped. A direct call of a script is a real run with real side effects — the page's existing Live-run wording already says so. ## 8. Tests - `FlowDebugRunnerTests`: write-kind script → `RouteToSetAttributesAsync` with the trigger's attribute; call-kind script → `RouteToCallAsync` with `Trace: true`, parent = root, parameters validated; unknown script → `NOT_FOUND`; missing `Value` on a write kind → `VALIDATION_FAILED`; bad parameters → `VALIDATION_FAILED`; out-of-scope site refused; root row redacted; `CALL_FAILED` on a site failure; `TIMEOUT` on cancellation. Existing `RunValueChange_*` tests keep passing through the shared helper. - `ScriptTriggerAttributeTests` (Commons): both key spellings, both kinds, case-insensitive type, malformed JSON → false. - `FlowGraphBuilderTests`: a `RouteCall` root with a `ScriptRun` child builds Central → Site. - `RequiredRoleMatrixTests` + `ManagementEndpoints` registration test for the new command. - `FlowDebugServiceTests`: script list from a flattened configuration, write/call classification, prediction shapes, Ask timeout selection. - `FlowDebuggerPageTests` (bUnit): script picker renders options with trigger annotations; Inputs card switches between value field and `ParameterValueForm`; `CanRun` rules; `?script=` deep link. - `FlowDebugCommandsTests` (CLI): `run-script` parsing incl. `--params` JSON errors. - Playwright `FlowDebuggerTests`: Live run of the rig's `OutboundDemo` Call script on `DelmiaSim01`. - `docker/verify-delmia-notifier.sh` step H gains a `run-script` leg against `OutboundDemo`. ## 9. Documents to update `2026-09-05-flow-debugger-design.md` §4 (pointer to this doc), `Component-CentralUI.md` (Flow Debugger entry points), `Component-InboundAPI.md` (FlowDebug commands table), `src/ZB.MOM.WW.ScadaBridge.CLI/README.md`, CLAUDE.md Flow Debugger bullet, and the umbrella index row's Flow Debugger sentence.