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

Microsoft Agent Framework: Workflow Builder & Execution

A Workflow ties executors and edges together into a directed graph and manages execution. It coordinates executor invocation, message routing, and event streaming. In this lesson we focus on how to build workflows with the WorkflowBuilder fluent API, how to execute them in streaming and non-streaming modes, and how the superstep execution model works.

Building Workflows

Workflows are constructed using the WorkflowBuilder class. The fluent API follows a simple pattern:

  1. Create a WorkflowBuilder with a start executor
  2. Add edges between executors with .AddEdge()
  3. Mark which executors produce final output with .WithOutputFrom()
  4. Call .Build() or .Build<T>()
var grader = new GraderExecutor().BindExecutor();
var reviewer = new ReviewerExecutor().BindExecutor();
var certificate = new CertificateExecutor().BindExecutor();

Workflow workflow = new WorkflowBuilder(grader)
.AddEdge(grader, reviewer)
.AddEdge(reviewer, certificate)
.WithOutputFrom(certificate)
.Build();

Typed Workflows — Build<T>()

When you call .Build<T>() instead of .Build(), you get a Workflow<T> that specifies the expected input message type. This provides compile-time safety — you cannot accidentally send the wrong type:

Workflow<ExamSubmission> workflow = new WorkflowBuilder(grader)
.AddEdge(grader, reviewer)
.AddEdge(reviewer, certificate)
.WithOutputFrom(certificate)
.Build<ExamSubmission>();

Workflow Validation

The framework performs comprehensive validation when building workflows:

CheckWhat it validates
Type CompatibilityMessage types are compatible between connected executors
Graph ConnectivityAll executors are reachable from the start executor
Executor BindingAll executors are properly bound and instantiated
Edge ValidationNo duplicate edges or invalid connections

If any validation fails, the framework throws an exception at build time — not at runtime — so you catch configuration errors early.

Workflow Execution

Workflows support two execution modes:

ModeAPIBehaviour
Non-StreamingInProcessExecution.RunAsync(workflow, input)Waits for the workflow to complete, then returns a Run object with all events in run.OutgoingEvents
StreamingInProcessExecution.StreamAsync(workflow, input)Returns a StreamingRun immediately; events arrive in real-time via run.WatchStreamAsync()

Non-Streaming Example

Run run = await InProcessExecution.RunAsync(workflow, input);

foreach (WorkflowEvent evt in run.OutgoingEvents)
{
switch (evt)
{
case ExecutorInvokedEvent inv:
Console.WriteLine($"[INVOKED] {inv.ExecutorId}");
break;
case ExecutorCompletedEvent comp:
Console.WriteLine($"[COMPLETED] {comp.ExecutorId}");
break;
case WorkflowOutputEvent output:
Console.WriteLine($"[OUTPUT] {output.Data}");
break;
}
}

Streaming Example

StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);

await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
switch (evt)
{
case ExecutorInvokedEvent inv:
Console.WriteLine($"[INVOKED] {inv.ExecutorId}");
break;
case WorkflowOutputEvent output:
Console.WriteLine($"[OUTPUT] {output.Data}");
break;
}
}

Execution Model: Supersteps

The framework uses a modified Pregel / Bulk Synchronous Parallel (BSP) execution model. Execution is organised into discrete supersteps. Each superstep:

  1. Collects all pending messages from the previous superstep
  2. Routes messages to target executors based on edge definitions
  3. Runs all target executors concurrently within the superstep
  4. Waits for all executors to complete (synchronisation barrier)
  5. Queues any new messages emitted by executors for the next superstep
Superstep 1: [Grader]
┌─────┴─────┐
Superstep 2: [Reviewer] [Archivist] ← fan-out, concurrent
Superstep 3: [Certificate]

Synchronisation Barrier

The most important characteristic is the synchronisation barrier between supersteps. All triggered executors within one superstep run in parallel, but the workflow does not advance to the next superstep until every executor completes. This means a fast path cannot "get ahead" of a slow path in the same superstep.

Why Supersteps?

BenefitExplanation
Deterministic executionSame input always produces the same execution order
Reliable checkpointingState can be saved at superstep boundaries for fault tolerance
Simpler reasoningNo race conditions between supersteps; each sees a consistent view of messages

Working with the Superstep Model

If you need truly independent parallel paths that don't block each other, consolidate sequential steps into a single executor. Instead of chaining step1 → step2 → step3, combine that logic into one executor so both parallel paths execute within a single superstep.

Demo Scenario: Student Exam Grading

Our demo simulates a student exam grading pipeline:

ExamSubmission → [Grader] → GradedExam → [Reviewer] → ReviewedExam → [Certificate] → output
[Archivist] → output (fan-out in Demo 3)

The demo includes three sub-demos:

  1. Demo 1 — Linear Workflow: Build and run a simple three-executor pipeline
  2. Demo 2 — Non-Streaming Events: Show all event types after workflow completes
  3. Demo 3 — Superstep Fan-Out: Grader fans out to Reviewer + Archivist in the same superstep, then Certificate runs in the next superstep

Full Example

using Microsoft.Agents.AI.Workflows;

namespace MicrosoftAgentFrameworkLesson.ConsoleApp.Builder;

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

public sealed class ExamSubmission
{
public string StudentName { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public int RawScore { get; set; } // 0-100
}

public sealed class GradedExam
{
public string StudentName { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public int RawScore { get; set; }
public string LetterGrade { get; set; } = string.Empty;
}

public sealed class ReviewedExam
{
public string StudentName { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public string LetterGrade { get; set; } = string.Empty;
public string ReviewComment { get; set; } = string.Empty;
}

// ── Executors ────────────────────────────────────────────

/// <summary>Assigns a letter grade based on raw score.</summary>
internal sealed class GraderExecutor()
: Executor<ExamSubmission, GradedExam>("Grader")
{
public override ValueTask<GradedExam> HandleAsync(
ExamSubmission exam, IWorkflowContext ctx, CancellationToken ct = default)
{
string grade = exam.RawScore switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};

return ValueTask.FromResult(new GradedExam
{
StudentName = exam.StudentName,
Subject = exam.Subject,
RawScore = exam.RawScore,
LetterGrade = grade
});
}
}

/// <summary>Adds a review comment based on the letter grade.</summary>
internal sealed class ReviewerExecutor()
: Executor<GradedExam, ReviewedExam>("Reviewer")
{
public override ValueTask<ReviewedExam> HandleAsync(
GradedExam exam, IWorkflowContext ctx, CancellationToken ct = default)
{
string comment = exam.LetterGrade switch
{
"A" => "Outstanding performance!",
"B" => "Good work, keep it up.",
"C" => "Satisfactory — room for improvement.",
"D" => "Below expectations — extra study recommended.",
_ => "Failed — retake required."
};

return ValueTask.FromResult(new ReviewedExam
{
StudentName = exam.StudentName,
Subject = exam.Subject,
LetterGrade = exam.LetterGrade,
ReviewComment = comment
});
}
}

/// <summary>Produces a certificate string from the reviewed exam.</summary>
internal sealed class CertificateExecutor()
: Executor<ReviewedExam, string>("Certificate")
{
public override ValueTask<string> HandleAsync(
ReviewedExam exam, IWorkflowContext ctx, CancellationToken ct = default)
{
string cert = $"[CERTIFICATE] {exam.StudentName} | {exam.Subject} | "
+ $"Grade: {exam.LetterGrade} | {exam.ReviewComment}";
return ValueTask.FromResult(cert);
}
}

/// <summary>Archives the graded exam (used in superstep fan-out demo).</summary>
internal sealed class ArchivistExecutor()
: Executor<GradedExam, string>("Archivist")
{
public override ValueTask<string> HandleAsync(
GradedExam exam, IWorkflowContext ctx, CancellationToken ct = default)
{
return ValueTask.FromResult(
$"[ARCHIVE] {exam.StudentName} — {exam.Subject} — Score {exam.RawScore} ({exam.LetterGrade}) archived.");
}
}

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

public static class ExamWorkflowDemo
{
public static async Task RunAsync()
{
Console.WriteLine("=== Microsoft Agent Framework — Workflow Builder & Execution Demo ===");
Console.WriteLine();

await Demo1_BuildAndRun();
await Demo2_NonStreaming();
await Demo3_SuperstepFanOut();
}

// ─────────────────────────────────────────────────────
// Demo 1: Linear Workflow
// Grader → Reviewer → Certificate
// ─────────────────────────────────────────────────────
private static async Task Demo1_BuildAndRun()
{
Console.WriteLine("── Demo 1: Linear Workflow ──");

var grader = new GraderExecutor().BindExecutor();
var reviewer = new ReviewerExecutor().BindExecutor();
var certificate = new CertificateExecutor().BindExecutor();

Workflow workflow = new WorkflowBuilder(grader)
.AddEdge(grader, reviewer)
.AddEdge(reviewer, certificate)
.WithOutputFrom(certificate)
.Build();

Run run = await InProcessExecution.RunAsync(
workflow,
new ExamSubmission { StudentName = "Alice", Subject = "Mathematics", RawScore = 92 });

PrintOutputsOnly(run);
Console.WriteLine();
}

// ─────────────────────────────────────────────────────
// Demo 2: Non-Streaming (All Events)
// ─────────────────────────────────────────────────────
private static async Task Demo2_NonStreaming()
{
Console.WriteLine("── Demo 2: Non-Streaming (All Events) ──");

var grader = new GraderExecutor().BindExecutor();
var reviewer = new ReviewerExecutor().BindExecutor();
var certificate = new CertificateExecutor().BindExecutor();

Workflow workflow = new WorkflowBuilder(grader)
.AddEdge(grader, reviewer)
.AddEdge(reviewer, certificate)
.WithOutputFrom(certificate)
.Build();

Run run = await InProcessExecution.RunAsync(
workflow,
new ExamSubmission { StudentName = "Bob", Subject = "Physics", RawScore = 55 });

Console.WriteLine(" (Workflow completed — iterating events)");
foreach (WorkflowEvent evt in run.OutgoingEvents)
{
switch (evt)
{
case SuperStepStartedEvent:
Console.WriteLine(" ── superstep started ──");
break;
case ExecutorInvokedEvent inv:
Console.WriteLine($" [INVOKED] {inv.ExecutorId}");
break;
case ExecutorCompletedEvent comp:
Console.WriteLine($" [COMPLETED] {comp.ExecutorId}");
break;
case SuperStepCompletedEvent:
Console.WriteLine(" ── superstep completed ──");
break;
case WorkflowOutputEvent output:
Console.WriteLine($" [OUTPUT] {output.Data}");
break;
case WorkflowErrorEvent error:
Console.WriteLine($" [ERROR] {error.Exception?.Message}");
break;
}
}

Console.WriteLine();
}

// ─────────────────────────────────────────────────────
// Demo 3: Superstep Fan-Out
// Grader → (Reviewer + Archivist) in the same superstep
// Reviewer → Certificate in the next superstep
// ─────────────────────────────────────────────────────
private static async Task Demo3_SuperstepFanOut()
{
Console.WriteLine("── Demo 3: Superstep Fan-Out ──");

var grader = new GraderExecutor().BindExecutor();
var reviewer = new ReviewerExecutor().BindExecutor();
var archivist = new ArchivistExecutor().BindExecutor();
var certificate = new CertificateExecutor().BindExecutor();

Workflow workflow = new WorkflowBuilder(grader)
.AddEdge(grader, reviewer)
.AddEdge(grader, archivist) // fan-out
.AddEdge(reviewer, certificate)
.WithOutputFrom(certificate, archivist)
.Build();

Run run = await InProcessExecution.RunAsync(
workflow,
new ExamSubmission { StudentName = "Diana", Subject = "Chemistry", RawScore = 88 });

foreach (WorkflowEvent evt in run.OutgoingEvents)
{
switch (evt)
{
case SuperStepStartedEvent:
Console.WriteLine(" ── superstep started ──");
break;
case ExecutorInvokedEvent inv:
Console.WriteLine($" [INVOKED] {inv.ExecutorId}");
break;
case ExecutorCompletedEvent comp:
Console.WriteLine($" [COMPLETED] {comp.ExecutorId}");
break;
case SuperStepCompletedEvent:
Console.WriteLine(" ── superstep completed ──");
break;
case WorkflowOutputEvent output:
Console.WriteLine($" [OUTPUT] {output.Data}");
break;
}
}

Console.WriteLine();
}

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

private static void PrintOutputsOnly(Run run)
{
foreach (WorkflowEvent evt in run.OutgoingEvents)
{
if (evt is WorkflowOutputEvent output)
Console.WriteLine($" {output.Data}");
}
}
}
Share this lesson: