Replaces the value-change (site/instance/attribute) entry with a script picker. ValueChange/Conditional scripts keep the write-and-poll path with the attribute derived from the trigger config; every other kind is invoked directly through the traced RouteToCall path with a parameter form from the script's ParameterDefinitions. New additive FlowDebugRunScriptCommand; run-value-change stays as the CLI escape hatch. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
13 KiB
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 ownParameterDefinitions, 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)
/// 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 asFlowDebugRunMethodCommand/FlowDebugRunValueChangeCommand. Replay roles unchanged.FlowDebugRunValueChangeCommandis kept (CLIrun-value-change, additive-only contracts).FlowDebugRunResultis unchanged.- No proto change: the direct call rides the existing
RouteToCallRequest.Trace/RouteToCallResponse.TraceEventsfields.
4. Runner (FlowDebugRunner.RunScriptAsync)
EnsureActiveNode; load instance (NOT_FOUND);EnforceSiteScope(user, instance.SiteId); load site.- Flatten; find
ResolvedScriptby canonical name, case-insensitive (NOT_FOUNDwith the messageScript 'X' is not defined on instance 'Y'.). - Write kinds (
ScriptTriggerAttribute.TryReadsucceeds):Valueis required (VALIDATION_FAILED); call the shared private helperWriteAndPollAsync(sp, user, instance, site, attribute, value, waitSeconds, ct)— the body of today'sRunValueChangeAsyncfrom the tracedRouteToSetAttributesAsynconward, extracted unchanged.RunValueChangeAsyncbecomes a thin caller of the same helper so both commands share one code path and one set of tests. - Call kinds:
ParameterValidator.Validate(cmd.Parameters, script.ParameterDefinitions, resolveRef)withSchemaRefResolver.BuildAsyncover the shared-schema repository, exactly asRunMethodAsync(VALIDATION_FAILEDon 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 aDefaultScriptTimeoutSeconds = 30constant, documented as mirroringSiteRuntimeOptions.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 asTIMEOUTlike the method path.- Synthesize the root row:
AuditChannel.ScriptTrace/AuditKind.RouteCall, status byresponse.Success, actor = operator, target ={instance}.{script},executionId = root,sourceSiteId/sourceInstanceId,durationMs,requestSummary= the parameters JSON,responseSummary= the return value serialized withScriptTraceExtra.JsonOptions,errorMessage,extra = ScriptTraceExtra { Source = "flow-debugger" }. Written throughWriteCentralAsync; the returned copy isredactor.Apply'd (review follow-up rule). - The site's
response.TraceEventsare pushed through aFlowTraceCollector(redactor:)so the inline copy is redacted and capped (arch-review F1), thenComplete()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):
sealed record FlowScriptOption(string CanonicalName, string? TriggerType, string? TriggerAttribute,
string? ParameterDefinitions, bool IsWriteKind);
Task<IReadOnlyList<FlowScriptOption>> GetInstanceScriptsAsync(int instanceId, CancellationToken ct = default);
Task<PredictedFlow> PredictScriptAsync(int instanceId, string scriptName, CancellationToken ct = default);
Task<FlowDebugOutcome> 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", replacingflow-entry-value-change;EntryKind.ValueChange→EntryKind.Script). - Pickers: Site, Instance, Script (
data-test="flow-script-select"); each option rendersName (ValueChange: Level)/(Conditional: Level)/(Call)/(Interval)/(Expression)/(no trigger). - Inputs card: write kinds → the existing single field, labelled New value for
<attribute>with the attribute read-only (flow-input-value); call kinds → the sharedParameterValueFormbound to the option'sParameterDefinitions(data-test="flow-script-params"), or the note This script takes no parameters when the definitions are empty.ParameterValueFormhands backDictionary<string, object?>, serialized to aJsonElementfor 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 becomeflow-debugger.script.{instanceId}.{script}. FlowStepDetail'sSiteDbId/InstanceDbIdpass-through keys onEntryKind.Script.- The status strip, refresh loop, Retry/Discard and Replay are untouched.
6. CLI
flow-debug run-script --instance-id <id> --script <canonical name> [--params '<json>'] [--value <v>] [--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 →RouteToSetAttributesAsyncwith the trigger's attribute; call-kind script →RouteToCallAsyncwithTrace: true, parent = root, parameters validated; unknown script →NOT_FOUND; missingValueon a write kind →VALIDATION_FAILED; bad parameters →VALIDATION_FAILED; out-of-scope site refused; root row redacted;CALL_FAILEDon a site failure;TIMEOUTon cancellation. ExistingRunValueChange_*tests keep passing through the shared helper.ScriptTriggerAttributeTests(Commons): both key spellings, both kinds, case-insensitive type, malformed JSON → false.FlowGraphBuilderTests: aRouteCallroot with aScriptRunchild builds Central → Site.RequiredRoleMatrixTests+ManagementEndpointsregistration 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 andParameterValueForm;CanRunrules;?script=deep link.FlowDebugCommandsTests(CLI):run-scriptparsing incl.--paramsJSON errors.- Playwright
FlowDebuggerTests: Live run of the rig'sOutboundDemoCall script onDelmiaSim01. docker/verify-delmia-notifier.shstep H gains arun-scriptleg againstOutboundDemo.
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.