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

Microsoft Agent Framework Workflows: Executors

Executors are the fundamental building blocks that process messages in a workflow. They are autonomous processing units that receive typed messages, perform operations, and can produce output messages or events. Each executor has a unique identifier and can handle specific message types.

Executors can be:

  1. Custom logic components — process data, call APIs, or transform messages
  2. AI agents — use LLMs to generate responses

Basic Executor Structure

The recommended way to define executor message handlers in C# is to use the [MessageHandler] attribute on methods within a partial class that derives from Executor. This uses compile-time source generation for handler registration, providing better performance, compile-time validation, and Native AOT compatibility.

The class must be marked partial to enable source generation. Each executor is given a unique string identifier via the constructor.

There are two ways to send messages from an executor:

  1. Return a value — The return value is automatically sent to connected executors.
[MessageHandler]
private ValueTask<InspectionResult> HandleAsync(ShipmentItem item, IWorkflowContext context)
{
var result = new InspectionResult { /* ... */ };
return ValueTask.FromResult(result); // Automatically sent downstream
}
  1. Manual send via context — Call context.SendMessageAsync() to explicitly send messages.
[MessageHandler]
private async ValueTask HandleAsync(InspectionResult result, IWorkflowContext context)
{
var label = new ShelfLabel { /* ... */ };
await context.SendMessageAsync(label); // Explicitly sent downstream
}

Multiple Input Types

A single executor can handle multiple input types by defining multiple [MessageHandler] methods. Each method receives a different message type and the framework routes the correct message to the correct handler.

internal sealed partial class QualityGateExecutor() : Executor("QualityGateExecutor")
{
[MessageHandler]
private ValueTask<InspectionResult> HandleItemAsync(ShipmentItem item, IWorkflowContext context)
{
// Handle individual items
return ValueTask.FromResult(new InspectionResult { /* ... */ });
}

[MessageHandler]
private ValueTask<InspectionResult> HandlePalletAsync(BulkPallet pallet, IWorkflowContext context)
{
// Handle bulk pallets
return ValueTask.FromResult(new InspectionResult { /* ... */ });
}
}

Both handlers produce the same output type (InspectionResult), so downstream executors receive a uniform message regardless of whether the input was a single item or a bulk pallet.

Function-Based Executors

For simple transformations, you can create an executor from a function using the BindExecutor extension method. This avoids defining a full class:

Func<string, string> generateTag = sku => $"TAG-{sku.Replace("-", "").ToUpperInvariant()}";
var tagGenerator = generateTag.BindExecutor("TagGeneratorExecutor");

The resulting executor can be used in a workflow just like any class-based executor. It receives a string input and automatically sends the string return value downstream.

The IWorkflowContext Object

Every [MessageHandler] method receives an IWorkflowContext parameter. This provides methods for interacting with the workflow during execution:

MethodPurpose
SendMessageAsync()Manually send messages to connected executors
YieldOutputAsync()Produce workflow outputs that are returned/streamed to the caller
QueueStateUpdateAsync()Store data in shared state accessible by other executors
ReadStateAsync()Read data from shared state
AddEventAsync()Emit custom events for observability

Agents in Workflows

AI agents can also serve as executors. The framework provides built-in integration where agents are wrapped as executors. When agents receive messages, they cache them and only start processing when they receive a TurnToken. This pattern enables:

  1. Sequential agent pipelines (e.g., Writer → Reviewer)
  2. Translation chains (e.g., French → Spanish → English)
  3. Shared sessions between agents created from the same client

Agent executors emit AgentResponseEvent (non-streaming) or AgentResponseUpdateEvent (streaming) events as they produce output.

Streaming vs. Non-Streaming Execution

Workflows can be executed in two modes:

  1. Streaming: InProcessExecution.StreamAsync(workflow, input) — returns a StreamingRun. You consume events in real-time with await foreach on run.WatchStreamAsync().
  2. Non-streaming: InProcessExecution.RunAsync(workflow, input) — returns a Run after the workflow completes. You iterate over run.NewEvents.

Demo Scenario: Warehouse Inventory Processing

This demo builds a warehouse inventory processing system that demonstrates all executor patterns:

  1. Demo 1 — Basic Executor Chain: A ReceivingDockExecutor receives a ShipmentItem, assigns a quality grade, and returns an InspectionResult (return-based pattern). A ShelfAssignmentExecutor determines the warehouse location and manually sends a ShelfLabel via context.SendMessageAsync(). A FinalLogExecutor yields the workflow output using context.YieldOutputAsync().
  2. Demo 2 — Multiple Input Types: A QualityGateExecutor defines two [MessageHandler] methods — one for ShipmentItem and one for BulkPallet. Both produce the same InspectionResult output. This demo processes a BulkPallet through the pipeline.
  3. Demo 3 — Function-Based Executor: A simple Func<string, string> generates inventory tags and is bound as an executor using BindExecutor(). This demo uses non-streaming execution (InProcessExecution.RunAsync) instead of streaming.
Demo 1 Workflow:

ShipmentItem
┌──────────────────────┐
│ ReceivingDockExecutor │ ← returns InspectionResult
└──────────┬───────────┘
┌────────────────────────┐
│ ShelfAssignmentExecutor │ ← sends ShelfLabel via context.SendMessageAsync()
└──────────┬─────────────┘
┌─────────────────┐
│ FinalLogExecutor │ ← yields output via context.YieldOutputAsync()
└─────────────────┘


Demo 3 Workflow:

string (SKU)
┌──────────────────────┐
│ TagGeneratorExecutor │ ← function-based via BindExecutor
└──────────┬───────────┘
┌──────────────────┐
│ PrinterExecutor │ ← function-based via BindExecutor
└──────────────────┘

Full Example

// ExecutorsDemo.cs
// Demonstrates Microsoft Agent Framework Workflows - Executors concepts:
// Basic Executor, Multiple Input Types, Function-Based Executors, and IWorkflowContext usage.
//
// Scenario: A warehouse inventory processing system.
// 1. Incoming shipments (ShipmentItem) are received by the ReceivingDockExecutor.
// 2. The ReceivingDockExecutor inspects items and sends them to QualityGateExecutor.
// 3. QualityGateExecutor handles multiple input types (ShipmentItem and BulkPallet).
// 4. A function-based executor calculates shelf labels.
// 5. The FinalLogExecutor yields the workflow output.

using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;

// ───────────────────────────────────────────────────────
// 1. DATA MODELS
// ───────────────────────────────────────────────────────

/// <summary>
/// Represents a single item arriving at the warehouse.
/// </summary>
public sealed class ShipmentItem
{
public string ProductName { get; set; } = string.Empty;
public string Sku { get; set; } = string.Empty;
public int Quantity { get; set; }
public double WeightKg { get; set; }
}

/// <summary>
/// Represents a bulk pallet containing multiple identical items.
/// </summary>
public sealed class BulkPallet
{
public string ProductName { get; set; } = string.Empty;
public string Sku { get; set; } = string.Empty;
public int PalletCount { get; set; }
public int ItemsPerPallet { get; set; }
}

/// <summary>
/// The result produced after quality inspection.
/// </summary>
public sealed class InspectionResult
{
public string Sku { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public int TotalQuantity { get; set; }
public string Grade { get; set; } = string.Empty; // "A", "B", or "C"
public string Notes { get; set; } = string.Empty;
}

/// <summary>
/// A shelf label assigned to an inspected item.
/// </summary>
public sealed class ShelfLabel
{
public string Sku { get; set; } = string.Empty;
public string Location { get; set; } = string.Empty;
public string LabelText { get; set; } = string.Empty;
}

// ───────────────────────────────────────────────────────
// 2. BASIC EXECUTOR — Single MessageHandler, returning a value
// ───────────────────────────────────────────────────────

/// <summary>
/// Receiving dock executor: accepts a ShipmentItem, logs it, and forwards
/// an InspectionResult. Demonstrates the simplest executor pattern where the
/// return value is automatically sent to connected executors.
/// </summary>
internal sealed partial class ReceivingDockExecutor() : Executor("ReceivingDockExecutor")
{
[MessageHandler]
private ValueTask<InspectionResult> HandleAsync(ShipmentItem item, IWorkflowContext context)
{
// Assign a quality grade based on weight thresholds (simulated logic).
string grade = item.WeightKg switch
{
<= 5.0 => "A",
<= 20.0 => "B",
_ => "C"
};

var result = new InspectionResult
{
Sku = item.Sku,
ProductName = item.ProductName,
TotalQuantity = item.Quantity,
Grade = grade,
Notes = $"Single shipment received. Weight: {item.WeightKg} kg. Grade assigned: {grade}."
};

// Returning the value automatically sends it to connected executors via edges.
return ValueTask.FromResult(result);
}
}

// ───────────────────────────────────────────────────────
// 3. MULTIPLE INPUT TYPES — Two [MessageHandler] methods
// ───────────────────────────────────────────────────────

/// <summary>
/// Quality gate executor: handles both ShipmentItem and BulkPallet inputs.
/// Demonstrates defining multiple [MessageHandler] methods in one executor.
/// Each handler produces the same output type (InspectionResult) so that
/// downstream executors receive a uniform message regardless of source.
/// </summary>
internal sealed partial class QualityGateExecutor() : Executor("QualityGateExecutor")
{
[MessageHandler]
private ValueTask<InspectionResult> HandleItemAsync(ShipmentItem item, IWorkflowContext context)
{
string grade = item.WeightKg <= 10.0 ? "A" : "B";

return ValueTask.FromResult(new InspectionResult
{
Sku = item.Sku,
ProductName = item.ProductName,
TotalQuantity = item.Quantity,
Grade = grade,
Notes = $"Item inspected at quality gate. Weight {item.WeightKg} kg → Grade {grade}."
});
}

[MessageHandler]
private ValueTask<InspectionResult> HandlePalletAsync(BulkPallet pallet, IWorkflowContext context)
{
int totalItems = pallet.PalletCount * pallet.ItemsPerPallet;
string grade = totalItems > 500 ? "C" : "A";

return ValueTask.FromResult(new InspectionResult
{
Sku = pallet.Sku,
ProductName = pallet.ProductName,
TotalQuantity = totalItems,
Grade = grade,
Notes = $"Bulk pallet inspected: {pallet.PalletCount} pallets x {pallet.ItemsPerPallet} items = {totalItems}. Grade {grade}."
});
}
}

// ───────────────────────────────────────────────────────
// 4. EXECUTOR WITH MANUAL MESSAGE SENDING
// ───────────────────────────────────────────────────────

/// <summary>
/// Shelf assignment executor: receives an InspectionResult, determines a
/// warehouse location, and manually sends a ShelfLabel using context.SendMessageAsync().
/// Demonstrates the alternative to returning a value.
/// </summary>
internal sealed partial class ShelfAssignmentExecutor() : Executor("ShelfAssignmentExecutor")
{
[MessageHandler]
private async ValueTask HandleAsync(InspectionResult result, IWorkflowContext context)
{
// Determine shelf location based on grade
string location = result.Grade switch
{
"A" => "Aisle-1, Rack-A",
"B" => "Aisle-2, Rack-B",
_ => "Aisle-3, Rack-C (Bulk Storage)"
};

var label = new ShelfLabel
{
Sku = result.Sku,
Location = location,
LabelText = $"[{result.Sku}] {result.ProductName} — Qty: {result.TotalQuantity}, Grade: {result.Grade} → {location}"
};

// Manually send the message to connected executors instead of returning.
await context.SendMessageAsync(label);
}
}

// ───────────────────────────────────────────────────────
// 5. FINAL OUTPUT EXECUTOR — Uses YieldOutputAsync
// ───────────────────────────────────────────────────────

/// <summary>
/// Final log executor: receives a ShelfLabel and yields workflow output.
/// </summary>
internal sealed partial class FinalLogExecutor() : Executor("FinalLogExecutor")
{
[MessageHandler]
private async ValueTask HandleAsync(ShelfLabel label, IWorkflowContext context)
{
await context.YieldOutputAsync($"INVENTORY LOG: {label.LabelText}");
}
}

// ───────────────────────────────────────────────────────
// 6. FUNCTION-BASED EXECUTOR (BindExecutor)
// ───────────────────────────────────────────────────────
// Shown inside Program.Main below using the BindExecutor extension method.

// ───────────────────────────────────────────────────────
// 7. WORKFLOW BUILDER & EXECUTION
// ───────────────────────────────────────────────────────

public static class Program
{
private static async Task Main()
{
// ── Demo 1: Basic executor chain (return-based) ──────────
Console.WriteLine("══════ Demo 1: Basic Executor Chain ══════");

var receivingDock = new ReceivingDockExecutor();
var shelfAssignment = new ShelfAssignmentExecutor();
var finalLog = new FinalLogExecutor();

var workflow1 = new WorkflowBuilder(receivingDock)
.AddEdge(receivingDock, shelfAssignment)
.AddEdge(shelfAssignment, finalLog)
.WithOutputFrom(finalLog)
.Build<ShipmentItem>();

var item = new ShipmentItem
{
ProductName = "Wireless Keyboard",
Sku = "KB-7720",
Quantity = 150,
WeightKg = 3.2
};

StreamingRun run1 = await InProcessExecution.StreamAsync(workflow1, item);
await foreach (WorkflowEvent evt in run1.WatchStreamAsync())
{
if (evt is ExecutorInvokedEvent invoked)
Console.WriteLine($" [STARTED] {invoked.ExecutorId}");
else if (evt is ExecutorCompletedEvent completed)
Console.WriteLine($" [COMPLETED] {completed.ExecutorId}");
else if (evt is WorkflowOutputEvent output)
Console.WriteLine($" [OUTPUT] {output.Data}");
}

// ── Demo 2: Multiple input types ─────────────────────────
Console.WriteLine();
Console.WriteLine("══════ Demo 2: Multiple Input Types ══════");

var qualityGate = new QualityGateExecutor();
var shelfAssignment2 = new ShelfAssignmentExecutor();
var finalLog2 = new FinalLogExecutor();

var workflow2 = new WorkflowBuilder(qualityGate)
.AddEdge(qualityGate, shelfAssignment2)
.AddEdge(shelfAssignment2, finalLog2)
.WithOutputFrom(finalLog2)
.Build<BulkPallet>();

var pallet = new BulkPallet
{
ProductName = "USB-C Charger",
Sku = "CHG-4410",
PalletCount = 12,
ItemsPerPallet = 50
};

StreamingRun run2 = await InProcessExecution.StreamAsync(workflow2, pallet);
await foreach (WorkflowEvent evt in run2.WatchStreamAsync())
{
if (evt is WorkflowOutputEvent output)
Console.WriteLine($" [OUTPUT] {output.Data}");
}

// ── Demo 3: Function-based executor ──────────────────────
Console.WriteLine();
Console.WriteLine("══════ Demo 3: Function-Based Executor ══════");

// Create an executor from a simple function using BindExecutor.
Func<string, string> generateTag = sku => $"TAG-{sku.Replace("-", "").ToUpperInvariant()}-{DateTime.UtcNow:yyyyMMdd}";
var tagGenerator = generateTag.BindExecutor("TagGeneratorExecutor");

// A small executor to print the output
Func<string, string> printTag = tag => { Console.WriteLine($" [TAG] {tag}"); return tag; };
var printer = printTag.BindExecutor("PrinterExecutor");

var workflow3 = new WorkflowBuilder(tagGenerator)
.AddEdge(tagGenerator, printer)
.WithOutputFrom(printer)
.Build<string>();

Run run3 = await InProcessExecution.RunAsync(workflow3, "KB-7720");
foreach (WorkflowEvent evt in run3.NewEvents)
{
if (evt is WorkflowOutputEvent output)
Console.WriteLine($" [OUTPUT] {output.Data}");
}
}
}

// Expected output:
// ══════ Demo 1: Basic Executor Chain ══════
// [STARTED] ReceivingDockExecutor
// [COMPLETED] ReceivingDockExecutor
// [STARTED] ShelfAssignmentExecutor
// [COMPLETED] ShelfAssignmentExecutor
// [STARTED] FinalLogExecutor
// [COMPLETED] FinalLogExecutor
// [OUTPUT] INVENTORY LOG: [KB-7720] Wireless Keyboard — Qty: 150, Grade: A → Aisle-1, Rack-A
//
// ══════ Demo 2: Multiple Input Types ══════
// [OUTPUT] INVENTORY LOG: [CHG-4410] USB-C Charger — Qty: 600, Grade: C → Aisle-3, Rack-C (Bulk Storage)
//
// ══════ Demo 3: Function-Based Executor ══════
// [TAG] TAG-KB7720-20260225
// [OUTPUT] TAG-KB7720-20260225
Share this lesson: