The previous lessons used sequential pipelines: every executor runs in a fixed order, and the path through the workflow is decided at build time. Handoff Orchestration turns that model upside-down. Here, each AI agent decides at runtime which specialist should handle the next turn. An agent simply tells the framework "I am done — pass control to that agent", and the framework routes accordingly. This mirrors the real-world pattern of a triage desk forwarding a patient to the right department based on their actual needs, not a predetermined schedule.
Key Concepts
1. CreateHandoffBuilderWith
Instead of new WorkflowBuilder(startExecutor), you call:
HandoffWorkflowBuilder builder =
AgentWorkflowBuilder.CreateHandoffBuilderWith(startAgent);
The start agent is the one that receives the very first user message. All subsequent routing is driven by the agents themselves at runtime.
2. WithHandoff and WithHandoffs
You describe the allowed handoff edges with these two methods:
// Single target
builder.WithHandoff(fromAgent, toAgent);
// Single target with a reason string the LLM can read
builder.WithHandoff(fromAgent, toAgent,
handoffReason: "Patient has a billing dispute requiring specialist review.");
// Multiple targets at once
builder.WithHandoffs(fromAgent, [targetA, targetB]);
The handoffReason is injected into the source agent's system prompt so it understands when it should trigger that specific handoff. Edges you do not declare are simply unavailable — an agent cannot hand off to a specialist that is not connected to it.
3. EnableReturnToPrevious
By default, follow-up turns from the user restart at the start agent. Calling .EnableReturnToPrevious() makes subsequent messages go directly to whichever specialist was active last, so a patient who continues talking about insurance does not get bounced back through the receptionist every time.
builder.EnableReturnToPrevious();
4. OpenStreamingAsync
Handoff workflows use InProcessExecution.OpenStreamingAsync instead of RunStreamingAsync. It returns a StreamingRun (an IAsyncDisposable) that stays open across multiple turns:
await using var session = await InProcessExecution.OpenStreamingAsync(workflow);
For each user turn, send the message and then consume the event stream:
await session.TrySendMessageAsync(userInput); // string
await foreach (var evt in session.WatchStreamAsync())
{
if (evt is AgentResponseUpdateEvent update)
Console.Write(update.Data);
}
WatchStreamAsync() completes naturally after the active agent finishes its response, so you loop over it once per user turn. No TurnToken is needed — TrySendMessageAsync both delivers the message and triggers processing.
5. AgentResponseUpdateEvent
Just as in sequential pipelines, the framework emits one AgentResponseUpdateEvent per generated token. The event exposes:
ExecutorId — which agent produced this tokenData — the token text
Because handoffs can chain several agents in a single turn, you may see ExecutorId change mid-stream — the triage agent hands off and the specialist immediately starts responding. Track the last seen ExecutorId to print clean speaker labels.
Setting Up the NuGet Packages
The same two packages used in earlier lessons cover handoff workflows:
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-rc1" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-rc1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
Set your OpenAI key in an environment variable before running:
set OPEN_AI_KEY=sk-...your-key...
The Hospital Patient Intake Scenario
The demo models a hospital intake desk with four AI agents:
- Receptionist — Start agent. Greets patients and routes them to the right specialist based on their chief complaint.
- AppointmentScheduler — Handles appointment booking, rescheduling, and cancellation. Can escalate insurance questions to InsuranceVerifier.
- InsuranceVerifier — Verifies coverage and eligibility. Escalates denied or disputed claims to ClaimsSpecialist.
- ClaimsSpecialist — Terminal specialist for complex claims. Explains the appeals process and required documentation.
Handoff Topology
Receptionist ──► AppointmentScheduler ──► Receptionist
│ │
│ └──► InsuranceVerifier ──► Receptionist
│ │
└───────────────────────────────────┘
└──► ClaimsSpecialist (terminal)
EnableReturnToPrevious() is active, so a patient's follow-up message goes directly to whichever specialist was last speaking.
Building the Workflow
var workflow = AgentWorkflowBuilder
.CreateHandoffBuilderWith(receptionist)
.WithHandoffs(receptionist, [appointmentScheduler, insuranceVerifier])
.WithHandoff(appointmentScheduler, receptionist)
.WithHandoff(appointmentScheduler, insuranceVerifier,
handoffReason: "Patient has an insurance question related to their appointment.")
.WithHandoff(insuranceVerifier, receptionist)
.WithHandoff(insuranceVerifier, claimsSpecialist,
handoffReason:
"Patient has a denied or disputed claim that requires specialist escalation.")
.EnableReturnToPrevious()
.Build();
Running Two Patient Turns
await using var session = await InProcessExecution.OpenStreamingAsync(workflow);
string[] patientMessages =
[
"Hi, I need to book a follow-up with an orthopedic specialist for my knee injury.",
"My insurer BlueCross denied my last MRI claim (policy BC-7890). What can I do?"
];
foreach (string message in patientMessages)
{
Console.WriteLine($"[Patient] {message}");
await session.TrySendMessageAsync(message);
string? currentAgent = null;
await foreach (var evt in session.WatchStreamAsync())
{
if (evt is AgentResponseUpdateEvent update)
{
if (currentAgent != update.ExecutorId)
{
if (currentAgent != null) Console.WriteLine();
currentAgent = update.ExecutorId;
Console.Write($"[{currentAgent}] ");
}
Console.Write(update.Data);
}
}
Console.WriteLine("\n");
}
The first message is routed by the Receptionist to the AppointmentScheduler. The second message (a denied-claim question) causes the InsuranceVerifier to escalate directly to the ClaimsSpecialist — all within a single streaming turn.
Full Example
// Lesson 16 — Handoff Orchestration
// Domain : Hospital patient intake
// Pattern : AgentWorkflowBuilder.CreateHandoffBuilderWith → multi-agent handoff
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using OpenAI;
namespace MicrosoftAgentFrameworkLesson.ConsoleApp.Handoff;
public static class HospitalIntakeHandoffDemo
{
public static async Task RunAsync()
{
var apiKey = Environment.GetEnvironmentVariable("OPEN_AI_KEY")
?? throw new InvalidOperationException("OPEN_AI_KEY environment variable is not set.");
IChatClient chatClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient();
Console.WriteLine("====== Handoff Orchestration — Hospital Patient Intake ======\n");
// ── Define Agents ──────────────────────────────────────────────────────────────
var receptionist = chatClient.AsAIAgent(
name: "Receptionist",
instructions:
"You are the hospital front-desk receptionist. Greet patients, collect their " +
"chief complaint in one or two sentences, and route them to the right specialist. " +
"If the patient needs to book or reschedule an appointment, hand off to " +
"AppointmentScheduler. If they have insurance or billing questions, hand off to " +
"InsuranceVerifier. Keep responses short and professional.");
var appointmentScheduler = chatClient.AsAIAgent(
name: "AppointmentScheduler",
instructions:
"You are the hospital appointment scheduler. Help patients book, reschedule, or " +
"cancel appointments. Confirm the department, preferred date, and any doctor " +
"preference. If the patient also raises insurance-related questions, hand off to " +
"InsuranceVerifier. Once done, let the patient know the booking is confirmed.");
var insuranceVerifier = chatClient.AsAIAgent(
name: "InsuranceVerifier",
instructions:
"You are the hospital insurance verification specialist. Verify insurance " +
"eligibility and coverage for requested treatments. Collect the insurance " +
"provider name and policy number. If the patient mentions a denied claim or " +
"billing dispute, hand off to ClaimsSpecialist for escalation.");
var claimsSpecialist = chatClient.AsAIAgent(
name: "ClaimsSpecialist",
instructions:
"You are a senior insurance claims specialist. Handle complex or disputed " +
"insurance claims. Explain the appeals process, required documentation, and " +
"realistic resolution timelines. Provide clear, actionable next steps.");
// ── Build Handoff Workflow ─────────────────────────────────────────────────────
var workflow = AgentWorkflowBuilder
.CreateHandoffBuilderWith(receptionist)
.WithHandoffs(receptionist, [appointmentScheduler, insuranceVerifier])
.WithHandoff(appointmentScheduler, receptionist)
.WithHandoff(appointmentScheduler, insuranceVerifier,
handoffReason: "Patient has an insurance question related to their appointment.")
.WithHandoff(insuranceVerifier, receptionist)
.WithHandoff(insuranceVerifier, claimsSpecialist,
handoffReason:
"Patient has a denied or disputed claim that requires specialist escalation.")
.EnableReturnToPrevious()
.Build();
// ── Simulate Patient Conversation (two turns) ──────────────────────────────────
string[] patientMessages =
[
"Hi, I need to book a follow-up appointment with an orthopedic specialist for my knee injury.",
"Also, my insurer BlueCross denied my last MRI claim (policy BC-7890). What can I do?"
];
await using var session = await InProcessExecution.OpenStreamingAsync(workflow);
foreach (string message in patientMessages)
{
Console.WriteLine($"[Patient] {message}");
Console.WriteLine();
await session.TrySendMessageAsync(message);
string? currentAgent = null;
await foreach (var evt in session.WatchStreamAsync())
{
if (evt is AgentResponseUpdateEvent update)
{
if (currentAgent == null || currentAgent != update.ExecutorId)
{
if (currentAgent != null) Console.WriteLine();
currentAgent = update.ExecutorId;
Console.Write($"[{currentAgent}] ");
}
Console.Write(update.Data);
}
}
Console.WriteLine("\n");
}
}
}
Reference
A Tour of Handoff Orchestration Pattern — Microsoft Agent Framework Blog