Microsoft Agent Framework Workflows Created: 25 Feb 2026 Updated: 25 Feb 2026

Microsoft Agent Framework Workflows: Events

The workflow event system provides real-time observability into workflow execution. Events are emitted at key points — when executors start, complete, fail, and when the workflow itself produces output or encounters errors. You can also define and emit your own custom events for domain-specific observability.

Built-in Event Types

The framework ships with the following built-in event types. They are all subclasses of WorkflowEvent:

CategoryEvent TypeWhen it fires
Workflow LifecycleWorkflowStartedEventWorkflow execution begins
WorkflowOutputEventWorkflow produces an output value
WorkflowErrorEventWorkflow encounters an error
WorkflowWarningEventWorkflow encounters a non-fatal warning
ExecutorExecutorInvokedEventAn executor starts processing a message
ExecutorCompletedEventAn executor finishes processing successfully
ExecutorFailedEventAn executor throws an exception
SuperstepSuperStepStartedEventA superstep (execution wave) begins
SuperStepCompletedEventA superstep completes
AgentAgentResponseEventAn agent-based executor produces output
RequestRequestInfoEventA request is issued during execution

How to Consume Events

There are two ways to consume events depending on which execution mode you use:

ModeAPIWhen to use
Non-StreamingInProcessExecution.RunAsync()run.OutgoingEventsWorkflow completes first, then you iterate all events
StreamingInProcessExecution.StreamAsync()run.WatchStreamAsync()Events arrive in real-time as the workflow executes

Both approaches use the same pattern-matching switch to handle events:

foreach (WorkflowEvent evt in run.OutgoingEvents) // non-streaming
{
switch (evt)
{
case ExecutorInvokedEvent inv:
Console.WriteLine($"[INVOKED] {inv.ExecutorId}");
break;
case ExecutorCompletedEvent comp:
Console.WriteLine($"[COMPLETED] {comp.ExecutorId} → {comp.Data}");
break;
case WorkflowOutputEvent output:
Console.WriteLine($"[OUTPUT] {output.Data}");
break;
case WorkflowErrorEvent error:
Console.WriteLine($"[ERROR] {error.Exception?.Message}");
break;
}
}

For streaming, replace the foreach with await foreach:

await foreach (WorkflowEvent evt in run.WatchStreamAsync()) // streaming
{
// same switch block
}

Custom Events

You can create your own event types by inheriting from WorkflowEvent. This is useful for domain-specific metrics, auditing, or progress tracking. Custom events are emitted inside executors using context.AddEventAsync():

Step 1 — Define the custom event

internal sealed class CookingStartedEvent(string message) : WorkflowEvent(message) { }
internal sealed class QualityCheckedEvent(string message) : WorkflowEvent(message) { }

Step 2 — Emit the event inside an executor

internal sealed class KitchenExecutor()
: Executor<FoodOrder, PreparedDish>("Kitchen")
{
public override async ValueTask<PreparedDish> HandleAsync(
FoodOrder order, IWorkflowContext ctx, CancellationToken ct = default)
{
// Emit custom event
await ctx.AddEventAsync(
new CookingStartedEvent($"Chef started preparing '{order.DishName}' x{order.Quantity}"));

// ... executor logic ...
return new PreparedDish { /* ... */ };
}
}

Step 3 — Consume the custom event

foreach (WorkflowEvent evt in run.OutgoingEvents)
{
switch (evt)
{
case CookingStartedEvent cook:
Console.WriteLine($"[COOKING] {cook.Data}");
break;
case QualityCheckedEvent qc:
Console.WriteLine($"[QUALITY] {qc.Data}");
break;
case WorkflowOutputEvent output:
Console.WriteLine($"[OUTPUT] {output.Data}");
break;
}
}

Custom events appear in the same event stream as built-in events. You can mix pattern-matching for both built-in and custom types in a single switch.

Filtering Events

In production, you may not need to handle every event type. A common pattern is to count all events but only react to specific ones:

int total = 0;
foreach (WorkflowEvent evt in run.OutgoingEvents)
{
total++;

if (evt is WorkflowOutputEvent output)
Console.WriteLine($"[RESULT] {output.Data}");
else if (evt is WorkflowErrorEvent error)
Console.WriteLine($"[ERROR] {error.Exception?.Message}");
}
Console.WriteLine($"Total events received: {total}");

Typical Event Flow

For a simple two-executor workflow (Kitchen → QualityCheck), the event sequence looks like this:

SuperStepStartedEvent ← step 1 begins
ExecutorInvokedEvent ← Kitchen starts
CookingStartedEvent ← custom event from Kitchen
ExecutorCompletedEvent ← Kitchen finishes
SuperStepCompletedEvent ← step 1 ends

SuperStepStartedEvent ← step 2 begins
ExecutorInvokedEvent ← QualityCheck starts
QualityCheckedEvent ← custom event from QualityCheck
ExecutorCompletedEvent ← QualityCheck finishes
SuperStepCompletedEvent ← step 2 ends

WorkflowOutputEvent ← final result

Demo Scenario: Restaurant Order Processing

Our demo simulates a restaurant kitchen workflow. A food order enters the system, the kitchen prepares it, and a quality check scores the result. Custom events provide observability at each stage.

FoodOrder → [Kitchen] → PreparedDish → [QualityCheck] → verdict string
↓ ↓
CookingStartedEvent QualityCheckedEvent

The demo includes three sub-demos:

  1. Demo 1 — Built-in Events: Displays every event type (superstep, invoked, completed, output, custom)
  2. Demo 2 — Custom Events: Focuses only on CookingStartedEvent and QualityCheckedEvent
  3. Demo 3 — Filtering: Counts all events but only prints outputs and errors

Full Example

using Microsoft.Agents.AI.Workflows;

namespace MicrosoftAgentFrameworkLesson.ConsoleApp.Events;

// ── Data Models ──────────────────────────────────────────

public sealed class FoodOrder
{
public int OrderId { get; set; }
public string DishName { get; set; } = string.Empty;
public int Quantity { get; set; }
}

public sealed class PreparedDish
{
public int OrderId { get; set; }
public string DishName { get; set; } = string.Empty;
public int Quantity { get; set; }
public string Chef { get; set; } = string.Empty;
public int CookMinutes { get; set; }
}

public sealed class QualityReport
{
public int OrderId { get; set; }
public string DishName { get; set; } = string.Empty;
public int Score { get; set; }
public string Verdict { get; set; } = string.Empty;
public override string ToString() =>
$"Order #{OrderId} '{DishName}' — Score: {Score}/10 — {Verdict}";
}

// ── Custom Events ────────────────────────────────────────

/// <summary>Custom event: emitted when the kitchen starts cooking.</summary>
internal sealed class CookingStartedEvent(string message) : WorkflowEvent(message) { }

/// <summary>Custom event: emitted after quality inspection.</summary>
internal sealed class QualityCheckedEvent(string message) : WorkflowEvent(message) { }

// ── Executors (Executor<TIn, TOut> pattern) ──────────────

/// <summary>Receives a FoodOrder, assigns a chef and cook time, returns PreparedDish.</summary>
internal sealed class KitchenExecutor()
: Executor<FoodOrder, PreparedDish>("Kitchen")
{
public override async ValueTask<PreparedDish> HandleAsync(
FoodOrder order, IWorkflowContext ctx, CancellationToken ct = default)
{
// Emit custom event
await ctx.AddEventAsync(
new CookingStartedEvent($"Chef started preparing '{order.DishName}' x{order.Quantity}"));

int cookMinutes = order.Quantity * 5;
string chef = order.Quantity > 3 ? "Head Chef" : "Line Cook";

return new PreparedDish
{
OrderId = order.OrderId,
DishName = order.DishName,
Quantity = order.Quantity,
Chef = chef,
CookMinutes = cookMinutes
};
}
}

/// <summary>Inspects the dish and returns a quality verdict string.</summary>
internal sealed class QualityCheckExecutor()
: Executor<PreparedDish, string>("QualityCheck")
{
public override async ValueTask<string> HandleAsync(
PreparedDish dish, IWorkflowContext ctx, CancellationToken ct = default)
{
int score = dish.Chef == "Head Chef" ? 9 : 7;
string verdict = score >= 8 ? "Excellent — ready to serve" : "Good — needs garnish";

await ctx.AddEventAsync(
new QualityCheckedEvent($"Quality check: '{dish.DishName}' scored {score}/10"));

var report = new QualityReport
{
OrderId = dish.OrderId,
DishName = dish.DishName,
Score = score,
Verdict = verdict
};

return report.ToString();
}
}

// ── Demo Runner ──────────────────────────────────────────

public static class RestaurantEventsDemo
{
public static async Task RunAsync()
{
Console.WriteLine("=== Microsoft Agent Framework — Events Demo ===");
Console.WriteLine();

await Demo1_BuiltInEvents();
await Demo2_CustomEvents();
await Demo3_FilterEvents();
}

// ─────────────────────────────────────────────────────
// Demo 1: Built-in Events
// Iterate run.OutgoingEvents to view every event type
// ─────────────────────────────────────────────────────
private static async Task Demo1_BuiltInEvents()
{
Console.WriteLine("── Demo 1: Built-in Events ──");

var workflow = BuildWorkflow();

Run run = await InProcessExecution.RunAsync(
workflow,
new FoodOrder { OrderId = 101, DishName = "Margherita Pizza", Quantity = 2 });

foreach (WorkflowEvent evt in run.OutgoingEvents)
{
switch (evt)
{
case SuperStepStartedEvent:
Console.WriteLine(" [SUPERSTEP] ── step started ──");
break;
case ExecutorInvokedEvent inv:
Console.WriteLine($" [INVOKED] {inv.ExecutorId}");
break;
case ExecutorCompletedEvent comp:
Console.WriteLine($" [COMPLETED] {comp.ExecutorId} → {comp.Data}");
break;
case SuperStepCompletedEvent:
Console.WriteLine(" [SUPERSTEP] ── step completed ──");
break;
case WorkflowOutputEvent output:
Console.WriteLine($" [OUTPUT] {output.Data}");
break;
case WorkflowErrorEvent error:
Console.WriteLine($" [ERROR] {error.Exception?.Message}");
break;
// Custom events also appear here
case CookingStartedEvent cook:
Console.WriteLine($" [CUSTOM] {cook.Data}");
break;
case QualityCheckedEvent qc:
Console.WriteLine($" [CUSTOM] {qc.Data}");
break;
default:
Console.WriteLine($" [EVENT] {evt.GetType().Name}: {evt.Data}");
break;
}
}

Console.WriteLine();
}

// ─────────────────────────────────────────────────────
// Demo 2: Custom Events
// Focus only on CookingStartedEvent and QualityCheckedEvent
// ─────────────────────────────────────────────────────
private static async Task Demo2_CustomEvents()
{
Console.WriteLine("── Demo 2: Custom Events ──");

var workflow = BuildWorkflow();

Run run = await InProcessExecution.RunAsync(
workflow,
new FoodOrder { OrderId = 202, DishName = "Beef Stew", Quantity = 5 });

foreach (WorkflowEvent evt in run.OutgoingEvents)
{
switch (evt)
{
case CookingStartedEvent cook:
Console.WriteLine($" [COOKING] {cook.Data}");
break;
case QualityCheckedEvent qc:
Console.WriteLine($" [QUALITY] {qc.Data}");
break;
case WorkflowOutputEvent output:
Console.WriteLine($" [OUTPUT] {output.Data}");
break;
}
}

Console.WriteLine();
}

// ─────────────────────────────────────────────────────
// Demo 3: Filtering — count all events, display only output
// ─────────────────────────────────────────────────────
private static async Task Demo3_FilterEvents()
{
Console.WriteLine("── Demo 3: Filtering Events ──");

var workflow = BuildWorkflow();

Run run = await InProcessExecution.RunAsync(
workflow,
new FoodOrder { OrderId = 303, DishName = "Caesar Salad", Quantity = 1 });

int total = 0;
foreach (WorkflowEvent evt in run.OutgoingEvents)
{
total++;

if (evt is WorkflowOutputEvent output)
Console.WriteLine($" [RESULT] {output.Data}");
else if (evt is WorkflowErrorEvent error)
Console.WriteLine($" [ERROR] {error.Exception?.Message}");
}

Console.WriteLine($" (Total events: {total} — only output/error displayed)");
Console.WriteLine();
}

// ── Helper ───────────────────────────────────────────

private static Workflow BuildWorkflow()
{
var kitchen = new KitchenExecutor().BindExecutor();
var quality = new QualityCheckExecutor().BindExecutor();

return new WorkflowBuilder(kitchen)
.AddEdge(kitchen, quality)
.WithOutputFrom(quality)
.Build();
}
}

Reference

Microsoft Agent Framework — Workflow Events (Official Documentation)


Share this lesson: