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

Microsoft Agent Framework Workflows: Edges

Edges define how messages flow between executors in a workflow. They are connections in the workflow graph that determine data-flow paths. Edges can include conditions to control routing based on message content.

Edge Types at a Glance

TypeDescriptionUse Case
DirectSimple one-to-one connection, no conditionLinear pipelines
ConditionalEdge with a Func<object?, bool> conditionBinary routing (if / else)
Switch-CaseOrdered list of conditions with a default fallbackMulti-branch routing
Fan-OutOne executor sends to multiple targets via a selectorParallel processing
Fan-InMultiple executors send to a single targetAggregation

Direct Edges

The simplest form — connect two executors with no conditions. Every message produced by the source is forwarded to the target:

var workflow = new WorkflowBuilder(classifier)
.AddEdge(classifier, logger) // direct — no condition
.WithOutputFrom(logger)
.Build<Ticket>();

Conditional Edges

A conditional edge receives a Func<object?, bool>. The function is evaluated at runtime for each message; if it returns true the message is delivered, otherwise it is dropped for that edge. You can add multiple conditional edges from the same source to implement if/else routing:

var workflow = new WorkflowBuilder(classifier)
.AddEdge(classifier, senior, condition: TicketConditions.PriorityIs("High"))
.AddEdge(classifier, general, condition: TicketConditions.PriorityIs("Normal"))
.WithOutputFrom(senior, general)
.Build<Ticket>();

The condition helper is a simple pattern-matching function:

static class TicketConditions
{
public static Func<object?, bool> PriorityIs(string priority) =>
msg => msg is ClassifiedTicket t && t.Priority == priority;
}

Switch-Case Edges

When you have more than two branches, AddSwitch provides a cleaner syntax. Cases are evaluated in order; the first match wins. WithDefault() catches anything that no case matches, guaranteeing that messages never get stuck:

builder.AddSwitch(classifier, sw => sw
.AddCase(TicketConditions.PriorityIs("Critical"), escalation)
.AddCase(TicketConditions.PriorityIs("High"), senior)
.AddCase(TicketConditions.PriorityIs("Normal"), general)
.WithDefault(selfService) // fallback
);

Conditional vs. Switch-Case

Conditional EdgesSwitch-Case
Each AddEdge is independentOne AddSwitch block groups all branches
Multiple edges can match simultaneouslyFirst match wins; only one branch activates
No built-in defaultWithDefault() guarantees routing
Good for 2 pathsBetter for 3+ paths

Fan-Out (Multi-Selection) Edges

A fan-out edge lets a single message activate multiple targets at once. You provide a targetSelector function that returns the indices of targets to activate for each message:

Func<ClassifiedTicket?, int, IEnumerable<int>> selector = (ticket, _) =>
{
return ticket?.Priority switch
{
"Critical" => [0, 3], // escalation + logger
"High" => [1, 3], // senior + logger
_ => [2] // general only
};
};

builder.AddFanOutEdge(classifier,
targets: [escalation, senior, general, logger],
targetSelector: selector);

This is the most flexible routing mechanism. The selector can return any combination of target indices based on message content, enabling conditional parallel processing.

Fan-In Edges

Fan-in is the reverse of fan-out — multiple sources feed into a single target. Use AddFanInEdge to collect messages from several executors:

builder.AddFanInEdge(aggregator, sources: [worker1, worker2, worker3]);

Demo Scenario: Tech Support Ticket Routing

The demo builds a ticket routing system that demonstrates all four edge types. Every ticket passes through a ClassifierExecutor that stamps the team name, then the workflow routes the ticket to the appropriate handler(s).

Ticket
┌────────────┐
│ Classifier │
└─────┬──────┘
├── Direct → Logger (Demo 1)
├── Conditional → Senior / General (Demo 2)
├── Switch-Case → Escalation / Senior / General / SelfService (Demo 3)
└── Fan-Out → Escalation+Logger / Senior+Logger / General (Demo 4)

Data Models

Ticket has three properties: Id, Subject, Priority (Low | Normal | High | Critical). ClassifiedTicket adds an AssignedTeam field.

Executors

  1. ClassifierExecutor — receives a Ticket, returns a ClassifiedTicket with the team name.
  2. SelfServiceExecutor, GeneralExecutor, SeniorExecutor, EscalationExecutor — leaf handlers that call YieldOutputAsync.
  3. LoggerExecutor — writes a log entry; used in direct-edge and fan-out demos.

Demo 1 — Direct Edge

Classifier → Logger. Every ticket is logged unconditionally.

Demo 2 — Conditional Edges

Two conditional edges from Classifier: one for "High" and one for "Normal". Each ticket follows exactly one path.

Demo 3 — Switch-Case

Four-way switch: Critical → Escalation, High → Senior, Normal → General, default → SelfService. Only the first matching case activates.

Demo 4 — Fan-Out

Critical tickets activate both Escalation and Logger in parallel. High tickets activate Senior and Logger. Normal/Low tickets go to General only.

Full Example

// EdgesDemo.cs
// Demonstrates Microsoft Agent Framework Workflows — Edge types:
// Direct, Conditional, Switch-Case, Fan-Out (Multi-Selection), and Fan-In.
//
// Scenario: A ticket routing system for a tech support centre.
// Tickets arrive with a priority (Low, Normal, High, Critical).
// The workflow classifies them and routes to the correct handler(s).

using Microsoft.Agents.AI.Workflows;

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

public sealed class Ticket
{
public int Id { get; set; }
public string Subject { get; set; } = string.Empty;
public string Priority { get; set; } = string.Empty; // Low | Normal | High | Critical
}

public sealed class ClassifiedTicket
{
public int Id { get; set; }
public string Subject { get; set; } = string.Empty;
public string Priority { get; set; } = string.Empty;
public string AssignedTeam { get; set; } = string.Empty;
}

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

internal sealed partial class ClassifierExecutor() : Executor("Classifier")
{
[MessageHandler]
private ValueTask<ClassifiedTicket> HandleAsync(Ticket ticket, IWorkflowContext ctx)
{
string team = ticket.Priority switch
{
"Critical" => "Escalation",
"High" => "Senior",
"Normal" => "General",
_ => "Self-Service"
};

return ValueTask.FromResult(new ClassifiedTicket
{
Id = ticket.Id,
Subject = ticket.Subject,
Priority = ticket.Priority,
AssignedTeam = team
});
}
}

internal sealed partial class SelfServiceExecutor() : Executor("SelfService")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[SELF-SERVICE] Ticket #{t.Id} auto-replied.");
}

internal sealed partial class GeneralExecutor() : Executor("General")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[GENERAL] Ticket #{t.Id} assigned to general queue.");
}

internal sealed partial class SeniorExecutor() : Executor("Senior")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[SENIOR] Ticket #{t.Id} assigned to senior engineer.");
}

internal sealed partial class EscalationExecutor() : Executor("Escalation")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[ESCALATION] Ticket #{t.Id} paged on-call team!");
}

internal sealed partial class LoggerExecutor() : Executor("Logger")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[LOG] Ticket #{t.Id} ({t.Priority}) → {t.AssignedTeam}");
}

// ── Condition helpers ───────────────────────────────────

static class TicketConditions
{
public static Func<object?, bool> PriorityIs(string priority) =>
msg => msg is ClassifiedTicket t && t.Priority == priority;
}

// ── Demo runner ─────────────────────────────────────────

public static class Program
{
private static async Task Main()
{
await Demo1_DirectEdge();
await Demo2_ConditionalEdge();
await Demo3_SwitchCase();
await Demo4_FanOut();
}

// Demo 1: Direct Edge (Classifier → Logger)
private static async Task Demo1_DirectEdge()
{
Console.WriteLine("══════ Demo 1: Direct Edge ══════");

var classifier = new ClassifierExecutor();
var logger = new LoggerExecutor();

var workflow = new WorkflowBuilder(classifier)
.AddEdge(classifier, logger)
.WithOutputFrom(logger)
.Build<Ticket>();

await RunAsync(workflow, new Ticket { Id = 1, Subject = "Printer jam", Priority = "Normal" });
Console.WriteLine();
}

// Demo 2: Conditional Edges (Classifier → High|Normal)
private static async Task Demo2_ConditionalEdge()
{
Console.WriteLine("══════ Demo 2: Conditional Edges ══════");

var classifier = new ClassifierExecutor();
var senior = new SeniorExecutor();
var general = new GeneralExecutor();

var workflow = new WorkflowBuilder(classifier)
.AddEdge(classifier, senior, condition: TicketConditions.PriorityIs("High"))
.AddEdge(classifier, general, condition: TicketConditions.PriorityIs("Normal"))
.WithOutputFrom(senior, general)
.Build<Ticket>();

await RunAsync(workflow, new Ticket { Id = 2, Subject = "VPN down", Priority = "High" });
await RunAsync(workflow, new Ticket { Id = 3, Subject = "Password reset", Priority = "Normal" });
Console.WriteLine();
}

// Demo 3: Switch-Case Edge
private static async Task Demo3_SwitchCase()
{
Console.WriteLine("══════ Demo 3: Switch-Case Edge ══════");

var classifier = new ClassifierExecutor();
var selfService = new SelfServiceExecutor();
var general = new GeneralExecutor();
var senior = new SeniorExecutor();
var escalation = new EscalationExecutor();

var builder = new WorkflowBuilder(classifier);
builder.AddSwitch(classifier, sw => sw
.AddCase(TicketConditions.PriorityIs("Critical"), escalation)
.AddCase(TicketConditions.PriorityIs("High"), senior)
.AddCase(TicketConditions.PriorityIs("Normal"), general)
.WithDefault(selfService)
)
.WithOutputFrom(selfService, general, senior, escalation);

var workflow = builder.Build<Ticket>();

await RunAsync(workflow, new Ticket { Id = 4, Subject = "Server fire", Priority = "Critical" });
await RunAsync(workflow, new Ticket { Id = 5, Subject = "Slow laptop", Priority = "Low" });
Console.WriteLine();
}

// Demo 4: Fan-Out (Multi-Selection)
private static async Task Demo4_FanOut()
{
Console.WriteLine("══════ Demo 4: Fan-Out (Multi-Selection) ══════");

var classifier = new ClassifierExecutor();
var escalation = new EscalationExecutor();
var senior = new SeniorExecutor();
var general = new GeneralExecutor();
var logger = new LoggerExecutor();

Func<ClassifiedTicket?, int, IEnumerable<int>> selector = (ticket, _) =>
{
if (ticket is null) return [2];
return ticket.Priority switch
{
"Critical" => [0, 3], // escalation + logger
"High" => [1, 3], // senior + logger
_ => [2] // general only
};
};

var builder = new WorkflowBuilder(classifier);
builder.AddFanOutEdge(classifier,
targets: [escalation, senior, general, logger],
targetSelector: selector)
.WithOutputFrom(escalation, senior, general, logger);

var workflow = builder.Build<Ticket>();

await RunAsync(workflow, new Ticket { Id = 6, Subject = "Database crash", Priority = "Critical" });
await RunAsync(workflow, new Ticket { Id = 7, Subject = "Email lag", Priority = "High" });
await RunAsync(workflow, new Ticket { Id = 8, Subject = "New mouse", Priority = "Normal" });
Console.WriteLine();
}

private static async Task RunAsync(Workflow<Ticket> workflow, Ticket ticket)
{
StreamingRun run = await InProcessExecution.StreamAsync(workflow, ticket);
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
if (evt is WorkflowOutputEvent e)
Console.WriteLine($" {e.Data}");
}
}
}

// Expected output:
// ══════ Demo 1: Direct Edge ══════
// [LOG] Ticket #1 (Normal) → General
//
// ══════ Demo 2: Conditional Edges ══════
// [SENIOR] Ticket #2 assigned to senior engineer.
// [GENERAL] Ticket #3 assigned to general queue.
//
// ══════ Demo 3: Switch-Case Edge ══════
// [ESCALATION] Ticket #4 paged on-call team!
// [SELF-SERVICE] Ticket #5 auto-replied.
//
// ══════ Demo 4: Fan-Out (Multi-Selection) ══════
// [ESCALATION] Ticket #6 paged on-call team!
// [LOG] Ticket #6 (Critical) → Escalation
// [SENIOR] Ticket #7 assigned to senior engineer.
// [LOG] Ticket #7 (High) → Senior
// [GENERAL] Ticket #8 assigned to general queue.

Required Packages

<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-rc1" />
Share this lesson: