.github/skills/durable-functions-dotnet/SKILL.md
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.
npx skillsauth add Azure-Samples/Durable-Task-Scheduler durable-functions-dotnetInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build fault-tolerant, stateful serverless workflows using Azure Durable Functions connected to Azure Durable Task Scheduler.
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="*" />
<PackageReference Include="Azure.Identity" Version="1.*" />
</ItemGroup>
{
"version": "2.0",
"extensions": {
"durableTask": {
"storageProvider": {
"type": "azureManaged",
"connectionStringName": "DTS_CONNECTION_STRING"
},
"hubName": "%TASKHUB_NAME%"
}
}
}
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None",
"TASKHUB_NAME": "default"
}
}
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<HttpResponseData> 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<string> MyOrchestration(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration));
logger.LogInformation("Starting orchestration");
var result1 = await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo");
var result2 = await context.CallActivityAsync<string>(nameof(SayHello), "Seattle");
var result3 = await context.CallActivityAsync<string>(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}!";
}
}
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.Build();
await host.RunAsync();
| 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 for detailed implementations.
[Function(nameof(MyOrchestration))]
public static async Task<string> MyOrchestration(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
string input = context.GetInput<string>()!;
return await context.CallActivityAsync<string>(nameof(MyActivity), input);
}
[Function(nameof(MyActivity))]
public static string MyActivity([ActivityTrigger] string input)
{
return $"Processed: {input}";
}
Requires Microsoft.DurableTask.Generators package:
[DurableTask(nameof(MyOrchestration))]
public class MyOrchestration : TaskOrchestrator<string, string>
{
public override async Task<string> RunAsync(TaskOrchestrationContext context, string input)
{
ILogger logger = context.CreateReplaySafeLogger<MyOrchestration>();
return await context.CallActivityAsync<string>(nameof(MyActivity), input);
}
}
[DurableTask(nameof(MyActivity))]
public class MyActivity : TaskActivity<string, string>
{
private readonly ILogger<MyActivity> _logger;
// Activities support DI - orchestrations do NOT
public MyActivity(ILogger<MyActivity> logger)
{
_logger = logger;
}
public override Task<string> RunAsync(TaskActivityContext context, string input)
{
_logger.LogInformation("Processing: {Input}", input);
return Task.FromResult($"Processed: {input}");
}
}
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.CurrentUtcDateTimeGuid.NewGuid() → Use context.NewGuid()Random → Pass random values from activitiesThread.Sleep(), Task.Delay() → Use context.CreateTimer()Task.Run(), ConfigureAwait(false)ALWAYS safe:
context.CallActivityAsync<T>()context.CallSubOrchestrationAsync<T>()context.CallHttpAsync()context.CreateTimer()context.WaitForExternalEvent<T>()context.CurrentUtcDateTimecontext.NewGuid()context.SetCustomStatus()context.CreateReplaySafeLogger()// 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);
}
}
// WRONG - Guid.NewGuid() generates different value on replay
[Function(nameof(BadOrchestration))]
public static async Task<string> 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<string> GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
string orderId = context.NewGuid().ToString(); // Deterministic
await context.CallActivityAsync(nameof(CreateOrder), orderId);
return orderId;
}
// 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<int>(nameof(GetRandomDelay), null);
await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(delay), CancellationToken.None);
}
// 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);
}
// WRONG - HttpClient in orchestrator is non-deterministic
[Function(nameof(BadOrchestration))]
public static async Task<string> 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<string> 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<string> FetchData([ActivityTrigger] string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url); // OK in activity
}
[Function(nameof(GoodOrchestration2))]
public static async Task<string> GoodOrchestration2([OrchestrationTrigger] TaskOrchestrationContext context)
{
return await context.CallActivityAsync<string>(nameof(FetchData), "https://api.example.com/data");
}
// 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<User> 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<User> GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
string userId = context.GetInput<string>()!;
return await context.CallActivityAsync<User>(nameof(GetUser), userId);
}
// 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<WorkflowConfig>()!;
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...
}
// WRONG - Dictionary iteration order may vary
[Function(nameof(BadOrchestration))]
public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
var items = context.GetInput<Dictionary<string, object>>()!;
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<Dictionary<string, object>>()!;
foreach (var key in items.Keys.OrderBy(k => k)) // Guaranteed order
{
await context.CallActivityAsync(nameof(Process), key);
}
}
Use CreateReplaySafeLogger to avoid duplicate log entries during replay:
[Function(nameof(MyOrchestration))]
public static async Task<string> 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<string>(nameof(MyActivity), "input");
logger.LogInformation("Activity completed with result: {Result}", result);
return result;
}
[Function(nameof(OrchestrationWithErrorHandling))]
public static async Task<string> OrchestrationWithErrorHandling(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
string input = context.GetInput<string>()!;
try
{
return await context.CallActivityAsync<string>(nameof(RiskyActivity), input);
}
catch (TaskFailedException ex)
{
// Activity failed - implement compensation
context.SetCustomStatus(new { Error = ex.Message });
return await context.CallActivityAsync<string>(nameof(CompensationActivity), input);
}
}
var options = new TaskOptions
{
Retry = new RetryPolicy(
maxNumberOfAttempts: 3,
firstRetryInterval: TimeSpan.FromSeconds(5),
backoffCoefficient: 2.0,
maxRetryInterval: TimeSpan.FromMinutes(1))
};
await context.CallActivityAsync<string>(nameof(UnreliableActivity), input, options);
Durable Functions exposes built-in HTTP APIs for orchestration management:
[Function("HttpStart")]
public static async Task<HttpResponseData> 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 statussendEventPostUri - POST endpoint to raise eventsterminatePostUri - POST endpoint to terminatepurgeHistoryDeleteUri - DELETE endpoint to purge history[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);
// 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=<client-id>"
Note: Durable Task Scheduler supports identity-based authentication only - no connection strings with keys.
# 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
Make HTTP calls directly from orchestrations (persisted and replay-safe):
[Function(nameof(CallExternalApi))]
public static async Task<string> 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")
};
development
Migrate existing Azure Durable Functions apps from existing backend storage providers (Azure Storage, Netherite, MSSQL) to the Durable Task Scheduler. Use when switching backends, converting to azureManaged storage provider, upgrading from Azure Storage default provider, migrating from Netherite Event Hubs-based backend, migrating from Microsoft SQL Server backend, or modernizing Durable Functions infrastructure. Applies to .NET, Python, JavaScript/TypeScript, and Java Durable Functions apps that need to adopt the managed Durable Task Scheduler service.
development
Build durable, fault-tolerant workflows in Python 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 Python application requiring durable execution, state persistence, or distributed transactions without Azure Functions dependency.
development
Build durable, fault-tolerant workflows in JavaScript/TypeScript 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 durable timers. Applies to any Node.js application requiring durable execution, state persistence, or distributed coordination without Azure Functions dependency.
development
Build durable, fault-tolerant workflows in Java using the Durable Task SDK with Azure Durable Task Scheduler. Use when creating orchestrations, activities, or implementing patterns like function chaining, fan-out/fan-in, human interaction, or monitoring. Applies to any Java application requiring durable execution, state persistence, or distributed transactions without Azure Functions dependency.