diff --git a/.github/skills/durable-functions-dotnet/SKILL.md b/.github/skills/durable-functions-dotnet/SKILL.md
new file mode 100644
index 0000000..0f00b6a
--- /dev/null
+++ b/.github/skills/durable-functions-dotnet/SKILL.md
@@ -0,0 +1,627 @@
+---
+name: durable-functions-dotnet
+description: Build durable, fault-tolerant workflows using Azure Durable Functions with .NET isolated worker and Durable Task Scheduler backend. Use when creating serverless orchestrations, activities, entities, or implementing patterns like function chaining, fan-out/fan-in, async HTTP APIs, human interaction, monitoring, or stateful aggregators. Applies to Azure Functions apps requiring durable execution, state persistence, or distributed coordination with built-in HTTP management APIs and Azure integration.
+---
+
+# Azure Durable Functions (.NET Isolated) with Durable Task Scheduler
+
+Build fault-tolerant, stateful serverless workflows using Azure Durable Functions connected to Azure Durable Task Scheduler.
+
+## Quick Start
+
+### Required NuGet Packages
+
+```xml
+
+
+
+
+
+
+
+
+
+```
+
+### host.json Configuration (Durable Task Scheduler)
+
+```json
+{
+ "version": "2.0",
+ "extensions": {
+ "durableTask": {
+ "storageProvider": {
+ "type": "azureManaged",
+ "connectionStringName": "DTS_CONNECTION_STRING"
+ },
+ "hubName": "%TASKHUB_NAME%"
+ }
+ }
+}
+```
+
+### local.settings.json
+
+```json
+{
+ "IsEncrypted": false,
+ "Values": {
+ "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
+ "AzureWebJobsStorage": "UseDevelopmentStorage=true",
+ "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None",
+ "TASKHUB_NAME": "default"
+ }
+}
+```
+
+### Minimal Example (Function-Based)
+
+```csharp
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.Logging;
+
+public static class DurableFunctionsApp
+{
+ // HTTP Starter - triggers orchestration
+ [Function("HttpStart")]
+ public static async Task HttpStart(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{functionName}")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string functionName,
+ FunctionContext executionContext)
+ {
+ string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(functionName);
+
+ var logger = executionContext.GetLogger("HttpStart");
+ logger.LogInformation("Started orchestration with ID = '{instanceId}'", instanceId);
+
+ return await client.CreateCheckStatusResponseAsync(req, instanceId);
+ }
+
+ // Orchestrator function
+ [Function(nameof(MyOrchestration))]
+ public static async Task MyOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+ {
+ ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration));
+ logger.LogInformation("Starting orchestration");
+
+ var result1 = await context.CallActivityAsync(nameof(SayHello), "Tokyo");
+ var result2 = await context.CallActivityAsync(nameof(SayHello), "Seattle");
+ var result3 = await context.CallActivityAsync(nameof(SayHello), "London");
+
+ return $"{result1}, {result2}, {result3}";
+ }
+
+ // Activity function
+ [Function(nameof(SayHello))]
+ public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext)
+ {
+ var logger = executionContext.GetLogger(nameof(SayHello));
+ logger.LogInformation("Saying hello to {name}", name);
+ return $"Hello {name}!";
+ }
+}
+```
+
+### Program.cs Setup
+
+```csharp
+using Microsoft.Extensions.Hosting;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults()
+ .Build();
+
+await host.RunAsync();
+```
+
+## Pattern Selection Guide
+
+| Pattern | Use When |
+|---------|----------|
+| **Function Chaining** | Sequential steps where each depends on the previous |
+| **Fan-Out/Fan-In** | Parallel processing with aggregated results |
+| **Async HTTP APIs** | Long-running operations with HTTP status polling |
+| **Monitor** | Periodic polling with configurable timeouts |
+| **Human Interaction** | Workflow pauses for external input/approval |
+| **Aggregator (Entities)** | Stateful objects with operations (counters, accounts) |
+
+See [references/patterns.md](references/patterns.md) for detailed implementations.
+
+## Two Approaches: Function-Based vs Class-Based
+
+### Function-Based (Default)
+
+```csharp
+[Function(nameof(MyOrchestration))]
+public static async Task MyOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ string input = context.GetInput()!;
+ return await context.CallActivityAsync(nameof(MyActivity), input);
+}
+
+[Function(nameof(MyActivity))]
+public static string MyActivity([ActivityTrigger] string input)
+{
+ return $"Processed: {input}";
+}
+```
+
+### Class-Based (With Source Generator)
+
+Requires `Microsoft.DurableTask.Generators` package:
+
+```csharp
+[DurableTask(nameof(MyOrchestration))]
+public class MyOrchestration : TaskOrchestrator
+{
+ public override async Task RunAsync(TaskOrchestrationContext context, string input)
+ {
+ ILogger logger = context.CreateReplaySafeLogger();
+ return await context.CallActivityAsync(nameof(MyActivity), input);
+ }
+}
+
+[DurableTask(nameof(MyActivity))]
+public class MyActivity : TaskActivity
+{
+ private readonly ILogger _logger;
+
+ // Activities support DI - orchestrations do NOT
+ public MyActivity(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public override Task RunAsync(TaskActivityContext context, string input)
+ {
+ _logger.LogInformation("Processing: {Input}", input);
+ return Task.FromResult($"Processed: {input}");
+ }
+}
+```
+
+## Critical Rules
+
+### Orchestration Determinism
+
+Orchestrations replay from history - all code MUST be deterministic. When an orchestration resumes, it replays all previous code to rebuild state. Non-deterministic code produces different results on replay, causing `NonDeterministicOrchestrationException`.
+
+**NEVER do inside orchestrations:**
+- `DateTime.Now`, `DateTime.UtcNow` → Use `context.CurrentUtcDateTime`
+- `Guid.NewGuid()` → Use `context.NewGuid()`
+- `Random` → Pass random values from activities
+- Direct I/O, HTTP calls, database access → Move to activities
+- `Thread.Sleep()`, `Task.Delay()` → Use `context.CreateTimer()`
+- Non-deterministic LINQ (parallel, unordered)
+- `Task.Run()`, `ConfigureAwait(false)`
+- Static mutable variables
+- Environment variables that may change → Pass as input or use activities
+
+**ALWAYS safe:**
+- `context.CallActivityAsync()`
+- `context.CallSubOrchestrationAsync()`
+- `context.CallHttpAsync()`
+- `context.CreateTimer()`
+- `context.WaitForExternalEvent()`
+- `context.CurrentUtcDateTime`
+- `context.NewGuid()`
+- `context.SetCustomStatus()`
+- `context.CreateReplaySafeLogger()`
+
+### Non-Determinism Patterns (WRONG vs CORRECT)
+
+#### Getting Current Time
+
+```csharp
+// WRONG - DateTime.UtcNow returns different value on replay
+[Function(nameof(BadOrchestration))]
+public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ DateTime currentTime = DateTime.UtcNow; // Non-deterministic!
+ if (currentTime.Hour < 12)
+ {
+ await context.CallActivityAsync(nameof(MorningActivity), null);
+ }
+}
+
+// CORRECT - context.CurrentUtcDateTime replays consistently
+[Function(nameof(GoodOrchestration))]
+public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ DateTime currentTime = context.CurrentUtcDateTime; // Deterministic
+ if (currentTime.Hour < 12)
+ {
+ await context.CallActivityAsync(nameof(MorningActivity), null);
+ }
+}
+```
+
+#### Generating GUIDs
+
+```csharp
+// WRONG - Guid.NewGuid() generates different value on replay
+[Function(nameof(BadOrchestration))]
+public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ string orderId = Guid.NewGuid().ToString(); // Non-deterministic!
+ await context.CallActivityAsync(nameof(CreateOrder), orderId);
+ return orderId;
+}
+
+// CORRECT - context.NewGuid() replays the same value
+[Function(nameof(GoodOrchestration))]
+public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ string orderId = context.NewGuid().ToString(); // Deterministic
+ await context.CallActivityAsync(nameof(CreateOrder), orderId);
+ return orderId;
+}
+```
+
+#### Random Numbers
+
+```csharp
+// WRONG - Random produces different values on replay
+[Function(nameof(BadOrchestration))]
+public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ int delay = new Random().Next(1, 10); // Non-deterministic!
+ await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(delay), CancellationToken.None);
+}
+
+// CORRECT - generate random in activity, pass to orchestrator
+[Function(nameof(GetRandomDelay))]
+public static int GetRandomDelay([ActivityTrigger] object? input)
+{
+ return new Random().Next(1, 10); // OK in activity
+}
+
+[Function(nameof(GoodOrchestration))]
+public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ int delay = await context.CallActivityAsync(nameof(GetRandomDelay), null);
+ await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(delay), CancellationToken.None);
+}
+```
+
+#### Sleeping/Delays
+
+```csharp
+// WRONG - Thread.Sleep/Task.Delay don't persist and block
+[Function(nameof(BadOrchestration))]
+public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ await context.CallActivityAsync(nameof(Step1), null);
+ await Task.Delay(60000); // Non-durable! Lost on restart, wastes resources
+ await context.CallActivityAsync(nameof(Step2), null);
+}
+
+// CORRECT - context.CreateTimer is durable
+[Function(nameof(GoodOrchestration))]
+public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ await context.CallActivityAsync(nameof(Step1), null);
+ await context.CreateTimer(context.CurrentUtcDateTime.AddMinutes(1), CancellationToken.None); // Durable
+ await context.CallActivityAsync(nameof(Step2), null);
+}
+```
+
+#### HTTP Calls and I/O
+
+```csharp
+// WRONG - HttpClient in orchestrator is non-deterministic
+[Function(nameof(BadOrchestration))]
+public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ using var client = new HttpClient();
+ var response = await client.GetStringAsync("https://api.example.com/data"); // Non-deterministic!
+ return response;
+}
+
+// CORRECT Option 1 - use CallHttpAsync (built-in durable HTTP)
+[Function(nameof(GoodOrchestration1))]
+public static async Task GoodOrchestration1([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ DurableHttpResponse response = await context.CallHttpAsync(
+ HttpMethod.Get, new Uri("https://api.example.com/data")); // Deterministic
+ return response.Content;
+}
+
+// CORRECT Option 2 - move I/O to activity
+[Function(nameof(FetchData))]
+public static async Task FetchData([ActivityTrigger] string url)
+{
+ using var client = new HttpClient();
+ return await client.GetStringAsync(url); // OK in activity
+}
+
+[Function(nameof(GoodOrchestration2))]
+public static async Task GoodOrchestration2([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ return await context.CallActivityAsync(nameof(FetchData), "https://api.example.com/data");
+}
+```
+
+#### Database Access
+
+```csharp
+// WRONG - database query in orchestrator
+[Function(nameof(BadOrchestration))]
+public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ using var conn = new SqlConnection(connectionString); // Non-deterministic!
+ await conn.OpenAsync();
+ // ...
+}
+
+// CORRECT - database access in activity
+[Function(nameof(GetUser))]
+public static async Task GetUser([ActivityTrigger] string userId)
+{
+ using var conn = new SqlConnection(connectionString); // OK in activity
+ await conn.OpenAsync();
+ // ...
+ return user;
+}
+
+[Function(nameof(GoodOrchestration))]
+public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ string userId = context.GetInput()!;
+ return await context.CallActivityAsync(nameof(GetUser), userId);
+}
+```
+
+#### Environment Variables
+
+```csharp
+// WRONG - env var might change between replays
+[Function(nameof(BadOrchestration))]
+public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ string apiEndpoint = Environment.GetEnvironmentVariable("API_ENDPOINT")!; // Could change!
+ await context.CallActivityAsync(nameof(CallApi), apiEndpoint);
+}
+
+// CORRECT - pass config as input
+[Function(nameof(GoodOrchestration))]
+public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var config = context.GetInput()!;
+ string apiEndpoint = config.ApiEndpoint; // From input, deterministic
+ await context.CallActivityAsync(nameof(CallApi), apiEndpoint);
+}
+
+// ALSO CORRECT - read env var in activity
+[Function(nameof(CallApi))]
+public static async Task CallApi([ActivityTrigger] object? input)
+{
+ string apiEndpoint = Environment.GetEnvironmentVariable("API_ENDPOINT")!; // OK in activity
+ // make the call...
+}
+```
+
+#### Collection Iteration Order
+
+```csharp
+// WRONG - Dictionary iteration order may vary
+[Function(nameof(BadOrchestration))]
+public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var items = context.GetInput>()!;
+ foreach (var key in items.Keys) // Order not guaranteed!
+ {
+ await context.CallActivityAsync(nameof(Process), key);
+ }
+}
+
+// CORRECT - use sorted keys for deterministic order
+[Function(nameof(GoodOrchestration))]
+public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var items = context.GetInput>()!;
+ foreach (var key in items.Keys.OrderBy(k => k)) // Guaranteed order
+ {
+ await context.CallActivityAsync(nameof(Process), key);
+ }
+}
+```
+
+### Logging in Orchestrations
+
+Use `CreateReplaySafeLogger` to avoid duplicate log entries during replay:
+
+```csharp
+[Function(nameof(MyOrchestration))]
+public static async Task MyOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration));
+ logger.LogInformation("Orchestration started"); // Only logs once, not on each replay
+
+ var result = await context.CallActivityAsync(nameof(MyActivity), "input");
+
+ logger.LogInformation("Activity completed with result: {Result}", result);
+ return result;
+}
+```
+
+### Error Handling
+
+```csharp
+[Function(nameof(OrchestrationWithErrorHandling))]
+public static async Task OrchestrationWithErrorHandling(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ string input = context.GetInput()!;
+ try
+ {
+ return await context.CallActivityAsync(nameof(RiskyActivity), input);
+ }
+ catch (TaskFailedException ex)
+ {
+ // Activity failed - implement compensation
+ context.SetCustomStatus(new { Error = ex.Message });
+ return await context.CallActivityAsync(nameof(CompensationActivity), input);
+ }
+}
+```
+
+### Retry Policies
+
+```csharp
+var options = new TaskOptions
+{
+ Retry = new RetryPolicy(
+ maxNumberOfAttempts: 3,
+ firstRetryInterval: TimeSpan.FromSeconds(5),
+ backoffCoefficient: 2.0,
+ maxRetryInterval: TimeSpan.FromMinutes(1))
+};
+
+await context.CallActivityAsync(nameof(UnreliableActivity), input, options);
+```
+
+## HTTP Management APIs
+
+Durable Functions exposes built-in HTTP APIs for orchestration management:
+
+### CreateCheckStatusResponse
+
+```csharp
+[Function("HttpStart")]
+public static async Task HttpStart(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{functionName}")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string functionName)
+{
+ // Parse input from request body
+ string? input = await new StreamReader(req.Body).ReadToEndAsync();
+
+ string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(functionName, input);
+
+ // Returns 202 Accepted with management URLs in response
+ return await client.CreateCheckStatusResponseAsync(req, instanceId);
+}
+```
+
+Response includes:
+- `statusQueryGetUri` - GET endpoint to check status
+- `sendEventPostUri` - POST endpoint to raise events
+- `terminatePostUri` - POST endpoint to terminate
+- `purgeHistoryDeleteUri` - DELETE endpoint to purge history
+
+### Client Operations
+
+```csharp
+[DurableClient] DurableTaskClient client
+
+// Schedule new orchestration
+string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input);
+
+// Schedule with custom instance ID
+string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
+ "MyOrchestration", input, new StartOrchestrationOptions { InstanceId = "my-custom-id" });
+
+// Get status
+OrchestrationMetadata? state = await client.GetInstanceAsync(instanceId, getInputsAndOutputs: true);
+
+// Wait for completion
+OrchestrationMetadata? result = await client.WaitForInstanceCompletionAsync(
+ instanceId, getInputsAndOutputs: true, cancellationToken);
+
+// Raise external event
+await client.RaiseEventAsync(instanceId, "ApprovalEvent", approvalData);
+
+// Terminate
+await client.TerminateInstanceAsync(instanceId, "User cancelled");
+
+// Suspend/Resume
+await client.SuspendInstanceAsync(instanceId, "Pausing for maintenance");
+await client.ResumeInstanceAsync(instanceId, "Resuming operation");
+
+// Purge history
+await client.PurgeInstanceAsync(instanceId);
+```
+
+## Connection & Authentication
+
+### Connection String Formats
+
+```csharp
+// Local emulator (no auth)
+"Endpoint=http://localhost:8080;Authentication=None"
+
+// Azure with Managed Identity (recommended for production)
+"Endpoint=https://my-scheduler.region.durabletask.io;Authentication=ManagedIdentity"
+
+// Azure with specific client ID (user-assigned managed identity)
+"Endpoint=https://my-scheduler.region.durabletask.io;Authentication=ManagedIdentity;ClientId="
+```
+
+Note: Durable Task Scheduler supports identity-based authentication only - no connection strings with keys.
+
+## Local Development with Emulator
+
+```bash
+# Start Azurite (required for Azure Functions)
+azurite start
+
+# Pull and run the Durable Task Scheduler emulator
+docker pull mcr.microsoft.com/dts/dts-emulator:latest
+docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
+
+# Dashboard available at http://localhost:8082
+
+# Start the function app
+func start
+```
+
+## Durable HTTP Calls
+
+Make HTTP calls directly from orchestrations (persisted and replay-safe):
+
+```csharp
+[Function(nameof(CallExternalApi))]
+public static async Task CallExternalApi([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ // Simple GET
+ DurableHttpResponse response = await context.CallHttpAsync(HttpMethod.Get, new Uri("https://api.example.com/data"));
+
+ if (response.StatusCode != HttpStatusCode.OK)
+ {
+ throw new Exception($"API call failed: {response.StatusCode}");
+ }
+
+ return response.Content;
+}
+
+// With headers and body
+var request = new DurableHttpRequest(
+ HttpMethod.Post,
+ new Uri("https://api.example.com/data"))
+{
+ Headers = { ["Content-Type"] = "application/json" },
+ Content = JsonSerializer.Serialize(payload)
+};
+
+DurableHttpResponse response = await context.CallHttpAsync(request);
+
+// With managed identity authentication
+var request = new DurableHttpRequest(
+ HttpMethod.Get,
+ new Uri("https://management.azure.com/..."))
+{
+ TokenSource = new ManagedIdentityTokenSource("https://management.azure.com/.default")
+};
+```
+
+## References
+
+- **[patterns.md](references/patterns.md)** - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Entities, Sub-Orchestrations, Monitor)
+- **[setup.md](references/setup.md)** - Azure Durable Task Scheduler provisioning, deployment, and project templates
diff --git a/.github/skills/durable-functions-dotnet/references/patterns.md b/.github/skills/durable-functions-dotnet/references/patterns.md
new file mode 100644
index 0000000..697bcf9
--- /dev/null
+++ b/.github/skills/durable-functions-dotnet/references/patterns.md
@@ -0,0 +1,888 @@
+# Durable Functions Patterns Reference
+
+Complete implementations for common Durable Functions workflow patterns.
+
+## Function Chaining
+
+Sequential workflow where each step depends on the previous result:
+
+```csharp
+[Function(nameof(ProcessOrderOrchestration))]
+public static async Task ProcessOrderOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var order = context.GetInput()!;
+ ILogger logger = context.CreateReplaySafeLogger(nameof(ProcessOrderOrchestration));
+
+ logger.LogInformation("Processing order {OrderId}", order.Id);
+
+ // Step 1: Validate order
+ var validation = await context.CallActivityAsync(nameof(ValidateOrder), order);
+ if (!validation.IsValid)
+ {
+ return new OrderResult { Success = false, Error = validation.Error };
+ }
+
+ // Step 2: Reserve inventory
+ var reservation = await context.CallActivityAsync(nameof(ReserveInventory), order);
+
+ // Step 3: Process payment
+ var payment = await context.CallActivityAsync(nameof(ProcessPayment),
+ new PaymentRequest { Order = order, ReservationId = reservation.Id });
+
+ // Step 4: Ship order
+ var shipment = await context.CallActivityAsync(nameof(ShipOrder),
+ new ShipmentRequest { Order = order, PaymentId = payment.TransactionId });
+
+ // Step 5: Notify customer
+ await context.CallActivityAsync(nameof(NotifyCustomer),
+ new Notification { OrderId = order.Id, TrackingNumber = shipment.TrackingNumber });
+
+ return new OrderResult { Success = true, TrackingNumber = shipment.TrackingNumber };
+}
+
+[Function(nameof(ValidateOrder))]
+public static ValidationResult ValidateOrder([ActivityTrigger] Order order) => /* ... */;
+
+[Function(nameof(ReserveInventory))]
+public static async Task ReserveInventory([ActivityTrigger] Order order) => /* ... */;
+
+[Function(nameof(ProcessPayment))]
+public static async Task ProcessPayment([ActivityTrigger] PaymentRequest request) => /* ... */;
+
+[Function(nameof(ShipOrder))]
+public static async Task ShipOrder([ActivityTrigger] ShipmentRequest request) => /* ... */;
+
+[Function(nameof(NotifyCustomer))]
+public static async Task NotifyCustomer([ActivityTrigger] Notification notification) => /* ... */;
+```
+
+## Fan-Out/Fan-In
+
+Process items in parallel and aggregate results:
+
+```csharp
+[Function(nameof(ParallelProcessingOrchestration))]
+public static async Task ParallelProcessingOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var items = context.GetInput>()!;
+ ILogger logger = context.CreateReplaySafeLogger(nameof(ParallelProcessingOrchestration));
+
+ logger.LogInformation("Processing {Count} items in parallel", items.Count);
+
+ // Fan-out: Create tasks for all items
+ var tasks = items.Select(item =>
+ context.CallActivityAsync(nameof(ProcessItem), item));
+
+ // Fan-in: Wait for all to complete
+ ItemResult[] results = await Task.WhenAll(tasks);
+
+ // Aggregate results
+ return new BatchResult
+ {
+ TotalProcessed = results.Length,
+ Successful = results.Count(r => r.Success),
+ Failed = results.Count(r => !r.Success)
+ };
+}
+
+[Function(nameof(ProcessItem))]
+public static async Task ProcessItem([ActivityTrigger] WorkItem item)
+{
+ try
+ {
+ // Process the item
+ return new ItemResult { ItemId = item.Id, Success = true };
+ }
+ catch (Exception ex)
+ {
+ return new ItemResult { ItemId = item.Id, Success = false, Error = ex.Message };
+ }
+}
+```
+
+### Fan-Out with Batching (Large Scale)
+
+For very large workloads, process in batches to avoid memory issues:
+
+```csharp
+[Function(nameof(BatchedFanOutOrchestration))]
+public static async Task BatchedFanOutOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var input = context.GetInput()!;
+ ILogger logger = context.CreateReplaySafeLogger(nameof(BatchedFanOutOrchestration));
+
+ const int batchSize = 100;
+ var allResults = new List();
+
+ // Process in batches
+ for (int i = 0; i < input.Items.Count; i += batchSize)
+ {
+ var batch = input.Items.Skip(i).Take(batchSize).ToList();
+ logger.LogInformation("Processing batch {BatchNumber} ({Count} items)",
+ i / batchSize + 1, batch.Count);
+
+ var batchTasks = batch.Select(item =>
+ context.CallActivityAsync(nameof(ProcessItem), item));
+
+ var batchResults = await Task.WhenAll(batchTasks);
+ allResults.AddRange(batchResults);
+ }
+
+ return new BatchResult
+ {
+ TotalProcessed = allResults.Count,
+ Successful = allResults.Count(r => r.Success),
+ Failed = allResults.Count(r => !r.Success)
+ };
+}
+```
+
+### Fan-Out with Partial Failure Handling
+
+```csharp
+[Function(nameof(ResilientFanOutOrchestration))]
+public static async Task ResilientFanOutOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var items = context.GetInput>()!;
+ var results = new List();
+ var errors = new List();
+
+ // Create tasks for all items
+ var taskItemPairs = items.Select(item => new
+ {
+ Task = context.CallActivityAsync(nameof(ProcessItem), item),
+ Item = item
+ }).ToList();
+
+ // Wait for all tasks, catching individual failures
+ foreach (var pair in taskItemPairs)
+ {
+ try
+ {
+ var result = await pair.Task;
+ results.Add(result);
+ }
+ catch (TaskFailedException ex)
+ {
+ errors.Add($"Item {pair.Item.Id} failed: {ex.Message}");
+ results.Add(new ItemResult { ItemId = pair.Item.Id, Success = false, Error = ex.Message });
+ }
+ }
+
+ return new ProcessingResult
+ {
+ Results = results,
+ Errors = errors,
+ AllSuccessful = errors.Count == 0
+ };
+}
+```
+
+## Human Interaction (Approval Workflow)
+
+Wait for external input with timeout:
+
+```csharp
+[Function(nameof(ApprovalOrchestration))]
+public static async Task ApprovalOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var request = context.GetInput()!;
+ ILogger logger = context.CreateReplaySafeLogger(nameof(ApprovalOrchestration));
+
+ // Notify approver
+ await context.CallActivityAsync(nameof(SendApprovalRequest), new ApprovalNotification
+ {
+ RequestId = context.InstanceId,
+ Requester = request.Requester,
+ Amount = request.Amount,
+ Approver = request.Approver
+ });
+
+ logger.LogInformation("Waiting for approval from {Approver}", request.Approver);
+
+ // Wait for approval event with 72-hour timeout
+ using var cts = new CancellationTokenSource();
+ var approvalTask = context.WaitForExternalEvent("ApprovalResponse");
+ var timeoutTask = context.CreateTimer(context.CurrentUtcDateTime.AddHours(72), cts.Token);
+
+ var winner = await Task.WhenAny(approvalTask, timeoutTask);
+
+ if (winner == approvalTask)
+ {
+ cts.Cancel(); // Cancel the timer
+ var response = await approvalTask;
+
+ if (response.Approved)
+ {
+ await context.CallActivityAsync(nameof(ExecuteApprovedAction), request);
+ return new ApprovalResult { Status = "Approved", ApprovedBy = response.ApproverName };
+ }
+ else
+ {
+ return new ApprovalResult { Status = "Rejected", Reason = response.RejectionReason };
+ }
+ }
+ else
+ {
+ // Timeout - escalate
+ logger.LogWarning("Approval timed out, escalating");
+ await context.CallActivityAsync(nameof(EscalateApproval), request);
+ return new ApprovalResult { Status = "Escalated", Reason = "Approval timeout" };
+ }
+}
+
+// HTTP endpoint to submit approval
+[Function("SubmitApproval")]
+public static async Task SubmitApproval(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "approval/{instanceId}")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string instanceId)
+{
+ var response = await req.ReadFromJsonAsync();
+
+ await client.RaiseEventAsync(instanceId, "ApprovalResponse", response);
+
+ var httpResponse = req.CreateResponse(HttpStatusCode.Accepted);
+ await httpResponse.WriteAsJsonAsync(new { Message = "Approval submitted" });
+ return httpResponse;
+}
+
+[Function(nameof(SendApprovalRequest))]
+public static async Task SendApprovalRequest([ActivityTrigger] ApprovalNotification notification)
+{
+ // Send email/notification to approver
+}
+
+[Function(nameof(ExecuteApprovedAction))]
+public static async Task ExecuteApprovedAction([ActivityTrigger] ApprovalRequest request)
+{
+ // Execute the approved action
+}
+
+[Function(nameof(EscalateApproval))]
+public static async Task EscalateApproval([ActivityTrigger] ApprovalRequest request)
+{
+ // Escalate to manager
+}
+```
+
+### Multi-Level Approval
+
+```csharp
+[Function(nameof(MultiLevelApprovalOrchestration))]
+public static async Task MultiLevelApprovalOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var request = context.GetInput()!;
+
+ foreach (var approver in request.ApprovalChain)
+ {
+ // Request approval at this level
+ await context.CallActivityAsync(nameof(SendApprovalRequest), new ApprovalNotification
+ {
+ RequestId = context.InstanceId,
+ Approver = approver.Email,
+ Level = approver.Level
+ });
+
+ // Wait for this level's approval
+ using var cts = new CancellationTokenSource();
+ var approvalTask = context.WaitForExternalEvent($"Approval_{approver.Level}");
+ var timeoutTask = context.CreateTimer(context.CurrentUtcDateTime.AddHours(24), cts.Token);
+
+ var winner = await Task.WhenAny(approvalTask, timeoutTask);
+
+ if (winner == timeoutTask)
+ {
+ return new ApprovalResult { Status = "TimedOut", Level = approver.Level };
+ }
+
+ cts.Cancel();
+ var response = await approvalTask;
+
+ if (!response.Approved)
+ {
+ return new ApprovalResult
+ {
+ Status = "Rejected",
+ Level = approver.Level,
+ Reason = response.RejectionReason
+ };
+ }
+ }
+
+ // All levels approved
+ await context.CallActivityAsync(nameof(ExecuteApprovedAction), request);
+ return new ApprovalResult { Status = "FullyApproved" };
+}
+```
+
+## Monitor Pattern
+
+Periodic polling with configurable timeout and exponential backoff:
+
+```csharp
+[Function(nameof(ResourceMonitorOrchestration))]
+public static async Task ResourceMonitorOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var config = context.GetInput()!;
+ ILogger logger = context.CreateReplaySafeLogger(nameof(ResourceMonitorOrchestration));
+
+ DateTime deadline = context.CurrentUtcDateTime.Add(config.MaxDuration);
+ TimeSpan pollingInterval = config.InitialPollingInterval;
+ int checkCount = 0;
+
+ while (context.CurrentUtcDateTime < deadline)
+ {
+ checkCount++;
+ logger.LogInformation("Check #{Count}: Polling resource {ResourceId}", checkCount, config.ResourceId);
+
+ var status = await context.CallActivityAsync(
+ nameof(CheckResourceStatus), config.ResourceId);
+
+ if (status.IsReady)
+ {
+ logger.LogInformation("Resource {ResourceId} is ready after {Count} checks",
+ config.ResourceId, checkCount);
+ return new MonitorResult
+ {
+ Success = true,
+ Status = status,
+ CheckCount = checkCount
+ };
+ }
+
+ if (status.IsFailed)
+ {
+ logger.LogError("Resource {ResourceId} failed", config.ResourceId);
+ return new MonitorResult
+ {
+ Success = false,
+ Error = "Resource provisioning failed",
+ Status = status
+ };
+ }
+
+ // Wait before next check (exponential backoff)
+ var nextCheck = context.CurrentUtcDateTime.Add(pollingInterval);
+ if (nextCheck >= deadline)
+ {
+ break; // Don't wait if we'll exceed deadline
+ }
+
+ await context.CreateTimer(nextCheck, CancellationToken.None);
+
+ // Exponential backoff with cap
+ pollingInterval = TimeSpan.FromSeconds(
+ Math.Min(pollingInterval.TotalSeconds * config.BackoffMultiplier,
+ config.MaxPollingInterval.TotalSeconds));
+ }
+
+ // Timeout
+ return new MonitorResult
+ {
+ Success = false,
+ Error = "Monitoring timeout exceeded",
+ CheckCount = checkCount
+ };
+}
+
+[Function(nameof(CheckResourceStatus))]
+public static async Task CheckResourceStatus([ActivityTrigger] string resourceId)
+{
+ // Check resource status via API
+ return new ResourceStatus { /* ... */ };
+}
+
+public record MonitorConfig
+{
+ public string ResourceId { get; init; } = "";
+ public TimeSpan MaxDuration { get; init; } = TimeSpan.FromHours(2);
+ public TimeSpan InitialPollingInterval { get; init; } = TimeSpan.FromSeconds(10);
+ public TimeSpan MaxPollingInterval { get; init; } = TimeSpan.FromMinutes(5);
+ public double BackoffMultiplier { get; init; } = 1.5;
+}
+```
+
+## Durable Entities (Aggregator Pattern)
+
+Stateful objects that maintain state across operations:
+
+### Counter Entity
+
+```csharp
+// Entity function
+[Function(nameof(Counter))]
+public static Task Counter([EntityTrigger] TaskEntityDispatcher dispatcher)
+ => dispatcher.DispatchAsync();
+
+// Entity class
+public class CounterEntity
+{
+ public int Value { get; set; }
+
+ public void Add(int amount) => Value += amount;
+ public void Subtract(int amount) => Value -= amount;
+ public void Reset() => Value = 0;
+ public int Get() => Value;
+}
+
+// Using entity from orchestration
+[Function(nameof(CounterOrchestration))]
+public static async Task CounterOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
+
+ // Signal (fire-and-forget)
+ await context.Entities.SignalEntityAsync(entityId, "Add", 5);
+ await context.Entities.SignalEntityAsync(entityId, "Add", 10);
+
+ // Call and get result
+ int value = await context.Entities.CallEntityAsync(entityId, "Get");
+
+ return value; // Returns 15
+}
+
+// HTTP endpoint to interact with entity
+[Function("CounterOperation")]
+public static async Task CounterOperation(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "counter/{entityKey}/{operation}")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string entityKey,
+ string operation)
+{
+ var entityId = new EntityInstanceId(nameof(Counter), entityKey);
+ var amount = await req.ReadFromJsonAsync();
+
+ // Signal the entity
+ await client.Entities.SignalEntityAsync(entityId, operation, amount);
+
+ var response = req.CreateResponse(HttpStatusCode.Accepted);
+ return response;
+}
+
+[Function("GetCounter")]
+public static async Task GetCounter(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "counter/{entityKey}")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string entityKey)
+{
+ var entityId = new EntityInstanceId(nameof(Counter), entityKey);
+ var state = await client.Entities.GetEntityAsync(entityId);
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ await response.WriteAsJsonAsync(state?.State ?? new CounterEntity());
+ return response;
+}
+```
+
+### Account Entity with Locking
+
+```csharp
+[Function(nameof(BankAccount))]
+public static Task BankAccount([EntityTrigger] TaskEntityDispatcher dispatcher)
+ => dispatcher.DispatchAsync();
+
+public class BankAccountEntity
+{
+ public decimal Balance { get; set; }
+ public List History { get; set; } = new();
+
+ public bool Deposit(decimal amount)
+ {
+ if (amount <= 0) return false;
+
+ Balance += amount;
+ History.Add(new Transaction { Type = "Deposit", Amount = amount, Timestamp = DateTime.UtcNow });
+ return true;
+ }
+
+ public bool Withdraw(decimal amount)
+ {
+ if (amount <= 0 || Balance < amount) return false;
+
+ Balance -= amount;
+ History.Add(new Transaction { Type = "Withdrawal", Amount = amount, Timestamp = DateTime.UtcNow });
+ return true;
+ }
+
+ public decimal GetBalance() => Balance;
+ public List GetHistory() => History;
+}
+
+// Transfer between accounts (uses locking)
+[Function(nameof(TransferOrchestration))]
+public static async Task TransferOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var request = context.GetInput()!;
+
+ var fromEntity = new EntityInstanceId(nameof(BankAccount), request.FromAccount);
+ var toEntity = new EntityInstanceId(nameof(BankAccount), request.ToAccount);
+
+ // Lock both accounts in a consistent order to prevent deadlocks
+ var entities = new[] { fromEntity.ToString(), toEntity.ToString() }.OrderBy(x => x).ToArray();
+
+ using (await context.Entities.LockEntitiesAsync(
+ entities.Select(e => EntityInstanceId.Parse(e)).ToArray()))
+ {
+ // Check balance
+ decimal balance = await context.Entities.CallEntityAsync(fromEntity, "GetBalance");
+ if (balance < request.Amount)
+ {
+ return new TransferResult { Success = false, Error = "Insufficient funds" };
+ }
+
+ // Perform transfer
+ await context.Entities.CallEntityAsync(fromEntity, "Withdraw", request.Amount);
+ await context.Entities.CallEntityAsync(toEntity, "Deposit", request.Amount);
+
+ return new TransferResult { Success = true };
+ }
+}
+```
+
+## Sub-Orchestrations
+
+Compose workflows by calling other orchestrations:
+
+```csharp
+[Function(nameof(OrderFulfillmentOrchestration))]
+public static async Task OrderFulfillmentOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var order = context.GetInput()!;
+ ILogger logger = context.CreateReplaySafeLogger(nameof(OrderFulfillmentOrchestration));
+
+ logger.LogInformation("Fulfilling order {OrderId}", order.Id);
+
+ // Call sub-orchestration for payment processing
+ var paymentResult = await context.CallSubOrchestrationAsync(
+ nameof(PaymentOrchestration),
+ new PaymentRequest { OrderId = order.Id, Amount = order.Total });
+
+ if (!paymentResult.Success)
+ {
+ return new FulfillmentResult { Success = false, Error = "Payment failed" };
+ }
+
+ // Call sub-orchestration for shipping (with custom instance ID)
+ var shipmentResult = await context.CallSubOrchestrationAsync(
+ nameof(ShippingOrchestration),
+ new ShipmentRequest { OrderId = order.Id, Items = order.Items },
+ new SubOrchestrationOptions { InstanceId = $"ship-{order.Id}" });
+
+ return new FulfillmentResult
+ {
+ Success = true,
+ PaymentId = paymentResult.TransactionId,
+ TrackingNumber = shipmentResult.TrackingNumber
+ };
+}
+
+[Function(nameof(PaymentOrchestration))]
+public static async Task PaymentOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var request = context.GetInput()!;
+
+ var authResult = await context.CallActivityAsync(nameof(AuthorizePayment), request);
+ if (!authResult.Authorized)
+ {
+ return new PaymentResult { Success = false, Error = authResult.DeclineReason };
+ }
+
+ var captureResult = await context.CallActivityAsync(nameof(CapturePayment), authResult.AuthorizationId);
+
+ return new PaymentResult { Success = true, TransactionId = captureResult.TransactionId };
+}
+
+[Function(nameof(ShippingOrchestration))]
+public static async Task ShippingOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var request = context.GetInput()!;
+
+ var label = await context.CallActivityAsync(nameof(CreateShippingLabel), request);
+ await context.CallActivityAsync(nameof(NotifyWarehouse), label);
+
+ return new ShipmentResult { TrackingNumber = label.TrackingNumber };
+}
+```
+
+## Saga Pattern (Distributed Transactions)
+
+Implement compensating transactions for handling partial failures:
+
+```csharp
+[Function(nameof(BookTravelOrchestration))]
+public static async Task BookTravelOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var request = context.GetInput()!;
+ ILogger logger = context.CreateReplaySafeLogger(nameof(BookTravelOrchestration));
+
+ var completedSteps = new Stack();
+
+ try
+ {
+ // Step 1: Book flight
+ var flight = await context.CallActivityAsync(nameof(BookFlight), request.Flight);
+ completedSteps.Push("flight");
+ logger.LogInformation("Flight booked: {ConfirmationNumber}", flight.ConfirmationNumber);
+
+ // Step 2: Book hotel
+ var hotel = await context.CallActivityAsync(nameof(BookHotel), request.Hotel);
+ completedSteps.Push("hotel");
+ logger.LogInformation("Hotel booked: {ConfirmationNumber}", hotel.ConfirmationNumber);
+
+ // Step 3: Book car
+ var car = await context.CallActivityAsync(nameof(BookCar), request.Car);
+ completedSteps.Push("car");
+ logger.LogInformation("Car booked: {ConfirmationNumber}", car.ConfirmationNumber);
+
+ return new TravelBookingResult
+ {
+ Success = true,
+ FlightConfirmation = flight.ConfirmationNumber,
+ HotelConfirmation = hotel.ConfirmationNumber,
+ CarConfirmation = car.ConfirmationNumber
+ };
+ }
+ catch (TaskFailedException ex)
+ {
+ logger.LogError(ex, "Booking failed, initiating compensation");
+
+ // Compensate in reverse order
+ var compensationErrors = new List();
+
+ while (completedSteps.Count > 0)
+ {
+ var step = completedSteps.Pop();
+ try
+ {
+ switch (step)
+ {
+ case "car":
+ await context.CallActivityAsync(nameof(CancelCar), request.Car);
+ break;
+ case "hotel":
+ await context.CallActivityAsync(nameof(CancelHotel), request.Hotel);
+ break;
+ case "flight":
+ await context.CallActivityAsync(nameof(CancelFlight), request.Flight);
+ break;
+ }
+ logger.LogInformation("Compensated: {Step}", step);
+ }
+ catch (TaskFailedException compEx)
+ {
+ compensationErrors.Add($"Failed to compensate {step}: {compEx.Message}");
+ }
+ }
+
+ return new TravelBookingResult
+ {
+ Success = false,
+ Error = ex.Message,
+ CompensationErrors = compensationErrors
+ };
+ }
+}
+
+// Activity functions
+[Function(nameof(BookFlight))]
+public static async Task BookFlight([ActivityTrigger] FlightRequest request) => /* ... */;
+
+[Function(nameof(BookHotel))]
+public static async Task BookHotel([ActivityTrigger] HotelRequest request) => /* ... */;
+
+[Function(nameof(BookCar))]
+public static async Task BookCar([ActivityTrigger] CarRequest request) => /* ... */;
+
+[Function(nameof(CancelFlight))]
+public static async Task CancelFlight([ActivityTrigger] FlightRequest request) => /* ... */;
+
+[Function(nameof(CancelHotel))]
+public static async Task CancelHotel([ActivityTrigger] HotelRequest request) => /* ... */;
+
+[Function(nameof(CancelCar))]
+public static async Task CancelCar([ActivityTrigger] CarRequest request) => /* ... */;
+```
+
+## Eternal Orchestration (Continue-As-New)
+
+Long-running workflows that periodically restart to manage history size:
+
+```csharp
+[Function(nameof(EternalProcessorOrchestration))]
+public static async Task EternalProcessorOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var state = context.GetInput() ?? new ProcessorState();
+ ILogger logger = context.CreateReplaySafeLogger(nameof(EternalProcessorOrchestration));
+
+ // Check for external stop signal
+ bool stopRequested = false;
+ try
+ {
+ stopRequested = await context.WaitForExternalEvent(
+ "StopRequested",
+ TimeSpan.Zero); // Non-blocking check
+ }
+ catch (TaskCanceledException)
+ {
+ stopRequested = false;
+ }
+
+ if (stopRequested)
+ {
+ logger.LogInformation("Stop requested, exiting eternal orchestration");
+ return;
+ }
+
+ // Do work
+ logger.LogInformation("Processing iteration {Iteration}", state.IterationCount);
+
+ var newItems = await context.CallActivityAsync>(nameof(GetNewItems), state.LastProcessedId);
+
+ if (newItems.Any())
+ {
+ var tasks = newItems.Select(item =>
+ context.CallActivityAsync(nameof(ProcessItem), item));
+ await Task.WhenAll(tasks);
+
+ state.LastProcessedId = newItems.Max(i => i.Id);
+ state.TotalProcessed += newItems.Count;
+ }
+
+ state.IterationCount++;
+
+ // Wait before next iteration
+ await context.CreateTimer(context.CurrentUtcDateTime.AddMinutes(1), CancellationToken.None);
+
+ // Continue-as-new to prevent unbounded history growth
+ context.ContinueAsNew(state);
+}
+
+public class ProcessorState
+{
+ public int IterationCount { get; set; }
+ public long LastProcessedId { get; set; }
+ public long TotalProcessed { get; set; }
+}
+
+// HTTP endpoint to stop the eternal orchestration
+[Function("StopEternalOrchestration")]
+public static async Task StopEternalOrchestration(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "eternal/{instanceId}/stop")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string instanceId)
+{
+ await client.RaiseEventAsync(instanceId, "StopRequested", true);
+ return req.CreateResponse(HttpStatusCode.Accepted);
+}
+```
+
+## Scheduled/Timer-Based Workflows
+
+### Delayed Execution
+
+```csharp
+[Function(nameof(ScheduledReminderOrchestration))]
+public static async Task ScheduledReminderOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var reminder = context.GetInput()!;
+
+ // Wait until scheduled time
+ await context.CreateTimer(reminder.ScheduledTime, CancellationToken.None);
+
+ // Send the reminder
+ await context.CallActivityAsync(nameof(SendReminder), reminder);
+}
+```
+
+### Recurring Execution with Cancellation
+
+```csharp
+[Function(nameof(RecurringJobOrchestration))]
+public static async Task RecurringJobOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var config = context.GetInput()!;
+ ILogger logger = context.CreateReplaySafeLogger(nameof(RecurringJobOrchestration));
+
+ DateTime endTime = context.CurrentUtcDateTime.Add(config.TotalDuration);
+
+ while (context.CurrentUtcDateTime < endTime)
+ {
+ logger.LogInformation("Executing scheduled job");
+
+ // Execute job
+ await context.CallActivityAsync(nameof(ExecuteJob), config.JobParameters);
+
+ // Wait for cancel OR next interval
+ using var cts = new CancellationTokenSource();
+ var cancelTask = context.WaitForExternalEvent("Cancel");
+ var timerTask = context.CreateTimer(
+ context.CurrentUtcDateTime.Add(config.Interval),
+ cts.Token);
+
+ var winner = await Task.WhenAny(cancelTask, timerTask);
+ if (winner == cancelTask && await cancelTask)
+ {
+ logger.LogInformation("Job cancelled");
+ return;
+ }
+
+ cts.Cancel();
+ }
+
+ logger.LogInformation("Recurring job completed");
+}
+```
+
+## Version-Aware Orchestrations
+
+Handle orchestration versioning with running instances:
+
+```csharp
+[Function(nameof(VersionedOrchestration))]
+public static async Task VersionedOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ var input = context.GetInput()!;
+ const int CurrentVersion = 2;
+
+ // Handle different versions
+ if (input.Version < CurrentVersion)
+ {
+ // Legacy behavior for in-flight instances
+ return await LegacyWorkflow(context, input);
+ }
+
+ // Current version behavior
+ return await CurrentWorkflow(context, input);
+}
+
+private static async Task LegacyWorkflow(TaskOrchestrationContext context, VersionedInput input)
+{
+ // Original implementation
+ var result = await context.CallActivityAsync(nameof(OldActivity), input.Data);
+ return result;
+}
+
+private static async Task CurrentWorkflow(TaskOrchestrationContext context, VersionedInput input)
+{
+ // New improved implementation
+ var step1 = await context.CallActivityAsync(nameof(NewActivityStep1), input.Data);
+ var step2 = await context.CallActivityAsync(nameof(NewActivityStep2), step1);
+ return step2;
+}
+
+public record VersionedInput
+{
+ public int Version { get; init; } = 2; // Default to current version
+ public string Data { get; init; } = "";
+}
+```
diff --git a/.github/skills/durable-functions-dotnet/references/setup.md b/.github/skills/durable-functions-dotnet/references/setup.md
new file mode 100644
index 0000000..03b9eae
--- /dev/null
+++ b/.github/skills/durable-functions-dotnet/references/setup.md
@@ -0,0 +1,845 @@
+# Durable Functions Setup Reference
+
+Complete setup and deployment guidance for Azure Durable Functions with Durable Task Scheduler.
+
+## Local Development
+
+### Prerequisites
+
+```bash
+# Install Azure Functions Core Tools
+brew tap azure/functions
+brew install azure-functions-core-tools@4
+
+# Install .NET SDK
+brew install dotnet
+
+# Install Azure CLI (optional, for Azure deployment)
+brew install azure-cli
+
+# Install Azurite (Azure Storage emulator)
+npm install -g azurite
+```
+
+### Start Local Emulator
+
+```bash
+# Terminal 1: Start Azurite (required for Azure Functions)
+azurite start
+
+# Terminal 2: Start Durable Task Scheduler emulator
+docker pull mcr.microsoft.com/dts/dts-emulator:latest
+docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
+
+# Dashboard available at http://localhost:8082
+```
+
+### Docker Compose (All-in-One)
+
+```yaml
+# docker-compose.yml
+version: '3.8'
+
+services:
+ azurite:
+ image: mcr.microsoft.com/azure-storage/azurite:latest
+ ports:
+ - "10000:10000" # Blob
+ - "10001:10001" # Queue
+ - "10002:10002" # Table
+ volumes:
+ - azurite-data:/data
+ command: azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0
+
+ dts-emulator:
+ image: mcr.microsoft.com/dts/dts-emulator:latest
+ ports:
+ - "8080:8080" # gRPC/HTTP endpoint
+ - "8082:8082" # Dashboard
+ environment:
+ - DTS_EMULATOR_LOG_LEVEL=Information
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8082/health"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+volumes:
+ azurite-data:
+```
+
+```bash
+docker-compose up -d
+```
+
+## Project Setup
+
+### Create New Project
+
+```bash
+# Create Functions project
+func init MyDurableFunctions --worker-runtime dotnet-isolated --target-framework net8.0
+
+cd MyDurableFunctions
+
+# Add required packages
+dotnet add package Microsoft.Azure.Functions.Worker.Extensions.DurableTask
+dotnet add package Azure.Identity
+
+# Optional: class-based orchestrations
+dotnet add package Microsoft.DurableTask.Generators
+```
+
+### Complete .csproj Template
+
+```xml
+
+
+ net8.0
+ v4
+ Exe
+ enable
+ enable
+ MyDurableFunctions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+```
+
+### host.json (Durable Task Scheduler)
+
+```json
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ },
+ "enableLiveMetricsFilters": true
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "storageProvider": {
+ "type": "azureManaged",
+ "connectionStringName": "DTS_CONNECTION_STRING"
+ },
+ "hubName": "%TASKHUB_NAME%"
+ }
+ }
+}
+```
+
+### local.settings.json
+
+```json
+{
+ "IsEncrypted": false,
+ "Values": {
+ "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
+ "AzureWebJobsStorage": "UseDevelopmentStorage=true",
+ "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None",
+ "TASKHUB_NAME": "default"
+ }
+}
+```
+
+### Program.cs with DI and Logging
+
+```csharp
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults()
+ .ConfigureServices(services =>
+ {
+ // Add Application Insights
+ services.AddApplicationInsightsTelemetryWorkerService();
+ services.ConfigureFunctionsApplicationInsights();
+
+ // Add your services
+ services.AddHttpClient();
+ services.AddSingleton();
+ })
+ .ConfigureLogging(logging =>
+ {
+ logging.SetMinimumLevel(LogLevel.Information);
+ })
+ .Build();
+
+await host.RunAsync();
+```
+
+## Complete Application Template
+
+### Functions.cs
+
+```csharp
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Entities;
+using Microsoft.Extensions.Logging;
+using System.Net;
+
+namespace MyDurableFunctions;
+
+public class Functions
+{
+ private readonly ILogger _logger;
+
+ public Functions(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ // HTTP Starter
+ [Function("HttpStart")]
+ public async Task HttpStart(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{functionName}")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string functionName,
+ FunctionContext executionContext)
+ {
+ string? requestBody = await new StreamReader(req.Body).ReadToEndAsync();
+
+ var options = new StartOrchestrationOptions
+ {
+ InstanceId = req.Headers.TryGetValues("X-Instance-Id", out var values)
+ ? values.First()
+ : null
+ };
+
+ string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
+ functionName, requestBody, options);
+
+ _logger.LogInformation("Started orchestration {FunctionName} with ID = {InstanceId}",
+ functionName, instanceId);
+
+ return await client.CreateCheckStatusResponseAsync(req, instanceId);
+ }
+
+ // Get Status
+ [Function("GetStatus")]
+ public async Task GetStatus(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "orchestrators/{instanceId}/status")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string instanceId)
+ {
+ var instance = await client.GetInstanceAsync(instanceId, getInputsAndOutputs: true);
+
+ if (instance == null)
+ {
+ return req.CreateResponse(HttpStatusCode.NotFound);
+ }
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ await response.WriteAsJsonAsync(new
+ {
+ instanceId = instance.InstanceId,
+ name = instance.Name,
+ runtimeStatus = instance.RuntimeStatus.ToString(),
+ createdTime = instance.CreatedAt,
+ lastUpdatedTime = instance.LastUpdatedAt,
+ input = instance.SerializedInput,
+ output = instance.SerializedOutput,
+ customStatus = instance.SerializedCustomStatus
+ });
+ return response;
+ }
+
+ // Raise Event
+ [Function("RaiseEvent")]
+ public async Task RaiseEvent(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{instanceId}/events/{eventName}")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string instanceId,
+ string eventName)
+ {
+ string? eventData = await new StreamReader(req.Body).ReadToEndAsync();
+
+ await client.RaiseEventAsync(instanceId, eventName, eventData);
+
+ return req.CreateResponse(HttpStatusCode.Accepted);
+ }
+
+ // Terminate
+ [Function("Terminate")]
+ public async Task Terminate(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{instanceId}/terminate")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string instanceId)
+ {
+ string? reason = await new StreamReader(req.Body).ReadToEndAsync();
+
+ await client.TerminateInstanceAsync(instanceId, reason);
+
+ return req.CreateResponse(HttpStatusCode.Accepted);
+ }
+
+ // Sample Orchestration
+ [Function(nameof(SampleOrchestration))]
+ public static async Task SampleOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+ {
+ ILogger logger = context.CreateReplaySafeLogger(nameof(SampleOrchestration));
+ string input = context.GetInput() ?? "World";
+
+ logger.LogInformation("Starting orchestration with input: {Input}", input);
+
+ var result1 = await context.CallActivityAsync(nameof(SayHello), "Tokyo");
+ var result2 = await context.CallActivityAsync(nameof(SayHello), "Seattle");
+ var result3 = await context.CallActivityAsync(nameof(SayHello), input);
+
+ return $"{result1}, {result2}, {result3}";
+ }
+
+ // Sample Activity
+ [Function(nameof(SayHello))]
+ public string SayHello([ActivityTrigger] string name)
+ {
+ _logger.LogInformation("Saying hello to {Name}", name);
+ return $"Hello {name}!";
+ }
+}
+```
+
+## Azure Provisioning
+
+### Azure CLI
+
+```bash
+# Variables
+RESOURCE_GROUP="my-durable-functions-rg"
+LOCATION="eastus"
+STORAGE_ACCOUNT="mydurablefuncssa"
+FUNCTION_APP="my-durable-functions"
+DTS_NAMESPACE="my-dts-namespace"
+DTS_SCHEDULER="my-scheduler"
+TASKHUB_NAME="default"
+
+# Create resource group
+az group create --name $RESOURCE_GROUP --location $LOCATION
+
+# Create storage account (required for Azure Functions)
+az storage account create \
+ --name $STORAGE_ACCOUNT \
+ --location $LOCATION \
+ --resource-group $RESOURCE_GROUP \
+ --sku Standard_LRS
+
+# Create Durable Task Scheduler namespace
+az durabletask namespace create \
+ --name $DTS_NAMESPACE \
+ --resource-group $RESOURCE_GROUP \
+ --location $LOCATION \
+ --sku "Basic"
+
+# Create scheduler within namespace
+az durabletask scheduler create \
+ --name $DTS_SCHEDULER \
+ --namespace-name $DTS_NAMESPACE \
+ --resource-group $RESOURCE_GROUP \
+ --ip-allow-list "[{\"name\": \"AllowAll\", \"startIPAddress\": \"0.0.0.0\", \"endIPAddress\": \"255.255.255.255\"}]"
+
+# Create task hub
+az durabletask taskhub create \
+ --name $TASKHUB_NAME \
+ --namespace-name $DTS_NAMESPACE \
+ --resource-group $RESOURCE_GROUP
+
+# Get scheduler endpoint
+DTS_ENDPOINT=$(az durabletask scheduler show \
+ --name $DTS_SCHEDULER \
+ --namespace-name $DTS_NAMESPACE \
+ --resource-group $RESOURCE_GROUP \
+ --query "endpoint" -o tsv)
+
+# Create Function App (Consumption plan)
+az functionapp create \
+ --name $FUNCTION_APP \
+ --storage-account $STORAGE_ACCOUNT \
+ --resource-group $RESOURCE_GROUP \
+ --consumption-plan-location $LOCATION \
+ --runtime dotnet-isolated \
+ --runtime-version 8 \
+ --functions-version 4 \
+ --assign-identity "[system]"
+
+# Get Function App identity
+FUNCTION_APP_IDENTITY=$(az functionapp identity show \
+ --name $FUNCTION_APP \
+ --resource-group $RESOURCE_GROUP \
+ --query "principalId" -o tsv)
+
+# Assign Durable Task Contributor role to Function App
+DTS_NAMESPACE_ID=$(az durabletask namespace show \
+ --name $DTS_NAMESPACE \
+ --resource-group $RESOURCE_GROUP \
+ --query "id" -o tsv)
+
+az role assignment create \
+ --assignee $FUNCTION_APP_IDENTITY \
+ --role "Durable Task Data Contributor" \
+ --scope $DTS_NAMESPACE_ID
+
+# Configure app settings
+az functionapp config appsettings set \
+ --name $FUNCTION_APP \
+ --resource-group $RESOURCE_GROUP \
+ --settings \
+ "DTS_CONNECTION_STRING=Endpoint=${DTS_ENDPOINT};Authentication=ManagedIdentity" \
+ "TASKHUB_NAME=$TASKHUB_NAME"
+```
+
+### Bicep Template
+
+```bicep
+// main.bicep
+@description('The location for all resources')
+param location string = resourceGroup().location
+
+@description('Base name for all resources')
+param baseName string = 'mydurablefunc'
+
+// Storage Account
+resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
+ name: '${baseName}sa'
+ location: location
+ kind: 'StorageV2'
+ sku: {
+ name: 'Standard_LRS'
+ }
+}
+
+// Durable Task Scheduler Namespace
+resource dtsNamespace 'Microsoft.DurableTask/namespaces@2025-11-01' = {
+ name: '${baseName}-dts'
+ location: location
+ sku: {
+ name: 'Basic'
+ capacity: 1
+ }
+ properties: {}
+}
+
+// Scheduler
+resource scheduler 'Microsoft.DurableTask/namespaces/schedulers@2025-11-01' = {
+ parent: dtsNamespace
+ name: 'scheduler'
+ location: location
+ properties: {
+ ipAllowlist: [
+ {
+ name: 'AllowAll'
+ startIPAddress: '0.0.0.0'
+ endIPAddress: '255.255.255.255'
+ }
+ ]
+ }
+}
+
+// Task Hub
+resource taskHub 'Microsoft.DurableTask/namespaces/taskHubs@2025-11-01' = {
+ parent: dtsNamespace
+ name: 'default'
+ properties: {}
+}
+
+// App Service Plan
+resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
+ name: '${baseName}-plan'
+ location: location
+ sku: {
+ name: 'Y1'
+ tier: 'Dynamic'
+ }
+ properties: {}
+}
+
+// Function App
+resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
+ name: '${baseName}-func'
+ location: location
+ kind: 'functionapp'
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ serverFarmId: appServicePlan.id
+ siteConfig: {
+ netFrameworkVersion: 'v8.0'
+ appSettings: [
+ {
+ name: 'FUNCTIONS_WORKER_RUNTIME'
+ value: 'dotnet-isolated'
+ }
+ {
+ name: 'FUNCTIONS_EXTENSION_VERSION'
+ value: '~4'
+ }
+ {
+ name: 'AzureWebJobsStorage'
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
+ }
+ {
+ name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
+ }
+ {
+ name: 'DTS_CONNECTION_STRING'
+ value: 'Endpoint=${scheduler.properties.endpoint};Authentication=ManagedIdentity'
+ }
+ {
+ name: 'TASKHUB_NAME'
+ value: 'default'
+ }
+ ]
+ }
+ }
+}
+
+// Role Assignment - Durable Task Data Contributor
+resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(dtsNamespace.id, functionApp.id, 'DurableTaskDataContributor')
+ scope: dtsNamespace
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '9d3a82f5-2d5a-4c3a-8e7f-6c2f8f8f8f8f') // Durable Task Data Contributor
+ principalId: functionApp.identity.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+output functionAppName string = functionApp.name
+output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}'
+output dtsEndpoint string = scheduler.properties.endpoint
+```
+
+```bash
+# Deploy
+az deployment group create \
+ --resource-group $RESOURCE_GROUP \
+ --template-file main.bicep \
+ --parameters baseName=mydurablefunc
+```
+
+## Deployment
+
+### Deploy Function App
+
+```bash
+# Build and publish
+dotnet publish -c Release -o ./publish
+
+# Create zip
+cd publish
+zip -r ../deploy.zip .
+cd ..
+
+# Deploy to Azure
+az functionapp deployment source config-zip \
+ --resource-group $RESOURCE_GROUP \
+ --name $FUNCTION_APP \
+ --src deploy.zip
+```
+
+### GitHub Actions Deployment
+
+```yaml
+# .github/workflows/deploy.yml
+name: Deploy Azure Functions
+
+on:
+ push:
+ branches: [main]
+
+env:
+ AZURE_FUNCTIONAPP_NAME: 'my-durable-functions'
+ AZURE_FUNCTIONAPP_PACKAGE_PATH: '.'
+ DOTNET_VERSION: '8.0.x'
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Build
+ run: dotnet build --configuration Release
+
+ - name: Publish
+ run: dotnet publish -c Release -o ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output
+
+ - name: Deploy to Azure Functions
+ uses: Azure/functions-action@v1
+ with:
+ app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
+ package: '${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output'
+ publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
+```
+
+## Deployment Options
+
+### Azure Container Apps
+
+```bash
+# Create environment
+az containerapp env create \
+ --name $ENVIRONMENT_NAME \
+ --resource-group $RESOURCE_GROUP \
+ --location $LOCATION
+
+# Build and push image
+az acr build \
+ --registry $ACR_NAME \
+ --image $IMAGE_NAME:$TAG .
+
+# Deploy
+az containerapp create \
+ --name $FUNCTION_APP \
+ --resource-group $RESOURCE_GROUP \
+ --environment $ENVIRONMENT_NAME \
+ --image $ACR_NAME.azurecr.io/$IMAGE_NAME:$TAG \
+ --target-port 80 \
+ --ingress 'external' \
+ --min-replicas 1 \
+ --max-replicas 10 \
+ --env-vars \
+ "FUNCTIONS_WORKER_RUNTIME=dotnet-isolated" \
+ "AzureWebJobsStorage=$STORAGE_CONNECTION_STRING" \
+ "DTS_CONNECTION_STRING=Endpoint=${DTS_ENDPOINT};Authentication=ManagedIdentity" \
+ "TASKHUB_NAME=$TASKHUB_NAME" \
+ --user-assigned $MANAGED_IDENTITY_ID
+```
+
+### Dockerfile
+
+```dockerfile
+FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 AS base
+WORKDIR /home/site/wwwroot
+EXPOSE 80
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+WORKDIR /src
+COPY ["MyDurableFunctions.csproj", "."]
+RUN dotnet restore
+COPY . .
+RUN dotnet build -c Release -o /app/build
+
+FROM build AS publish
+RUN dotnet publish -c Release -o /app/publish
+
+FROM base AS final
+WORKDIR /home/site/wwwroot
+COPY --from=publish /app/publish .
+ENV AzureWebJobsScriptRoot=/home/site/wwwroot
+ENV AzureFunctionsJobHost__Logging__Console__IsEnabled=true
+```
+
+## Testing
+
+### Unit Testing with Moq
+
+```csharp
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.DurableTask;
+using Moq;
+using Xunit;
+
+public class OrchestratorTests
+{
+ [Fact]
+ public async Task SampleOrchestration_ReturnsExpectedResult()
+ {
+ // Arrange
+ var contextMock = new Mock();
+
+ contextMock.Setup(x => x.GetInput()).Returns("Test");
+
+ contextMock
+ .Setup(x => x.CallActivityAsync(nameof(Functions.SayHello), "Tokyo", It.IsAny()))
+ .ReturnsAsync("Hello Tokyo!");
+
+ contextMock
+ .Setup(x => x.CallActivityAsync(nameof(Functions.SayHello), "Seattle", It.IsAny()))
+ .ReturnsAsync("Hello Seattle!");
+
+ contextMock
+ .Setup(x => x.CallActivityAsync(nameof(Functions.SayHello), "Test", It.IsAny()))
+ .ReturnsAsync("Hello Test!");
+
+ contextMock
+ .Setup(x => x.CreateReplaySafeLogger(It.IsAny()))
+ .Returns(Mock.Of());
+
+ // Act
+ var result = await Functions.SampleOrchestration(contextMock.Object);
+
+ // Assert
+ Assert.Equal("Hello Tokyo!, Hello Seattle!, Hello Test!", result);
+ }
+}
+```
+
+### Integration Testing
+
+```csharp
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+public class IntegrationTests : IAsyncLifetime
+{
+ private IHost _host = null!;
+ private DurableTaskClient _client = null!;
+
+ public async Task InitializeAsync()
+ {
+ _host = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults()
+ .Build();
+
+ await _host.StartAsync();
+
+ _client = _host.Services.GetRequiredService();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _host.StopAsync();
+ _host.Dispose();
+ }
+
+ [Fact]
+ public async Task Orchestration_Completes_Successfully()
+ {
+ // Schedule orchestration
+ string instanceId = await _client.ScheduleNewOrchestrationInstanceAsync(
+ nameof(Functions.SampleOrchestration), "IntegrationTest");
+
+ // Wait for completion
+ var result = await _client.WaitForInstanceCompletionAsync(
+ instanceId,
+ getInputsAndOutputs: true,
+ cancellationToken: new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token);
+
+ Assert.Equal(OrchestrationRuntimeStatus.Completed, result!.RuntimeStatus);
+ }
+}
+```
+
+## Monitoring and Logging
+
+### Application Insights
+
+```csharp
+// Program.cs
+using Microsoft.ApplicationInsights.Extensibility;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults()
+ .ConfigureServices(services =>
+ {
+ services.AddApplicationInsightsTelemetryWorkerService();
+ services.ConfigureFunctionsApplicationInsights();
+
+ // Custom telemetry processor
+ services.AddSingleton();
+ })
+ .Build();
+
+await host.RunAsync();
+
+public class CustomTelemetryInitializer : ITelemetryInitializer
+{
+ public void Initialize(ITelemetry telemetry)
+ {
+ telemetry.Context.Cloud.RoleName = "MyDurableFunctions";
+ }
+}
+```
+
+### Custom Status and Metrics
+
+```csharp
+[Function(nameof(TrackedOrchestration))]
+public static async Task TrackedOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ ILogger logger = context.CreateReplaySafeLogger(nameof(TrackedOrchestration));
+
+ // Set progress status
+ context.SetCustomStatus(new { Stage = "Starting", Progress = 0 });
+
+ await context.CallActivityAsync(nameof(Step1), null);
+ context.SetCustomStatus(new { Stage = "Step1Complete", Progress = 33 });
+
+ await context.CallActivityAsync(nameof(Step2), null);
+ context.SetCustomStatus(new { Stage = "Step2Complete", Progress = 66 });
+
+ await context.CallActivityAsync(nameof(Step3), null);
+ context.SetCustomStatus(new { Stage = "Completed", Progress = 100 });
+
+ return "Done";
+}
+```
+
+### KQL Queries for Monitoring
+
+```kql
+// Orchestration completion times
+traces
+| where message contains "orchestration"
+| summarize avg(duration) by bin(timestamp, 1h)
+
+// Failed orchestrations
+traces
+| where severityLevel >= 3
+| where message contains "orchestration" or message contains "activity"
+| project timestamp, message, severityLevel
+
+// Activity execution times
+customMetrics
+| where name == "ActivityDuration"
+| summarize avg(value), percentile(value, 95) by bin(timestamp, 5m)
+```
diff --git a/.github/skills/durable-task-dotnet/SKILL.md b/.github/skills/durable-task-dotnet/SKILL.md
new file mode 100644
index 0000000..46cce2b
--- /dev/null
+++ b/.github/skills/durable-task-dotnet/SKILL.md
@@ -0,0 +1,216 @@
+---
+name: durable-task-dotnet
+description: Build durable, fault-tolerant workflows in .NET using the Durable Task SDK with Azure Durable Task Scheduler. Use when creating orchestrations, activities, entities, or implementing patterns like function chaining, fan-out/fan-in, human interaction, or stateful agents. Applies to any .NET application requiring durable execution, state persistence, or distributed transactions without Azure Functions dependency.
+---
+
+# Durable Task .NET SDK with Durable Task Scheduler
+
+Build fault-tolerant, stateful workflows in .NET applications using the Durable Task SDK connected to Azure Durable Task Scheduler.
+
+## Quick Start
+
+### Required NuGet Packages
+
+```xml
+
+
+
+
+
+
+
+```
+
+### Minimal Worker Setup
+
+```csharp
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Worker;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+// Connection string format: "Endpoint={url};TaskHub={name};Authentication={type}"
+var connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+
+builder.Services.AddDurableTaskWorker()
+ .AddTasks(registry =>
+ {
+ registry.AddAllGeneratedTasks(); // Registers all [DurableTask] decorated classes
+ })
+ .UseDurableTaskScheduler(connectionString);
+
+var host = builder.Build();
+await host.RunAsync();
+```
+
+### Minimal Client Setup
+
+```csharp
+using Microsoft.DurableTask.Client;
+
+var connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+
+var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build();
+
+// Schedule an orchestration
+string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input);
+
+// Wait for completion
+var result = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true);
+```
+
+## Pattern Selection Guide
+
+| Pattern | Use When |
+|---------|----------|
+| **Function Chaining** | Sequential steps where each depends on the previous |
+| **Fan-Out/Fan-In** | Parallel processing with aggregated results |
+| **Human Interaction** | Workflow pauses for external input/approval |
+| **Durable Entities** | Stateful objects with operations (counters, accounts) |
+| **Sub-Orchestrations** | Reusable workflow components or version isolation |
+
+See [references/patterns.md](references/patterns.md) for detailed implementations.
+
+## Orchestration Structure
+
+### Basic Orchestration
+
+```csharp
+[DurableTask(nameof(MyOrchestration))]
+public class MyOrchestration : TaskOrchestrator
+{
+ public override async Task RunAsync(TaskOrchestrationContext context, string input)
+ {
+ // Call activities
+ var result1 = await context.CallActivityAsync(nameof(Step1Activity), input);
+ var result2 = await context.CallActivityAsync(nameof(Step2Activity), result1);
+ return result2;
+ }
+}
+```
+
+### Basic Activity
+
+```csharp
+[DurableTask(nameof(MyActivity))]
+public class MyActivity : TaskActivity
+{
+ private readonly ILogger _logger;
+
+ public MyActivity(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ public override Task RunAsync(TaskActivityContext context, string input)
+ {
+ _logger.LogInformation("Processing: {Input}", input);
+ return Task.FromResult($"Processed: {input}");
+ }
+}
+```
+
+## Critical Rules
+
+### Orchestration Determinism
+Orchestrations replay from history - all code MUST be deterministic:
+
+**NEVER do inside orchestrations:**
+- `DateTime.Now`, `DateTime.UtcNow` → Use `context.CurrentUtcDateTime`
+- `Guid.NewGuid()` → Use `context.NewGuid()`
+- `Random` → Pass random values from activities
+- Direct I/O, HTTP calls, database access → Move to activities
+- `Task.Delay()` → Use `context.CreateTimer()`
+- Non-deterministic LINQ (parallel, unordered)
+
+**ALWAYS safe:**
+- `context.CallActivityAsync()`
+- `context.CallSubOrchestrationAsync()`
+- `context.CreateTimer()`
+- `context.WaitForExternalEvent()`
+- `context.CurrentUtcDateTime`
+- `context.NewGuid()`
+- `context.SetCustomStatus()`
+
+### Error Handling
+
+```csharp
+public override async Task RunAsync(TaskOrchestrationContext context, string input)
+{
+ try
+ {
+ return await context.CallActivityAsync(nameof(RiskyActivity), input);
+ }
+ catch (TaskFailedException ex)
+ {
+ // Activity failed - implement compensation or retry
+ context.SetCustomStatus(new { Error = ex.Message });
+ return await context.CallActivityAsync(nameof(CompensationActivity), input);
+ }
+}
+```
+
+### Retry Policies
+
+```csharp
+var options = new TaskOptions
+{
+ Retry = new RetryPolicy(
+ maxNumberOfAttempts: 3,
+ firstRetryInterval: TimeSpan.FromSeconds(5),
+ backoffCoefficient: 2.0,
+ maxRetryInterval: TimeSpan.FromMinutes(1))
+};
+
+await context.CallActivityAsync(nameof(UnreliableActivity), input, options);
+```
+
+## Connection & Authentication
+
+### Connection String Formats
+
+```csharp
+// Local emulator (no auth)
+"Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
+
+// Azure with DefaultAzureCredential
+"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=DefaultAzure"
+
+// Azure with Managed Identity
+"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=ManagedIdentity"
+
+// Azure with specific credential
+"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=AzureCLI"
+```
+
+### Authentication Helper
+
+```csharp
+static string GetConnectionString()
+{
+ var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:8080";
+ var taskHub = Environment.GetEnvironmentVariable("TASKHUB") ?? "default";
+
+ var authType = endpoint.StartsWith("http://localhost") ? "None" : "DefaultAzure";
+ return $"Endpoint={endpoint};TaskHub={taskHub};Authentication={authType}";
+}
+```
+
+## Local Development with Emulator
+
+```bash
+# Pull and run the emulator
+docker pull mcr.microsoft.com/dts/dts-emulator:latest
+docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
+
+# Dashboard available at http://localhost:8082
+```
+
+## References
+
+- **[patterns.md](references/patterns.md)** - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Entities, Sub-Orchestrations)
+- **[setup.md](references/setup.md)** - Azure Durable Task Scheduler provisioning and deployment
diff --git a/.github/skills/durable-task-dotnet/references/patterns.md b/.github/skills/durable-task-dotnet/references/patterns.md
new file mode 100644
index 0000000..8e557e0
--- /dev/null
+++ b/.github/skills/durable-task-dotnet/references/patterns.md
@@ -0,0 +1,575 @@
+# Durable Task Workflow Patterns
+
+## Table of Contents
+- [Function Chaining](#function-chaining)
+- [Fan-Out/Fan-In](#fan-outfan-in)
+- [Human Interaction](#human-interaction)
+- [Durable Entities](#durable-entities)
+- [Sub-Orchestrations](#sub-orchestrations)
+- [Scheduling/Timers](#schedulingtimers)
+
+---
+
+## Function Chaining
+
+Sequential execution where output of one activity feeds into the next.
+
+### Implementation
+
+```csharp
+[DurableTask(nameof(OrderProcessingOrchestration))]
+public class OrderProcessingOrchestration : TaskOrchestrator
+{
+ public override async Task RunAsync(TaskOrchestrationContext context, OrderRequest order)
+ {
+ // Step 1: Validate order
+ var validationResult = await context.CallActivityAsync(
+ nameof(ValidateOrderActivity), order);
+
+ if (!validationResult.IsValid)
+ return new OrderResult { Status = "ValidationFailed", Message = validationResult.Error };
+
+ // Step 2: Process payment
+ var paymentResult = await context.CallActivityAsync(
+ nameof(ProcessPaymentActivity), order);
+
+ // Step 3: Reserve inventory
+ var inventoryResult = await context.CallActivityAsync(
+ nameof(ReserveInventoryActivity), new ReservationRequest
+ {
+ OrderId = order.Id,
+ PaymentId = paymentResult.TransactionId
+ });
+
+ // Step 4: Ship order
+ var shippingResult = await context.CallActivityAsync(
+ nameof(ShipOrderActivity), new ShipRequest
+ {
+ OrderId = order.Id,
+ InventoryReservationId = inventoryResult.ReservationId
+ });
+
+ return new OrderResult
+ {
+ Status = "Completed",
+ TrackingNumber = shippingResult.TrackingNumber
+ };
+ }
+}
+
+[DurableTask(nameof(ValidateOrderActivity))]
+public class ValidateOrderActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, OrderRequest order)
+ {
+ // Validation logic
+ if (order.Items == null || !order.Items.Any())
+ return Task.FromResult(new ValidationResult { IsValid = false, Error = "No items in order" });
+
+ return Task.FromResult(new ValidationResult { IsValid = true });
+ }
+}
+```
+
+---
+
+## Fan-Out/Fan-In
+
+Execute multiple activities in parallel, then aggregate results.
+
+### Implementation
+
+```csharp
+[DurableTask(nameof(BatchProcessingOrchestration))]
+public class BatchProcessingOrchestration : TaskOrchestrator, ProcessingResults>
+{
+ public override async Task RunAsync(
+ TaskOrchestrationContext context, List workItems)
+ {
+ // Fan-out: create parallel tasks
+ var parallelTasks = new List>();
+
+ foreach (var item in workItems)
+ {
+ var task = context.CallActivityAsync(
+ nameof(ProcessItemActivity), item);
+ parallelTasks.Add(task);
+ }
+
+ // Wait for all parallel tasks
+ var results = await Task.WhenAll(parallelTasks);
+
+ // Fan-in: aggregate results
+ var aggregatedResult = await context.CallActivityAsync(
+ nameof(AggregateResultsActivity), results);
+
+ return aggregatedResult;
+ }
+}
+
+[DurableTask(nameof(ProcessItemActivity))]
+public class ProcessItemActivity : TaskActivity
+{
+ private readonly ILogger _logger;
+
+ public ProcessItemActivity(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ public override async Task RunAsync(TaskActivityContext context, string item)
+ {
+ _logger.LogInformation("Processing item: {Item}", item);
+
+ // Simulate processing
+ await Task.Delay(TimeSpan.FromMilliseconds(100));
+
+ return new ItemResult
+ {
+ ItemId = item,
+ ProcessedValue = item.Length,
+ ProcessedAt = DateTime.UtcNow
+ };
+ }
+}
+
+[DurableTask(nameof(AggregateResultsActivity))]
+public class AggregateResultsActivity : TaskActivity
+{
+ public override Task RunAsync(
+ TaskActivityContext context, ItemResult[] results)
+ {
+ return Task.FromResult(new ProcessingResults
+ {
+ TotalItems = results.Length,
+ TotalValue = results.Sum(r => r.ProcessedValue),
+ ItemResults = results.ToList()
+ });
+ }
+}
+```
+
+### With Batching for Large Workloads
+
+```csharp
+public override async Task RunAsync(
+ TaskOrchestrationContext context, List workItems)
+{
+ const int batchSize = 10;
+ var allResults = new List();
+
+ // Process in batches to avoid overwhelming resources
+ for (int i = 0; i < workItems.Count; i += batchSize)
+ {
+ var batch = workItems.Skip(i).Take(batchSize).ToList();
+
+ var batchTasks = batch.Select(item =>
+ context.CallActivityAsync(nameof(ProcessItemActivity), item));
+
+ var batchResults = await Task.WhenAll(batchTasks);
+ allResults.AddRange(batchResults);
+
+ // Update progress
+ context.SetCustomStatus(new {
+ Processed = Math.Min(i + batchSize, workItems.Count),
+ Total = workItems.Count
+ });
+ }
+
+ return await context.CallActivityAsync(
+ nameof(AggregateResultsActivity), allResults.ToArray());
+}
+```
+
+---
+
+## Human Interaction
+
+Pause workflow for external approval or input.
+
+### Implementation
+
+```csharp
+[DurableTask(nameof(ApprovalOrchestration))]
+public class ApprovalOrchestration : TaskOrchestrator
+{
+ private const string ApprovalEventName = "approval_response";
+
+ public override async Task RunAsync(
+ TaskOrchestrationContext context, ApprovalRequest request)
+ {
+ // Step 1: Submit approval request
+ var submission = await context.CallActivityAsync(
+ nameof(SubmitApprovalRequestActivity), request);
+
+ // Make request details available via custom status
+ context.SetCustomStatus(new {
+ RequestId = request.Id,
+ Status = "PendingApproval",
+ SubmittedAt = submission.SubmittedAt,
+ ApprovalUrl = submission.ApprovalUrl
+ });
+
+ // Step 2: Wait for approval or timeout
+ var timeout = context.CurrentUtcDateTime.Add(request.TimeoutDuration);
+
+ using var timeoutCts = new CancellationTokenSource();
+ var timeoutTask = context.CreateTimer(timeout, timeoutCts.Token);
+ var approvalTask = context.WaitForExternalEvent(ApprovalEventName);
+
+ var completedTask = await Task.WhenAny(approvalTask, timeoutTask);
+
+ ApprovalResult result;
+
+ if (completedTask == approvalTask)
+ {
+ // Approval received before timeout
+ timeoutCts.Cancel();
+ var response = approvalTask.Result;
+
+ result = await context.CallActivityAsync(
+ nameof(ProcessApprovalActivity), new ProcessApprovalInput
+ {
+ Request = request,
+ Response = response
+ });
+ }
+ else
+ {
+ // Timeout occurred
+ result = new ApprovalResult
+ {
+ RequestId = request.Id,
+ Status = ApprovalStatus.TimedOut,
+ ProcessedAt = context.CurrentUtcDateTime
+ };
+ }
+
+ // Notify requester of outcome
+ await context.CallActivityAsync(nameof(SendNotificationActivity), new NotificationInput
+ {
+ RequestId = request.Id,
+ Result = result
+ });
+
+ return result;
+ }
+}
+
+// Client code to send approval
+public async Task ApproveRequest(DurableTaskClient client, string instanceId, bool approved)
+{
+ var response = new ApprovalResponse
+ {
+ IsApproved = approved,
+ ApprovedBy = "user@example.com",
+ Comments = approved ? "Looks good!" : "Rejected due to policy violation"
+ };
+
+ await client.RaiseEventAsync(instanceId, "approval_response", response);
+}
+```
+
+### Multi-Step Approval
+
+```csharp
+public override async Task RunAsync(
+ TaskOrchestrationContext context, MultiLevelApprovalRequest request)
+{
+ var approvers = new[] { "manager", "director", "vp" };
+
+ foreach (var level in approvers)
+ {
+ if (request.Amount < GetThresholdForLevel(level))
+ continue;
+
+ context.SetCustomStatus(new {
+ CurrentLevel = level,
+ PendingApproval = true
+ });
+
+ // Wait for approval at this level
+ var response = await context.WaitForExternalEvent($"approval_{level}");
+
+ if (!response.IsApproved)
+ {
+ return new ApprovalResult { Status = ApprovalStatus.Rejected, RejectedBy = level };
+ }
+ }
+
+ return new ApprovalResult { Status = ApprovalStatus.Approved };
+}
+```
+
+---
+
+## Durable Entities
+
+Stateful objects for managing state with atomic operations.
+
+### Entity Definition
+
+```csharp
+public interface IAccountEntity
+{
+ void Deposit(decimal amount);
+ void Withdraw(decimal amount);
+ decimal GetBalance();
+ void Reset();
+}
+
+[DurableTask(nameof(AccountEntity))]
+public class AccountEntity : TaskEntity, IAccountEntity
+{
+ public void Deposit(decimal amount)
+ {
+ if (amount <= 0)
+ throw new ArgumentException("Amount must be positive");
+
+ State.Balance += amount;
+ State.LastModified = DateTime.UtcNow;
+ State.TransactionHistory.Add(new Transaction
+ {
+ Type = "Deposit",
+ Amount = amount,
+ Timestamp = State.LastModified
+ });
+ }
+
+ public void Withdraw(decimal amount)
+ {
+ if (amount <= 0)
+ throw new ArgumentException("Amount must be positive");
+
+ if (State.Balance < amount)
+ throw new InvalidOperationException("Insufficient funds");
+
+ State.Balance -= amount;
+ State.LastModified = DateTime.UtcNow;
+ State.TransactionHistory.Add(new Transaction
+ {
+ Type = "Withdrawal",
+ Amount = amount,
+ Timestamp = State.LastModified
+ });
+ }
+
+ public decimal GetBalance() => State.Balance;
+
+ public void Reset()
+ {
+ State = new AccountState();
+ }
+
+ protected override AccountState InitializeState() => new AccountState();
+}
+
+public class AccountState
+{
+ public decimal Balance { get; set; }
+ public DateTime LastModified { get; set; }
+ public List TransactionHistory { get; set; } = new();
+}
+```
+
+### Using Entities from Orchestrations
+
+```csharp
+[DurableTask(nameof(TransferFundsOrchestration))]
+public class TransferFundsOrchestration : TaskOrchestrator
+{
+ public override async Task RunAsync(
+ TaskOrchestrationContext context, TransferRequest request)
+ {
+ var sourceEntity = new EntityInstanceId(nameof(AccountEntity), request.SourceAccountId);
+ var destEntity = new EntityInstanceId(nameof(AccountEntity), request.DestinationAccountId);
+
+ // Check source balance
+ var sourceBalance = await context.Entities.CallEntityAsync(
+ sourceEntity, nameof(IAccountEntity.GetBalance));
+
+ if (sourceBalance < request.Amount)
+ {
+ return new TransferResult
+ {
+ Success = false,
+ Error = "Insufficient funds"
+ };
+ }
+
+ // Perform transfer atomically using critical section
+ using (await context.Entities.LockEntitiesAsync(sourceEntity, destEntity))
+ {
+ try
+ {
+ // Withdraw from source
+ await context.Entities.CallEntityAsync(
+ sourceEntity, nameof(IAccountEntity.Withdraw), request.Amount);
+
+ // Deposit to destination
+ await context.Entities.CallEntityAsync(
+ destEntity, nameof(IAccountEntity.Deposit), request.Amount);
+
+ return new TransferResult
+ {
+ Success = true,
+ TransactionId = context.NewGuid().ToString()
+ };
+ }
+ catch (Exception ex)
+ {
+ // Compensation logic if needed
+ return new TransferResult { Success = false, Error = ex.Message };
+ }
+ }
+ }
+}
+```
+
+### Signaling Entities from Client
+
+```csharp
+// Fire-and-forget signal (one-way)
+await client.Entities.SignalEntityAsync(
+ new EntityInstanceId(nameof(AccountEntity), "account-123"),
+ nameof(IAccountEntity.Deposit),
+ 100.00m);
+
+// Query entity state
+var balance = await client.Entities.GetEntityAsync(
+ new EntityInstanceId(nameof(AccountEntity), "account-123"));
+```
+
+---
+
+## Sub-Orchestrations
+
+Compose orchestrations for modularity and version management.
+
+### Implementation
+
+```csharp
+[DurableTask(nameof(MainOrchestration))]
+public class MainOrchestration : TaskOrchestrator
+{
+ public override async Task RunAsync(
+ TaskOrchestrationContext context, MainRequest request)
+ {
+ // Call sub-orchestration
+ var orderResult = await context.CallSubOrchestrationAsync(
+ nameof(OrderProcessingOrchestration),
+ new OrderRequest { CustomerId = request.CustomerId, Items = request.Items });
+
+ // Call another sub-orchestration with custom instance ID
+ var notificationResult = await context.CallSubOrchestrationAsync(
+ nameof(NotificationOrchestration),
+ new NotificationRequest { OrderId = orderResult.OrderId },
+ new SubOrchestrationOptions
+ {
+ InstanceId = $"notification-{orderResult.OrderId}"
+ });
+
+ return new MainResult
+ {
+ OrderResult = orderResult,
+ NotificationResult = notificationResult
+ };
+ }
+}
+```
+
+### Parallel Sub-Orchestrations
+
+```csharp
+public override async Task RunAsync(
+ TaskOrchestrationContext context, List customerIds)
+{
+ var tasks = customerIds.Select(customerId =>
+ context.CallSubOrchestrationAsync(
+ nameof(ProcessCustomerOrchestration),
+ new CustomerRequest { CustomerId = customerId },
+ new SubOrchestrationOptions { InstanceId = $"customer-{customerId}" }));
+
+ var results = await Task.WhenAll(tasks);
+
+ return new BatchResult { CustomerResults = results.ToList() };
+}
+```
+
+---
+
+## Scheduling/Timers
+
+Implement delays, schedules, and recurring workflows.
+
+### Delayed Execution
+
+```csharp
+public override async Task RunAsync(
+ TaskOrchestrationContext context, ReminderRequest request)
+{
+ // Wait until specified time
+ var reminderTime = request.ReminderDateTime;
+ await context.CreateTimer(reminderTime, CancellationToken.None);
+
+ // Send reminder
+ return await context.CallActivityAsync(
+ nameof(SendReminderActivity), request);
+}
+```
+
+### Recurring Schedule (Eternal Orchestration)
+
+```csharp
+[DurableTask(nameof(RecurringJobOrchestration))]
+public class RecurringJobOrchestration : TaskOrchestrator
+{
+ public override async Task