diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index c052057a58..d8694b50be 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -118,14 +118,14 @@
-
-
-
-
+
+
+
+
-
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 9801ccc105..c4cdfca7fe 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -60,6 +60,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -506,4 +525,4 @@
-
\ No newline at end of file
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj
new file mode 100644
index 0000000000..0c0e4f7fe0
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj
@@ -0,0 +1,42 @@
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+ SingleAgent
+ SingleAgent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/OrderCancelExecutors.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/OrderCancelExecutors.cs
new file mode 100644
index 0000000000..6d86bfe757
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/OrderCancelExecutors.cs
@@ -0,0 +1,215 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace SequentialWorkflow;
+
+///
+/// Looks up an order by its ID and return an Order object.
+///
+internal sealed class OrderLookup() : Executor("OrderLookup")
+{
+ public override async ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Magenta;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Activity] OrderLookup: Starting lookup for order '{message}'");
+ Console.ResetColor();
+
+ // Simulate database lookup with delay
+ await Task.Delay(TimeSpan.FromMicroseconds(100), cancellationToken);
+
+ Order order = new(
+ Id: message,
+ OrderDate: DateTime.UtcNow.AddDays(-1),
+ IsCancelled: false,
+ Customer: new Customer(Name: "Jerry", Email: "jerry@example.com"));
+
+ Console.ForegroundColor = ConsoleColor.Magenta;
+ Console.WriteLine($"│ [Activity] OrderLookup: Found order '{message}' for customer '{order.Customer.Name}'");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return order;
+ }
+}
+
+///
+/// Cancels an order.
+///
+internal sealed class OrderCancel() : Executor("OrderCancel")
+{
+ public override async ValueTask HandleAsync(
+ Order message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Activity] OrderCancel: Starting cancellation for order '{message.Id}'");
+ Console.ResetColor();
+
+ // Simulate a slow cancellation process (e.g., calling external payment system)
+ for (int i = 1; i <= 3; i++)
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.WriteLine("│ [Activity] OrderCancel: Processing...");
+ Console.ResetColor();
+ }
+
+ Order cancelledOrder = message with { IsCancelled = true };
+
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"│ [Activity] OrderCancel: ✓ Order '{cancelledOrder.Id}' has been cancelled");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return cancelledOrder;
+ }
+}
+
+///
+/// Sends a cancellation confirmation email to the customer.
+///
+internal sealed class SendEmail() : Executor("SendEmail")
+{
+ public override ValueTask HandleAsync(
+ Order message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Activity] SendEmail: Sending email to '{message.Customer.Email}'...");
+ Console.ResetColor();
+
+ string result = $"Cancellation email sent for order {message.Id} to {message.Customer.Email}.";
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("│ [Activity] SendEmail: ✓ Email sent successfully!");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return ValueTask.FromResult(result);
+ }
+}
+
+internal sealed record Order(string Id, DateTime OrderDate, bool IsCancelled, Customer Customer);
+
+internal sealed record Customer(string Name, string Email);
+
+///
+/// Represents a batch cancellation request with multiple order IDs and a reason.
+/// This demonstrates using a complex typed object as workflow input.
+///
+#pragma warning disable CA1812 // Instantiated via JSON deserialization at runtime
+internal sealed record BatchCancelRequest(string[] OrderIds, string Reason, bool NotifyCustomers);
+#pragma warning restore CA1812
+
+///
+/// Represents the result of processing a batch cancellation.
+///
+internal sealed record BatchCancelResult(int TotalOrders, int CancelledCount, string Reason);
+
+///
+/// Generates a status report for an order.
+///
+internal sealed class StatusReport() : Executor("StatusReport")
+{
+ public override ValueTask HandleAsync(
+ Order message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Activity] StatusReport: Generating report for order '{message.Id}'");
+ Console.ResetColor();
+
+ string status = message.IsCancelled ? "Cancelled" : "Active";
+ string result = $"Order {message.Id} for {message.Customer.Name}: Status={status}, Date={message.OrderDate:yyyy-MM-dd}";
+
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine($"│ [Activity] StatusReport: ✓ {result}");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return ValueTask.FromResult(result);
+ }
+}
+
+///
+/// Processes a batch cancellation request. Accepts a complex object
+/// as input, demonstrating how workflows can receive structured JSON input.
+///
+internal sealed class BatchCancelProcessor() : Executor("BatchCancelProcessor")
+{
+ public override async ValueTask HandleAsync(
+ BatchCancelRequest message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Activity] BatchCancelProcessor: Processing {message.OrderIds.Length} orders");
+ Console.WriteLine($"│ [Activity] BatchCancelProcessor: Reason: {message.Reason}");
+ Console.WriteLine($"│ [Activity] BatchCancelProcessor: Notify customers: {message.NotifyCustomers}");
+ Console.ResetColor();
+
+ // Simulate processing each order
+ int cancelledCount = 0;
+ foreach (string orderId in message.OrderIds)
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+ cancelledCount++;
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.WriteLine($"│ [Activity] BatchCancelProcessor: ✓ Cancelled order '{orderId}'");
+ Console.ResetColor();
+ }
+
+ BatchCancelResult result = new(message.OrderIds.Length, cancelledCount, message.Reason);
+
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"│ [Activity] BatchCancelProcessor: ✓ Batch complete: {cancelledCount}/{message.OrderIds.Length} cancelled");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return result;
+ }
+}
+
+///
+/// Generates a summary of the batch cancellation.
+///
+internal sealed class BatchCancelSummary() : Executor("BatchCancelSummary")
+{
+ public override ValueTask HandleAsync(
+ BatchCancelResult message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine("│ [Activity] BatchCancelSummary: Generating summary");
+ Console.ResetColor();
+
+ string result = $"Batch cancellation complete: {message.CancelledCount}/{message.TotalOrders} orders cancelled. Reason: {message.Reason}";
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine($"│ [Activity] BatchCancelSummary: ✓ {result}");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return ValueTask.FromResult(result);
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/Program.cs
new file mode 100644
index 0000000000..20da58d1a1
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/Program.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates three workflows that share executors.
+// The CancelOrder workflow cancels an order and notifies the customer.
+// The OrderStatus workflow looks up an order and generates a status report.
+// The BatchCancelOrders workflow accepts a complex JSON input to cancel multiple orders.
+// Both CancelOrder and OrderStatus reuse the same OrderLookup executor, demonstrating executor sharing.
+
+using Microsoft.Agents.AI.Hosting.AzureFunctions;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.Hosting;
+using SequentialWorkflow;
+
+// Define executors for all workflows
+OrderLookup orderLookup = new();
+OrderCancel orderCancel = new();
+SendEmail sendEmail = new();
+StatusReport statusReport = new();
+BatchCancelProcessor batchCancelProcessor = new();
+BatchCancelSummary batchCancelSummary = new();
+
+// Build the CancelOrder workflow: OrderLookup -> OrderCancel -> SendEmail
+Workflow cancelOrder = new WorkflowBuilder(orderLookup)
+ .WithName("CancelOrder")
+ .WithDescription("Cancel an order and notify the customer")
+ .AddEdge(orderLookup, orderCancel)
+ .AddEdge(orderCancel, sendEmail)
+ .Build();
+
+// Build the OrderStatus workflow: OrderLookup -> StatusReport
+// This workflow shares the OrderLookup executor with the CancelOrder workflow.
+Workflow orderStatus = new WorkflowBuilder(orderLookup)
+ .WithName("OrderStatus")
+ .WithDescription("Look up an order and generate a status report")
+ .AddEdge(orderLookup, statusReport)
+ .Build();
+
+// Build the BatchCancelOrders workflow: BatchCancelProcessor -> BatchCancelSummary
+// This workflow demonstrates using a complex JSON object as the workflow input.
+Workflow batchCancelOrders = new WorkflowBuilder(batchCancelProcessor)
+ .WithName("BatchCancelOrders")
+ .WithDescription("Cancel multiple orders in a batch using a complex JSON input")
+ .AddEdge(batchCancelProcessor, batchCancelSummary)
+ .Build();
+
+using IHost app = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+ .ConfigureDurableWorkflows(workflows => workflows.AddWorkflows(cancelOrder, orderStatus, batchCancelOrders))
+ .Build();
+app.Run();
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/README.md b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/README.md
new file mode 100644
index 0000000000..384fd358a7
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/README.md
@@ -0,0 +1,100 @@
+# Sequential Workflow Sample
+
+This sample demonstrates how to use the Microsoft Agent Framework to create an Azure Functions app that hosts durable workflows with sequential executor chains. It showcases two workflows that share a common executor, demonstrating executor reuse across workflows.
+
+## Key Concepts Demonstrated
+
+- Defining workflows with sequential executor chains using `WorkflowBuilder`
+- Sharing executors across multiple workflows (the `OrderLookup` executor is used by both workflows)
+- Registering workflows with the Function app using `ConfigureDurableWorkflows`
+- Durable orchestration ensuring workflows survive process restarts and failures
+- Starting workflows via HTTP requests
+- Viewing workflow execution history and status in the Durable Task Scheduler (DTS) dashboard
+
+## Workflows
+
+This sample defines two workflows:
+
+1. **CancelOrder**: `OrderLookup` → `OrderCancel` → `SendEmail` — Looks up an order, cancels it, and sends a confirmation email.
+2. **OrderStatus**: `OrderLookup` → `StatusReport` — Looks up an order and generates a status report.
+
+Both workflows share the `OrderLookup` executor, which is registered only once by the framework.
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
+
+## Running the Sample
+
+With the environment setup and function app running, you can test the sample by sending HTTP requests to the workflow endpoints.
+
+You can use the `demo.http` file to trigger the workflows, or a command line tool like `curl` as shown below:
+
+### Cancel an Order
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl -X POST http://localhost:7071/api/workflows/CancelOrder/run \
+ -H "Content-Type: text/plain" \
+ -d "12345"
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Method Post `
+ -Uri http://localhost:7071/api/workflows/CancelOrder/run `
+ -ContentType text/plain `
+ -Body "12345"
+```
+
+The response will confirm the workflow orchestration has started:
+
+```text
+Workflow orchestration started for CancelOrder. Orchestration runId: abc123def456
+```
+
+> **Tip:** You can provide a custom run ID by appending a `runId` query parameter:
+>
+> ```bash
+> curl -X POST "http://localhost:7071/api/workflows/CancelOrder/run?runId=my-order-123" \
+> -H "Content-Type: text/plain" \
+> -d "12345"
+> ```
+>
+> If not provided, a unique run ID is auto-generated.
+
+In the function app logs, you will see the sequential execution of each executor:
+
+```text
+│ [Activity] OrderLookup: Starting lookup for order '12345'
+│ [Activity] OrderLookup: Found order '12345' for customer 'Jerry'
+│ [Activity] OrderCancel: Starting cancellation for order '12345'
+│ [Activity] OrderCancel: ✓ Order '12345' has been cancelled
+│ [Activity] SendEmail: Sending email to 'jerry@example.com'...
+│ [Activity] SendEmail: ✓ Email sent successfully!
+```
+
+### Get Order Status
+
+```bash
+curl -X POST http://localhost:7071/api/workflows/OrderStatus/run \
+ -H "Content-Type: text/plain" \
+ -d "12345"
+```
+
+The `OrderStatus` workflow reuses the same `OrderLookup` executor and then generates a status report:
+
+```text
+│ [Activity] OrderLookup: Starting lookup for order '12345'
+│ [Activity] OrderLookup: Found order '12345' for customer 'Jerry'
+│ [Activity] StatusReport: Generating report for order '12345'
+│ [Activity] StatusReport: ✓ Order 12345 for Jerry: Status=Active, Date=2025-01-01
+```
+
+### Viewing Workflows in the DTS Dashboard
+
+After running a workflow, you can navigate to the Durable Task Scheduler (DTS) dashboard to visualize the completed orchestration, inspect inputs/outputs for each step, and view execution history.
+
+If you are using the DTS emulator, the dashboard is available at `http://localhost:8082`.
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/demo.http b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/demo.http
new file mode 100644
index 0000000000..8366216a6c
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/demo.http
@@ -0,0 +1,26 @@
+# Default endpoint address for local testing
+@authority=http://localhost:7071
+
+### Cancel an order
+POST {{authority}}/api/workflows/CancelOrder/run
+Content-Type: text/plain
+
+12345
+
+### Cancel an order with a custom run ID
+POST {{authority}}/api/workflows/CancelOrder/run?runId=my-custom-id-123
+Content-Type: text/plain
+
+99999
+
+### Get order status (shares OrderLookup executor with CancelOrder)
+POST {{authority}}/api/workflows/OrderStatus/run
+Content-Type: text/plain
+
+12345
+
+### Batch cancel orders with a complex JSON input
+POST {{authority}}/api/workflows/BatchCancelOrders/run
+Content-Type: application/json
+
+{"orderIds": ["1001", "1002", "1003"], "reason": "Customer requested cancellation", "notifyCustomers": true}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/host.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/host.json
new file mode 100644
index 0000000000..9384a0a583
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/host.json
@@ -0,0 +1,20 @@
+{
+ "version": "2.0",
+ "logging": {
+ "logLevel": {
+ "Microsoft.Agents.AI.DurableTask": "Information",
+ "Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
+ "DurableTask": "Information",
+ "Microsoft.DurableTask": "Information"
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "hubName": "default",
+ "storageProvider": {
+ "type": "AzureManaged",
+ "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj
new file mode 100644
index 0000000000..0c0e4f7fe0
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj
@@ -0,0 +1,42 @@
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+ SingleAgent
+ SingleAgent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/ExpertExecutors.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/ExpertExecutors.cs
new file mode 100644
index 0000000000..40674126f6
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/ExpertExecutors.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowConcurrency;
+
+///
+/// Parses and validates the incoming question before sending to AI agents.
+///
+internal sealed class ParseQuestionExecutor() : Executor("ParseQuestion")
+{
+ public override ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Magenta;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine("│ [ParseQuestion] Preparing question for AI agents...");
+
+ string formattedQuestion = message.Trim();
+ if (!formattedQuestion.EndsWith('?'))
+ {
+ formattedQuestion += "?";
+ }
+
+ Console.WriteLine($"│ [ParseQuestion] Question: \"{formattedQuestion}\"");
+ Console.WriteLine("│ [ParseQuestion] → Sending to Physicist and Chemist in PARALLEL...");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return ValueTask.FromResult(formattedQuestion);
+ }
+}
+
+///
+/// Aggregates responses from all AI agents into a comprehensive answer.
+/// This is the Fan-in point where parallel results are collected.
+///
+internal sealed class AggregatorExecutor() : Executor("Aggregator")
+{
+ public override ValueTask HandleAsync(
+ string[] message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Aggregator] 📋 Received {message.Length} AI agent responses");
+ Console.WriteLine("│ [Aggregator] Combining into comprehensive answer...");
+ Console.WriteLine("│ [Aggregator] ✓ Aggregation complete!");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ string aggregatedResult = "═══════════════════════════════════════════════════════════════\n" +
+ " AI EXPERT PANEL RESPONSES\n" +
+ "═══════════════════════════════════════════════════════════════\n\n";
+
+ for (int i = 0; i < message.Length; i++)
+ {
+ string expertLabel = i == 0 ? "⚛️ PHYSICIST" : "🧪 CHEMIST";
+ aggregatedResult += $"{expertLabel}:\n{message[i]}\n\n";
+ }
+
+ aggregatedResult += "═══════════════════════════════════════════════════════════════\n" +
+ $"Summary: Received perspectives from {message.Length} AI experts.\n" +
+ "═══════════════════════════════════════════════════════════════";
+
+ return ValueTask.FromResult(aggregatedResult);
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/Program.cs
new file mode 100644
index 0000000000..6532009d4b
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/Program.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.Hosting.AzureFunctions;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.Hosting;
+using OpenAI.Chat;
+using WorkflowConcurrency;
+
+string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
+string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
+
+// Create Azure OpenAI client
+AzureOpenAIClient openAiClient = !string.IsNullOrEmpty(azureOpenAiKey)
+ ? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
+ : new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
+ChatClient chatClient = openAiClient.GetChatClient(deploymentName);
+
+// Define the 4 executors for the workflow
+ParseQuestionExecutor parseQuestion = new();
+AIAgent physicist = chatClient.AsAIAgent("You are a physics expert. Be concise (2-3 sentences).", "Physicist");
+AIAgent chemist = chatClient.AsAIAgent("You are a chemistry expert. Be concise (2-3 sentences).", "Chemist");
+AggregatorExecutor aggregator = new();
+
+// Build workflow: ParseQuestion -> [Physicist, Chemist] (parallel) -> Aggregator
+Workflow workflow = new WorkflowBuilder(parseQuestion)
+ .WithName("ExpertReview")
+ .AddFanOutEdge(parseQuestion, [physicist, chemist])
+ .AddFanInBarrierEdge([physicist, chemist], aggregator)
+ .Build();
+
+using IHost app = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+ .ConfigureDurableWorkflows(workflows => workflows.AddWorkflows(workflow))
+ .Build();
+app.Run();
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/README.md b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/README.md
new file mode 100644
index 0000000000..73230ff048
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/README.md
@@ -0,0 +1,90 @@
+# Concurrent Workflow Sample
+
+This sample demonstrates how to use the Microsoft Agent Framework to create an Azure Functions app that orchestrates concurrent execution of multiple AI agents using the fan-out/fan-in pattern within a durable workflow.
+
+## Key Concepts Demonstrated
+
+- Defining workflows with fan-out/fan-in edges for parallel execution using `WorkflowBuilder`
+- Mixing custom executors with AI agents in a single workflow
+- Concurrent execution of multiple AI agents (physics and chemistry experts)
+- Response aggregation from parallel branches into a unified result
+- Durable orchestration with automatic checkpointing and resumption from failures
+- Viewing workflow execution history and status in the Durable Task Scheduler (DTS) dashboard
+
+## Workflow
+
+This sample defines a single workflow:
+
+**ExpertReview**: `ParseQuestion` → [`Physicist`, `Chemist`] (parallel) → `Aggregator`
+
+1. **ParseQuestion** — A custom executor that validates and formats the incoming question.
+2. **Physicist** and **Chemist** — AI agents that run concurrently, each providing an expert perspective.
+3. **Aggregator** — A custom executor that combines the parallel responses into a comprehensive answer.
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
+
+This sample requires Azure OpenAI. Set the following environment variables:
+
+- `AZURE_OPENAI_ENDPOINT` — Your Azure OpenAI endpoint URL.
+- `AZURE_OPENAI_DEPLOYMENT` — The name of your chat model deployment.
+- `AZURE_OPENAI_KEY` (optional) — Your Azure OpenAI API key. If not set, Azure CLI credentials are used.
+
+## Running the Sample
+
+With the environment setup and function app running, you can test the sample by sending an HTTP request with a science question to the workflow endpoint.
+
+You can use the `demo.http` file to trigger the workflow, or a command line tool like `curl` as shown below:
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl -X POST http://localhost:7071/api/workflows/ExpertReview/run \
+ -H "Content-Type: text/plain" \
+ -d "What is temperature?"
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Method Post `
+ -Uri http://localhost:7071/api/workflows/ExpertReview/run `
+ -ContentType text/plain `
+ -Body "What is temperature?"
+```
+
+The response will confirm the workflow orchestration has started:
+
+```text
+Workflow orchestration started for ExpertReview. Orchestration runId: abc123def456
+```
+
+> **Tip:** You can provide a custom run ID by appending a `runId` query parameter:
+>
+> ```bash
+> curl -X POST "http://localhost:7071/api/workflows/ExpertReview/run?runId=my-review-123" \
+> -H "Content-Type: text/plain" \
+> -d "What is temperature?"
+> ```
+>
+> If not provided, a unique run ID is auto-generated.
+
+In the function app logs, you will see the fan-out/fan-in execution pattern:
+
+```text
+│ [ParseQuestion] Preparing question for AI agents...
+│ [ParseQuestion] Question: "What is temperature?"
+│ [ParseQuestion] → Sending to Physicist and Chemist in PARALLEL...
+│ [Aggregator] 📋 Received 2 AI agent responses
+│ [Aggregator] Combining into comprehensive answer...
+│ [Aggregator] ✓ Aggregation complete!
+```
+
+The Physicist and Chemist AI agents execute concurrently, and the Aggregator combines their responses into a formatted expert panel result.
+
+### Viewing Workflows in the DTS Dashboard
+
+After running a workflow, you can navigate to the Durable Task Scheduler (DTS) dashboard to visualize the completed orchestration, inspect inputs/outputs for each step, and view execution history.
+
+If you are using the DTS emulator, the dashboard is available at `http://localhost:8082`.
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/demo.http b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/demo.http
new file mode 100644
index 0000000000..1a9e563126
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/demo.http
@@ -0,0 +1,14 @@
+# Default endpoint address for local testing
+@authority=http://localhost:7071
+
+### Prompt the agent
+POST {{authority}}/api/workflows/ExpertReview/run
+Content-Type: text/plain
+
+What is temperature?
+
+### Start with a custom run ID
+POST {{authority}}/api/workflows/ExpertReview/run?runId=my-review-123
+Content-Type: text/plain
+
+What is gravity?
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/host.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/host.json
new file mode 100644
index 0000000000..9384a0a583
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/host.json
@@ -0,0 +1,20 @@
+{
+ "version": "2.0",
+ "logging": {
+ "logLevel": {
+ "Microsoft.Agents.AI.DurableTask": "Information",
+ "Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
+ "DurableTask": "Information",
+ "Microsoft.DurableTask": "Information"
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "hubName": "default",
+ "storageProvider": {
+ "type": "AzureManaged",
+ "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj
new file mode 100644
index 0000000000..c569deacd0
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj
@@ -0,0 +1,43 @@
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+ WorkflowHITLFunctions
+ WorkflowHITLFunctions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/Executors.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/Executors.cs
new file mode 100644
index 0000000000..c299ee2cd5
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/Executors.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowHITLFunctions;
+
+/// Expense approval request passed to the RequestPort.
+public record ApprovalRequest(string ExpenseId, decimal Amount, string EmployeeName);
+
+/// Approval response received from the RequestPort.
+public record ApprovalResponse(bool Approved, string? Comments);
+
+/// Looks up expense details and creates an approval request.
+internal sealed class CreateApprovalRequest() : Executor("RetrieveRequest")
+{
+ public override ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ // In a real scenario, this would look up expense details from a database
+ return new ValueTask(new ApprovalRequest(message, 1500.00m, "Jerry"));
+ }
+}
+
+/// Prepares the approval request for finance review after manager approval.
+internal sealed class PrepareFinanceReview() : Executor("PrepareFinanceReview")
+{
+ public override ValueTask HandleAsync(
+ ApprovalResponse message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ if (!message.Approved)
+ {
+ throw new InvalidOperationException("Cannot proceed to finance review — manager denied the expense.");
+ }
+
+ // In a real scenario, this would retrieve the original expense details
+ return new ValueTask(new ApprovalRequest("EXP-2025-001", 1500.00m, "Jerry"));
+ }
+}
+
+/// Processes the expense reimbursement based on the parallel approval responses.
+internal sealed class ExpenseReimburse() : Executor("Reimburse")
+{
+ public override async ValueTask HandleAsync(
+ ApprovalResponse[] message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ // Check that all parallel approvals passed
+ ApprovalResponse? denied = Array.Find(message, r => !r.Approved);
+ if (denied is not null)
+ {
+ return $"Expense reimbursement denied. Comments: {denied.Comments}";
+ }
+
+ // Simulate payment processing
+ await Task.Delay(1000, cancellationToken);
+ return $"Expense reimbursed at {DateTime.UtcNow:O}";
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/Program.cs
new file mode 100644
index 0000000000..1aa1972e62
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/Program.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates a Human-in-the-Loop (HITL) workflow hosted in Azure Functions.
+//
+// ┌──────────────────────┐ ┌────────────────┐ ┌─────────────────────┐ ┌────────────────────┐
+// │ CreateApprovalRequest│──►│ManagerApproval │──►│PrepareFinanceReview │──┬►│ BudgetApproval │──┐
+// └──────────────────────┘ │ (RequestPort) │ └─────────────────────┘ │ │ (RequestPort) │ │
+// └────────────────┘ │ └────────────────────┘ │ ┌─────────────────┐
+// │ ├─►│ExpenseReimburse │
+// │ ┌────────────────────┐ │ └─────────────────┘
+// └►│ComplianceApproval │──┘
+// │ (RequestPort) │
+// └────────────────────┘
+//
+// The workflow pauses at three RequestPorts — one for the manager, then two in parallel for finance.
+// After manager approval, BudgetApproval and ComplianceApproval run concurrently via fan-out/fan-in.
+// The framework auto-generates three HTTP endpoints for each workflow:
+// POST /api/workflows/{name}/run - Start the workflow
+// GET /api/workflows/{name}/status/{id} - Check status and pending approvals
+// POST /api/workflows/{name}/respond/{id} - Send approval response to resume
+
+using Microsoft.Agents.AI.Hosting.AzureFunctions;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.Hosting;
+using WorkflowHITLFunctions;
+
+// Define executors and RequestPorts for the three HITL pause points
+CreateApprovalRequest createRequest = new();
+RequestPort managerApproval = RequestPort.Create("ManagerApproval");
+PrepareFinanceReview prepareFinanceReview = new();
+RequestPort budgetApproval = RequestPort.Create("BudgetApproval");
+RequestPort complianceApproval = RequestPort.Create("ComplianceApproval");
+ExpenseReimburse reimburse = new();
+
+// Build the workflow: CreateApprovalRequest -> ManagerApproval -> PrepareFinanceReview -> [BudgetApproval AND ComplianceApproval] -> ExpenseReimburse
+Workflow expenseApproval = new WorkflowBuilder(createRequest)
+ .WithName("ExpenseReimbursement")
+ .WithDescription("Expense reimbursement with manager and parallel finance approvals")
+ .AddEdge(createRequest, managerApproval)
+ .AddEdge(managerApproval, prepareFinanceReview)
+ .AddFanOutEdge(prepareFinanceReview, [budgetApproval, complianceApproval])
+ .AddFanInBarrierEdge([budgetApproval, complianceApproval], reimburse)
+ .Build();
+
+using IHost app = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+ .ConfigureDurableWorkflows(workflows => workflows.AddWorkflow(expenseApproval, exposeStatusEndpoint: true))
+ .Build();
+app.Run();
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/README.md b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/README.md
new file mode 100644
index 0000000000..27322b7b6a
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/README.md
@@ -0,0 +1,266 @@
+# Human-in-the-Loop (HITL) Workflow — Azure Functions
+
+This sample demonstrates a durable workflow with Human-in-the-Loop support hosted in Azure Functions. The workflow pauses at three `RequestPort` nodes — one sequential manager approval, then two parallel finance approvals (budget and compliance) via fan-out/fan-in. Approval responses are sent via HTTP endpoints.
+
+## Key Concepts Demonstrated
+
+- Using multiple `RequestPort` nodes for sequential and parallel human-in-the-loop interactions in a durable workflow
+- Fan-out/fan-in pattern for parallel approval steps
+- Auto-generated HTTP endpoints for running workflows, checking status, and sending HITL responses
+- Pausing orchestrations via `WaitForExternalEvent` and resuming via `RaiseEventAsync`
+- Viewing inputs the workflow is waiting for via the status endpoint
+
+## Workflow
+
+This sample implements the following workflow:
+
+```
+┌──────────────────────┐ ┌────────────────┐ ┌─────────────────────┐ ┌────────────────────┐
+│ CreateApprovalRequest│──►│ManagerApproval │──►│PrepareFinanceReview │──┬►│ BudgetApproval │──┐
+└──────────────────────┘ │ (RequestPort) │ └─────────────────────┘ │ │ (RequestPort) │ │
+ └────────────────┘ │ └────────────────────┘ │ ┌─────────────────┐
+ │ ├─►│ExpenseReimburse │
+ │ ┌────────────────────┐ │ └─────────────────┘
+ └►│ComplianceApproval │──┘
+ │ (RequestPort) │
+ └────────────────────┘
+```
+
+## HTTP Endpoints
+
+The framework auto-generates these endpoints for workflows with `RequestPort` nodes:
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| POST | `/api/workflows/ExpenseReimbursement/run` | Start the workflow |
+| GET | `/api/workflows/ExpenseReimbursement/status/{runId}` | Check status and inputs the workflow is waiting for |
+| POST | `/api/workflows/ExpenseReimbursement/respond/{runId}` | Send approval response to resume |
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for information on how to configure the environment, including how to install and run the Durable Task Scheduler.
+
+## Running the Sample
+
+With the environment setup and function app running, you can test the sample by sending HTTP requests to the workflow endpoints.
+
+You can use the `demo.http` file to trigger the workflow, or a command line tool like `curl` as shown below:
+
+### Step 1: Start the Workflow
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl -X POST http://localhost:7071/api/workflows/ExpenseReimbursement/run \
+ -H "Content-Type: text/plain" -d "EXP-2025-001"
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Method Post `
+ -Uri http://localhost:7071/api/workflows/ExpenseReimbursement/run `
+ -ContentType text/plain `
+ -Body "EXP-2025-001"
+```
+
+The response will confirm the workflow orchestration has started:
+
+```text
+Workflow orchestration started for ExpenseReimbursement. Orchestration runId: abc123def456
+```
+
+> [!TIP]
+> You can provide a custom run ID by appending a `runId` query parameter:
+>
+> Bash (Linux/macOS/WSL):
+>
+> ```bash
+> curl -X POST "http://localhost:7071/api/workflows/ExpenseReimbursement/run?runId=expense-001" \
+> -H "Content-Type: text/plain" -d "EXP-2025-001"
+> ```
+>
+> PowerShell:
+>
+> ```powershell
+> Invoke-RestMethod -Method Post `
+> -Uri "http://localhost:7071/api/workflows/ExpenseReimbursement/run?runId=expense-001" `
+> -ContentType text/plain `
+> -Body "EXP-2025-001"
+> ```
+>
+> If not provided, a unique run ID is auto-generated.
+
+### Step 2: Check Workflow Status
+
+The workflow pauses at the `ManagerApproval` RequestPort. Query the status endpoint to see what input it is waiting for:
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl http://localhost:7071/api/workflows/ExpenseReimbursement/status/{runId}
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Uri http://localhost:7071/api/workflows/ExpenseReimbursement/status/{runId}
+```
+
+```json
+{
+ "runId": "{runId}",
+ "status": "Running",
+ "waitingForInput": [
+ { "eventName": "ManagerApproval", "input": { "ExpenseId": "EXP-2025-001", "Amount": 1500.00, "EmployeeName": "Jerry" } }
+ ]
+}
+```
+
+> [!TIP]
+> You can also verify this in the DTS dashboard at `http://localhost:8082`. Find the orchestration by its `runId` and you will see it is in a "Running" state, paused at a `WaitForExternalEvent` call for the `ManagerApproval` event.
+
+### Step 3: Send Manager Approval Response
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl -X POST http://localhost:7071/api/workflows/ExpenseReimbursement/respond/{runId} \
+ -H "Content-Type: application/json" \
+ -d '{"eventName": "ManagerApproval", "response": {"Approved": true, "Comments": "Approved by manager."}}'
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Method Post `
+ -Uri http://localhost:7071/api/workflows/ExpenseReimbursement/respond/{runId} `
+ -ContentType application/json `
+ -Body '{"eventName": "ManagerApproval", "response": {"Approved": true, "Comments": "Approved by manager."}}'
+```
+
+```json
+{
+ "message": "Response sent to workflow.",
+ "runId": "{runId}",
+ "eventName": "ManagerApproval",
+ "validated": true
+}
+```
+
+### Step 4: Check Workflow Status Again
+
+The workflow now pauses at both the `BudgetApproval` and `ComplianceApproval` RequestPorts in parallel:
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl http://localhost:7071/api/workflows/ExpenseReimbursement/status/{runId}
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Uri http://localhost:7071/api/workflows/ExpenseReimbursement/status/{runId}
+```
+
+```json
+{
+ "runId": "{runId}",
+ "status": "Running",
+ "waitingForInput": [
+ { "eventName": "BudgetApproval", "input": { "ExpenseId": "EXP-2025-001", "Amount": 1500.00, "EmployeeName": "Jerry" } },
+ { "eventName": "ComplianceApproval", "input": { "ExpenseId": "EXP-2025-001", "Amount": 1500.00, "EmployeeName": "Jerry" } }
+ ]
+}
+```
+
+### Step 5a: Send Budget Approval Response
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl -X POST http://localhost:7071/api/workflows/ExpenseReimbursement/respond/{runId} \
+ -H "Content-Type: application/json" \
+ -d '{"eventName": "BudgetApproval", "response": {"Approved": true, "Comments": "Budget approved."}}'
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Method Post `
+ -Uri http://localhost:7071/api/workflows/ExpenseReimbursement/respond/{runId} `
+ -ContentType application/json `
+ -Body '{"eventName": "BudgetApproval", "response": {"Approved": true, "Comments": "Budget approved."}}'
+```
+
+```json
+{
+ "message": "Response sent to workflow.",
+ "runId": "{runId}",
+ "eventName": "BudgetApproval",
+ "validated": true
+}
+```
+
+### Step 5b: Send Compliance Approval Response
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl -X POST http://localhost:7071/api/workflows/ExpenseReimbursement/respond/{runId} \
+ -H "Content-Type: application/json" \
+ -d '{"eventName": "ComplianceApproval", "response": {"Approved": true, "Comments": "Compliance approved."}}'
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Method Post `
+ -Uri http://localhost:7071/api/workflows/ExpenseReimbursement/respond/{runId} `
+ -ContentType application/json `
+ -Body '{"eventName": "ComplianceApproval", "response": {"Approved": true, "Comments": "Compliance approved."}}'
+```
+
+```json
+{
+ "message": "Response sent to workflow.",
+ "runId": "{runId}",
+ "eventName": "ComplianceApproval",
+ "validated": true
+}
+```
+
+### Step 6: Check Final Status
+
+After all approvals, the workflow completes and the expense is reimbursed:
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl http://localhost:7071/api/workflows/ExpenseReimbursement/status/{runId}
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Uri http://localhost:7071/api/workflows/ExpenseReimbursement/status/{runId}
+```
+
+```json
+{
+ "runId": "{runId}",
+ "status": "Completed",
+ "waitingForInput": null
+}
+```
+
+### Viewing Workflows in the DTS Dashboard
+
+After running a workflow, you can navigate to the Durable Task Scheduler (DTS) dashboard to visualize the orchestration and inspect its execution history.
+
+If you are using the DTS emulator, the dashboard is available at `http://localhost:8082`.
+
+1. Open the dashboard and look for the orchestration instance matching the `runId` returned in Step 1 (e.g., `abc123def456` or your custom ID like `expense-001`).
+2. Click into the instance to see the execution timeline, which shows each executor activity and the `WaitForExternalEvent` pauses where the workflow waited for human input — including the two parallel finance approvals.
+3. Expand individual activity steps to inspect inputs and outputs — for example, the `ManagerApproval`, `BudgetApproval`, and `ComplianceApproval` external events will show the approval request sent and the response received.
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/demo.http b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/demo.http
new file mode 100644
index 0000000000..5e2993ac1c
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/demo.http
@@ -0,0 +1,53 @@
+# Default endpoint address for local testing
+@authority=http://localhost:7071
+
+### Step 1: Start the expense reimbursement workflow
+POST {{authority}}/api/workflows/ExpenseReimbursement/run
+Content-Type: text/plain
+
+EXP-2025-001
+
+### Step 1 (alternative): Start the workflow with a custom run ID
+POST {{authority}}/api/workflows/ExpenseReimbursement/run?runId=expense-001
+Content-Type: text/plain
+
+EXP-2025-001
+
+### Step 2: Check workflow status (replace {runId} with actual run ID from Step 1)
+GET {{authority}}/api/workflows/ExpenseReimbursement/status/{runId}
+
+### Step 3: Send manager approval (replace {runId} with actual run ID from Step 1)
+POST {{authority}}/api/workflows/ExpenseReimbursement/respond/{runId}
+Content-Type: application/json
+
+{"eventName": "ManagerApproval", "response": {"Approved": true, "Comments": "Approved by manager."}}
+
+### Step 3 (alternative): Deny the expense at manager level
+POST {{authority}}/api/workflows/ExpenseReimbursement/respond/{runId}
+Content-Type: application/json
+
+{"eventName": "ManagerApproval", "response": {"Approved": false, "Comments": "Insufficient documentation. Please resubmit."}}
+
+### Step 4: Check workflow status after manager approval (now waiting for parallel finance approvals)
+GET {{authority}}/api/workflows/ExpenseReimbursement/status/{runId}
+
+### Step 5a: Send budget approval (replace {runId} with actual run ID from Step 1)
+POST {{authority}}/api/workflows/ExpenseReimbursement/respond/{runId}
+Content-Type: application/json
+
+{"eventName": "BudgetApproval", "response": {"Approved": true, "Comments": "Budget approved."}}
+
+### Step 5b: Send compliance approval (replace {runId} with actual run ID from Step 1)
+POST {{authority}}/api/workflows/ExpenseReimbursement/respond/{runId}
+Content-Type: application/json
+
+{"eventName": "ComplianceApproval", "response": {"Approved": true, "Comments": "Compliance approved."}}
+
+### Step 5b (alternative): Deny the expense at compliance level
+POST {{authority}}/api/workflows/ExpenseReimbursement/respond/{runId}
+Content-Type: application/json
+
+{"eventName": "ComplianceApproval", "response": {"Approved": false, "Comments": "Compliance requirements not met."}}
+
+### Step 6: Check final workflow status after all approvals
+GET {{authority}}/api/workflows/ExpenseReimbursement/status/{runId}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/host.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/host.json
new file mode 100644
index 0000000000..9384a0a583
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/host.json
@@ -0,0 +1,20 @@
+{
+ "version": "2.0",
+ "logging": {
+ "logLevel": {
+ "Microsoft.Agents.AI.DurableTask": "Information",
+ "Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
+ "DurableTask": "Information",
+ "Microsoft.DurableTask": "Information"
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "hubName": "default",
+ "storageProvider": {
+ "type": "AzureManaged",
+ "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/01_SequentialWorkflow.csproj b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/01_SequentialWorkflow.csproj
new file mode 100644
index 0000000000..8a5308a6f5
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/01_SequentialWorkflow.csproj
@@ -0,0 +1,29 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ SequentialWorkflow
+ SequentialWorkflow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/OrderCancelExecutors.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/OrderCancelExecutors.cs
new file mode 100644
index 0000000000..474cb8bcaa
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/OrderCancelExecutors.cs
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace SequentialWorkflow;
+
+///
+/// Represents a request to cancel an order.
+///
+/// The ID of the order to cancel.
+/// The reason for cancellation.
+internal sealed record OrderCancelRequest(string OrderId, string Reason);
+
+///
+/// Looks up an order by its ID and return an Order object.
+///
+internal sealed class OrderLookup() : Executor("OrderLookup")
+{
+ public override async ValueTask HandleAsync(
+ OrderCancelRequest message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Magenta;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Activity] OrderLookup: Starting lookup for order '{message.OrderId}'");
+ Console.WriteLine($"│ [Activity] OrderLookup: Cancellation reason: '{message.Reason}'");
+ Console.ResetColor();
+
+ // Simulate database lookup with delay
+ await Task.Delay(TimeSpan.FromMicroseconds(100), cancellationToken);
+
+ Order order = new(
+ Id: message.OrderId,
+ OrderDate: DateTime.UtcNow.AddDays(-1),
+ IsCancelled: false,
+ CancelReason: message.Reason,
+ Customer: new Customer(Name: "Jerry", Email: "jerry@example.com"));
+
+ Console.ForegroundColor = ConsoleColor.Magenta;
+ Console.WriteLine($"│ [Activity] OrderLookup: Found order '{message.OrderId}' for customer '{order.Customer.Name}'");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return order;
+ }
+}
+
+///
+/// Cancels an order.
+///
+internal sealed class OrderCancel() : Executor("OrderCancel")
+{
+ public override async ValueTask HandleAsync(
+ Order message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ // Log that this activity is executing (not replaying)
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Activity] OrderCancel: Starting cancellation for order '{message.Id}'");
+ Console.ResetColor();
+
+ // Simulate a slow cancellation process (e.g., calling external payment system)
+ for (int i = 1; i <= 3; i++)
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.WriteLine("│ [Activity] OrderCancel: Processing...");
+ Console.ResetColor();
+ }
+
+ Order cancelledOrder = message with { IsCancelled = true };
+
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"│ [Activity] OrderCancel: ✓ Order '{cancelledOrder.Id}' has been cancelled");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return cancelledOrder;
+ }
+}
+
+///
+/// Sends a cancellation confirmation email to the customer.
+///
+internal sealed class SendEmail() : Executor("SendEmail")
+{
+ public override ValueTask HandleAsync(
+ Order message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Activity] SendEmail: Sending email to '{message.Customer.Email}'...");
+ Console.ResetColor();
+
+ string result = $"Cancellation email sent for order {message.Id} to {message.Customer.Email}.";
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("│ [Activity] SendEmail: ✓ Email sent successfully!");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return ValueTask.FromResult(result);
+ }
+}
+
+internal sealed record Order(string Id, DateTime OrderDate, bool IsCancelled, string? CancelReason, Customer Customer);
+
+internal sealed record Customer(string Name, string Email);
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/Program.cs
new file mode 100644
index 0000000000..03e4ed5928
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/Program.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using SequentialWorkflow;
+
+// Get DTS connection string from environment variable
+string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+
+// Define executors for the workflow
+OrderLookup orderLookup = new();
+OrderCancel orderCancel = new();
+SendEmail sendEmail = new();
+
+// Build the CancelOrder workflow: OrderLookup -> OrderCancel -> SendEmail
+Workflow cancelOrder = new WorkflowBuilder(orderLookup)
+ .WithName("CancelOrder")
+ .WithDescription("Cancel an order and notify the customer")
+ .AddEdge(orderLookup, orderCancel)
+ .AddEdge(orderCancel, sendEmail)
+ .Build();
+
+IHost host = Host.CreateDefaultBuilder(args)
+.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
+.ConfigureServices(services =>
+{
+ services.ConfigureDurableWorkflows(
+ workflowOptions => workflowOptions.AddWorkflow(cancelOrder),
+ workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
+ clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
+})
+.Build();
+
+await host.StartAsync();
+
+IWorkflowClient workflowClient = host.Services.GetRequiredService();
+
+Console.WriteLine("Durable Workflow Sample");
+Console.WriteLine("Workflow: OrderLookup -> OrderCancel -> SendEmail");
+Console.WriteLine();
+Console.WriteLine("Enter an order ID (or 'exit'):");
+
+while (true)
+{
+ Console.Write("> ");
+ string? input = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ try
+ {
+ OrderCancelRequest request = new(OrderId: input, Reason: "Customer requested cancellation");
+ await StartNewWorkflowAsync(request, cancelOrder, workflowClient);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ }
+
+ Console.WriteLine();
+}
+
+await host.StopAsync();
+
+// Start a new workflow using IWorkflowClient with typed input
+static async Task StartNewWorkflowAsync(OrderCancelRequest request, Workflow workflow, IWorkflowClient client)
+{
+ Console.WriteLine($"Starting workflow for order '{request.OrderId}' (Reason: {request.Reason})...");
+
+ // RunAsync returns IWorkflowRun, cast to IAwaitableWorkflowRun for completion waiting
+ IAwaitableWorkflowRun run = (IAwaitableWorkflowRun)await client.RunAsync(workflow, request);
+ Console.WriteLine($"Run ID: {run.RunId}");
+
+ try
+ {
+ Console.WriteLine("Waiting for workflow to complete...");
+ string? result = await run.WaitForCompletionAsync();
+ Console.WriteLine($"Workflow completed. {result}");
+ }
+ catch (InvalidOperationException ex)
+ {
+ Console.WriteLine($"Failed: {ex.Message}");
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/README.md b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/README.md
new file mode 100644
index 0000000000..ac5a3e43f5
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/README.md
@@ -0,0 +1,83 @@
+# Sequential Workflow Sample
+
+This sample demonstrates how to run a sequential workflow as a durable orchestration from a console application using the Durable Task Framework. It showcases the **durability** aspect - if the process crashes mid-execution, the workflow automatically resumes without re-executing completed activities.
+
+## Key Concepts Demonstrated
+
+- Building a sequential workflow with the `WorkflowBuilder` API
+- Using `ConfigureDurableWorkflows` to register workflows with dependency injection
+- Running workflows with `IWorkflowClient`
+- **Durability**: Automatic resume of interrupted workflows
+- **Activity caching**: Completed activities are not re-executed on replay
+
+## Overview
+
+The sample implements an order cancellation workflow with three executors:
+
+```
+OrderLookup --> OrderCancel --> SendEmail
+```
+
+| Executor | Description |
+|----------|-------------|
+| OrderLookup | Looks up an order by ID |
+| OrderCancel | Marks the order as cancelled |
+| SendEmail | Sends a cancellation confirmation email |
+
+## Durability Demonstration
+
+The key feature of Durable Task Framework is **durability**:
+
+- **Activity results are persisted**: When an activity completes, its result is saved
+- **Orchestrations replay**: On restart, the orchestration replays from the beginning
+- **Completed activities skip execution**: The framework uses cached results
+- **Automatic resume**: The worker automatically picks up pending work on startup
+
+### Try It Yourself
+
+> **Tip:** To give yourself more time to stop the application during `OrderCancel`, consider increasing the loop iteration count or `Task.Delay` duration in the `OrderCancel` executor in `OrderCancelExecutors.cs`.
+
+1. Start the application and enter an order ID (e.g., `12345`)
+2. Wait for `OrderLookup` to complete, then stop the app (Ctrl+C) during `OrderCancel`
+3. Restart the application
+4. Observe:
+ - `OrderLookup` is **NOT** re-executed (result was cached)
+ - `OrderCancel` **restarts** (it didn't complete before the interruption)
+ - `SendEmail` runs after `OrderCancel` completes
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for information on configuring the environment, including how to install and run the Durable Task Scheduler.
+
+## Running the Sample
+
+```bash
+cd dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow
+dotnet run --framework net10.0
+```
+
+### Sample Output
+
+```text
+Durable Workflow Sample
+Workflow: OrderLookup -> OrderCancel -> SendEmail
+
+Enter an order ID (or 'exit'):
+> 12345
+Starting workflow for order: 12345
+Run ID: abc123...
+
+[OrderLookup] Looking up order '12345'...
+[OrderLookup] Found order for customer 'Jerry'
+
+[OrderCancel] Cancelling order '12345'...
+[OrderCancel] Order cancelled successfully
+
+[SendEmail] Sending email to 'jerry@example.com'...
+[SendEmail] Email sent successfully
+
+Workflow completed!
+
+> exit
+```
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj
new file mode 100644
index 0000000000..a05822a286
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj
@@ -0,0 +1,30 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ WorkflowConcurrency
+ WorkflowConcurrency
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/ExpertExecutors.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/ExpertExecutors.cs
new file mode 100644
index 0000000000..40674126f6
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/ExpertExecutors.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowConcurrency;
+
+///
+/// Parses and validates the incoming question before sending to AI agents.
+///
+internal sealed class ParseQuestionExecutor() : Executor("ParseQuestion")
+{
+ public override ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Magenta;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine("│ [ParseQuestion] Preparing question for AI agents...");
+
+ string formattedQuestion = message.Trim();
+ if (!formattedQuestion.EndsWith('?'))
+ {
+ formattedQuestion += "?";
+ }
+
+ Console.WriteLine($"│ [ParseQuestion] Question: \"{formattedQuestion}\"");
+ Console.WriteLine("│ [ParseQuestion] → Sending to Physicist and Chemist in PARALLEL...");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return ValueTask.FromResult(formattedQuestion);
+ }
+}
+
+///
+/// Aggregates responses from all AI agents into a comprehensive answer.
+/// This is the Fan-in point where parallel results are collected.
+///
+internal sealed class AggregatorExecutor() : Executor("Aggregator")
+{
+ public override ValueTask HandleAsync(
+ string[] message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Aggregator] 📋 Received {message.Length} AI agent responses");
+ Console.WriteLine("│ [Aggregator] Combining into comprehensive answer...");
+ Console.WriteLine("│ [Aggregator] ✓ Aggregation complete!");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ string aggregatedResult = "═══════════════════════════════════════════════════════════════\n" +
+ " AI EXPERT PANEL RESPONSES\n" +
+ "═══════════════════════════════════════════════════════════════\n\n";
+
+ for (int i = 0; i < message.Length; i++)
+ {
+ string expertLabel = i == 0 ? "⚛️ PHYSICIST" : "🧪 CHEMIST";
+ aggregatedResult += $"{expertLabel}:\n{message[i]}\n\n";
+ }
+
+ aggregatedResult += "═══════════════════════════════════════════════════════════════\n" +
+ $"Summary: Received perspectives from {message.Length} AI experts.\n" +
+ "═══════════════════════════════════════════════════════════════";
+
+ return ValueTask.FromResult(aggregatedResult);
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/Program.cs
new file mode 100644
index 0000000000..ae68a56562
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/Program.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates the Fan-out/Fan-in pattern in a durable workflow.
+// The workflow uses 4 executors: 2 class-based executors and 2 AI agents.
+//
+// WORKFLOW PATTERN:
+//
+// ParseQuestion (class-based)
+// |
+// +----------+----------+
+// | |
+// Physicist Chemist
+// (AI Agent) (AI Agent)
+// | |
+// +----------+----------+
+// |
+// Aggregator (class-based)
+
+using Azure;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using OpenAI.Chat;
+using WorkflowConcurrency;
+
+// Configuration
+string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
+string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
+
+// Create Azure OpenAI client
+AzureOpenAIClient openAiClient = !string.IsNullOrEmpty(azureOpenAiKey)
+ ? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
+ : new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
+ChatClient chatClient = openAiClient.GetChatClient(deploymentName);
+
+// Define the 4 executors for the workflow
+ParseQuestionExecutor parseQuestion = new();
+AIAgent physicist = chatClient.AsAIAgent("You are a physics expert. Be concise (2-3 sentences).", "Physicist");
+AIAgent chemist = chatClient.AsAIAgent("You are a chemistry expert. Be concise (2-3 sentences).", "Chemist");
+AggregatorExecutor aggregator = new();
+
+// Build workflow: ParseQuestion -> [Physicist, Chemist] (parallel) -> Aggregator
+Workflow workflow = new WorkflowBuilder(parseQuestion)
+ .WithName("ExpertReview")
+ .AddFanOutEdge(parseQuestion, [physicist, chemist])
+ .AddFanInBarrierEdge([physicist, chemist], aggregator)
+ .Build();
+
+// Configure and start the host
+IHost host = Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
+ .ConfigureServices(services =>
+ {
+ services.ConfigureDurableOptions(
+ options => options.Workflows.AddWorkflow(workflow),
+ workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
+ clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
+ })
+ .Build();
+
+await host.StartAsync();
+
+IWorkflowClient workflowClient = host.Services.GetRequiredService();
+
+Console.WriteLine("Fan-out/Fan-in Workflow Sample");
+Console.WriteLine("ParseQuestion -> [Physicist, Chemist] -> Aggregator");
+Console.WriteLine();
+Console.WriteLine("Enter a science question (or 'exit' to quit):");
+
+while (true)
+{
+ Console.Write("> ");
+ string? input = Console.ReadLine();
+
+ if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ try
+ {
+ IWorkflowRun run = await workflowClient.RunAsync(workflow, input);
+ Console.WriteLine($"Run ID: {run.RunId}");
+
+ if (run is IAwaitableWorkflowRun awaitableRun)
+ {
+ string? result = await awaitableRun.WaitForCompletionAsync();
+
+ Console.WriteLine("Workflow completed!");
+ Console.WriteLine(result);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ }
+
+ Console.WriteLine();
+}
+
+await host.StopAsync();
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/README.md b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/README.md
new file mode 100644
index 0000000000..4887a77ccc
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/README.md
@@ -0,0 +1,100 @@
+# Concurrent Workflow Sample (Fan-Out/Fan-In)
+
+This sample demonstrates the **fan-out/fan-in** pattern in a durable workflow, combining class-based executors with AI agents running in parallel.
+
+## Key Concepts Demonstrated
+
+- **Fan-out/Fan-in pattern**: Parallel execution with result aggregation
+- **Mixed executor types**: Class-based executors and AI agents in the same workflow
+- **AI agents as executors**: Using `ChatClient.AsAIAgent()` to create workflow-compatible agents
+- **Workflow registration**: Auto-registration of agents used within workflows
+- **Standalone agents**: Registering agents outside of workflows
+
+## Overview
+
+The sample implements an expert review workflow with four executors:
+
+```
+ ParseQuestion
+ |
+ +----------+----------+
+ | |
+ Physicist Chemist
+ (AI Agent) (AI Agent)
+ | |
+ +----------+----------+
+ |
+ Aggregator
+```
+
+| Executor | Type | Description |
+|----------|------|-------------|
+| ParseQuestion | Class-based | Parses the user's question for expert review |
+| Physicist | AI Agent | Provides physics perspective (runs in parallel) |
+| Chemist | AI Agent | Provides chemistry perspective (runs in parallel) |
+| Aggregator | Class-based | Combines expert responses into a final answer |
+
+## Fan-Out/Fan-In Pattern
+
+The workflow demonstrates the fan-out/fan-in pattern:
+
+1. **Fan-out**: `ParseQuestion` sends the question to both `Physicist` and `Chemist` simultaneously
+2. **Parallel execution**: Both AI agents process the question concurrently
+3. **Fan-in**: `Aggregator` waits for both agents to complete, then combines their responses
+
+This pattern is useful for:
+- Gathering multiple perspectives on a problem
+- Parallel processing of independent tasks
+- Reducing overall execution time through concurrency
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for information on configuring the environment.
+
+### Required Environment Variables
+
+```bash
+# Durable Task Scheduler (optional, defaults to localhost)
+DURABLE_TASK_SCHEDULER_CONNECTION_STRING="Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
+
+# Azure OpenAI (required)
+AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
+AZURE_OPENAI_DEPLOYMENT="gpt-4o"
+AZURE_OPENAI_KEY="your-key" # Optional if using Azure CLI credentials
+```
+
+## Running the Sample
+
+```bash
+cd dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow
+dotnet run --framework net10.0
+```
+
+### Sample Output
+
+```text
++-----------------------------------------------------------------------+
+| Fan-out/Fan-in Workflow Sample (4 Executors) |
+| |
+| ParseQuestion -> [Physicist, Chemist] -> Aggregator |
+| (class-based) (AI agents, parallel) (class-based) |
++-----------------------------------------------------------------------+
+
+Enter a science question (or 'exit' to quit):
+
+Question: Why is the sky blue?
+Instance: abc123...
+
+[ParseQuestion] Parsing question for expert review...
+[Physicist] Analyzing from physics perspective...
+[Chemist] Analyzing from chemistry perspective...
+[Aggregator] Combining expert responses...
+
+Workflow completed!
+
+Physics perspective: The sky appears blue due to Rayleigh scattering...
+Chemistry perspective: The molecular composition of our atmosphere...
+Combined answer: ...
+
+Question: exit
+```
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/03_ConditionalEdges.csproj b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/03_ConditionalEdges.csproj
new file mode 100644
index 0000000000..b488b10425
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/03_ConditionalEdges.csproj
@@ -0,0 +1,29 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ ConditionalEdges
+ ConditionalEdges
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/NotifyFraud.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/NotifyFraud.cs
new file mode 100644
index 0000000000..d22ac39e68
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/NotifyFraud.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace ConditionalEdges;
+
+internal sealed class Order
+{
+ public Order(string id, decimal amount)
+ {
+ this.Id = id;
+ this.Amount = amount;
+ }
+ public string Id { get; }
+ public decimal Amount { get; }
+ public Customer? Customer { get; set; }
+ public string? PaymentReferenceNumber { get; set; }
+}
+
+public sealed record Customer(int Id, string Name, bool IsBlocked);
+
+internal sealed class OrderIdParser() : Executor("OrderIdParser")
+{
+ public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ return GetOrder(message);
+ }
+
+ private static Order GetOrder(string id)
+ {
+ // Simulate fetching order details
+ return new Order(id, 100.0m);
+ }
+}
+
+internal sealed class OrderEnrich() : Executor("EnrichOrder")
+{
+ public override async ValueTask HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ message.Customer = GetCustomerForOrder(message.Id);
+ return message;
+ }
+
+ private static Customer GetCustomerForOrder(string orderId)
+ {
+ if (orderId.Contains('B'))
+ {
+ return new Customer(101, "George", true);
+ }
+
+ return new Customer(201, "Jerry", false);
+ }
+}
+
+internal sealed class PaymentProcessor() : Executor("PaymentProcessor")
+{
+ public override async ValueTask HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ // Call payment gateway.
+ message.PaymentReferenceNumber = Guid.NewGuid().ToString().Substring(0, 4);
+ return message;
+ }
+}
+
+internal sealed class NotifyFraud() : Executor("NotifyFraud")
+{
+ public override async ValueTask HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ // Notify fraud team.
+ return $"Order {message.Id} flagged as fraudulent for customer {message.Customer?.Name}.";
+ }
+}
+
+internal static class OrderRouteConditions
+{
+ ///
+ /// Returns a condition that evaluates to true when the customer is blocked.
+ ///
+ internal static Func WhenBlocked() => order => order?.Customer?.IsBlocked == true;
+
+ ///
+ /// Returns a condition that evaluates to true when the customer is not blocked.
+ ///
+ internal static Func WhenNotBlocked() => order => order?.Customer?.IsBlocked == false;
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/Program.cs
new file mode 100644
index 0000000000..b7f9ff9944
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/Program.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates conditional edges in a workflow.
+// Orders are routed to different executors based on customer status:
+// - Blocked customers → NotifyFraud
+// - Valid customers → PaymentProcessor
+
+using ConditionalEdges;
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+
+// Create executor instances
+OrderIdParser orderParser = new();
+OrderEnrich orderEnrich = new();
+PaymentProcessor paymentProcessor = new();
+NotifyFraud notifyFraud = new();
+
+// Build workflow with conditional edges
+// The condition functions evaluate the Order output from OrderEnrich
+WorkflowBuilder builder = new(orderParser);
+builder
+ .AddEdge(orderParser, orderEnrich)
+ .AddEdge(orderEnrich, notifyFraud, condition: OrderRouteConditions.WhenBlocked())
+ .AddEdge(orderEnrich, paymentProcessor, condition: OrderRouteConditions.WhenNotBlocked());
+
+Workflow auditOrder = builder.WithName("AuditOrder").Build();
+
+IHost host = Host.CreateDefaultBuilder(args)
+.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
+.ConfigureServices(services =>
+{
+ services.ConfigureDurableWorkflows(
+ workflowOptions => workflowOptions.AddWorkflow(auditOrder),
+ workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
+ clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
+})
+.Build();
+
+await host.StartAsync();
+
+IWorkflowClient workflowClient = host.Services.GetRequiredService();
+
+Console.WriteLine("Enter an order ID (or 'exit'):");
+Console.WriteLine("Tip: Order IDs containing 'B' are flagged as blocked customers.\n");
+
+while (true)
+{
+ Console.Write("> ");
+ string? input = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ try
+ {
+ await StartNewWorkflowAsync(input, auditOrder, workflowClient);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ }
+
+ Console.WriteLine();
+}
+
+await host.StopAsync();
+
+// Start a new workflow and wait for completion
+static async Task StartNewWorkflowAsync(string orderId, Workflow workflow, IWorkflowClient client)
+{
+ Console.WriteLine($"Starting workflow for order '{orderId}'...");
+
+ // Cast to IAwaitableWorkflowRun to access WaitForCompletionAsync
+ IAwaitableWorkflowRun run = (IAwaitableWorkflowRun)await client.RunAsync(workflow, orderId);
+ Console.WriteLine($"Run ID: {run.RunId}");
+
+ try
+ {
+ Console.WriteLine("Waiting for workflow to complete...");
+ string? result = await run.WaitForCompletionAsync();
+ Console.WriteLine($"Workflow completed. {result}");
+ }
+ catch (InvalidOperationException ex)
+ {
+ Console.WriteLine($"Failed: {ex.Message}");
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/README.md b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/README.md
new file mode 100644
index 0000000000..fb8c26bf80
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/README.md
@@ -0,0 +1,92 @@
+# Conditional Edges Workflow Sample
+
+This sample demonstrates how to build a workflow with **conditional edges** that route execution to different paths based on runtime conditions. The workflow evaluates conditions on the output of an executor to determine which downstream executor to run.
+
+## Key Concepts Demonstrated
+
+- Building workflows with **conditional edges** using `AddEdge` with a `condition` parameter
+- Defining reusable condition functions for routing logic
+- Branching workflow execution based on data-driven decisions
+- Using `ConfigureDurableWorkflows` to register workflows with dependency injection
+
+## Overview
+
+The sample implements an order audit workflow that routes orders differently based on whether the customer is blocked (flagged for fraud):
+
+```
+OrderIdParser --> OrderEnrich --[IsBlocked]--> NotifyFraud
+ |
+ +--[NotBlocked]--> PaymentProcessor
+```
+
+| Executor | Description |
+|----------|-------------|
+| OrderIdParser | Parses the order ID and retrieves order details |
+| OrderEnrich | Enriches the order with customer information |
+| PaymentProcessor | Processes payment for valid orders |
+| NotifyFraud | Notifies the fraud team for blocked customers |
+
+## How Conditional Edges Work
+
+Conditional edges allow you to specify a condition function that determines whether the edge should be traversed:
+
+```csharp
+builder
+ .AddEdge(orderParser, orderEnrich)
+ .AddEdge(orderEnrich, notifyFraud, condition: OrderRouteConditions.WhenBlocked())
+ .AddEdge(orderEnrich, paymentProcessor, condition: OrderRouteConditions.WhenNotBlocked());
+```
+
+The condition functions receive the output of the source executor and return a boolean:
+
+```csharp
+internal static class OrderRouteConditions
+{
+ // Routes to NotifyFraud when customer is blocked
+ internal static Func WhenBlocked() =>
+ order => order?.Customer?.IsBlocked == true;
+
+ // Routes to PaymentProcessor when customer is not blocked
+ internal static Func WhenNotBlocked() =>
+ order => order?.Customer?.IsBlocked == false;
+}
+```
+
+### Routing Logic
+
+In this sample, the routing is based on the order ID:
+- Order IDs containing the letter **'B'** are associated with blocked customers → routed to `NotifyFraud`
+- All other order IDs are associated with valid customers → routed to `PaymentProcessor`
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for information on configuring the environment, including how to install and run the Durable Task Scheduler.
+
+## Running the Sample
+
+```bash
+cd dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges
+dotnet run --framework net10.0
+```
+
+### Sample Output
+
+**Valid order (routes to PaymentProcessor):**
+```text
+Enter an order ID (or 'exit'):
+> 12345
+Starting workflow for order '12345'...
+Run ID: abc123...
+Waiting for workflow to complete...
+Workflow completed. {"Id":"12345","Amount":100.0,"Customer":{"Id":201,"Name":"Jerry","IsBlocked":false},"PaymentReferenceNumber":"a1b2"}
+```
+
+**Blocked order (routes to NotifyFraud):**
+```text
+Enter an order ID (or 'exit'):
+> 12345B
+Starting workflow for order '12345B'...
+Run ID: def456...
+Waiting for workflow to complete...
+Workflow completed. Order 12345B flagged as fraudulent for customer George.
+```
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/04_WorkflowAndAgents.csproj b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/04_WorkflowAndAgents.csproj
new file mode 100644
index 0000000000..a05822a286
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/04_WorkflowAndAgents.csproj
@@ -0,0 +1,30 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ WorkflowConcurrency
+ WorkflowConcurrency
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/ParseQuestionExecutor.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/ParseQuestionExecutor.cs
new file mode 100644
index 0000000000..e9a6712393
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/ParseQuestionExecutor.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowConcurrency;
+
+///
+/// Parses and validates the incoming question before sending to AI agents.
+///
+internal sealed class ParseQuestionExecutor() : Executor("ParseQuestion")
+{
+ public override ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Magenta;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine("│ [ParseQuestion] Preparing question for AI agents...");
+
+ string formattedQuestion = message.Trim();
+ if (!formattedQuestion.EndsWith('?'))
+ {
+ formattedQuestion += "?";
+ }
+
+ Console.WriteLine($"│ [ParseQuestion] Question: \"{formattedQuestion}\"");
+ Console.WriteLine("│ [ParseQuestion] → Sending to experts...");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return ValueTask.FromResult(formattedQuestion);
+ }
+}
+
+///
+/// Aggregates responses from multiple AI agents into a unified response.
+/// This executor collects all expert opinions and synthesizes them.
+///
+internal sealed class ResponseAggregatorExecutor() : Executor("ResponseAggregator")
+{
+ public override ValueTask HandleAsync(
+ string[] message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [Aggregator] 📋 Received {message.Length} AI agent responses");
+ Console.WriteLine("│ [Aggregator] Combining into comprehensive answer...");
+ Console.WriteLine("│ [Aggregator] ✓ Aggregation complete!");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ string aggregatedResult = "═══════════════════════════════════════════════════════════════\n" +
+ " AI EXPERT PANEL RESPONSES\n" +
+ "═══════════════════════════════════════════════════════════════\n\n";
+
+ for (int i = 0; i < message.Length; i++)
+ {
+ string expertLabel = i == 0 ? "⚛️ PHYSICIST" : "🧪 CHEMIST";
+ aggregatedResult += $"{expertLabel}:\n{message[i]}\n\n";
+ }
+
+ aggregatedResult += "═══════════════════════════════════════════════════════════════\n" +
+ $"Summary: Received perspectives from {message.Length} AI experts.\n" +
+ "═══════════════════════════════════════════════════════════════";
+
+ return ValueTask.FromResult(aggregatedResult);
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/Program.cs
new file mode 100644
index 0000000000..5dfec4f277
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/Program.cs
@@ -0,0 +1,133 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates the THREE ways to configure durable agents and workflows:
+//
+// 1. ConfigureDurableAgents() - For standalone agents only
+// 2. ConfigureDurableWorkflows() - For workflows only
+// 3. ConfigureDurableOptions() - For both agents AND workflows
+//
+// KEY: All methods can be called MULTIPLE times - configurations are ADDITIVE.
+
+using Azure;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using OpenAI.Chat;
+using WorkflowConcurrency;
+
+// Configuration
+string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
+string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
+
+// Create AI agents
+AzureOpenAIClient openAiClient = !string.IsNullOrEmpty(azureOpenAiKey)
+ ? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
+ : new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
+ChatClient chatClient = openAiClient.GetChatClient(deploymentName);
+
+AIAgent biologist = chatClient.AsAIAgent("You are a biology expert. Explain concepts clearly in 2-3 sentences.", "Biologist");
+AIAgent physicist = chatClient.AsAIAgent("You are a physics expert. Explain concepts clearly in 2-3 sentences.", "Physicist");
+AIAgent chemist = chatClient.AsAIAgent("You are a chemistry expert. Explain concepts clearly in 2-3 sentences.", "Chemist");
+
+// Create workflows
+ParseQuestionExecutor questionParser = new();
+ResponseAggregatorExecutor responseAggregator = new();
+
+Workflow physicsWorkflow = new WorkflowBuilder(questionParser)
+ .WithName("PhysicsExpertReview")
+ .AddEdge(questionParser, physicist)
+ .Build();
+
+Workflow expertTeamWorkflow = new WorkflowBuilder(questionParser)
+.WithName("ExpertTeamReview")
+.AddFanOutEdge(questionParser, [biologist, physicist])
+.AddFanInBarrierEdge([biologist, physicist], responseAggregator)
+.Build();
+
+Workflow chemistryWorkflow = new WorkflowBuilder(questionParser)
+ .WithName("ChemistryExpertReview")
+ .AddEdge(questionParser, chemist)
+ .Build();
+
+// Configure services - demonstrating all 3 methods (each can be called multiple times)
+IHost host = Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
+ .ConfigureServices(services =>
+ {
+ // METHOD 1: ConfigureDurableAgents - for standalone agents only
+ services.ConfigureDurableAgents(
+ options => options.AddAIAgent(biologist),
+ workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
+ clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
+
+ // METHOD 2: ConfigureDurableWorkflows - for workflows only
+ services.ConfigureDurableWorkflows(options => options.AddWorkflow(physicsWorkflow));
+
+ // METHOD 3: ConfigureDurableOptions - for both agents AND workflows
+ services.ConfigureDurableOptions(options =>
+ {
+ options.Agents.AddAIAgent(chemist);
+ options.Workflows.AddWorkflow(expertTeamWorkflow);
+ });
+
+ // Second call to ConfigureDurableOptions (additive - adds to existing config)
+ services.ConfigureDurableOptions(options => options.Workflows.AddWorkflow(chemistryWorkflow));
+ })
+ .Build();
+
+await host.StartAsync();
+IServiceProvider services = host.Services;
+IWorkflowClient workflowClient = services.GetRequiredService();
+
+// DEMO 1: Direct agent conversation (standalone agents)
+Console.WriteLine("\n═══ DEMO 1: Direct Agent Conversation ═══\n");
+
+AIAgent biologistProxy = services.GetRequiredKeyedService("Biologist");
+AgentSession session = await biologistProxy.CreateSessionAsync();
+AgentResponse response = await biologistProxy.RunAsync("What is photosynthesis?", session);
+Console.WriteLine($"🧬 Biologist: {response.Text}\n");
+
+AIAgent chemistProxy = services.GetRequiredKeyedService("Chemist");
+session = await chemistProxy.CreateSessionAsync();
+response = await chemistProxy.RunAsync("What is a chemical bond?", session);
+Console.WriteLine($"🧪 Chemist: {response.Text}\n");
+
+// DEMO 2: Single-agent workflow
+Console.WriteLine("═══ DEMO 2: Single-Agent Workflow ═══\n");
+await RunWorkflowAsync(workflowClient, physicsWorkflow, "What is the relationship between energy and mass?");
+
+// DEMO 3: Multi-agent workflow
+Console.WriteLine("═══ DEMO 3: Multi-Agent Workflow ═══\n");
+await RunWorkflowAsync(workflowClient, expertTeamWorkflow, "How does radiation affect living cells?");
+
+// DEMO 4: Workflow from second ConfigureDurableOptions call
+Console.WriteLine("═══ DEMO 4: Workflow (added via 2nd ConfigureDurableOptions) ═══\n");
+await RunWorkflowAsync(workflowClient, chemistryWorkflow, "What happens during combustion?");
+
+Console.WriteLine("\n✅ All demos completed!");
+await host.StopAsync();
+
+// Helper method
+static async Task RunWorkflowAsync(IWorkflowClient client, Workflow workflow, string question)
+{
+ Console.WriteLine($"📋 {workflow.Name}: \"{question}\"");
+ IWorkflowRun run = await client.RunAsync(workflow, question);
+ if (run is IAwaitableWorkflowRun awaitable)
+ {
+ string? result = await awaitable.WaitForCompletionAsync();
+ Console.WriteLine($"✅ {result}\n");
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj
new file mode 100644
index 0000000000..09e20ef622
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj
@@ -0,0 +1,28 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ WorkflowEvents
+ WorkflowEvents
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/Executors.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/Executors.cs
new file mode 100644
index 0000000000..47880f0fff
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/Executors.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowEvents;
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// Custom event types - callers observe these via WatchStreamAsync
+// ═══════════════════════════════════════════════════════════════════════════════
+
+internal sealed class OrderLookupStartedEvent(string orderId) : WorkflowEvent(orderId)
+{
+ public string OrderId { get; } = orderId;
+}
+
+internal sealed class OrderFoundEvent(string customerName) : WorkflowEvent(customerName)
+{
+ public string CustomerName { get; } = customerName;
+}
+
+internal sealed class CancellationProgressEvent(int percentComplete, string status) : WorkflowEvent(status)
+{
+ public int PercentComplete { get; } = percentComplete;
+ public string Status { get; } = status;
+}
+
+internal sealed class OrderCancelledEvent() : WorkflowEvent("Order cancelled");
+
+internal sealed class EmailSentEvent(string email) : WorkflowEvent(email)
+{
+ public string Email { get; } = email;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// Domain models
+// ═══════════════════════════════════════════════════════════════════════════════
+
+internal sealed record Order(string Id, DateTime OrderDate, bool IsCancelled, string? CancelReason, Customer Customer);
+
+internal sealed record Customer(string Name, string Email);
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// Executors - emit events via AddEventAsync and YieldOutputAsync
+// ═══════════════════════════════════════════════════════════════════════════════
+
+///
+/// Looks up an order by ID, emitting progress events.
+///
+internal sealed class OrderLookup() : Executor("OrderLookup")
+{
+ public override async ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ await context.AddEventAsync(new OrderLookupStartedEvent(message), cancellationToken);
+
+ // Simulate database lookup
+ await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
+
+ Order order = new(
+ Id: message,
+ OrderDate: DateTime.UtcNow.AddDays(-1),
+ IsCancelled: false,
+ CancelReason: "Customer requested cancellation",
+ Customer: new Customer(Name: "Jerry", Email: "jerry@example.com"));
+
+ await context.AddEventAsync(new OrderFoundEvent(order.Customer.Name), cancellationToken);
+
+ // YieldOutputAsync emits a WorkflowOutputEvent observable via streaming
+ await context.YieldOutputAsync(order, cancellationToken);
+
+ return order;
+ }
+}
+
+///
+/// Cancels an order, emitting progress events during the multi-step process.
+///
+internal sealed class OrderCancel() : Executor("OrderCancel")
+{
+ public override async ValueTask HandleAsync(
+ Order message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ await context.AddEventAsync(new CancellationProgressEvent(0, "Starting cancellation"), cancellationToken);
+
+ // Simulate a multi-step cancellation process
+ await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken);
+ await context.AddEventAsync(new CancellationProgressEvent(33, "Contacting payment provider"), cancellationToken);
+
+ await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken);
+ await context.AddEventAsync(new CancellationProgressEvent(66, "Processing refund"), cancellationToken);
+
+ await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken);
+
+ Order cancelledOrder = message with { IsCancelled = true };
+ await context.AddEventAsync(new CancellationProgressEvent(100, "Complete"), cancellationToken);
+ await context.AddEventAsync(new OrderCancelledEvent(), cancellationToken);
+
+ await context.YieldOutputAsync(cancelledOrder, cancellationToken);
+
+ return cancelledOrder;
+ }
+}
+
+///
+/// Sends a cancellation confirmation email, emitting an event on completion.
+///
+internal sealed class SendEmail() : Executor("SendEmail")
+{
+ public override async ValueTask HandleAsync(
+ Order message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ // Simulate sending email
+ await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken);
+
+ string result = $"Cancellation email sent for order {message.Id} to {message.Customer.Email}.";
+
+ await context.AddEventAsync(new EmailSentEvent(message.Customer.Email), cancellationToken);
+
+ await context.YieldOutputAsync(result, cancellationToken);
+
+ return result;
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/Program.cs
new file mode 100644
index 0000000000..3ddec1db37
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/Program.cs
@@ -0,0 +1,138 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// SAMPLE: Workflow Events and Streaming
+// ═══════════════════════════════════════════════════════════════════════════════
+//
+// This sample demonstrates how to use IWorkflowContext event methods in executors
+// and stream events from the caller side:
+//
+// 1. AddEventAsync - Emit custom events that callers can observe in real-time
+// 2. StreamAsync - Start a workflow and obtain a streaming handle
+// 3. WatchStreamAsync - Observe events as they occur (custom, framework, and terminal)
+//
+// The sample uses IWorkflowClient.StreamAsync to start a workflow and
+// WatchStreamAsync to observe events as they occur in real-time.
+//
+// Workflow: OrderLookup -> OrderCancel -> SendEmail
+// ═══════════════════════════════════════════════════════════════════════════════
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using WorkflowEvents;
+
+// Get DTS connection string from environment variable
+string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+
+// Define executors and build workflow
+OrderLookup orderLookup = new();
+OrderCancel orderCancel = new();
+SendEmail sendEmail = new();
+
+Workflow cancelOrder = new WorkflowBuilder(orderLookup)
+ .WithName("CancelOrder")
+ .WithDescription("Cancel an order and notify the customer")
+ .AddEdge(orderLookup, orderCancel)
+ .AddEdge(orderCancel, sendEmail)
+ .Build();
+
+// Configure host with durable workflow support
+IHost host = Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
+ .ConfigureServices(services =>
+ {
+ services.ConfigureDurableWorkflows(
+ workflowOptions => workflowOptions.AddWorkflow(cancelOrder),
+ workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
+ clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
+ })
+ .Build();
+
+await host.StartAsync();
+
+IWorkflowClient workflowClient = host.Services.GetRequiredService();
+
+Console.WriteLine("Workflow Events Demo - Enter order ID (or 'exit'):");
+
+while (true)
+{
+ Console.Write("> ");
+ string? input = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ try
+ {
+ await RunWorkflowWithStreamingAsync(input, cancelOrder, workflowClient);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ }
+
+ Console.WriteLine();
+}
+
+await host.StopAsync();
+
+// Runs a workflow and streams events as they occur
+static async Task RunWorkflowWithStreamingAsync(string orderId, Workflow workflow, IWorkflowClient client)
+{
+ // StreamAsync starts the workflow and returns a streaming handle for observing events
+ IStreamingWorkflowRun run = await client.StreamAsync(workflow, orderId);
+ Console.WriteLine($"Started run: {run.RunId}");
+
+ // WatchStreamAsync yields events as they're emitted by executors
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync())
+ {
+ Console.WriteLine($" New event received at {DateTime.Now:HH:mm:ss.ffff} ({evt.GetType().Name})");
+
+ switch (evt)
+ {
+ // Custom domain events (emitted via AddEventAsync)
+ case OrderLookupStartedEvent e:
+ WriteColored($" [Lookup] Looking up order {e.OrderId}", ConsoleColor.Cyan);
+ break;
+ case OrderFoundEvent e:
+ WriteColored($" [Lookup] Found: {e.CustomerName}", ConsoleColor.Cyan);
+ break;
+ case CancellationProgressEvent e:
+ WriteColored($" [Cancel] {e.PercentComplete}% - {e.Status}", ConsoleColor.Yellow);
+ break;
+ case OrderCancelledEvent:
+ WriteColored(" [Cancel] Done", ConsoleColor.Yellow);
+ break;
+ case EmailSentEvent e:
+ WriteColored($" [Email] Sent to {e.Email}", ConsoleColor.Magenta);
+ break;
+
+ case WorkflowOutputEvent e:
+ WriteColored($" [Output] {e.ExecutorId}", ConsoleColor.DarkGray);
+ break;
+
+ // Workflow completion
+ case DurableWorkflowCompletedEvent e:
+ WriteColored($" Completed: {e.Result}", ConsoleColor.Green);
+ break;
+ case DurableWorkflowFailedEvent e:
+ WriteColored($" Failed: {e.ErrorMessage}", ConsoleColor.Red);
+ break;
+ }
+ }
+}
+
+static void WriteColored(string message, ConsoleColor color)
+{
+ Console.ForegroundColor = color;
+ Console.WriteLine(message);
+ Console.ResetColor();
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/README.md b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/README.md
new file mode 100644
index 0000000000..b519ec8d5c
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/README.md
@@ -0,0 +1,127 @@
+# Workflow Events Sample
+
+This sample demonstrates how to use workflow events and streaming in durable workflows.
+
+## What it demonstrates
+
+1. **Custom Events** (`AddEventAsync`) — Executors emit domain-specific events during execution
+2. **Event Streaming** (`StreamAsync` / `WatchStreamAsync`) — Callers observe events in real-time as the workflow progresses
+3. **Framework Events** — Automatic `ExecutorInvokedEvent`, `ExecutorCompletedEvent`, and `WorkflowOutputEvent` events emitted by the framework
+
+## Emitting Custom Events
+
+Executors can emit custom domain events during execution using the `IWorkflowContext` instance passed to `HandleAsync`. These events are streamed to callers in real-time via `WatchStreamAsync`.
+
+### Defining a custom event
+
+Create a class that inherits from `WorkflowEvent`. Pass any data payload to the base constructor:
+
+```csharp
+public class CancellationProgressEvent(int percentComplete, string status) : WorkflowEvent(status)
+{
+ public int PercentComplete { get; } = percentComplete;
+ public string Status { get; } = status;
+}
+```
+
+### Emitting the event from an executor
+
+Call `AddEventAsync` on the `IWorkflowContext` inside your executor's `HandleAsync` method:
+
+```csharp
+public override async ValueTask HandleAsync(
+ Order message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+{
+ await context.AddEventAsync(new CancellationProgressEvent(33, "Processing refund"), cancellationToken);
+ // ... rest of the executor logic
+}
+```
+
+### Observing events from the caller
+
+Use `StreamAsync` to start the workflow and `WatchStreamAsync` to observe events. Pattern match on your custom event types:
+
+```csharp
+IStreamingWorkflowRun run = await workflowClient.StreamAsync(workflow, input);
+
+await foreach (WorkflowEvent evt in run.WatchStreamAsync())
+{
+ switch (evt)
+ {
+ case CancellationProgressEvent e:
+ Console.WriteLine($"{e.PercentComplete}% - {e.Status}");
+ break;
+ }
+}
+```
+
+## Workflow Structure
+
+```
+OrderLookup → OrderCancel → SendEmail
+```
+
+Each executor emits custom events during execution:
+- `OrderLookup` emits `OrderLookupStartedEvent` and `OrderFoundEvent`
+- `OrderCancel` emits `CancellationProgressEvent` (with percentage) and `OrderCancelledEvent`
+- `SendEmail` emits `EmailSentEvent`
+
+## Prerequisites
+
+- [Durable Task Scheduler](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) running locally or in Azure
+- Set the `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` environment variable (defaults to local emulator)
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
+
+## Running the sample
+
+```bash
+dotnet run
+```
+
+Enter an order ID at the prompt to start a workflow and watch events stream in real-time:
+
+```text
+> order-42
+Started run: b6ba4d19...
+ New event received at 13:27:41.4956 (ExecutorInvokedEvent)
+ New event received at 13:27:41.5019 (OrderLookupStartedEvent)
+ [Lookup] Looking up order order-42
+ New event received at 13:27:41.5025 (OrderFoundEvent)
+ [Lookup] Found: Jerry
+ New event received at 13:27:41.5026 (ExecutorCompletedEvent)
+ New event received at 13:27:41.5026 (WorkflowOutputEvent)
+ [Output] OrderLookup
+ New event received at 13:27:43.0772 (ExecutorInvokedEvent)
+ New event received at 13:27:43.0773 (CancellationProgressEvent)
+ [Cancel] 0% - Starting cancellation
+ New event received at 13:27:43.0775 (CancellationProgressEvent)
+ [Cancel] 33% - Contacting payment provider
+ New event received at 13:27:43.0776 (CancellationProgressEvent)
+ [Cancel] 66% - Processing refund
+ New event received at 13:27:43.0777 (CancellationProgressEvent)
+ [Cancel] 100% - Complete
+ New event received at 13:27:43.0779 (OrderCancelledEvent)
+ [Cancel] Done
+ New event received at 13:27:43.0780 (ExecutorCompletedEvent)
+ New event received at 13:27:43.0780 (WorkflowOutputEvent)
+ [Output] OrderCancel
+ New event received at 13:27:43.6610 (ExecutorInvokedEvent)
+ New event received at 13:27:43.6611 (EmailSentEvent)
+ [Email] Sent to jerry@example.com
+ New event received at 13:27:43.6613 (ExecutorCompletedEvent)
+ New event received at 13:27:43.6613 (WorkflowOutputEvent)
+ [Output] SendEmail
+ New event received at 13:27:43.6619 (DurableWorkflowCompletedEvent)
+ Completed: Cancellation email sent for order order-42 to jerry@example.com.
+```
+
+### Viewing Workflows in the DTS Dashboard
+
+After running a workflow, you can navigate to the Durable Task Scheduler (DTS) dashboard to inspect the workflow execution and events.
+
+If you are using the DTS emulator, the dashboard is available at `http://localhost:8082`.
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj
new file mode 100644
index 0000000000..c7efbb7d1b
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj
@@ -0,0 +1,29 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ WorkflowSharedState
+ WorkflowSharedState
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/Executors.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/Executors.cs
new file mode 100644
index 0000000000..c0c8cfd097
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/Executors.cs
@@ -0,0 +1,182 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowSharedState;
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// Domain models
+// ═══════════════════════════════════════════════════════════════════════════════
+
+///
+/// The primary order data passed through the pipeline via return values.
+///
+internal sealed record OrderDetails(string OrderId, string CustomerName, decimal Amount, DateTime OrderDate);
+
+///
+/// Cross-cutting audit trail accumulated in shared state across executors.
+/// Each executor appends its step name and timestamp. This data does not flow
+/// through return values — it lives only in shared state.
+///
+internal sealed record AuditEntry(string Step, string Timestamp, string Detail);
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// Executors
+// ═══════════════════════════════════════════════════════════════════════════════
+
+///
+/// Validates the order and writes the initial audit entry and tax rate to shared state.
+/// The order details are returned as the executor output (normal message flow),
+/// while the audit trail and tax rate are stored in shared state (side-channel).
+/// If the order ID starts with "INVALID", the executor halts the workflow early
+/// using .
+///
+internal sealed class ValidateOrder() : Executor("ValidateOrder")
+{
+ public override async ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);
+
+ // Halt the workflow early if the order ID is invalid.
+ // No downstream executors will run after this.
+ if (message.StartsWith("INVALID", StringComparison.OrdinalIgnoreCase))
+ {
+ await context.YieldOutputAsync($"Order '{message}' failed validation. Halting workflow.", cancellationToken);
+ await context.RequestHaltAsync();
+ return new OrderDetails(message, "Unknown", 0, DateTime.UtcNow);
+ }
+
+ OrderDetails details = new(message, "Jerry", 249.99m, DateTime.UtcNow);
+
+ // Store the tax rate in shared state — downstream ProcessPayment reads it
+ // without needing it in the message chain.
+ await context.QueueStateUpdateAsync("taxRate", 0.085m, cancellationToken: cancellationToken);
+ Console.WriteLine(" Wrote to shared state: taxRate = 8.5%");
+
+ // Start the audit trail in shared state
+ AuditEntry audit = new("ValidateOrder", DateTime.UtcNow.ToString("o"), $"Validated order {message}");
+ await context.QueueStateUpdateAsync("auditValidate", audit, cancellationToken: cancellationToken);
+ Console.WriteLine(" Wrote to shared state: auditValidate");
+
+ await context.YieldOutputAsync($"Order '{message}' validated. Customer: {details.CustomerName}, Amount: {details.Amount:C}", cancellationToken);
+
+ return details;
+ }
+}
+
+///
+/// Enriches the order with shipping information.
+/// Reads the audit trail from shared state and appends its own entry.
+/// Uses ReadOrInitStateAsync to lazily initialize a shipping tier.
+/// Demonstrates custom scopes by writing shipping details under the "shipping" scope.
+///
+internal sealed class EnrichOrder() : Executor("EnrichOrder")
+{
+ public override async ValueTask HandleAsync(
+ OrderDetails message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);
+
+ // Use ReadOrInitStateAsync — only initializes if no value exists yet
+ string shippingTier = await context.ReadOrInitStateAsync(
+ "shippingTier",
+ () => "Express",
+ cancellationToken: cancellationToken);
+ Console.WriteLine($" Read from shared state: shippingTier = {shippingTier}");
+
+ // Write carrier under a custom "shipping" scope.
+ // This keeps the key separate from keys written without a scope,
+ // so "carrier" here won't collide with a "carrier" key written elsewhere.
+ await context.QueueStateUpdateAsync("carrier", "Contoso Express", scopeName: "shipping", cancellationToken: cancellationToken);
+ Console.WriteLine(" Wrote to shared state: carrier = Contoso Express (scope: shipping)");
+
+ // Verify we can read the audit entry from the previous step
+ AuditEntry? previousAudit = await context.ReadStateAsync("auditValidate", cancellationToken: cancellationToken);
+ string auditStatus = previousAudit is not null ? $"(previous step: {previousAudit.Step})" : "(no prior audit)";
+ Console.WriteLine($" Read from shared state: auditValidate {auditStatus}");
+
+ // Append our own audit entry
+ AuditEntry audit = new("EnrichOrder", DateTime.UtcNow.ToString("o"), $"Enriched with {shippingTier} shipping {auditStatus}");
+ await context.QueueStateUpdateAsync("auditEnrich", audit, cancellationToken: cancellationToken);
+ Console.WriteLine(" Wrote to shared state: auditEnrich");
+
+ await context.YieldOutputAsync($"Order enriched. Shipping: {shippingTier} {auditStatus}", cancellationToken);
+
+ return message;
+ }
+}
+
+///
+/// Processes payment using the tax rate from shared state (written by ValidateOrder).
+/// The tax rate is side-channel data — it doesn't flow through return values.
+///
+internal sealed class ProcessPayment() : Executor("ProcessPayment")
+{
+ public override async ValueTask HandleAsync(
+ OrderDetails message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(300), cancellationToken);
+
+ // Read tax rate written by ValidateOrder — not available in the message chain
+ decimal taxRate = await context.ReadOrInitStateAsync("taxRate", () => 0.0m, cancellationToken: cancellationToken);
+ Console.WriteLine($" Read from shared state: taxRate = {taxRate:P1}");
+
+ decimal tax = message.Amount * taxRate;
+ decimal total = message.Amount + tax;
+ string paymentRef = $"PAY-{Guid.NewGuid():N}"[..16];
+
+ // Append audit entry
+ AuditEntry audit = new("ProcessPayment", DateTime.UtcNow.ToString("o"), $"Charged {total:C} (tax: {tax:C})");
+ await context.QueueStateUpdateAsync("auditPayment", audit, cancellationToken: cancellationToken);
+ Console.WriteLine(" Wrote to shared state: auditPayment");
+
+ await context.YieldOutputAsync($"Payment processed. Total: {total:C} (tax: {tax:C}). Ref: {paymentRef}", cancellationToken);
+
+ return paymentRef;
+ }
+}
+
+///
+/// Generates the final invoice by reading the full audit trail from shared state.
+/// Demonstrates reading multiple state entries written by different executors
+/// and clearing a scope with .
+///
+internal sealed class GenerateInvoice() : Executor("GenerateInvoice")
+{
+ public override async ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+
+ // Read the full audit trail from shared state — each step wrote its own entry
+ AuditEntry? validateAudit = await context.ReadStateAsync("auditValidate", cancellationToken: cancellationToken);
+ AuditEntry? enrichAudit = await context.ReadStateAsync("auditEnrich", cancellationToken: cancellationToken);
+ AuditEntry? paymentAudit = await context.ReadStateAsync("auditPayment", cancellationToken: cancellationToken);
+ int auditCount = new[] { validateAudit, enrichAudit, paymentAudit }.Count(a => a is not null);
+ Console.WriteLine($" Read from shared state: {auditCount} audit entries");
+
+ // Read carrier from the "shipping" scope (written by EnrichOrder)
+ string? carrier = await context.ReadStateAsync("carrier", scopeName: "shipping", cancellationToken: cancellationToken);
+ Console.WriteLine($" Read from shared state: carrier = {carrier} (scope: shipping)");
+
+ // Clear the "shipping" scope — no longer needed after invoice generation.
+ await context.QueueClearScopeAsync("shipping", cancellationToken);
+ Console.WriteLine(" Cleared shared state scope: shipping");
+
+ string auditSummary = string.Join(" → ", new[]
+ {
+ validateAudit?.Step, enrichAudit?.Step, paymentAudit?.Step
+ }.Where(s => s is not null));
+
+ return $"Invoice complete. Payment: {message}. Audit trail: [{auditSummary}]";
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/Program.cs
new file mode 100644
index 0000000000..2513cc2dad
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/Program.cs
@@ -0,0 +1,117 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// SAMPLE: Shared State During Workflow Execution
+// ═══════════════════════════════════════════════════════════════════════════════
+//
+// This sample demonstrates how executors in a durable workflow can share state
+// via IWorkflowContext. State is persisted across supersteps and survives
+// process restarts because the orchestration passes it to each activity.
+//
+// Key concepts:
+// 1. QueueStateUpdateAsync - Write a value to shared state
+// 2. ReadStateAsync - Read a value written by a previous executor
+// 3. ReadOrInitStateAsync - Read or lazily initialize a state value
+// 4. QueueClearScopeAsync - Clear all entries under a scope
+// 5. RequestHaltAsync - Stop the workflow early (e.g., validation failure)
+//
+// Workflow: ValidateOrder -> EnrichOrder -> ProcessPayment -> GenerateInvoice
+//
+// Return values carry primary business data through the pipeline (OrderDetails,
+// payment ref). Shared state carries side-channel data that doesn't belong in
+// the message chain: a tax rate (set by ValidateOrder, read by ProcessPayment)
+// and an audit trail (each executor appends its own entry).
+// ═══════════════════════════════════════════════════════════════════════════════
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using WorkflowSharedState;
+
+// Get DTS connection string from environment variable
+string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+
+// Define executors
+ValidateOrder validateOrder = new();
+EnrichOrder enrichOrder = new();
+ProcessPayment processPayment = new();
+GenerateInvoice generateInvoice = new();
+
+// Build the workflow: ValidateOrder -> EnrichOrder -> ProcessPayment -> GenerateInvoice
+Workflow orderPipeline = new WorkflowBuilder(validateOrder)
+ .WithName("OrderPipeline")
+ .WithDescription("Order processing pipeline with shared state across executors")
+ .AddEdge(validateOrder, enrichOrder)
+ .AddEdge(enrichOrder, processPayment)
+ .AddEdge(processPayment, generateInvoice)
+ .Build();
+
+// Configure host with durable workflow support
+IHost host = Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
+ .ConfigureServices(services =>
+ {
+ services.ConfigureDurableWorkflows(
+ workflowOptions => workflowOptions.AddWorkflow(orderPipeline),
+ workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
+ clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
+ })
+ .Build();
+
+await host.StartAsync();
+
+IWorkflowClient workflowClient = host.Services.GetRequiredService();
+
+Console.WriteLine("Shared State Workflow Demo");
+Console.WriteLine("Workflow: ValidateOrder -> EnrichOrder -> ProcessPayment -> GenerateInvoice");
+Console.WriteLine();
+Console.WriteLine("Enter an order ID (or 'exit'):");
+
+while (true)
+{
+ Console.Write("> ");
+ string? input = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ try
+ {
+ // Start the workflow and stream events to see shared state in action
+ IStreamingWorkflowRun run = await workflowClient.StreamAsync(orderPipeline, input);
+ Console.WriteLine($"Started run: {run.RunId}");
+
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync())
+ {
+ switch (evt)
+ {
+ case WorkflowOutputEvent e:
+ Console.WriteLine($" [Output] {e.ExecutorId}: {e.Data}");
+ break;
+
+ case DurableWorkflowCompletedEvent e:
+ Console.WriteLine($" Completed: {e.Result}");
+ break;
+
+ case DurableWorkflowFailedEvent e:
+ Console.WriteLine($" Failed: {e.ErrorMessage}");
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ }
+
+ Console.WriteLine();
+}
+
+await host.StopAsync();
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/README.md b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/README.md
new file mode 100644
index 0000000000..31ff55ce84
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/README.md
@@ -0,0 +1,71 @@
+# Shared State Workflow Sample
+
+This sample demonstrates how executors in a durable workflow can share state via `IWorkflowContext`. State written by one executor is accessible to all downstream executors, persisted across supersteps, and survives process restarts.
+
+## Key Concepts Demonstrated
+
+- Writing state with `QueueStateUpdateAsync` — executors store data for downstream executors
+- Reading state with `ReadStateAsync` — executors access data written by earlier executors
+- Lazy initialization with `ReadOrInitStateAsync` — initialize state only if not already present
+- Custom scopes with `scopeName` — partition state into isolated namespaces (e.g., `"shipping"`)
+- Clearing scopes with `QueueClearScopeAsync` — remove all entries under a scope when no longer needed
+- Early termination with `RequestHaltAsync` — halt the workflow when validation fails
+- State persistence across supersteps — the orchestration passes shared state to each executor
+- Event streaming with `IStreamingWorkflowRun` — observe executor progress in real time
+
+## Workflow
+
+**OrderPipeline**: `ValidateOrder` → `EnrichOrder` → `ProcessPayment` → `GenerateInvoice`
+
+Return values carry primary business data through the pipeline (`OrderDetails` → `OrderDetails` → payment ref → invoice string). Shared state carries side-channel data that doesn't belong in the message chain:
+
+| Executor | Returns (message flow) | Reads from State | Writes to State |
+|----------|----------------------|-----------------|-----------------|
+| **ValidateOrder** | `OrderDetails` | — | `taxRate`, `auditValidate` |
+| **EnrichOrder** | `OrderDetails` (pass-through) | `auditValidate` | `shippingTier`, `auditEnrich`, `carrier` (scope: shipping) |
+| **ProcessPayment** | payment ref string | `taxRate` | `auditPayment` |
+| **GenerateInvoice** | invoice string | `auditValidate`, `auditEnrich`, `auditPayment`, `carrier` (scope: shipping) | clears `shipping` scope |
+
+> [!NOTE]
+> `EnrichOrder` writes `carrier` under the `"shipping"` scope using `scopeName: "shipping"`. This keeps the key separate from keys written without a scope, so `"carrier"` in the `"shipping"` scope won't collide with a `"carrier"` key written elsewhere.
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
+
+## Running the Sample
+
+```bash
+dotnet run
+```
+
+Enter an order ID when prompted. The workflow will process the order through all four executors, streaming events as they occur:
+
+```text
+> ORD-001
+Started run: abc123
+ Wrote to shared state: taxRate = 8.5%
+ Wrote to shared state: auditValidate
+ [Output] ValidateOrder: Order 'ORD-001' validated. Customer: Jerry, Amount: $249.99
+ Read from shared state: shippingTier = Express
+ Wrote to shared state: carrier = Contoso Express (scope: shipping)
+ Read from shared state: auditValidate (previous step: ValidateOrder)
+ Wrote to shared state: auditEnrich
+ [Output] EnrichOrder: Order enriched. Shipping: Express (previous step: ValidateOrder)
+ Read from shared state: taxRate = 8.5%
+ Wrote to shared state: auditPayment
+ [Output] ProcessPayment: Payment processed. Total: $271.24 (tax: $21.25). Ref: PAY-abc123def456
+ Read from shared state: 3 audit entries
+ Read from shared state: carrier = Contoso Express (scope: shipping)
+ Cleared shared state scope: shipping
+ [Output] GenerateInvoice: Invoice complete. Payment: "PAY-abc123def456". Audit trail: [ValidateOrder → EnrichOrder → ProcessPayment]
+ Completed: Invoice complete. Payment: "PAY-abc123def456". Audit trail: [ValidateOrder → EnrichOrder → ProcessPayment]
+```
+
+### Viewing Workflows in the DTS Dashboard
+
+After running a workflow, you can navigate to the Durable Task Scheduler (DTS) dashboard to inspect the orchestration status, executor inputs/outputs, and events.
+
+If you are using the DTS emulator, the dashboard is available at `http://localhost:8082`.
+
+To inspect shared state in the dashboard, click on an executor to view its input and output. The input contains a snapshot of the shared state the executor ran with, and the output includes any state updates it made (as `stateUpdates` with scoped keys).
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/07_SubWorkflows.csproj b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/07_SubWorkflows.csproj
new file mode 100644
index 0000000000..d8d36ead01
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/07_SubWorkflows.csproj
@@ -0,0 +1,28 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ SubWorkflows
+ SubWorkflows
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/Executors.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/Executors.cs
new file mode 100644
index 0000000000..121db7af67
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/Executors.cs
@@ -0,0 +1,232 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace SubWorkflows;
+
+///
+/// Event emitted when the fraud check risk score is calculated.
+///
+internal sealed class FraudRiskAssessedEvent(int riskScore) : WorkflowEvent($"Risk score: {riskScore}/100")
+{
+ public int RiskScore => riskScore;
+}
+
+///
+/// Represents an order being processed through the workflow.
+///
+internal sealed class OrderInfo
+{
+ public required string OrderId { get; set; }
+
+ public decimal Amount { get; set; }
+
+ public string? PaymentTransactionId { get; set; }
+
+ public string? TrackingNumber { get; set; }
+
+ public string? Carrier { get; set; }
+}
+
+// Main workflow executors
+
+///
+/// Entry point executor that receives the order ID and creates an OrderInfo object.
+///
+internal sealed class OrderReceived() : Executor("OrderReceived")
+{
+ public override ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine($"[OrderReceived] Processing order '{message}'");
+ Console.ResetColor();
+
+ OrderInfo order = new()
+ {
+ OrderId = message,
+ Amount = 99.99m // Simulated order amount
+ };
+
+ return ValueTask.FromResult(order);
+ }
+}
+
+///
+/// Final executor that outputs the completed order summary.
+///
+internal sealed class OrderCompleted() : Executor("OrderCompleted")
+{
+ public override ValueTask HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
+ Console.WriteLine($"│ [OrderCompleted] Order '{message.OrderId}' successfully processed!");
+ Console.WriteLine($"│ Payment: {message.PaymentTransactionId}");
+ Console.WriteLine($"│ Shipping: {message.Carrier} - {message.TrackingNumber}");
+ Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
+ Console.ResetColor();
+
+ return ValueTask.FromResult($"Order {message.OrderId} completed. Tracking: {message.TrackingNumber}");
+ }
+}
+
+// Payment sub-workflow executors
+
+///
+/// Validates payment information for an order.
+///
+internal sealed class ValidatePayment() : Executor("ValidatePayment")
+{
+ public override async ValueTask HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($" [Payment/ValidatePayment] Validating payment for order '{message.OrderId}'...");
+ Console.ResetColor();
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($" [Payment/ValidatePayment] Payment validated for ${message.Amount}");
+ Console.ResetColor();
+
+ return message;
+ }
+}
+
+///
+/// Charges the payment for an order.
+///
+internal sealed class ChargePayment() : Executor("ChargePayment")
+{
+ public override async ValueTask HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($" [Payment/ChargePayment] Charging ${message.Amount} for order '{message.OrderId}'...");
+ Console.ResetColor();
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+
+ message.PaymentTransactionId = $"TXN-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
+
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($" [Payment/ChargePayment] ✓ Payment processed: {message.PaymentTransactionId}");
+ Console.ResetColor();
+
+ return message;
+ }
+}
+
+// FraudCheck sub-sub-workflow executors (nested inside Payment)
+
+///
+/// Analyzes transaction patterns for potential fraud.
+///
+internal sealed class AnalyzePatterns() : Executor("AnalyzePatterns")
+{
+ public override async ValueTask HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.WriteLine($" [Payment/FraudCheck/AnalyzePatterns] Analyzing patterns for order '{message.OrderId}'...");
+ Console.ResetColor();
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+
+ // Store analysis results in shared state for the next executor in this sub-workflow
+ int patternsFound = new Random().Next(0, 5);
+ await context.QueueStateUpdateAsync("patternsFound", patternsFound, cancellationToken: cancellationToken);
+
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.WriteLine($" [Payment/FraudCheck/AnalyzePatterns] ✓ Pattern analysis complete ({patternsFound} suspicious patterns)");
+ Console.ResetColor();
+
+ return message;
+ }
+}
+
+///
+/// Calculates a risk score for the transaction.
+///
+internal sealed class CalculateRiskScore() : Executor("CalculateRiskScore")
+{
+ public override async ValueTask HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.WriteLine($" [Payment/FraudCheck/CalculateRiskScore] Calculating risk score for order '{message.OrderId}'...");
+ Console.ResetColor();
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+
+ // Read the pattern count from shared state (written by AnalyzePatterns)
+ int patternsFound = await context.ReadStateAsync("patternsFound", cancellationToken: cancellationToken);
+ int riskScore = Math.Min(patternsFound * 20 + new Random().Next(1, 20), 100);
+
+ // Emit a workflow event from within a nested sub-workflow
+ await context.AddEventAsync(new FraudRiskAssessedEvent(riskScore), cancellationToken);
+
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.WriteLine($" [Payment/FraudCheck/CalculateRiskScore] ✓ Risk score: {riskScore}/100 (based on {patternsFound} patterns)");
+ Console.ResetColor();
+
+ return message;
+ }
+}
+
+// Shipping sub-workflow executors
+
+///
+/// Selects a shipping carrier for an order.
+///
+///
+/// This executor uses (void return) combined with
+/// to forward the order to the next
+/// connected executor (CreateShipment). This demonstrates explicit typed message passing
+/// as an alternative to returning a value from the handler.
+///
+internal sealed class SelectCarrier() : Executor("SelectCarrier")
+{
+ public override async ValueTask HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Blue;
+ Console.WriteLine($" [Shipping/SelectCarrier] Selecting carrier for order '{message.OrderId}'...");
+ Console.ResetColor();
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+
+ message.Carrier = message.Amount > 50 ? "Express" : "Standard";
+
+ Console.ForegroundColor = ConsoleColor.Blue;
+ Console.WriteLine($" [Shipping/SelectCarrier] ✓ Selected carrier: {message.Carrier}");
+ Console.ResetColor();
+
+ // Use SendMessageAsync to forward the updated order to connected executors.
+ // With a void-return executor, this is the mechanism for passing data downstream.
+ await context.SendMessageAsync(message, cancellationToken: cancellationToken);
+ }
+}
+
+///
+/// Creates shipment and generates tracking number.
+///
+internal sealed class CreateShipment() : Executor("CreateShipment")
+{
+ public override async ValueTask HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ Console.ForegroundColor = ConsoleColor.Blue;
+ Console.WriteLine($" [Shipping/CreateShipment] Creating shipment for order '{message.OrderId}'...");
+ Console.ResetColor();
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+
+ message.TrackingNumber = $"TRACK-{Guid.NewGuid().ToString("N")[..10].ToUpperInvariant()}";
+
+ Console.ForegroundColor = ConsoleColor.Blue;
+ Console.WriteLine($" [Shipping/CreateShipment] ✓ Shipment created: {message.TrackingNumber}");
+ Console.ResetColor();
+
+ return message;
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/Program.cs
new file mode 100644
index 0000000000..d542f4aba5
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/Program.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates nested sub-workflows. A sub-workflow can act as an executor
+// within another workflow, including multi-level nesting (sub-workflow within sub-workflow).
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using SubWorkflows;
+
+// Get DTS connection string from environment variable
+string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+
+// Build the FraudCheck sub-workflow (this will be nested inside the Payment sub-workflow)
+AnalyzePatterns analyzePatterns = new();
+CalculateRiskScore calculateRiskScore = new();
+
+Workflow fraudCheckWorkflow = new WorkflowBuilder(analyzePatterns)
+ .WithName("SubFraudCheck")
+ .WithDescription("Analyzes transaction patterns and calculates risk score")
+ .AddEdge(analyzePatterns, calculateRiskScore)
+ .Build();
+
+// Build the Payment sub-workflow: ValidatePayment -> FraudCheck (sub-workflow) -> ChargePayment
+ValidatePayment validatePayment = new();
+ExecutorBinding fraudCheckExecutor = fraudCheckWorkflow.BindAsExecutor("FraudCheck");
+ChargePayment chargePayment = new();
+
+Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
+ .WithName("SubPaymentProcessing")
+ .WithDescription("Validates and processes payment for an order")
+ .AddEdge(validatePayment, fraudCheckExecutor)
+ .AddEdge(fraudCheckExecutor, chargePayment)
+ .Build();
+
+// Build the Shipping sub-workflow: SelectCarrier -> CreateShipment
+SelectCarrier selectCarrier = new();
+CreateShipment createShipment = new();
+
+Workflow shippingWorkflow = new WorkflowBuilder(selectCarrier)
+ .WithName("SubShippingArrangement")
+ .WithDescription("Selects carrier and creates shipment")
+ .AddEdge(selectCarrier, createShipment)
+ .Build();
+
+// Build the main workflow using sub-workflows as executors
+// OrderReceived -> Payment (sub-workflow) -> Shipping (sub-workflow) -> OrderCompleted
+OrderReceived orderReceived = new();
+OrderCompleted orderCompleted = new();
+ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
+ExecutorBinding shippingExecutor = shippingWorkflow.BindAsExecutor("Shipping");
+
+Workflow orderProcessingWorkflow = new WorkflowBuilder(orderReceived)
+ .WithName("OrderProcessing")
+ .WithDescription("Processes an order through payment and shipping")
+ .AddEdge(orderReceived, paymentExecutor)
+ .AddEdge(paymentExecutor, shippingExecutor)
+ .AddEdge(shippingExecutor, orderCompleted)
+ .Build();
+
+// Configure and start the host
+// Register only the main workflow - sub-workflows are discovered automatically!
+IHost host = Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
+ .ConfigureServices(services =>
+ {
+ services.ConfigureDurableWorkflows(
+ workflowOptions => workflowOptions.AddWorkflow(orderProcessingWorkflow),
+ workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
+ clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
+ })
+ .Build();
+
+await host.StartAsync();
+
+IWorkflowClient workflowClient = host.Services.GetRequiredService();
+
+Console.WriteLine("Durable Sub-Workflows Sample");
+Console.WriteLine("Workflow: OrderReceived -> Payment(sub) -> Shipping(sub) -> OrderCompleted");
+Console.WriteLine(" Payment contains nested FraudCheck sub-workflow (Level 2 nesting)");
+Console.WriteLine();
+Console.WriteLine("Enter an order ID (or 'exit'):");
+
+while (true)
+{
+ Console.Write("> ");
+ string? input = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ try
+ {
+ await StartNewWorkflowAsync(input, orderProcessingWorkflow, workflowClient);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ }
+
+ Console.WriteLine();
+}
+
+await host.StopAsync();
+
+// Start a new workflow using streaming to observe events (including from sub-workflows)
+static async Task StartNewWorkflowAsync(string orderId, Workflow workflow, IWorkflowClient client)
+{
+ Console.WriteLine($"\nStarting order processing for '{orderId}'...");
+
+ IStreamingWorkflowRun run = await client.StreamAsync(workflow, orderId);
+ Console.WriteLine($"Run ID: {run.RunId}");
+ Console.WriteLine();
+
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync())
+ {
+ switch (evt)
+ {
+ // Custom event emitted from the FraudCheck sub-sub-workflow
+ case FraudRiskAssessedEvent e:
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.WriteLine($" [Event from sub-workflow] {e.GetType().Name}: Risk score {e.RiskScore}/100");
+ Console.ResetColor();
+ break;
+
+ case DurableWorkflowCompletedEvent e:
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine($"✓ Order completed: {e.Result}");
+ Console.ResetColor();
+ break;
+
+ case DurableWorkflowFailedEvent e:
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"✗ Failed: {e.ErrorMessage}");
+ Console.ResetColor();
+ break;
+ }
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/README.md b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/README.md
new file mode 100644
index 0000000000..83968eee0e
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/README.md
@@ -0,0 +1,105 @@
+# Sub-Workflows Sample (Nested Workflows)
+
+This sample demonstrates how to compose complex workflows from simpler, reusable sub-workflows. Sub-workflows are built using `WorkflowBuilder` and embedded as executors via `BindAsExecutor()`. Unlike the in-process workflow runner, the durable workflow backend persists execution state across process restarts — each sub-workflow runs as a separate orchestration instance on the Durable Task Scheduler, providing independent checkpointing, fault tolerance, and hierarchical visualization in the DTS dashboard.
+
+## Key Concepts Demonstrated
+
+- **Sub-workflows**: Using `Workflow.BindAsExecutor()` to embed a workflow as an executor in another workflow
+- **Multi-level nesting**: Sub-workflows within sub-workflows (Level 2 nesting)
+- **Automatic discovery**: Registering only the main workflow; sub-workflows are discovered automatically
+- **Failure isolation**: Each sub-workflow runs as a separate orchestration instance on the DTS backend
+- **Hierarchical visualization**: Parent-child orchestration hierarchy visible in the DTS dashboard
+- **Event propagation**: Custom workflow events (`FraudRiskAssessedEvent`) bubble up from nested sub-workflows to the streaming client
+- **Message passing**: Using `Executor` (void return) with `SendMessageAsync` to forward typed messages to connected executors (`SelectCarrier`)
+- **Shared state within sub-workflows**: Using `QueueStateUpdateAsync`/`ReadStateAsync` to share data between executors within a sub-workflow (`AnalyzePatterns` → `CalculateRiskScore`)
+
+## Overview
+
+The sample implements an order processing workflow composed of two sub-workflows, one of which contains its own nested sub-workflow:
+
+```
+OrderProcessing (main workflow)
+├── OrderReceived
+├── Payment (sub-workflow)
+│ ├── ValidatePayment
+│ ├── FraudCheck (sub-sub-workflow) ← Level 2 nesting!
+│ │ ├── AnalyzePatterns
+│ │ └── CalculateRiskScore
+│ └── ChargePayment
+├── Shipping (sub-workflow)
+│ ├── SelectCarrier ← Uses SendMessageAsync (void-return executor)
+│ └── CreateShipment
+└── OrderCompleted
+```
+
+| Executor | Sub-Workflow | Description |
+|----------|-------------|-------------|
+| OrderReceived | Main | Receives order ID and creates order info |
+| ValidatePayment | Payment | Validates payment information |
+| AnalyzePatterns | FraudCheck (nested in Payment) | Analyzes transaction patterns, stores results in shared state |
+| CalculateRiskScore | FraudCheck (nested in Payment) | Reads shared state, calculates risk score, emits `FraudRiskAssessedEvent` |
+| ChargePayment | Payment | Charges payment amount |
+| SelectCarrier | Shipping | Selects carrier using `SendMessageAsync` (void-return executor) |
+| CreateShipment | Shipping | Creates shipment with tracking |
+| OrderCompleted | Main | Outputs completed order summary |
+
+## How Sub-Workflows Work
+
+For an introduction to sub-workflows and the `BindAsExecutor()` API, see the [Sub-Workflows foundational sample](../../../../03-workflows/_StartHere/05_SubWorkflows).
+
+This durable sample extends the same pattern — the key difference is that each sub-workflow runs as a **separate orchestration instance** on the Durable Task Scheduler, providing independent checkpointing, fault tolerance, and hierarchical visualization in the DTS dashboard.
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for information on configuring the environment, including how to install and run the Durable Task Scheduler.
+
+## Running the Sample
+
+```bash
+cd dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows
+dotnet run --framework net10.0
+```
+
+### Sample Output
+
+```text
+Durable Sub-Workflows Sample
+Workflow: OrderReceived -> Payment(sub) -> Shipping(sub) -> OrderCompleted
+ Payment contains nested FraudCheck sub-workflow (Level 2 nesting)
+
+Enter an order ID (or 'exit'):
+> ORD-001
+Starting order processing for 'ORD-001'...
+Run ID: abc123...
+
+[OrderReceived] Processing order 'ORD-001'
+ [Payment/ValidatePayment] Validating payment for order 'ORD-001'...
+ [Payment/ValidatePayment] Payment validated for $99.99
+ [Payment/FraudCheck/AnalyzePatterns] Analyzing patterns for order 'ORD-001'...
+ [Payment/FraudCheck/AnalyzePatterns] ✓ Pattern analysis complete (2 suspicious patterns)
+ [Payment/FraudCheck/CalculateRiskScore] Calculating risk score for order 'ORD-001'...
+ [Payment/FraudCheck/CalculateRiskScore] ✓ Risk score: 53/100 (based on 2 patterns)
+ [Event from sub-workflow] FraudRiskAssessedEvent: Risk score 53/100
+ [Payment/ChargePayment] Charging $99.99 for order 'ORD-001'...
+ [Payment/ChargePayment] ✓ Payment processed: TXN-A1B2C3D4
+ [Shipping/SelectCarrier] Selecting carrier for order 'ORD-001'...
+ [Shipping/SelectCarrier] ✓ Selected carrier: Express
+ [Shipping/CreateShipment] Creating shipment for order 'ORD-001'...
+ [Shipping/CreateShipment] ✓ Shipment created: TRACK-I9J0K1L2M3
+┌─────────────────────────────────────────────────────────────────┐
+│ [OrderCompleted] Order 'ORD-001' successfully processed!
+│ Payment: TXN-A1B2C3D4
+│ Shipping: Express - TRACK-I9J0K1L2M3
+└─────────────────────────────────────────────────────────────────┘
+✓ Order completed: Order ORD-001 completed. Tracking: TRACK-I9J0K1L2M3
+
+> exit
+```
+
+### Viewing Workflows in the DTS Dashboard
+
+After running the workflow, you can navigate to the Durable Task Scheduler (DTS) dashboard to inspect the orchestration hierarchy, including sub-orchestrations.
+
+If you are using the DTS emulator, the dashboard is available at `http://localhost:8082`.
+
+Because each sub-workflow runs as a separate orchestration instance, the dashboard shows a parent-child hierarchy: the top-level `OrderProcessing` orchestration with `Payment` and `Shipping` as child orchestrations, and `FraudCheck` nested under `Payment`. You can click into each orchestration to inspect its executor inputs/outputs, events, and execution timeline independently.
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/08_WorkflowHITL.csproj b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/08_WorkflowHITL.csproj
new file mode 100644
index 0000000000..a9103b6e48
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/08_WorkflowHITL.csproj
@@ -0,0 +1,28 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ WorkflowHITL
+ WorkflowHITL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/Executors.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/Executors.cs
new file mode 100644
index 0000000000..2006b1cd19
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/Executors.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowHITL;
+
+///
+/// Represents an expense approval request.
+///
+/// The unique identifier of the expense.
+/// The amount of the expense.
+/// The name of the employee submitting the expense.
+public record ApprovalRequest(string ExpenseId, decimal Amount, string EmployeeName);
+
+///
+/// Represents the response to an approval request.
+///
+/// Whether the expense was approved.
+/// Optional comments from the approver.
+public record ApprovalResponse(bool Approved, string? Comments);
+
+///
+/// Retrieves expense details and creates an approval request.
+///
+internal sealed class CreateApprovalRequest() : Executor("RetrieveRequest")
+{
+ ///
+ public override ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ // In a real scenario, this would look up expense details from a database
+ return new ValueTask(new ApprovalRequest(message, 1500.00m, "Jerry"));
+ }
+}
+
+///
+/// Prepares the approval request for finance review after manager approval.
+///
+internal sealed class PrepareFinanceReview() : Executor("PrepareFinanceReview")
+{
+ ///
+ public override ValueTask HandleAsync(
+ ApprovalResponse message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ if (!message.Approved)
+ {
+ throw new InvalidOperationException("Cannot proceed to finance review — manager denied the expense.");
+ }
+
+ // In a real scenario, this would retrieve the original expense details
+ return new ValueTask(new ApprovalRequest("EXP-2025-001", 1500.00m, "Jerry"));
+ }
+}
+
+///
+/// Processes the expense reimbursement based on the parallel approval responses from budget and compliance.
+///
+internal sealed class ExpenseReimburse() : Executor("Reimburse")
+{
+ ///
+ public override async ValueTask HandleAsync(
+ ApprovalResponse[] message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ // Check that all parallel approvals passed
+ ApprovalResponse? denied = Array.Find(message, r => !r.Approved);
+ if (denied is not null)
+ {
+ return $"Expense reimbursement denied. Comments: {denied.Comments}";
+ }
+
+ // Simulate payment processing
+ await Task.Delay(1000, cancellationToken);
+ return $"Expense reimbursed at {DateTime.UtcNow:O}";
+ }
+}
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/Program.cs b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/Program.cs
new file mode 100644
index 0000000000..bc8fe00341
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/Program.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates a Human-in-the-Loop (HITL) workflow using Durable Tasks.
+//
+// ┌──────────────────────┐ ┌────────────────┐ ┌─────────────────────┐ ┌────────────────────┐
+// │ CreateApprovalRequest│──►│ManagerApproval │──►│PrepareFinanceReview │──┬►│ BudgetApproval │──┐
+// └──────────────────────┘ │ (RequestPort) │ └─────────────────────┘ │ │ (RequestPort) │ │
+// └────────────────┘ │ └────────────────────┘ │ ┌─────────────────┐
+// │ ├─►│ExpenseReimburse │
+// │ ┌────────────────────┐ │ └─────────────────┘
+// └►│ComplianceApproval │──┘
+// │ (RequestPort) │
+// └────────────────────┘
+//
+// The workflow pauses at three RequestPorts — one for the manager, then two in parallel for finance.
+// After manager approval, BudgetApproval and ComplianceApproval run concurrently via fan-out/fan-in.
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using WorkflowHITL;
+
+string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+
+// Define executors and RequestPorts for the three HITL pause points
+CreateApprovalRequest createRequest = new();
+RequestPort managerApproval = RequestPort.Create("ManagerApproval");
+PrepareFinanceReview prepareFinanceReview = new();
+RequestPort budgetApproval = RequestPort.Create("BudgetApproval");
+RequestPort complianceApproval = RequestPort.Create("ComplianceApproval");
+ExpenseReimburse reimburse = new();
+
+// Build the workflow: CreateApprovalRequest -> ManagerApproval -> PrepareFinanceReview -> [BudgetApproval AND ComplianceApproval] -> ExpenseReimburse
+Workflow expenseApproval = new WorkflowBuilder(createRequest)
+ .WithName("ExpenseReimbursement")
+ .WithDescription("Expense reimbursement with manager and parallel finance approvals")
+ .AddEdge(createRequest, managerApproval)
+ .AddEdge(managerApproval, prepareFinanceReview)
+ .AddFanOutEdge(prepareFinanceReview, [budgetApproval, complianceApproval])
+ .AddFanInBarrierEdge([budgetApproval, complianceApproval], reimburse)
+ .Build();
+
+IHost host = Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
+ .ConfigureServices(services =>
+ {
+ services.ConfigureDurableWorkflows(
+ options => options.AddWorkflow(expenseApproval),
+ workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
+ clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
+ })
+ .Build();
+
+await host.StartAsync();
+
+IWorkflowClient workflowClient = host.Services.GetRequiredService();
+
+// Start the workflow with streaming to observe events including HITL pauses
+string expenseId = "EXP-2025-001";
+Console.WriteLine($"Starting expense reimbursement workflow for expense: {expenseId}");
+IStreamingWorkflowRun run = await workflowClient.StreamAsync(expenseApproval, expenseId);
+Console.WriteLine($"Workflow started with instance ID: {run.RunId}\n");
+
+// Watch for workflow events — handle HITL requests as they arrive
+await foreach (WorkflowEvent evt in run.WatchStreamAsync())
+{
+ switch (evt)
+ {
+ case DurableWorkflowWaitingForInputEvent requestEvent:
+ Console.WriteLine($"Workflow paused at RequestPort: {requestEvent.RequestPort.Id}");
+ Console.WriteLine($" Input: {requestEvent.Input}");
+
+ // In a real scenario, this would involve human interaction (UI, email, Teams, etc.)
+ ApprovalRequest? request = requestEvent.GetInputAs();
+ Console.WriteLine($" Approval for: {request?.EmployeeName}, Amount: {request?.Amount:C}");
+
+ ApprovalResponse approvalResponse = new(Approved: true, Comments: "Approved by manager.");
+ await run.SendResponseAsync(requestEvent, approvalResponse);
+ Console.WriteLine($" Response sent: Approved={approvalResponse.Approved}\n");
+ break;
+
+ case DurableWorkflowCompletedEvent completedEvent:
+ Console.WriteLine($"Workflow completed: {completedEvent.Result}");
+ break;
+
+ case DurableWorkflowFailedEvent failedEvent:
+ Console.WriteLine($"Workflow failed: {failedEvent.ErrorMessage}");
+ break;
+ }
+}
+
+await host.StopAsync();
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/README.md b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/README.md
new file mode 100644
index 0000000000..f659077371
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/README.md
@@ -0,0 +1,106 @@
+# Workflow Human-in-the-Loop (HITL) Sample
+
+This sample demonstrates a **Human-in-the-Loop** pattern in durable workflows using `RequestPort`. The workflow pauses execution at a manager approval point, then fans out to two parallel finance approval points — budget and compliance — before resuming.
+
+## Key Concepts Demonstrated
+
+- Using `RequestPort` to define external input points in a workflow
+- Sequential and parallel HITL pause points in a single workflow using fan-out/fan-in
+- Streaming workflow events with `IStreamingWorkflowRun`
+- Handling `DurableWorkflowWaitingForInputEvent` to detect HITL pauses
+- Using `SendResponseAsync` to provide responses and resume the workflow
+- **Durability**: The workflow survives process restarts while waiting for human input
+
+## Workflow
+
+This sample implements the following workflow:
+
+```
+┌──────────────────────┐ ┌────────────────┐ ┌─────────────────────┐ ┌────────────────────┐
+│ CreateApprovalRequest│──►│ManagerApproval │──►│PrepareFinanceReview │──┬►│ BudgetApproval │──┐
+└──────────────────────┘ │ (RequestPort) │ └─────────────────────┘ │ │ (RequestPort) │ │
+ └────────────────┘ │ └────────────────────┘ │ ┌─────────────────┐
+ │ ├─►│ExpenseReimburse │
+ │ ┌────────────────────┐ │ └─────────────────┘
+ └►│ComplianceApproval │──┘
+ │ (RequestPort) │
+ └────────────────────┘
+```
+
+| Step | Description |
+|------|-------------|
+| CreateApprovalRequest | Retrieves expense details and creates an approval request |
+| ManagerApproval (RequestPort) | **PAUSES** the workflow and waits for manager approval |
+| PrepareFinanceReview | Prepares the request for finance review after manager approval |
+| BudgetApproval (RequestPort) | **PAUSES** the workflow and waits for budget approval (parallel) |
+| ComplianceApproval (RequestPort) | **PAUSES** the workflow and waits for compliance approval (parallel) |
+| ExpenseReimburse | Processes the reimbursement after all approvals pass |
+
+## How It Works
+
+A `RequestPort` defines a typed external input point in the workflow:
+
+```csharp
+RequestPort managerApproval =
+ RequestPort.Create("ManagerApproval");
+```
+
+Use `WatchStreamAsync` to observe events. When the workflow reaches a `RequestPort`, a `DurableWorkflowWaitingForInputEvent` is emitted. Call `SendResponseAsync` to provide the response and resume the workflow:
+
+```csharp
+await foreach (WorkflowEvent evt in run.WatchStreamAsync())
+{
+ switch (evt)
+ {
+ case DurableWorkflowWaitingForInputEvent requestEvent:
+ ApprovalRequest? request = requestEvent.GetInputAs();
+ await run.SendResponseAsync(requestEvent, new ApprovalResponse(Approved: true, Comments: "Approved."));
+ break;
+ }
+}
+```
+
+## Environment Setup
+
+See the [README.md](../../README.md) file in the parent directory for information on configuring the environment, including how to install and run the Durable Task Scheduler.
+
+## Running the Sample
+
+```bash
+cd dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL
+dotnet run --framework net10.0
+```
+
+### Sample Output
+
+```text
+Starting expense reimbursement workflow for expense: EXP-2025-001
+Workflow started with instance ID: abc123...
+
+Workflow paused at RequestPort: ManagerApproval
+ Input: {"expenseId":"EXP-2025-001","amount":1500.00,"employeeName":"Jerry"}
+ Approval for: Jerry, Amount: $1,500.00
+ Response sent: Approved=True
+
+Workflow paused at RequestPort: BudgetApproval
+ Input: {"expenseId":"EXP-2025-001","amount":1500.00,"employeeName":"Jerry"}
+ Approval for: Jerry, Amount: $1,500.00
+ Response sent: Approved=True
+
+Workflow paused at RequestPort: ComplianceApproval
+ Input: {"expenseId":"EXP-2025-001","amount":1500.00,"employeeName":"Jerry"}
+ Approval for: Jerry, Amount: $1,500.00
+ Response sent: Approved=True
+
+Workflow completed: Expense reimbursed at 2025-01-23T17:30:00.0000000Z
+```
+
+### Viewing Workflows in the DTS Dashboard
+
+After running the sample, you can navigate to the Durable Task Scheduler (DTS) dashboard to visualize the completed orchestration and inspect its execution history.
+
+If you are using the DTS emulator, the dashboard is available at `http://localhost:8082`.
+
+1. Open the dashboard and look for the orchestration instance matching the instance ID logged in the console output (e.g., `abc123...`).
+2. Click into the instance to see the execution timeline, which shows each executor activity and the `WaitForExternalEvent` pauses where the workflow waited for human input — including the two parallel finance approvals.
+3. Expand individual activity steps to inspect inputs and outputs — for example, the `ManagerApproval`, `BudgetApproval`, and `ComplianceApproval` external events will show the approval request sent and the response received.
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/Directory.Build.props b/dotnet/samples/04-hosting/DurableWorkflows/Directory.Build.props
new file mode 100644
index 0000000000..3723bee3cc
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/Directory.Build.props
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/DurableWorkflows/README.md b/dotnet/samples/04-hosting/DurableWorkflows/README.md
new file mode 100644
index 0000000000..2b7103de50
--- /dev/null
+++ b/dotnet/samples/04-hosting/DurableWorkflows/README.md
@@ -0,0 +1,50 @@
+# Durable Workflow Samples
+
+This directory contains samples demonstrating how to build durable workflows using the Microsoft Agent Framework.
+
+## Environment Setup
+
+### Prerequisites
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) or later
+- [Durable Task Scheduler](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) running locally or in Azure
+
+### Running the Durable Task Scheduler Emulator
+
+To run the emulator locally using Docker:
+
+```bash
+docker run -d -p 8080:8080 --name durabletask-emulator mcr.microsoft.com/durabletask/emulator:latest
+```
+
+Set the connection string environment variable to point to the local emulator:
+
+```bash
+# Linux/macOS
+export DURABLE_TASK_SCHEDULER_CONNECTION_STRING="AccountEndpoint=http://localhost:8080"
+
+# Windows (PowerShell)
+$env:DURABLE_TASK_SCHEDULER_CONNECTION_STRING = "AccountEndpoint=http://localhost:8080"
+```
+
+## Samples
+
+### Console Apps
+
+| Sample | Description |
+|--------|-------------|
+| [01_SequentialWorkflow](ConsoleApps/01_SequentialWorkflow/) | Basic sequential workflow with ordered executor steps |
+| [02_ConcurrentWorkflow](ConsoleApps/02_ConcurrentWorkflow/) | Fan-out/fan-in concurrent workflow execution |
+| [03_ConditionalEdges](ConsoleApps/03_ConditionalEdges/) | Workflows with conditional routing between executors |
+| [05_WorkflowEvents](ConsoleApps/05_WorkflowEvents/) | Publishing and subscribing to workflow events |
+| [06_WorkflowSharedState](ConsoleApps/06_WorkflowSharedState/) | Sharing state across workflow executors |
+| [07_SubWorkflows](ConsoleApps/07_SubWorkflows/) | Nested sub-workflow composition |
+| [08_WorkflowHITL](ConsoleApps/08_WorkflowHITL/) | Human-in-the-loop workflow with approval gates |
+
+### Azure Functions
+
+| Sample | Description |
+|--------|-------------|
+| [01_SequentialWorkflow](AzureFunctions/01_SequentialWorkflow/) | Sequential workflow hosted in Azure Functions |
+| [02_ConcurrentWorkflow](AzureFunctions/02_ConcurrentWorkflow/) | Concurrent workflow hosted in Azure Functions |
+| [03_WorkflowHITL](AzureFunctions/03_WorkflowHITL/) | Human-in-the-loop workflow hosted in Azure Functions |
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md
index e3e90fdae0..3ddf3a39c4 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md
@@ -2,20 +2,40 @@
## [Unreleased]
-### Changed
+- Added support for durable workflows ([#4436](https://github.com/microsoft/agent-framework/pull/4436))
-- Added TTL configuration for durable agent entities ([#2679](https://github.com/microsoft/agent-framework/pull/2679))
-- Switch to new "Run" method name ([#2843](https://github.com/microsoft/agent-framework/pull/2843))
-- Removed AgentThreadMetadata and used AgentSessionId directly instead ([#3067](https://github.com/microsoft/agent-framework/pull/3067));
-- Renamed AgentThread to AgentSession ([#3430](https://github.com/microsoft/agent-framework/pull/3430))
-- Moved AgentSession.Serialize to AIAgent.SerializeSession ([#3650](https://github.com/microsoft/agent-framework/pull/3650))
-- Renamed serializedSession parameter to serializedState on DeserializeSessionAsync for consistency ([#3681](https://github.com/microsoft/agent-framework/pull/3681))
-- Introduce Core method pattern for Session management methods on AIAgent ([#3699](https://github.com/microsoft/agent-framework/pull/3699))
-- Changed AIAgent.SerializeSession to AIAgent.SerializeSessionAsync ([#3879](https://github.com/microsoft/agent-framework/pull/3879))
-- Changed ChatHistory and AIContext Providers to have pipeline semantics ([#3806](https://github.com/microsoft/agent-framework/pull/3806))
+## v1.0.0-preview.260219.1
+
+- [BREAKING] Changed ChatHistory and AIContext Providers to have pipeline semantics ([#3806](https://github.com/microsoft/agent-framework/pull/3806))
- Marked all `RunAsync` overloads as `new`, added missing ones, and added support for primitives and arrays ([#3803](https://github.com/microsoft/agent-framework/pull/3803))
- Improve session cast error message quality and consistency ([#3973](https://github.com/microsoft/agent-framework/pull/3973))
+## v1.0.0-preview.260212.1
+
+- [BREAKING] Changed AIAgent.SerializeSession to AIAgent.SerializeSessionAsync ([#3879](https://github.com/microsoft/agent-framework/pull/3879))
+
+## v1.0.0-preview.260209.1
+
+- [BREAKING] Introduce Core method pattern for Session management methods on AIAgent ([#3699](https://github.com/microsoft/agent-framework/pull/3699))
+
+## v1.0.0-preview.260205.1
+
+- [BREAKING] Moved AgentSession.Serialize to AIAgent.SerializeSession ([#3650](https://github.com/microsoft/agent-framework/pull/3650))
+- [BREAKING] Renamed serializedSession parameter to serializedState on DeserializeSessionAsync for consistency ([#3681](https://github.com/microsoft/agent-framework/pull/3681))
+
+## v1.0.0-preview.260127.1
+
+- [BREAKING] Renamed AgentThread to AgentSession ([#3430](https://github.com/microsoft/agent-framework/pull/3430))
+
+## v1.0.0-preview.260108.1
+
+- [BREAKING] Removed AgentThreadMetadata and used AgentSessionId directly instead ([#3067](https://github.com/microsoft/agent-framework/pull/3067))
+
+## v1.0.0-preview.251219.1
+
+- Added TTL configuration for durable agent entities ([#2679](https://github.com/microsoft/agent-framework/pull/2679))
+- Switch to new "Run" method name ([#2843](https://github.com/microsoft/agent-framework/pull/2843))
+
## v1.0.0-preview.251204.1
- Added orchestration ID to durable agent entity state ([#2137](https://github.com/microsoft/agent-framework/pull/2137))
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs
index cefcad323a..1b84f9f49f 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs
@@ -141,4 +141,15 @@ internal IReadOnlyDictionary> GetAgentFa
{
return this._agentTimeToLive.TryGetValue(agentName, out TimeSpan? ttl) ? ttl : this.DefaultTimeToLive;
}
+
+ ///
+ /// Determines whether an agent with the specified name is registered.
+ ///
+ /// The name of the agent to locate. Cannot be null.
+ /// true if an agent with the specified name is registered; otherwise, false.
+ internal bool ContainsAgent(string agentName)
+ {
+ ArgumentNullException.ThrowIfNull(agentName);
+ return this._agentFactories.ContainsKey(agentName);
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableDataConverter.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableDataConverter.cs
new file mode 100644
index 0000000000..08dddf6852
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableDataConverter.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
+using Microsoft.Agents.AI.DurableTask.State;
+using Microsoft.DurableTask;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Custom data converter for durable agents and workflows that ensures proper JSON serialization.
+///
+///
+/// This converter handles special cases like using source-generated
+/// JSON contexts for AOT compatibility, and falls back to reflection-based serialization for other types.
+///
+internal sealed class DurableDataConverter : DataConverter
+{
+ private static readonly JsonSerializerOptions s_options = new(DurableAgentJsonUtilities.DefaultOptions)
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ };
+
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Fallback uses reflection when metadata unavailable.")]
+ [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Fallback uses reflection when metadata unavailable.")]
+ public override object? Deserialize(string? data, Type targetType)
+ {
+ if (data is null)
+ {
+ return null;
+ }
+
+ if (targetType == typeof(DurableAgentState))
+ {
+ return JsonSerializer.Deserialize(data, DurableAgentStateJsonContext.Default.DurableAgentState);
+ }
+
+ JsonTypeInfo? typeInfo = s_options.GetTypeInfo(targetType);
+ return typeInfo is not null
+ ? JsonSerializer.Deserialize(data, typeInfo)
+ : JsonSerializer.Deserialize(data, targetType, s_options);
+ }
+
+ [return: NotNullIfNotNull(nameof(value))]
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Fallback uses reflection when metadata unavailable.")]
+ [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Fallback uses reflection when metadata unavailable.")]
+ public override string? Serialize(object? value)
+ {
+ if (value is null)
+ {
+ return null;
+ }
+
+ if (value is DurableAgentState durableAgentState)
+ {
+ return JsonSerializer.Serialize(durableAgentState, DurableAgentStateJsonContext.Default.DurableAgentState);
+ }
+
+ JsonTypeInfo? typeInfo = s_options.GetTypeInfo(value.GetType());
+ return typeInfo is not null
+ ? JsonSerializer.Serialize(value, typeInfo)
+ : JsonSerializer.Serialize(value, s_options);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableOptions.cs
new file mode 100644
index 0000000000..d7f289b223
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableOptions.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Provides configuration options for durable agents and workflows.
+///
+[DebuggerDisplay("Workflows = {Workflows.Workflows.Count}, Agents = {Agents.AgentCount}")]
+public class DurableOptions
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal DurableOptions()
+ {
+ this.Workflows = new DurableWorkflowOptions(this);
+ }
+
+ ///
+ /// Gets the configuration options for durable agents.
+ ///
+ public DurableAgentsOptions Agents { get; } = new();
+
+ ///
+ /// Gets the configuration options for durable workflows.
+ ///
+ public DurableWorkflowOptions Workflows { get; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableServicesMarker.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableServicesMarker.cs
new file mode 100644
index 0000000000..58dea9b20f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableServicesMarker.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Marker class used to track whether core durable task services have been registered.
+///
+///
+///
+/// Problem it solves: Users may call configuration methods multiple times:
+///
+/// services.ConfigureDurableOptions(...); // 1st call - registers agent A
+/// services.ConfigureDurableOptions(...); // 2nd call - registers workflow X
+/// services.ConfigureDurableOptions(...); // 3rd call - registers agent B and workflow Y
+///
+/// Each call invokes EnsureDurableServicesRegistered. Without this marker, core services like
+/// AddDurableTaskWorker and AddDurableTaskClient would be registered multiple times,
+/// causing runtime errors or unexpected behavior.
+///
+///
+/// How it works:
+///
+/// - First call: No marker in services → register marker + all core services
+/// - Subsequent calls: Marker exists → early return, skip core service registration
+///
+///
+///
+/// Why not use TryAddSingleton for everything?
+/// While TryAddSingleton prevents duplicate simple service registrations, it doesn't work for
+/// complex registrations like AddDurableTaskWorker which have side effects and configure
+/// internal builders. The marker pattern provides a clean, explicit guard for the entire registration block.
+///
+///
+internal sealed class DurableServicesMarker;
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs
index ba310441df..57ef010a2f 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs
@@ -100,4 +100,131 @@ public static partial void LogTTLRescheduled(
public static partial void LogTTLExpirationTimeCleared(
this ILogger logger,
AgentSessionId sessionId);
+
+ // Durable workflow logs (EventIds 100-199)
+
+ [LoggerMessage(
+ EventId = 100,
+ Level = LogLevel.Information,
+ Message = "Starting workflow '{WorkflowName}' with instance '{InstanceId}'")]
+ public static partial void LogWorkflowStarting(
+ this ILogger logger,
+ string workflowName,
+ string instanceId);
+
+ [LoggerMessage(
+ EventId = 101,
+ Level = LogLevel.Information,
+ Message = "Superstep {Step}: {Count} active executor(s)")]
+ public static partial void LogSuperstepStarting(
+ this ILogger logger,
+ int step,
+ int count);
+
+ [LoggerMessage(
+ EventId = 102,
+ Level = LogLevel.Debug,
+ Message = "Superstep {Step} executors: [{Executors}]")]
+ public static partial void LogSuperstepExecutors(
+ this ILogger logger,
+ int step,
+ string executors);
+
+ [LoggerMessage(
+ EventId = 103,
+ Level = LogLevel.Information,
+ Message = "Workflow completed")]
+ public static partial void LogWorkflowCompleted(
+ this ILogger logger);
+
+ [LoggerMessage(
+ EventId = 104,
+ Level = LogLevel.Warning,
+ Message = "Workflow '{InstanceId}' terminated early: reached maximum superstep limit ({MaxSupersteps}) with {RemainingExecutors} executor(s) still queued")]
+ public static partial void LogWorkflowMaxSuperstepsExceeded(
+ this ILogger logger,
+ string instanceId,
+ int maxSupersteps,
+ int remainingExecutors);
+
+ [LoggerMessage(
+ EventId = 105,
+ Level = LogLevel.Debug,
+ Message = "Fan-In executor {ExecutorId}: aggregated {Count} messages from [{Sources}]")]
+ public static partial void LogFanInAggregated(
+ this ILogger logger,
+ string executorId,
+ int count,
+ string sources);
+
+ [LoggerMessage(
+ EventId = 106,
+ Level = LogLevel.Debug,
+ Message = "Executor '{ExecutorId}' returned result (length: {Length}, messages: {MessageCount})")]
+ public static partial void LogExecutorResultReceived(
+ this ILogger logger,
+ string executorId,
+ int length,
+ int messageCount);
+
+ [LoggerMessage(
+ EventId = 107,
+ Level = LogLevel.Debug,
+ Message = "Dispatching executor '{ExecutorId}' (agentic: {IsAgentic})")]
+ public static partial void LogDispatchingExecutor(
+ this ILogger logger,
+ string executorId,
+ bool isAgentic);
+
+ [LoggerMessage(
+ EventId = 108,
+ Level = LogLevel.Warning,
+ Message = "Agent '{AgentName}' not found")]
+ public static partial void LogAgentNotFound(
+ this ILogger logger,
+ string agentName);
+
+ [LoggerMessage(
+ EventId = 109,
+ Level = LogLevel.Debug,
+ Message = "Edge {Source} -> {Sink}: condition returned false, skipping")]
+ public static partial void LogEdgeConditionFalse(
+ this ILogger logger,
+ string source,
+ string sink);
+
+ [LoggerMessage(
+ EventId = 110,
+ Level = LogLevel.Warning,
+ Message = "Failed to evaluate condition for edge {Source} -> {Sink}, skipping")]
+ public static partial void LogEdgeConditionEvaluationFailed(
+ this ILogger logger,
+ Exception ex,
+ string source,
+ string sink);
+
+ [LoggerMessage(
+ EventId = 111,
+ Level = LogLevel.Debug,
+ Message = "Edge {Source} -> {Sink}: routing message")]
+ public static partial void LogEdgeRoutingMessage(
+ this ILogger logger,
+ string source,
+ string sink);
+
+ [LoggerMessage(
+ EventId = 112,
+ Level = LogLevel.Information,
+ Message = "Workflow waiting for external input at RequestPort '{RequestPortId}'")]
+ public static partial void LogWaitingForExternalEvent(
+ this ILogger logger,
+ string requestPortId);
+
+ [LoggerMessage(
+ EventId = 113,
+ Level = LogLevel.Information,
+ Message = "Received external event for RequestPort '{RequestPortId}'")]
+ public static partial void LogReceivedExternalEvent(
+ this ILogger logger,
+ string requestPortId);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj b/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj
index 28046894db..77c877939e 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj
@@ -17,7 +17,6 @@
- true
true
@@ -28,6 +27,7 @@
+
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
index 79d44924ca..456e4ae98d 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
@@ -1,18 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json;
-using System.Text.Json.Serialization.Metadata;
-using Microsoft.Agents.AI.DurableTask.State;
+using Microsoft.Agents.AI.DurableTask.Workflows;
+using Microsoft.Agents.AI.Workflows;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.DurableTask;
///
-/// Agent-specific extension methods for the class.
+/// Extension methods for configuring durable agents and workflows with dependency injection.
///
public static class ServiceCollectionExtensions
{
@@ -30,77 +30,331 @@ public static AIAgent GetDurableAgentProxy(this IServiceProvider services, strin
}
///
- /// Configures the Durable Agents services via the service collection.
+ /// Configures durable agents, automatically registering agent entities.
///
+ ///
+ ///
+ /// This method provides an agent-focused configuration experience.
+ /// If you need to configure both agents and workflows, consider using
+ /// instead.
+ ///
+ ///
+ /// Multiple calls to this method are supported and configurations are composed additively.
+ ///
+ ///
/// The service collection.
/// A delegate to configure the durable agents.
- /// A delegate to configure the Durable Task worker.
- /// A delegate to configure the Durable Task client.
- /// The service collection.
+ /// Optional delegate to configure the Durable Task worker.
+ /// Optional delegate to configure the Durable Task client.
+ /// The service collection for chaining.
public static IServiceCollection ConfigureDurableAgents(
this IServiceCollection services,
Action configure,
Action? workerBuilder = null,
Action? clientBuilder = null)
{
+ return services.ConfigureDurableOptions(
+ options => configure(options.Agents),
+ workerBuilder,
+ clientBuilder);
+ }
+
+ ///
+ /// Configures durable workflows, automatically registering orchestrations and activities.
+ ///
+ ///
+ ///
+ /// This method provides a workflow-focused configuration experience.
+ /// If you need to configure both agents and workflows, consider using
+ /// instead.
+ ///
+ ///
+ /// Multiple calls to this method are supported and configurations are composed additively.
+ ///
+ ///
+ /// The service collection to configure.
+ /// A delegate to configure the workflow options.
+ /// Optional delegate to configure the durable task worker.
+ /// Optional delegate to configure the durable task client.
+ /// The service collection for chaining.
+ public static IServiceCollection ConfigureDurableWorkflows(
+ this IServiceCollection services,
+ Action configure,
+ Action? workerBuilder = null,
+ Action? clientBuilder = null)
+ {
+ return services.ConfigureDurableOptions(
+ options => configure(options.Workflows),
+ workerBuilder,
+ clientBuilder);
+ }
+
+ ///
+ /// Configures durable agents and workflows, automatically registering orchestrations, activities, and agent entities.
+ ///
+ ///
+ ///
+ /// This is the recommended entry point for configuring durable functionality. It provides unified configuration
+ /// for both agents and workflows through a single instance, ensuring agents
+ /// referenced in workflows are automatically registered.
+ ///
+ ///
+ /// Multiple calls to this method (or to
+ /// and ) are supported and configurations are composed additively.
+ ///
+ ///
+ /// The service collection to configure.
+ /// A delegate to configure the durable options for both agents and workflows.
+ /// Optional delegate to configure the durable task worker.
+ /// Optional delegate to configure the durable task client.
+ /// The service collection for chaining.
+ ///
+ ///
+ /// services.ConfigureDurableOptions(options =>
+ /// {
+ /// // Register agents not part of workflows
+ /// options.Agents.AddAIAgent(standaloneAgent);
+ ///
+ /// // Register workflows - agents in workflows are auto-registered
+ /// options.Workflows.AddWorkflow(myWorkflow);
+ /// },
+ /// workerBuilder: builder => builder.UseDurableTaskScheduler(connectionString),
+ /// clientBuilder: builder => builder.UseDurableTaskScheduler(connectionString));
+ ///
+ ///
+ public static IServiceCollection ConfigureDurableOptions(
+ this IServiceCollection services,
+ Action configure,
+ Action? workerBuilder = null,
+ Action? clientBuilder = null)
+ {
+ ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
- DurableAgentsOptions options = services.ConfigureDurableAgents(configure);
+ // Get or create the shared DurableOptions instance for configuration
+ DurableOptions sharedOptions = GetOrCreateSharedOptions(services);
+
+ // Apply the configuration immediately to capture agent names for keyed service registration
+ configure(sharedOptions);
+
+ // Register keyed services for any new agents
+ RegisterAgentKeyedServices(services, sharedOptions);
+
+ // Register core services only once
+ EnsureDurableServicesRegistered(services, sharedOptions, workerBuilder, clientBuilder);
+
+ return services;
+ }
+
+ private static DurableOptions GetOrCreateSharedOptions(IServiceCollection services)
+ {
+ // Look for an existing DurableOptions registration
+ ServiceDescriptor? existingDescriptor = services.FirstOrDefault(
+ d => d.ServiceType == typeof(DurableOptions) && d.ImplementationInstance is not null);
+
+ if (existingDescriptor?.ImplementationInstance is DurableOptions existing)
+ {
+ return existing;
+ }
+
+ // Create a new shared options instance
+ DurableOptions options = new();
+ services.AddSingleton(options);
+ return options;
+ }
+
+ private static void RegisterAgentKeyedServices(IServiceCollection services, DurableOptions options)
+ {
+ foreach (KeyValuePair> factory in options.Agents.GetAgentFactories())
+ {
+ // Only add if not already registered (to support multiple Configure* calls)
+ if (!services.Any(d => d.ServiceType == typeof(AIAgent) && d.IsKeyedService && Equals(d.ServiceKey, factory.Key)))
+ {
+ services.AddKeyedSingleton(factory.Key, (sp, _) => factory.Value(sp).AsDurableAgentProxy(sp));
+ }
+ }
+ }
- // A worker is required to run the agent entities
- services.AddDurableTaskWorker(builder =>
+ ///
+ /// Ensures that the core durable services are registered only once, regardless of how many
+ /// times the configuration methods are called.
+ ///
+ private static void EnsureDurableServicesRegistered(
+ IServiceCollection services,
+ DurableOptions sharedOptions,
+ Action? workerBuilder,
+ Action? clientBuilder)
+ {
+ // Use a marker to ensure we only register core services once
+ if (services.Any(d => d.ServiceType == typeof(DurableServicesMarker)))
{
- workerBuilder?.Invoke(builder);
+ return;
+ }
+
+ services.AddSingleton();
+
+ services.TryAddSingleton();
- builder.AddTasks(registry =>
+ // Configure Durable Task Worker - capture sharedOptions reference in closure.
+ // The options object is populated by all Configure* calls before the worker starts.
+
+ if (workerBuilder is not null)
+ {
+ services.AddDurableTaskWorker(builder =>
{
- foreach (string name in options.GetAgentFactories().Keys)
- {
- registry.AddEntity(AgentSessionId.ToEntityName(name));
- }
+ workerBuilder?.Invoke(builder);
+
+ builder.AddTasks(registry => RegisterTasksFromOptions(registry, sharedOptions));
});
- });
+ }
- // The client is needed to send notifications to the agent entities from non-orchestrator code
- if (clientBuilder != null)
+ // Configure Durable Task Client
+ if (clientBuilder is not null)
{
services.AddDurableTaskClient(clientBuilder);
+ services.TryAddSingleton();
+ services.TryAddSingleton();
}
- services.AddSingleton();
+ // Register workflow and agent services
+ services.TryAddSingleton();
- return services;
+ // Register agent factories resolver - returns factories from the shared options
+ services.TryAddSingleton(
+ sp => sp.GetRequiredService().Agents.GetAgentFactories());
+
+ // Register DurableAgentsOptions resolver
+ services.TryAddSingleton(sp => sp.GetRequiredService().Agents);
}
- // This is internal because it's also used by Microsoft.Azure.Functions.DurableAgents, which is a friend assembly project.
- internal static DurableAgentsOptions ConfigureDurableAgents(
- this IServiceCollection services,
- Action configure)
+ private static void RegisterTasksFromOptions(DurableTaskRegistry registry, DurableOptions durableOptions)
{
- DurableAgentsOptions options = new();
- configure(options);
+ // Build registrations for all workflows including sub-workflows
+ List registrations = [];
+ HashSet registeredActivities = [];
+ HashSet registeredOrchestrations = [];
- IReadOnlyDictionary> agents = options.GetAgentFactories();
+ DurableWorkflowOptions workflowOptions = durableOptions.Workflows;
+ foreach (Workflow workflow in workflowOptions.Workflows.Values.ToList())
+ {
+ BuildWorkflowRegistrationRecursive(
+ workflow,
+ workflowOptions,
+ registrations,
+ registeredActivities,
+ registeredOrchestrations);
+ }
- // The agent dictionary contains the real agent factories, which is used by the agent entities.
- services.AddSingleton(agents);
+ IReadOnlyDictionary> agentFactories =
+ durableOptions.Agents.GetAgentFactories();
- // Register the options so AgentEntity can access TTL configuration
- services.AddSingleton(options);
+ // Register orchestrations and activities
+ foreach (WorkflowRegistrationInfo registration in registrations)
+ {
+ // Register with DurableWorkflowInput