diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs
index fdd5f487..ebf1e14f 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs
@@ -185,7 +185,9 @@ public partial class FlowGraphView : ComponentBase
FlowStepKind.AttributeWrite => "WRITE",
FlowStepKind.AttributeWait => "WAIT",
FlowStepKind.Response => "RESPONSE",
- FlowStepKind.External => "EXTERNAL",
+ FlowStepKind.ApiCall => "API CALL",
+ FlowStepKind.DbCall => "DATABASE",
+ FlowStepKind.Notify => "NOTIFY",
_ => "STEP",
};
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraph.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraph.cs
index b4a7bbf7..e8afef16 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraph.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraph.cs
@@ -5,7 +5,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
/// Target step.
/// True when the edge crosses the central ↔ site boundary (drawn in the accent colour).
/// True for an inferred relationship the system did not observe directly — a device echo satisfying a wait after a write (drawn dashed).
-public sealed record FlowEdge(Guid FromEventId, Guid ToEventId, bool CrossesBoundary, bool External);
+/// True when the target is a fire-and-forget branch the source did not wait for — a cached call or a notification (drawn dashed, plan D4).
+/// True when the target is the first step of a child execution spawned by something other than a route call — a trigger cascade (bolt marker, plan D9).
+public sealed record FlowEdge(Guid FromEventId, Guid ToEventId, bool CrossesBoundary, bool External, bool Async = false, bool Spawn = false);
/// The projected flow of one execution tree (Flow Debugger design 2026-09-05 §7.2).
/// Steps in display order (time, then kind rank, then event id).
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs
index 152e1c7f..3e98d657 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs
@@ -129,7 +129,7 @@ public static class FlowGraphBuilder
FlowStepKind.ScriptRun => 2,
FlowStepKind.AttributeWrite => 3,
FlowStepKind.AttributeWait => 4,
- FlowStepKind.External => 5,
+ FlowStepKind.ApiCall or FlowStepKind.DbCall or FlowStepKind.Notify => 5,
FlowStepKind.Unknown => 6,
FlowStepKind.Response => 9,
_ => 7,
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStep.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStep.cs
index 53be7d1e..e69e2234 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStep.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStep.cs
@@ -9,6 +9,8 @@ public enum FlowLane
Site,
/// Attribute / tag layer: attribute writes and their device echoes.
Tags,
+ /// Actors outside ScadaBridge: outbound API calls, database calls and notifications (v1.1, 2026-09-06 plan D2).
+ External,
}
/// What a flow step represents — the projection of an audit channel/kind pair.
@@ -26,8 +28,12 @@ public enum FlowStepKind
AttributeWait,
/// Synthesized final node — the inbound response (from the inbound row's response summary).
Response,
- /// An outbound API / DB / notification row emitted by a traced run (shown, not chained across the boundary).
- External,
+ /// An ApiOutbound row (ExternalSystem.Call / CachedCall) — one node per operation, lifecycle folded in.
+ ApiCall,
+ /// A DbOutbound row (Database.Query / Execute / CachedWrite).
+ DbCall,
+ /// A Notification row (Notify.Send and its central delivery).
+ Notify,
/// A channel/kind this build does not recognise — rendered, never dropped.
Unknown,
}
@@ -45,8 +51,29 @@ public enum FlowStepOutcome
Predicted,
/// No trace event exists for this position (e.g. site on an older build).
NotTraced,
+ /// An outbound operation whose latest lifecycle row is non-terminal (Submitted / Forwarded / Attempted).
+ Pending,
+ /// An outbound operation that ended Parked or Discarded — retry/discard lives on the Site Calls / Notification pages.
+ Parked,
}
+/// One audit row of a folded outbound operation's lifecycle ().
+/// The audit row's id.
+/// Audit kind name (CachedSubmit, ApiCallCached, CachedResolve, NotifyDeliver, …).
+/// Audit status name (Submitted, Forwarded, Attempted, Delivered, Parked, …).
+/// When the row was written.
+/// Node that wrote it.
+/// Error summary on a failed attempt.
+/// The row's own duration, when it has one.
+public sealed record FlowLifecycleRow(
+ Guid EventId,
+ string Kind,
+ string Status,
+ DateTimeOffset OccurredAtUtc,
+ string? Node,
+ string? ErrorMessage,
+ int? DurationMs);
+
///
/// One node of the flow graph — a central-side projection of a ScriptTrace /
/// ApiInbound.InboundRequest . Never serialized
@@ -70,4 +97,5 @@ public sealed record FlowStep(
string? Node,
string? Instance,
string? Target,
- ScriptTraceExtra? Extra);
+ ScriptTraceExtra? Extra,
+ IReadOnlyList? Lifecycle = null);
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs
index 723e0d4a..0803c7ad 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs
@@ -63,10 +63,12 @@ public static class FlowStepProjector
&& string.Equals(kind, nameof(AuditKind.InboundRequest), StringComparison.Ordinal))
return FlowStepKind.InboundRequest;
- if (string.Equals(channel, nameof(AuditChannel.ApiOutbound), StringComparison.Ordinal)
- || string.Equals(channel, nameof(AuditChannel.DbOutbound), StringComparison.Ordinal)
- || string.Equals(channel, nameof(AuditChannel.Notification), StringComparison.Ordinal))
- return FlowStepKind.External;
+ if (string.Equals(channel, nameof(AuditChannel.ApiOutbound), StringComparison.Ordinal))
+ return FlowStepKind.ApiCall;
+ if (string.Equals(channel, nameof(AuditChannel.DbOutbound), StringComparison.Ordinal))
+ return FlowStepKind.DbCall;
+ if (string.Equals(channel, nameof(AuditChannel.Notification), StringComparison.Ordinal))
+ return FlowStepKind.Notify;
return FlowStepKind.Unknown;
}
diff --git a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs
index 7bd313a5..2e6468b6 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs
@@ -385,20 +385,20 @@ public static class FlowShapePredictor
if (receiverText == "ExternalSystem" && member is "Call" or "CachedCall")
{
var (system, dependent) = LiteralOrExpr(args.FirstOrDefault()?.Expression);
- _b.AddNode(FlowStepKind.External, _lane, $"ExternalSystem.{member}({system})", dependent, null);
+ _b.AddNode(FlowStepKind.ApiCall, _lane, $"ExternalSystem.{member}({system})", dependent, null);
return true;
}
if (receiverText == "Database")
{
var (conn, dependent) = LiteralOrExpr(args.FirstOrDefault()?.Expression);
- _b.AddNode(FlowStepKind.External, _lane, $"Database.{member}({conn})", dependent, null);
+ _b.AddNode(FlowStepKind.DbCall, _lane, $"Database.{member}({conn})", dependent, null);
return true;
}
if (receiverText == "Notify" && member == "Send")
{
- _b.AddNode(FlowStepKind.External, _lane, "Notify.Send", false, null);
+ _b.AddNode(FlowStepKind.Notify, _lane, "Notify.Send", false, null);
return true;
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Debugging/FlowStepProjectorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Debugging/FlowStepProjectorTests.cs
new file mode 100644
index 00000000..fe9e8731
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Debugging/FlowStepProjectorTests.cs
@@ -0,0 +1,50 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
+
+namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging;
+
+///
+/// Flow Debugger outbound-calls plan (2026-09-06) Task 0: the Commons vocabulary — External lane,
+/// the three outbound kinds, Pending/Parked outcomes, the lifecycle row, and additive edge/step shapes.
+///
+public class FlowStepProjectorTests
+{
+ private static readonly DateTimeOffset T0 = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero);
+
+ [Fact]
+ public void Vocabulary_HasExternalLane_ThreeOutboundKinds_AndPendingParkedOutcomes()
+ {
+ Assert.Equal(3, (int)FlowLane.External);
+ Assert.True(Enum.IsDefined(FlowStepKind.ApiCall));
+ Assert.True(Enum.IsDefined(FlowStepKind.DbCall));
+ Assert.True(Enum.IsDefined(FlowStepKind.Notify));
+ Assert.True(Enum.IsDefined(FlowStepKind.Unknown));
+ Assert.False(Enum.GetNames().Contains("External"));
+ Assert.True(Enum.IsDefined(FlowStepOutcome.Pending));
+ Assert.True(Enum.IsDefined(FlowStepOutcome.Parked));
+ }
+
+ [Fact]
+ public void FlowStep_WithoutLifecycle_StillConstructs_AndDefaultsToNull()
+ {
+ var step = new FlowStep(Guid.NewGuid(), null, null, FlowStepKind.ScriptRun, FlowLane.Site, "S", null,
+ T0, null, FlowStepOutcome.Ok, null, null, null, null, null, null, null, null);
+ Assert.Null(step.Lifecycle);
+
+ var row = new FlowLifecycleRow(Guid.NewGuid(), "CachedSubmit", "Submitted", T0, "node-a", null, null);
+ var withLifecycle = step with { Lifecycle = [row] };
+ Assert.Single(withLifecycle.Lifecycle!);
+ Assert.Equal("Submitted", withLifecycle.Lifecycle![0].Status);
+ }
+
+ [Fact]
+ public void FlowEdge_AsyncAndSpawn_DefaultFalse()
+ {
+ var a = Guid.NewGuid();
+ var b = Guid.NewGuid();
+ var edge = new FlowEdge(a, b, false, false);
+ Assert.False(edge.Async);
+ Assert.False(edge.Spawn);
+ Assert.True((edge with { Async = true }).Async);
+ Assert.True(new FlowEdge(a, b, false, false, Async: false, Spawn: true).Spawn);
+ }
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs
index 084b6ef4..1fa20161 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs
@@ -199,8 +199,8 @@ public class FlowShapePredictorTests
(FlowStepKind.ScriptRun, "Handshake"),
(FlowStepKind.AttributeWrite, "Cmd"),
(FlowStepKind.AttributeWait, "Ack"),
- (FlowStepKind.External, "ExternalSystem.Call(Mes)"),
- (FlowStepKind.External, "Notify.Send"),
+ (FlowStepKind.ApiCall, "ExternalSystem.Call(Mes)"),
+ (FlowStepKind.Notify, "Notify.Send"),
], flow.Nodes.Select(n => (n.Kind, n.Title)).ToList());
Assert.DoesNotContain(flow.Nodes, n => n.Lane == FlowLane.Central);
Assert.All(flow.Edges, e => Assert.False(e.CrossesBoundary));