diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 19547875b3..2c5ea815c5 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -52,6 +52,8 @@
+
+
diff --git a/dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj b/dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj
new file mode 100644
index 0000000000..09e20ef622
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj
@@ -0,0 +1,28 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ WorkflowEvents
+ WorkflowEvents
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/Executors.cs b/dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/Executors.cs
new file mode 100644
index 0000000000..47880f0fff
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/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/Durable/Workflow/ConsoleApps/05_WorkflowEvents/Program.cs b/dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/Program.cs
new file mode 100644
index 0000000000..7ca3ae77c1
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/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.SourceId}", 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/Durable/Workflow/ConsoleApps/05_WorkflowEvents/README.md b/dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/README.md
new file mode 100644
index 0000000000..00012c5afb
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/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/azure/azure-functions/durable/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/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj b/dotnet/samples/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj
new file mode 100644
index 0000000000..c7efbb7d1b
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj
@@ -0,0 +1,29 @@
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ WorkflowSharedState
+ WorkflowSharedState
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/Executors.cs b/dotnet/samples/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/Executors.cs
new file mode 100644
index 0000000000..05afbab71d
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/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/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/Program.cs b/dotnet/samples/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/Program.cs
new file mode 100644
index 0000000000..4b46779eb8
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/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.SourceId}: {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/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/README.md b/dotnet/samples/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/README.md
new file mode 100644
index 0000000000..31ff55ce84
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/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/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
index 22cfc06518..29e56ea398 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
@@ -312,11 +312,8 @@ private static WorkflowRegistrationInfo BuildWorkflowRegistration(
Dictionary executorBindings = workflow.ReflectExecutors();
List activities = [];
- // Filter out AI agents and subworkflows - they are not registered as activities.
- // AI agents use Durable Entities for stateful execution, and subworkflows are
- // registered as separate orchestrations via BuildWorkflowRegistrationRecursive.
foreach (KeyValuePair entry in executorBindings
- .Where(e => e.Value is not AIAgentBinding and not SubworkflowBinding))
+ .Where(e => IsActivityBinding(e.Value)))
{
string executorName = WorkflowNamingHelper.GetExecutorName(entry.Key);
string activityName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
@@ -330,6 +327,15 @@ private static WorkflowRegistrationInfo BuildWorkflowRegistration(
return new WorkflowRegistrationInfo(orchestrationName, activities);
}
+ ///
+ /// Returns for bindings that should be registered as Durable Task activities.
+ /// (Durable Entities) and (sub-orchestrations)
+ /// use specialized dispatch and are excluded.
+ ///
+ private static bool IsActivityBinding(ExecutorBinding binding)
+ => binding is not AIAgentBinding
+ and not SubworkflowBinding;
+
private static async Task RunWorkflowOrchestrationAsync(
TaskOrchestrationContext context,
DurableWorkflowInput