diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index de753d0e3f..00a1882018 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -249,6 +249,9 @@
+
+
+
@@ -297,7 +300,7 @@
-
+
diff --git a/dotnet/samples/03-workflows/Orchestration/Handoff/AgentRegistry.cs b/dotnet/samples/03-workflows/Orchestration/Handoff/AgentRegistry.cs
new file mode 100644
index 0000000000..3a21dd8d28
--- /dev/null
+++ b/dotnet/samples/03-workflows/Orchestration/Handoff/AgentRegistry.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Extensions.AI;
+
+///
+/// The registry of agents used in the workflow.
+///
+/// The to use as the agent backend.
+internal sealed class AgentRegistry(IChatClient chatClient)
+{
+ internal const string IntakeAgentName = "Assistant";
+ public AIAgent IntakeAgent { get; } = chatClient.AsAIAgent(
+ instructions:
+ """
+ You receive a user request and are responsible for routing to the correct initial expert agent.
+ """,
+ IntakeAgentName
+ );
+
+ internal const string LiquidityAnalysisAgentName = "Liquidity Analysis";
+ public AIAgent LiquidityAnalysisAgent { get; } = chatClient.AsAIAgent(
+ instructions:
+ """
+ You are responsible for Liquidity Analysis.
+ """,
+ LiquidityAnalysisAgentName
+ );
+
+ internal const string TaxAnalysisAgentName = "Tax Analysis";
+ public AIAgent TaxAnalysisAgent { get; } = chatClient.AsAIAgent(
+ instructions:
+ """
+ You are responsible for Tax Analysis.
+ """,
+ TaxAnalysisAgentName
+ );
+
+ internal const string ForeignExchangeAgentName = "Foreign Exchange Analysis";
+ public AIAgent ForeignExchangeAgent { get; } = chatClient.AsAIAgent(
+ instructions:
+ """
+ You are responsible for Foreign Exchange Analysis.
+ """,
+ ForeignExchangeAgentName
+ );
+
+ internal const string EquityAgentName = "Equity Analysis";
+ public AIAgent EquityAgent { get; } = chatClient.AsAIAgent(
+ instructions:
+ """
+ You are responsible for Equity Analysis.
+ """,
+ EquityAgentName
+ );
+
+ public IEnumerable Experts => [this.LiquidityAnalysisAgent, this.TaxAnalysisAgent, this.ForeignExchangeAgent, this.EquityAgent];
+
+ public HashSet All
+ {
+ get
+ {
+ if (field == null)
+ {
+ field = [this.IntakeAgent, .. this.Experts];
+ }
+
+ return field;
+ }
+ }
+}
diff --git a/dotnet/samples/03-workflows/Orchestration/Handoff/Handoff.csproj b/dotnet/samples/03-workflows/Orchestration/Handoff/Handoff.csproj
new file mode 100644
index 0000000000..5fe709e505
--- /dev/null
+++ b/dotnet/samples/03-workflows/Orchestration/Handoff/Handoff.csproj
@@ -0,0 +1,29 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+ MAAIW001
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs b/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs
new file mode 100644
index 0000000000..69cf8c168b
--- /dev/null
+++ b/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs
@@ -0,0 +1,125 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Extensions.AI;
+
+string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
+
+// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
+// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
+AIProjectClient projectClient = new(new Uri(endpoint), new DefaultAzureCredential());
+
+IChatClient chatClient = projectClient.ProjectOpenAIClient
+ .GetChatClient(deploymentName)
+ .AsIChatClient();
+
+Workflow workflow = CreateWorkflow(chatClient);
+
+await RunWorkflowAsync(workflow).ConfigureAwait(false);
+
+static Workflow CreateWorkflow(IChatClient chatClient)
+{
+ AgentRegistry agents = new(chatClient);
+
+ HandoffWorkflowBuilder handoffBuilder = AgentWorkflowBuilder.CreateHandoffBuilderWith(agents.IntakeAgent);
+
+ // Add a handoff to each of the experts from every agent in the registry (experts + Intake)
+ foreach (AIAgent expert in agents.Experts)
+ {
+ handoffBuilder.WithHandoffs(agents.All.Except([expert]), expert);
+ }
+
+ // Let agents request more user information and return to the asking agent (rather than going back to the intake agent)
+ handoffBuilder.EnableReturnToPrevious();
+
+ return handoffBuilder.Build();
+}
+
+static async Task RunWorkflowAsync(Workflow workflow)
+{
+ using CancellationTokenSource cts = CreateConsoleCancelKeySource();
+ await using StreamingRun run = await InProcessExecution.OpenStreamingAsync(workflow, cancellationToken: cts.Token)
+ .ConfigureAwait(false);
+
+ bool hadError = false;
+ do
+ {
+ Console.Write("> ");
+ string userInput = Console.ReadLine() ?? string.Empty;
+
+ if (userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ await run.TrySendMessageAsync(userInput);
+ string? speakingAgent = null;
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync(cts.Token))
+ {
+ switch (evt)
+ {
+ case AgentResponseUpdateEvent update:
+ {
+ if (speakingAgent == null || speakingAgent != update.Update.AuthorName)
+ {
+ speakingAgent = update.Update.AuthorName;
+ Console.Write($"\n{speakingAgent}: ");
+ }
+
+ Console.Write(update.Update.Text);
+ break;
+ }
+
+ case WorkflowErrorEvent workflowError:
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+
+ if (workflowError.Exception != null)
+ {
+ Console.WriteLine($"\nWorkflow error: {workflowError.Exception}");
+ }
+ else
+ {
+ Console.WriteLine("\nUnknown workflow error occurred.");
+ }
+
+ Console.ResetColor();
+
+ hadError = true;
+ break;
+ }
+
+ case WorkflowWarningEvent workflowWarning when workflowWarning.Data is string message:
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine(message);
+ Console.ResetColor();
+ break;
+ }
+ }
+ }
+ } while (!hadError);
+}
+
+static CancellationTokenSource CreateConsoleCancelKeySource()
+{
+ CancellationTokenSource cts = new();
+
+ // Normally, support a way to detach events, but in this case this is a termination signal, so cleanup will happen
+ // as part of application shutdown.
+ Console.CancelKeyPress += (s, args) =>
+ {
+ cts.Cancel();
+
+ // We handle cleanup + termination ourselves
+ args.Cancel = true;
+ };
+
+ return cts;
+}
diff --git a/dotnet/samples/03-workflows/README.md b/dotnet/samples/03-workflows/README.md
index d17148d60d..600a4c70ca 100644
--- a/dotnet/samples/03-workflows/README.md
+++ b/dotnet/samples/03-workflows/README.md
@@ -56,3 +56,9 @@ Once completed, please proceed to the other samples listed below.
| [Edge Conditions](./ConditionalEdges/01_EdgeCondition) | Introduces conditional edges for dynamic routing based on executor outputs |
| [Switch-Case Routing](./ConditionalEdges/02_SwitchCase) | Extends conditional edges with switch-case routing for multiple paths |
| [Multi-Selection Routing](./ConditionalEdges/03_MultiSelection) | Demonstrates multi-selection routing where one executor can trigger multiple downstream executors |
+
+### Orchestration Patterns
+
+| Sample | Concepts |
+|--------|----------|
+| [Handoff Orchestration](./Orchestration/Handoff) | Introduces the Handoff Orchestration pattern |