Workflow Events
When you call ExecuteStreamingAsync(), the workflow emits a unified stream of events. These include both workflow lifecycle events (from the graph orchestrator) and agent events (text deltas, tool calls, etc.) from each node — all in one stream.
Use WorkflowEventCoordinator when you need approval responses or observers; otherwise call with just input:
await foreach (var evt in workflow.ExecuteStreamingAsync(input))
{
switch (evt)
{
case WorkflowStartedEvent e:
Console.WriteLine($"Workflow '{e.WorkflowName}' started ({e.NodeCount} nodes)");
break;
case WorkflowNodeStartedEvent e:
Console.WriteLine($" → Node '{e.NodeId}' started");
break;
case TextDeltaEvent e:
Console.Write(e.Text); // Token-by-token streaming from a node
break;
case WorkflowNodeCompletedEvent e:
Console.WriteLine($" ✓ Node '{e.NodeId}' done in {e.Duration.TotalSeconds:F1}s");
// Access node outputs
foreach (var (key, value) in e.Outputs ?? [])
Console.WriteLine($" {key} = {value}");
break;
case WorkflowCompletedEvent e:
Console.WriteLine($"Workflow done. Success: {e.Success}, Duration: {e.Duration}");
break;
case WorkflowDiagnosticEvent e when e.Level >= LogLevel.Warning:
Console.WriteLine($"[{e.Level}] {e.Source}: {e.Message}");
break;
}
}Event Reference
WorkflowStartedEvent
Emitted once when the workflow begins execution.
| Property | Type | Description |
|---|---|---|
WorkflowName | string | Name of the workflow |
NodeCount | int | Number of agent nodes in the graph |
LayerCount | int? | Number of execution layers (null for cyclic graphs) |
ExecutionContext | AgentExecutionContext | Agent hierarchy context. Contains AgentId (unique run ID), AgentChain (list of parent names), Depth (nesting level), and ParentAgentId. Useful for correlating events when a workflow is invoked as a toolkit capability inside a parent agent. |
WorkflowNodeStartedEvent
Emitted when an individual node begins executing.
| Property | Type | Description |
|---|---|---|
WorkflowName | string | Parent workflow name |
NodeId | string | The node's ID (as registered with AddAgent) |
AgentName | string? | Name of the agent at this node |
LayerIndex | int? | Parallel execution layer (null for sequential) |
WorkflowNodeCompletedEvent
Emitted when a node finishes. Contains the node's outputs — use this instead of RunAsync().FinalAnswer for reliable output access.
| Property | Type | Description |
|---|---|---|
WorkflowName | string | Parent workflow name |
NodeId | string | The node's ID |
AgentName | string? | Name of the agent |
Success | bool | Whether the node succeeded |
Duration | TimeSpan | Node execution time |
Progress | float | Completion percentage (0.0–1.0) across all nodes |
Outputs | IReadOnlyDictionary<string, object>? | Node outputs keyed by output key |
ErrorMessage | string? | Error message if the node failed |
Example — reading outputs from streaming:
if (evt is WorkflowNodeCompletedEvent node && node.NodeId == "writer")
{
var report = node.Outputs?["report"] as string;
}WorkflowNodeSkippedEvent
Emitted when a node is skipped (e.g. its incoming conditional edge did not match, or OnErrorSkip was triggered).
| Property | Type | Description |
|---|---|---|
WorkflowName | string | Parent workflow name |
NodeId | string | The skipped node's ID |
Reason | string | Why the node was skipped |
WorkflowCompletedEvent
Emitted once when the entire workflow finishes.
| Property | Type | Description |
|---|---|---|
WorkflowName | string | Workflow name |
Duration | TimeSpan | Total workflow duration |
SuccessfulNodes | int | Nodes that completed successfully |
FailedNodes | int | Nodes that failed |
SkippedNodes | int | Nodes that were skipped |
Success | bool | True when FailedNodes == 0 |
WorkflowLayerStartedEvent / WorkflowLayerCompletedEvent
Emitted for each parallel execution layer (a group of nodes that run concurrently in a fan-out).
| Property | Type | Description |
|---|---|---|
LayerIndex | int | 0-based layer index |
NodeCount | int | Nodes in this layer (Started only) |
Duration | TimeSpan | Layer duration (Completed only) |
SuccessfulNodes | int | Nodes succeeded (Completed only) |
WorkflowEdgeTraversedEvent
Diagnostic event emitted when a routing edge is followed. Useful for debugging conditional routing decisions.
| Property | Type | Description |
|---|---|---|
FromNodeId | string | Source node |
ToNodeId | string | Target node |
HasCondition | bool | Whether a condition was evaluated |
ConditionDescription | string? | Human-readable condition description |
WorkflowDiagnosticEvent
Internal orchestrator log messages surfaced as events. Filter by Level to control verbosity.
| Property | Type | Description |
|---|---|---|
Level | HPD.MultiAgent.LogLevel | Trace, Debug, Information, Warning, Error, Critical |
Source | string | Component that emitted it (e.g. "Orchestrator", node ID) |
Message | string | Diagnostic message |
NodeId | string? | Related node, if applicable |
Note:
Levelis typed asHPD.MultiAgent.LogLevel, notMicrosoft.Extensions.Logging.LogLevel. The two enums have the same numeric values and member names, but are distinct types. If you have both namespaces in scope, qualify the type explicitly to avoid an ambiguous reference error:csharpcase WorkflowDiagnosticEvent e when e.Level >= HPD.MultiAgent.LogLevel.Warning:
Agent Events
All AgentEvent subtypes (from HPD.Agent) pass through the stream unchanged. This includes:
| Event | Description |
|---|---|
TextDeltaEvent | A token of streamed text from an agent |
ToolCallStartEvent | A tool invocation is starting |
ToolCallEndEvent | A tool invocation completed |
MessageTurnFinishedEvent | An agent's full turn completed (includes token usage) |
NodeApprovalRequestEvent | The workflow is paused, waiting for human approval |
→ See 06.3 Node Options for how to respond to approval events.
Filtering events by agent
Every AgentEvent carries an ExecutionContext property with AgentName, AgentId, AgentChain (the full parent hierarchy), and Depth. Because all agent events from all nodes flow through the same stream, you can use ExecutionContext.AgentName to route or filter them per agent:
await foreach (var evt in workflow.ExecuteStreamingAsync(input))
{
// Workflow lifecycle events don't have ExecutionContext — handle them first
if (evt is WorkflowNodeStartedEvent nodeStart)
{
Console.WriteLine($"--- {nodeStart.NodeId} ---");
continue;
}
// All agent events carry ExecutionContext
if (evt is AgentEvent agentEvt)
{
var agentName = agentEvt.ExecutionContext?.AgentName ?? "unknown";
switch (evt)
{
case TextDeltaEvent delta:
// Only show text from the verifier node
if (agentName == "Verifier")
Console.Write(delta.Text);
break;
case MessageTurnFinishedEvent finished:
Console.WriteLine($"[{agentName}] turn finished — {finished.Usage?.OutputTokenCount} tokens");
break;
}
}
}AgentChain for nested agents — if a node runs a SubAgent internally, events from the sub-agent are also in the stream. AgentChain gives you the full hierarchy:
if (evt is AgentEvent agentEvt)
{
var ctx = agentEvt.ExecutionContext;
// Ignore events from sub-agents — only show root-level agent output
if (ctx?.Depth == 0 && evt is TextDeltaEvent delta)
Console.Write(delta.Text);
// Or show indented output at every nesting level
if (evt is TextDeltaEvent d)
{
var indent = new string(' ', (ctx?.Depth ?? 0) * 2);
Console.Write($"{indent}{d.Text}");
}
}Depth is 0 for the workflow's direct agents, 1 for any SubAgent they spawn, 2 for sub-sub-agents, and so on.
Event Order
For a simple linear workflow (A → B → C), events arrive in this order:
WorkflowStartedEvent
WorkflowLayerStartedEvent (layer 0)
WorkflowNodeStartedEvent (A)
TextDeltaEvent ... (streaming from A)
WorkflowNodeCompletedEvent (A)
WorkflowLayerCompletedEvent (layer 0)
WorkflowLayerStartedEvent (layer 1)
WorkflowNodeStartedEvent (B)
...
WorkflowLayerCompletedEvent (last layer)
WorkflowCompletedEventFor fan-out layers, all nodes in the layer emit their events interleaved, since they run concurrently.