From 40a54a9c6a9c9ff6f602bc095ae549914da07560 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Wed, 11 Feb 2026 13:56:59 -0800 Subject: [PATCH 01/27] Add Durable Task Scheduler skill --- .../services/durable-task-scheduler.md | 466 ++++++++++++++++++ 1 file changed, 466 insertions(+) create mode 100644 plugin/skills/azure-prepare/references/services/durable-task-scheduler.md diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md new file mode 100644 index 000000000..8d930f08e --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md @@ -0,0 +1,466 @@ +# Durable Task Scheduler + +Build reliable, fault-tolerant workflows using durable execution with Azure Durable Task Scheduler. + +## When to Use + +- Long-running workflows requiring state persistence +- Distributed transactions with compensating actions (saga pattern) +- Multi-step orchestrations with checkpointing +- Fan-out/fan-in parallel processing +- Workflows requiring human interaction or external events +- Stateful entities (aggregators, counters, state machines) +- Multi-agent AI orchestration +- Data processing pipelines + +## Framework Selection + +| Framework | Best For | Hosting | +|-----------|----------|---------| +| **Durable Functions** | Serverless event-driven apps | Azure Functions | +| **Durable Task SDKs** | Any compute (containers, VMs) | ACA, AKS, App Service, VMs | + +> **💡 TIP**: Use Durable Functions for serverless with built-in triggers. Use Durable Task SDKs for hosting flexibility. + +## Quick Start - Local Emulator + +```bash +# Start 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 +``` + +## Durable Functions Setup + +### .NET - Required NuGet Packages + +```xml + + + + + +``` + +### Python - Required Packages + +```txt +# requirements.txt +azure-functions +azure-functions-durable +azure-identity +``` + +### host.json Configuration + +```json +{ + "version": "2.0", + "extensions": { + "durableTask": { + "storageProvider": { + "type": "azureManaged", + "connectionStringName": "DTS_CONNECTION_STRING" + }, + "hubName": "%TASKHUB_NAME%" + } + } +} +``` + +### local.settings.json (.NET) + +```json +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None", + "TASKHUB_NAME": "default" + } +} +``` + +### local.settings.json (Python) + +```json +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "python", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None", + "TASKHUB_NAME": "default" + } +} +``` + +### Minimal Example (.NET) + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; + +public static class DurableFunctionsApp +{ + [Function("HttpStart")] + public static async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req, + [DurableClient] DurableTaskClient client) + { + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestration)); + return await client.CreateCheckStatusResponseAsync(req, instanceId); + } + + [Function(nameof(MyOrchestration))] + public static async Task MyOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) + { + var result1 = await context.CallActivityAsync(nameof(SayHello), "Tokyo"); + var result2 = await context.CallActivityAsync(nameof(SayHello), "Seattle"); + return $"{result1}, {result2}"; + } + + [Function(nameof(SayHello))] + public static string SayHello([ActivityTrigger] string name) => $"Hello {name}!"; +} +``` + +### Minimal Example (Python) + +```python +import azure.functions as func +import azure.durable_functions as df + +my_app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +# HTTP Starter +@my_app.route(route="orchestrators/{function_name}", methods=["POST"]) +@my_app.durable_client_input(client_name="client") +async def http_start(req: func.HttpRequest, client): + function_name = req.route_params.get('function_name') + instance_id = await client.start_new(function_name) + return client.create_check_status_response(req, instance_id) + +# Orchestrator +@my_app.orchestration_trigger(context_name="context") +def my_orchestration(context: df.DurableOrchestrationContext): + result1 = yield context.call_activity("say_hello", "Tokyo") + result2 = yield context.call_activity("say_hello", "Seattle") + return f"{result1}, {result2}" + +# Activity +@my_app.activity_trigger(input_name="name") +def say_hello(name: str) -> str: + return f"Hello {name}!" +``` + +## Workflow Patterns + +| Pattern | Use When | +|---------|----------| +| **Function Chaining** | Sequential steps, each depends on previous | +| **Fan-Out/Fan-In** | Parallel processing with aggregated results | +| **Async HTTP APIs** | Long-running operations with HTTP polling | +| **Monitor** | Periodic polling with configurable timeouts | +| **Human Interaction** | Workflow pauses for external input/approval | +| **Saga** | Distributed transactions with compensation | +| **Durable Entities** | Stateful objects (counters, accounts) | + +### Fan-Out/Fan-In Example (.NET) + +```csharp +[Function(nameof(FanOutFanIn))] +public static async Task FanOutFanIn([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var workItems = await context.CallActivityAsync>(nameof(GetWorkItems), null); + + // Fan-out: schedule all in parallel + var tasks = workItems.Select(item => context.CallActivityAsync(nameof(ProcessItem), item)); + + // Fan-in: wait for all + return await Task.WhenAll(tasks); +} +``` + +### Fan-Out/Fan-In Example (Python) + +```python +@my_app.orchestration_trigger(context_name="context") +def fan_out_fan_in(context: df.DurableOrchestrationContext): + work_items = [f"item-{i}" for i in range(5)] + + # Fan-out: schedule all in parallel + parallel_tasks = [] + for item in work_items: + task = context.call_activity("process_item", item) + parallel_tasks.append(task) + + # Fan-in: wait for all + results = yield context.task_all(parallel_tasks) + return {"items_processed": len(results), "results": results} + +@my_app.activity_trigger(input_name="item") +def process_item(item: str) -> int: + return len(item) * 10 +``` + +### Human Interaction Example (.NET) + +```csharp +[Function(nameof(ApprovalWorkflow))] +public static async Task ApprovalWorkflow([OrchestrationTrigger] TaskOrchestrationContext context) +{ + await context.CallActivityAsync(nameof(SendApprovalRequest), context.GetInput()); + + // Wait for approval event with timeout + using var cts = new CancellationTokenSource(); + var approvalTask = context.WaitForExternalEvent("ApprovalEvent"); + var timeoutTask = context.CreateTimer(context.CurrentUtcDateTime.AddDays(3), cts.Token); + + var winner = await Task.WhenAny(approvalTask, timeoutTask); + + if (winner == approvalTask) + { + cts.Cancel(); + return await approvalTask ? "Approved" : "Rejected"; + } + return "Timed out"; +} +``` + +### Human Interaction Example (Python) + +```python +import datetime + +@my_app.orchestration_trigger(context_name="context") +def approval_workflow(context: df.DurableOrchestrationContext): + yield context.call_activity("send_approval_request", context.get_input()) + + # Wait for approval event with timeout + timeout = context.current_utc_datetime + datetime.timedelta(days=3) + approval_task = context.wait_for_external_event("ApprovalEvent") + timeout_task = context.create_timer(timeout) + + winner = yield context.task_any([approval_task, timeout_task]) + + if winner == approval_task: + approved = approval_task.result + return "Approved" if approved else "Rejected" + return "Timed out" +``` + +## Critical: Orchestration Determinism + +Orchestrations replay from history — all code MUST be deterministic. + +### .NET Determinism Rules + +| ❌ NEVER | ✅ ALWAYS USE | +|----------|--------------| +| `DateTime.Now` | `context.CurrentUtcDateTime` | +| `Guid.NewGuid()` | `context.NewGuid()` | +| `Random` | Pass random values from activities | +| `Task.Delay()`, `Thread.Sleep()` | `context.CreateTimer()` | +| Direct I/O, HTTP, database | `context.CallActivityAsync()` | + +### Python Determinism Rules + +| ❌ NEVER | ✅ ALWAYS USE | +|----------|--------------| +| `datetime.now()` | `context.current_utc_datetime` | +| `uuid.uuid4()` | `context.new_uuid()` | +| `random.random()` | Pass random values from activities | +| `time.sleep()` | `context.create_timer()` | +| Direct I/O, HTTP, database | `context.call_activity()` | + +### Logging in Orchestrations + +Use replay-safe logging to avoid duplicate log entries: + +**.NET:** +```csharp +[Function(nameof(MyOrchestration))] +public static async Task MyOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration)); + logger.LogInformation("Started"); // Only logs once, not on replay + return await context.CallActivityAsync(nameof(MyActivity), "input"); +} +``` + +**Python:** +```python +import logging + +@my_app.orchestration_trigger(context_name="context") +def my_orchestration(context: df.DurableOrchestrationContext): + # Check if replaying to avoid duplicate logs + if not context.is_replaying: + logging.info("Started") # Only logs once, not on replay + result = yield context.call_activity("my_activity", "input") + return result +``` + +## Connection & Authentication + +| Environment | Connection String | +|-------------|-------------------| +| Local Emulator | `Endpoint=http://localhost:8080;Authentication=None` | +| Azure (Managed Identity) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity` | + +> **⚠️ NOTE**: Durable Task Scheduler uses identity-based authentication only — no connection strings with keys. + +## Error Handling & Retry + +**.NET:** +```csharp +var retryOptions = new TaskOptions +{ + Retry = new RetryPolicy( + maxNumberOfAttempts: 3, + firstRetryInterval: TimeSpan.FromSeconds(5), + backoffCoefficient: 2.0, + maxRetryInterval: TimeSpan.FromMinutes(1)) +}; + +try +{ + await context.CallActivityAsync(nameof(UnreliableService), input, retryOptions); +} +catch (TaskFailedException ex) +{ + context.SetCustomStatus(new { Error = ex.Message }); + await context.CallActivityAsync(nameof(CompensationActivity), input); +} +``` + +**Python:** +```python +retry_options = df.RetryOptions( + first_retry_interval_in_milliseconds=5000, + max_number_of_attempts=3, + backoff_coefficient=2.0, + max_retry_interval_in_milliseconds=60000 +) + +@my_app.orchestration_trigger(context_name="context") +def workflow_with_retry(context: df.DurableOrchestrationContext): + try: + result = yield context.call_activity_with_retry( + "unreliable_service", + retry_options, + context.get_input() + ) + return result + except Exception as ex: + context.set_custom_status({"error": str(ex)}) + yield context.call_activity("compensation_activity", context.get_input()) + return "Compensated" +``` + +## Durable Task SDKs (Non-Functions) + +For applications running outside Azure Functions: + +### .NET SDK + +```csharp +var connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + +// Worker +builder.Services.AddDurableTaskWorker() + .AddTasks(registry => registry.AddAllGeneratedTasks()) + .UseDurableTaskScheduler(connectionString); + +// Client +var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build(); +string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input); +``` + +### Python SDK + +```python +import asyncio +from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker + +# Activity function +def say_hello(ctx, name: str) -> str: + return f"Hello {name}!" + +# Orchestrator function +def my_orchestration(ctx, name: str) -> str: + result = yield ctx.call_activity('say_hello', input=name) + return result + +async def main(): + with DurableTaskSchedulerWorker( + host_address="http://localhost:8080", + secure_channel=False, + taskhub="default" + ) as worker: + worker.add_activity(say_hello) + worker.add_orchestrator(my_orchestration) + worker.start() + + # Keep worker running + while True: + await asyncio.sleep(1) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Azure Deployment + +### Provision Durable Task Scheduler + +```bash +# Create scheduler +az durabletask scheduler create \ + --resource-group myResourceGroup \ + --name my-scheduler \ + --location eastus \ + --sku basic +``` + +### Bicep Example + +```bicep +resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { + name: schedulerName + location: location + properties: { + sku: { name: 'basic' } + } +} + +resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { + parent: scheduler + name: 'default' +} +``` + +### Configure Managed Identity Access + +```bash +# Get Function App identity +PRINCIPAL_ID=$(az functionapp identity show --name my-func-app --resource-group myRG --query principalId -o tsv) + +# Grant access to scheduler +az role assignment create \ + --assignee $PRINCIPAL_ID \ + --role "Durable Task Data Contributor" \ + --scope /subscriptions//resourceGroups/myRG/providers/Microsoft.DurableTask/schedulers/my-scheduler +``` + +## References + +- [Official Documentation](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) +- [Durable Functions Overview](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview) +- [Sample Repository](https://github.com/Azure-Samples/Durable-Task-Scheduler) +- [Choosing an Orchestration Framework](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/choose-orchestration-framework) From 28d208e1be4e6a6a458d6e817f876adfd68f8385 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Fri, 13 Feb 2026 14:52:10 -0800 Subject: [PATCH 02/27] Update DTS skill to include RBAC guidance and SKU guidance --- .../services/durable-task-scheduler.md | 100 ++++++++++++++++-- 1 file changed, 94 insertions(+), 6 deletions(-) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md index 8d930f08e..d6a3f3b4f 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md @@ -311,9 +311,10 @@ def my_orchestration(context: df.DurableOrchestrationContext): | Environment | Connection String | |-------------|-------------------| | Local Emulator | `Endpoint=http://localhost:8080;Authentication=None` | -| Azure (Managed Identity) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity` | +| Azure (System-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity` | +| Azure (User-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=` | -> **⚠️ NOTE**: Durable Task Scheduler uses identity-based authentication only — no connection strings with keys. +> **⚠️ NOTE**: Durable Task Scheduler uses identity-based authentication only — no connection strings with keys. When using a User-Assigned Managed Identity (UAMI), you must include the `ClientID` in the connection string. ## Error Handling & Retry @@ -417,25 +418,38 @@ if __name__ == "__main__": ## Azure Deployment +### SKU Selection + +| SKU | Best For | +|-----|----------| +| **Consumption** | QuickStarts, variable or bursty workloads, pay-per-use | +| **Dedicated** | High-demand workloads, predictable throughput requirements | + +> **💡 TIP**: Start with `consumption` for development and variable workloads. Switch to `dedicated` when you need consistent, high-throughput performance. + ### Provision Durable Task Scheduler ```bash -# Create scheduler +# Create scheduler (consumption SKU for getting started) az durabletask scheduler create \ --resource-group myResourceGroup \ --name my-scheduler \ --location eastus \ - --sku basic + --sku consumption ``` ### Bicep Example ```bicep +@allowed(['consumption', 'dedicated']) +@description('Use consumption for QuickStarts/variable workloads, dedicated for high-demand/predictable throughput') +param skuName string = 'consumption' + resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { name: schedulerName location: location properties: { - sku: { name: 'basic' } + sku: { name: skuName } } } @@ -443,12 +457,57 @@ resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { parent: scheduler name: 'default' } + +// REQUIRED: Assign Durable Task Data Contributor to the Function App's managed identity +var durableTaskDataContributorRoleId = '5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09' + +// --- Option A: System-Assigned Managed Identity (SAMI) --- +resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(scheduler.id, functionApp.id, durableTaskDataContributorRoleId) + scope: scheduler + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) + principalId: functionApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// --- Option B: User-Assigned Managed Identity (UAMI) --- +// 1. Create or reference the UAMI +resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '' + location: location +} + +// 2. Assign the UAMI to the Function App +// (include in functionApp resource properties) +// identity: { +// type: 'UserAssigned' +// userAssignedIdentities: { '${uami.id}': {} } +// } + +// 3. Assign Durable Task Data Contributor to the UAMI +resource durableTaskUamiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(scheduler.id, uami.id, durableTaskDataContributorRoleId) + scope: scheduler + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) + principalId: uami.properties.principalId + principalType: 'ServicePrincipal' + } +} + +// 4. Set the connection string app setting to include the UAMI ClientID: +// DTS_CONNECTION_STRING = 'Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=${uami.properties.clientId}' ``` +> **⚠️ WARNING**: Without the `Durable Task Data Contributor` role assignment, the Function App will receive a **403 PermissionDenied** error when attempting to start orchestrations via gRPC. Always include this role assignment in your infrastructure-as-code. + ### Configure Managed Identity Access +**System-Assigned Managed Identity (SAMI):** ```bash -# Get Function App identity +# Get Function App system-assigned identity PRINCIPAL_ID=$(az functionapp identity show --name my-func-app --resource-group myRG --query principalId -o tsv) # Grant access to scheduler @@ -458,6 +517,35 @@ az role assignment create \ --scope /subscriptions//resourceGroups/myRG/providers/Microsoft.DurableTask/schedulers/my-scheduler ``` +**User-Assigned Managed Identity (UAMI):** +```bash +# Get UAMI principal ID +UAMI_PRINCIPAL_ID=$(az identity show --name my-uami --resource-group myRG --query principalId -o tsv) +UAMI_CLIENT_ID=$(az identity show --name my-uami --resource-group myRG --query clientId -o tsv) + +# Assign UAMI to Function App +az functionapp identity assign --name my-func-app --resource-group myRG \ + --identities /subscriptions//resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-uami + +# Grant Durable Task Data Contributor to the UAMI +az role assignment create \ + --assignee $UAMI_PRINCIPAL_ID \ + --role "Durable Task Data Contributor" \ + --scope /subscriptions//resourceGroups/myRG/providers/Microsoft.DurableTask/schedulers/my-scheduler + +# Set the connection string with ClientID in app settings +az functionapp config appsettings set --name my-func-app --resource-group myRG \ + --settings "DTS_CONNECTION_STRING=Endpoint=https://my-scheduler.durabletask.io;Authentication=ManagedIdentity;ClientID=$UAMI_CLIENT_ID" +``` + +## Troubleshooting + +| Error | Cause | Fix | +|-------|-------|-----| +| **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource | Assign `Durable Task Data Contributor` role (`5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=` | +| **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | +| **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure `TASKHUB_NAME` app setting matches the provisioned task hub name | + ## References - [Official Documentation](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) From 44c659e5656a7b47fcd724d99d5f81186a14b128 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Fri, 13 Feb 2026 15:02:25 -0800 Subject: [PATCH 03/27] Add DTS to architecture guidance table for workflow --- .../azure-prepare/references/architecture.md | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/plugin/skills/azure-prepare/references/architecture.md b/plugin/skills/azure-prepare/references/architecture.md index e340c7a19..127ca4360 100644 --- a/plugin/skills/azure-prepare/references/architecture.md +++ b/plugin/skills/azure-prepare/references/architecture.md @@ -51,7 +51,7 @@ Select hosting stack and map components to Azure services. | Message Queue | Service Bus | | Pub/Sub | Event Grid | | Streaming | Event Hubs | -| Workflow | Logic Apps, Durable Functions | +| Workflow | Logic Apps, Durable Functions, Durable Task Scheduler | ### Supporting (Always Include) @@ -67,3 +67,23 @@ Select hosting stack and map components to Azure services. ## Document Architecture Record selections in `.azure/plan.md` with rationale for each choice. + +## Service References + +Consult these guides when the architecture includes these services: + +| Service | Reference | +|---------|-----------| +| AKS | [services/aks.md](services/aks.md) | +| App Service | [services/app-service.md](services/app-service.md) | +| Container Apps | [services/container-apps.md](services/container-apps.md) | +| Cosmos DB | [services/cosmos-db.md](services/cosmos-db.md) | +| Durable Task Scheduler | [services/durable-task-scheduler.md](services/durable-task-scheduler.md) | +| Event Grid | [services/event-grid.md](services/event-grid.md) | +| Functions | [services/functions.md](services/functions.md) | +| Key Vault | [services/key-vault.md](services/key-vault.md) | +| Logic Apps | [services/logic-apps.md](services/logic-apps.md) | +| Service Bus | [services/service-bus.md](services/service-bus.md) | +| SQL Database | [services/sql-database.md](services/sql-database.md) | +| Static Web Apps | [services/static-web-apps.md](services/static-web-apps.md) | +| Storage | [services/storage.md](services/storage.md) | From 7ee8a8e05024d0fbccf81a8674ebe2dc6dbbf3de Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 10:38:58 -0800 Subject: [PATCH 04/27] Address PR #919 feedback: fix auth levels, capitalization, and Bicep options - Change AuthorizationLevel from Anonymous to Function in all 4 language examples - Fix 'QuickStarts' capitalization to lowercase 'quickstarts' - Comment out Bicep Option B (UAMI) to prevent deploying both identity options --- .../services/durable-task-scheduler.md | 489 ++++++++++++++++-- 1 file changed, 443 insertions(+), 46 deletions(-) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md index d6a3f3b4f..e7031eaa1 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md @@ -53,7 +53,35 @@ azure-functions-durable azure-identity ``` -### host.json Configuration +### Java - Required Maven Dependencies + +```xml + + + com.microsoft.azure.functions + azure-functions-java-library + 3.2.3 + + + com.microsoft + durabletask-azure-functions + 1.7.0 + + +``` + +### JavaScript - Required npm Packages + +```json +{ + "dependencies": { + "@azure/functions": "^4.0.0", + "durable-functions": "^3.0.0" + } +} +``` + +### host.json Configuration (.NET / Python) ```json { @@ -70,6 +98,27 @@ azure-identity } ``` +### host.json Configuration (Java / JavaScript) + +```json +{ + "version": "2.0", + "extensions": { + "durableTask": { + "hubName": "default", + "storageProvider": { + "type": "durabletask-scheduler", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} +``` + ### local.settings.json (.NET) ```json @@ -98,6 +147,32 @@ azure-identity } ``` +### local.settings.json (Java) + +```json +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "java", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + } +} +``` + +### local.settings.json (JavaScript) + +```json +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + } +} +``` + ### Minimal Example (.NET) ```csharp @@ -109,7 +184,7 @@ public static class DurableFunctionsApp { [Function("HttpStart")] public static async Task HttpStart( - [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req, + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, [DurableClient] DurableTaskClient client) { string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestration)); @@ -135,7 +210,7 @@ public static class DurableFunctionsApp import azure.functions as func import azure.durable_functions as df -my_app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS) +my_app = df.DFApp(http_auth_level=func.AuthLevel.FUNCTION) # HTTP Starter @my_app.route(route="orchestrators/{function_name}", methods=["POST"]) @@ -158,6 +233,73 @@ def say_hello(name: str) -> str: return f"Hello {name}!" ``` +### Minimal Example (Java) + +```java +import com.microsoft.azure.functions.*; +import com.microsoft.azure.functions.annotation.*; +import com.microsoft.durabletask.*; +import com.microsoft.durabletask.azurefunctions.*; + +public class DurableFunctionsApp { + + @FunctionName("HttpStart") + public HttpResponseMessage httpStart( + @HttpTrigger(name = "req", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.FUNCTION) + HttpRequestMessage request, + @DurableClientInput(name = "durableContext") DurableClientContext durableContext) { + DurableTaskClient client = durableContext.getClient(); + String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration"); + return durableContext.createCheckStatusResponse(request, instanceId); + } + + @FunctionName("MyOrchestration") + public String myOrchestration( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + String result1 = ctx.callActivity("SayHello", "Tokyo", String.class).await(); + String result2 = ctx.callActivity("SayHello", "Seattle", String.class).await(); + return result1 + ", " + result2; + } + + @FunctionName("SayHello") + public String sayHello(@DurableActivityTrigger(name = "name") String name) { + return "Hello " + name + "!"; + } +} +``` + +### Minimal Example (JavaScript) + +```javascript +const { app } = require("@azure/functions"); +const df = require("durable-functions"); + +// Activity +df.app.activity("sayHello", { + handler: (city) => `Hello ${city}!`, +}); + +// Orchestrator +df.app.orchestration("myOrchestration", function* (context) { + const result1 = yield context.df.callActivity("sayHello", "Tokyo"); + const result2 = yield context.df.callActivity("sayHello", "Seattle"); + return `${result1}, ${result2}`; +}); + +// HTTP Starter +app.http("HttpStart", { + route: "orchestrators/{orchestrationName}", + methods: ["POST"], + authLevel: "function", + extraInputs: [df.input.durableClient()], + handler: async (request, context) => { + const client = df.getClient(context); + const instanceId = await client.startNew(request.params.orchestrationName); + return client.createCheckStatusResponse(request, instanceId); + }, +}); +``` + ## Workflow Patterns | Pattern | Use When | @@ -174,13 +316,13 @@ def say_hello(name: str) -> str: ```csharp [Function(nameof(FanOutFanIn))] -public static async Task FanOutFanIn([OrchestrationTrigger] TaskOrchestrationContext context) +public static async Task FanOutFanIn([OrchestrationTrigger] TaskOrchestrationContext context) { - var workItems = await context.CallActivityAsync>(nameof(GetWorkItems), null); - + string[] cities = { "Tokyo", "Seattle", "London", "Paris", "Berlin" }; + // Fan-out: schedule all in parallel - var tasks = workItems.Select(item => context.CallActivityAsync(nameof(ProcessItem), item)); - + var tasks = cities.Select(city => context.CallActivityAsync(nameof(SayHello), city)); + // Fan-in: wait for all return await Task.WhenAll(tasks); } @@ -191,21 +333,56 @@ public static async Task FanOutFanIn([OrchestrationTrigger] TaskOrchestra ```python @my_app.orchestration_trigger(context_name="context") def fan_out_fan_in(context: df.DurableOrchestrationContext): - work_items = [f"item-{i}" for i in range(5)] + cities = ["Tokyo", "Seattle", "London", "Paris", "Berlin"] # Fan-out: schedule all in parallel parallel_tasks = [] - for item in work_items: - task = context.call_activity("process_item", item) + for city in cities: + task = context.call_activity("say_hello", city) parallel_tasks.append(task) # Fan-in: wait for all results = yield context.task_all(parallel_tasks) - return {"items_processed": len(results), "results": results} + return results +``` + +### Fan-Out/Fan-In Example (Java) + +```java +@FunctionName("FanOutFanIn") +public List fanOutFanIn( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + String[] cities = {"Tokyo", "Seattle", "London", "Paris", "Berlin"}; + List> parallelTasks = new ArrayList<>(); + + // Fan-out: schedule all activities in parallel + for (String city : cities) { + parallelTasks.add(ctx.callActivity("SayHello", city, String.class)); + } + + // Fan-in: wait for all to complete + List results = new ArrayList<>(); + for (Task task : parallelTasks) { + results.add(task.await()); + } + + return results; +} +``` + +### Fan-Out/Fan-In Example (JavaScript) + +```javascript +df.app.orchestration("fanOutFanIn", function* (context) { + const cities = ["Tokyo", "Seattle", "London", "Paris", "Berlin"]; -@my_app.activity_trigger(input_name="item") -def process_item(item: str) -> int: - return len(item) * 10 + // Fan-out: schedule all activities in parallel + const tasks = cities.map((city) => context.df.callActivity("sayHello", city)); + + // Fan-in: wait for all to complete + const results = yield context.df.Task.all(tasks); + return results; +}); ``` ### Human Interaction Example (.NET) @@ -254,6 +431,49 @@ def approval_workflow(context: df.DurableOrchestrationContext): return "Timed out" ``` +### Human Interaction Example (Java) + +```java +@FunctionName("ApprovalWorkflow") +public String approvalWorkflow( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + ctx.callActivity("SendApprovalRequest", ctx.getInput(String.class)).await(); + + // Wait for approval event with timeout + Task approvalTask = ctx.waitForExternalEvent("ApprovalEvent", Boolean.class); + Task timeoutTask = ctx.createTimer(Duration.ofDays(3)); + + Task winner = ctx.anyOf(approvalTask, timeoutTask).await(); + + if (winner == approvalTask) { + return approvalTask.await() ? "Approved" : "Rejected"; + } + return "Timed out"; +} +``` + +### Human Interaction Example (JavaScript) + +```javascript +df.app.orchestration("approvalWorkflow", function* (context) { + yield context.df.callActivity("sendApprovalRequest", context.df.getInput()); + + // Wait for approval event with timeout + const expiration = new Date(context.df.currentUtcDateTime); + expiration.setDate(expiration.getDate() + 3); + + const approvalTask = context.df.waitForExternalEvent("ApprovalEvent"); + const timeoutTask = context.df.createTimer(expiration); + + const winner = yield context.df.Task.any([approvalTask, timeoutTask]); + + if (winner === approvalTask) { + return approvalTask.result ? "Approved" : "Rejected"; + } + return "Timed out"; +}); +``` + ## Critical: Orchestration Determinism Orchestrations replay from history — all code MUST be deterministic. @@ -278,6 +498,24 @@ Orchestrations replay from history — all code MUST be deterministic. | `time.sleep()` | `context.create_timer()` | | Direct I/O, HTTP, database | `context.call_activity()` | +### Java Determinism Rules + +| ❌ NEVER | ✅ ALWAYS USE | +|----------|--------------| +| `System.currentTimeMillis()` | `ctx.getCurrentInstant()` | +| `UUID.randomUUID()` | Pass random values from activities | +| `Thread.sleep()` | `ctx.createTimer()` | +| Direct I/O, HTTP, database | `ctx.callActivity()` | + +### JavaScript Determinism Rules + +| ❌ NEVER | ✅ ALWAYS USE | +|----------|--------------| +| `new Date()` | `context.df.currentUtcDateTime` | +| `Math.random()` | Pass random values from activities | +| `setTimeout()` | `context.df.createTimer()` | +| Direct I/O, HTTP, database | `context.df.callActivity()` | + ### Logging in Orchestrations Use replay-safe logging to avoid duplicate log entries: @@ -306,6 +544,30 @@ def my_orchestration(context: df.DurableOrchestrationContext): return result ``` +**Java:** +```java +@FunctionName("MyOrchestration") +public String myOrchestration( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + // Use isReplaying to avoid duplicate logs + if (!ctx.getIsReplaying()) { + logger.info("Started"); // Only logs once, not on replay + } + return ctx.callActivity("MyActivity", "input", String.class).await(); +} +``` + +**JavaScript:** +```javascript +df.app.orchestration("myOrchestration", function* (context) { + if (!context.df.isReplaying) { + console.log("Started"); // Only logs once, not on replay + } + const result = yield context.df.callActivity("myActivity", "input"); + return result; +}); +``` + ## Connection & Authentication | Environment | Connection String | @@ -364,6 +626,49 @@ def workflow_with_retry(context: df.DurableOrchestrationContext): return "Compensated" ``` +**Java:** +```java +@FunctionName("WorkflowWithRetry") +public String workflowWithRetry( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + TaskOptions retryOptions = new TaskOptions(new RetryPolicy( + 3, // maxNumberOfAttempts + Duration.ofSeconds(5) // firstRetryInterval + )); + + try { + return ctx.callActivity("UnreliableService", ctx.getInput(String.class), + retryOptions, String.class).await(); + } catch (TaskFailedException ex) { + ctx.setCustomStatus(Map.of("Error", ex.getMessage())); + ctx.callActivity("CompensationActivity", ctx.getInput(String.class)).await(); + return "Compensated"; + } +} +``` + +**JavaScript:** +```javascript +df.app.orchestration("workflowWithRetry", function* (context) { + const retryOptions = new df.RetryOptions(5000, 3); // firstRetryInterval, maxAttempts + retryOptions.backoffCoefficient = 2.0; + retryOptions.maxRetryIntervalInMilliseconds = 60000; + + try { + const result = yield context.df.callActivityWithRetry( + "unreliableService", + retryOptions, + context.df.getInput() + ); + return result; + } catch (ex) { + context.df.setCustomStatus({ error: ex.message }); + yield context.df.callActivity("compensationActivity", context.df.getInput()); + return "Compensated"; + } +}); +``` + ## Durable Task SDKs (Non-Functions) For applications running outside Azure Functions: @@ -407,22 +712,113 @@ async def main(): worker.add_activity(say_hello) worker.add_orchestrator(my_orchestration) worker.start() - - # Keep worker running - while True: - await asyncio.sleep(1) + + # Client + from durabletask.azuremanaged.client import DurableTaskSchedulerClient + client = DurableTaskSchedulerClient( + host_address="http://localhost:8080", + taskhub="default", + token_credential=None, + secure_channel=False + ) + instance_id = client.schedule_new_orchestration("my_orchestration", input="World") + result = client.wait_for_orchestration_completion(instance_id, timeout=30) + print(f"Output: {result.serialized_output}") if __name__ == "__main__": asyncio.run(main()) ``` +### Java SDK + +```java +import com.microsoft.durabletask.*; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientExtensions; + +import java.time.Duration; + +public class App { + public static void main(String[] args) throws Exception { + String connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + + // Worker + DurableTaskGrpcWorker worker = DurableTaskSchedulerWorkerExtensions + .createWorkerBuilder(connectionString) + .addOrchestration(new TaskOrchestrationFactory() { + @Override public String getName() { return "MyOrchestration"; } + @Override public TaskOrchestration create() { + return ctx -> { + String result = ctx.callActivity("SayHello", + ctx.getInput(String.class), String.class).await(); + ctx.complete(result); + }; + } + }) + .addActivity(new TaskActivityFactory() { + @Override public String getName() { return "SayHello"; } + @Override public TaskActivity create() { + return ctx -> "Hello " + ctx.getInput(String.class) + "!"; + } + }) + .build(); + + worker.start(); + + // Client + DurableTaskClient client = DurableTaskSchedulerClientExtensions + .createClientBuilder(connectionString).build(); + String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration", "World"); + OrchestrationMetadata result = client.waitForInstanceCompletion( + instanceId, Duration.ofSeconds(30), true); + System.out.println("Output: " + result.readOutputAs(String.class)); + + worker.stop(); + } +} +``` + +### JavaScript SDK + +```javascript +import { createAzureManagedWorkerBuilder, createAzureManagedClient } from "@microsoft/durabletask-js-azuremanaged"; + +const connectionString = "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; + +// Activity +const sayHello = async (_ctx, name) => `Hello ${name}!`; + +// Orchestrator +const myOrchestration = async function* (ctx, name) { + const result = yield ctx.callActivity(sayHello, name); + return result; +}; + +// Worker +const worker = createAzureManagedWorkerBuilder(connectionString) + .addOrchestrator(myOrchestration) + .addActivity(sayHello) + .build(); + +await worker.start(); + +// Client +const client = createAzureManagedClient(connectionString); +const instanceId = await client.scheduleNewOrchestration("myOrchestration", "World"); +const state = await client.waitForOrchestrationCompletion(instanceId, true, 30); +console.log("Output:", state.serializedOutput); + +await client.stop(); +await worker.stop(); +``` + ## Azure Deployment ### SKU Selection | SKU | Best For | |-----|----------| -| **Consumption** | QuickStarts, variable or bursty workloads, pay-per-use | +| **Consumption** | quickstarts, variable or bursty workloads, pay-per-use | | **Dedicated** | High-demand workloads, predictable throughput requirements | > **💡 TIP**: Start with `consumption` for development and variable workloads. Switch to `dedicated` when you need consistent, high-throughput performance. @@ -442,7 +838,7 @@ az durabletask scheduler create \ ```bicep @allowed(['consumption', 'dedicated']) -@description('Use consumption for QuickStarts/variable workloads, dedicated for high-demand/predictable throughput') +@description('Use consumption for quickstarts/variable workloads, dedicated for high-demand/predictable throughput') param skuName string = 'consumption' resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { @@ -473,32 +869,33 @@ resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022 } // --- Option B: User-Assigned Managed Identity (UAMI) --- -// 1. Create or reference the UAMI -resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: '' - location: location -} - -// 2. Assign the UAMI to the Function App -// (include in functionApp resource properties) -// identity: { -// type: 'UserAssigned' -// userAssignedIdentities: { '${uami.id}': {} } +// Uncomment this block and comment out Option A above if you prefer UAMI. +// +// resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { +// name: '' +// location: location // } - -// 3. Assign Durable Task Data Contributor to the UAMI -resource durableTaskUamiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(scheduler.id, uami.id, durableTaskDataContributorRoleId) - scope: scheduler - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) - principalId: uami.properties.principalId - principalType: 'ServicePrincipal' - } -} - -// 4. Set the connection string app setting to include the UAMI ClientID: -// DTS_CONNECTION_STRING = 'Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=${uami.properties.clientId}' +// +// // Assign the UAMI to the Function App +// // (include in functionApp resource properties) +// // identity: { +// // type: 'UserAssigned' +// // userAssignedIdentities: { '${uami.id}': {} } +// // } +// +// // Assign Durable Task Data Contributor to the UAMI +// resource durableTaskUamiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +// name: guid(scheduler.id, uami.id, durableTaskDataContributorRoleId) +// scope: scheduler +// properties: { +// roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) +// principalId: uami.properties.principalId +// principalType: 'ServicePrincipal' +// } +// } +// +// // Set the connection string app setting to include the UAMI ClientID: +// // DTS_CONNECTION_STRING = 'Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=${uami.properties.clientId}' ``` > **⚠️ WARNING**: Without the `Durable Task Data Contributor` role assignment, the Function App will receive a **403 PermissionDenied** error when attempting to start orchestrations via gRPC. Always include this role assignment in your infrastructure-as-code. From 5029f1ddc7c852b4ae168dc40e7850c3bacb7a70 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 10:51:53 -0800 Subject: [PATCH 05/27] Move DTS deployment content to azure-deploy skill --- .../azd/durable-task-scheduler-deploy.md | 203 ++++++++++++++++++ .../services/durable-task-scheduler.md | 121 +---------- 2 files changed, 204 insertions(+), 120 deletions(-) create mode 100644 plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md diff --git a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md new file mode 100644 index 000000000..7e5ec1560 --- /dev/null +++ b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md @@ -0,0 +1,203 @@ +# Durable Task Scheduler Deployment + +Deployment workflows for Durable Task Scheduler on Azure, including provisioning, Bicep templates, and managed identity configuration. + +## Prerequisites + +- Application prepared with azure-prepare skill +- `azure.yaml` exists and validated +- `.azure/preparation-manifest.md` status = `Validated` +- Docker (for local emulator development) + +## SKU Selection + +| SKU | Best For | +|-----|----------| +| **Consumption** | quickstarts, variable or bursty workloads, pay-per-use | +| **Dedicated** | High-demand workloads, predictable throughput requirements | + +> **💡 TIP**: Start with `consumption` for development and variable workloads. Switch to `dedicated` when you need consistent, high-throughput performance. + +## Provision Durable Task Scheduler + +```bash +# Create scheduler (consumption SKU for getting started) +az durabletask scheduler create \ + --resource-group myResourceGroup \ + --name my-scheduler \ + --location eastus \ + --sku consumption +``` + +## Bicep Example + +```bicep +@allowed(['consumption', 'dedicated']) +@description('Use consumption for quickstarts/variable workloads, dedicated for high-demand/predictable throughput') +param skuName string = 'consumption' + +resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { + name: schedulerName + location: location + properties: { + sku: { name: skuName } + } +} + +resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { + parent: scheduler + name: 'default' +} + +// REQUIRED: Assign Durable Task Data Contributor to the Function App's managed identity +var durableTaskDataContributorRoleId = '5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09' + +// --- Option A: System-Assigned Managed Identity (SAMI) --- +resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(scheduler.id, functionApp.id, durableTaskDataContributorRoleId) + scope: scheduler + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) + principalId: functionApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// --- Option B: User-Assigned Managed Identity (UAMI) --- +// Uncomment this block and comment out Option A above if you prefer UAMI. +// +// resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { +// name: '' +// location: location +// } +// +// // Assign the UAMI to the Function App +// // (include in functionApp resource properties) +// // identity: { +// // type: 'UserAssigned' +// // userAssignedIdentities: { '${uami.id}': {} } +// // } +// +// // Assign Durable Task Data Contributor to the UAMI +// resource durableTaskUamiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +// name: guid(scheduler.id, uami.id, durableTaskDataContributorRoleId) +// scope: scheduler +// properties: { +// roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) +// principalId: uami.properties.principalId +// principalType: 'ServicePrincipal' +// } +// } +// +// // Set the connection string app setting to include the UAMI ClientID: +// // DTS_CONNECTION_STRING = 'Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=${uami.properties.clientId}' +``` + +> **⚠️ WARNING**: Without the `Durable Task Data Contributor` role assignment, the Function App will receive a **403 PermissionDenied** error when attempting to start orchestrations via gRPC. Always include this role assignment in your infrastructure-as-code. + +## Configure Managed Identity Access + +**Option A: System-Assigned Managed Identity (SAMI) — Recommended default:** +```bash +# Get Function App system-assigned identity +PRINCIPAL_ID=$(az functionapp identity show --name my-func-app --resource-group myRG --query principalId -o tsv) + +# Grant access to scheduler +az role assignment create \ + --assignee $PRINCIPAL_ID \ + --role "Durable Task Data Contributor" \ + --scope /subscriptions//resourceGroups/myRG/providers/Microsoft.DurableTask/schedulers/my-scheduler +``` + + + +## AZD Deployment + +### Full Deployment (Infrastructure + Code) + +```bash +azd up --no-prompt +``` + +### Infrastructure Only + +```bash +azd provision --no-prompt +``` + +### Application Only + +```bash +azd deploy --no-prompt +``` + +## Verify Deployment + +```bash +# Show deployment details +azd show + +# Verify scheduler is provisioned +az durabletask scheduler show \ + --resource-group myResourceGroup \ + --name my-scheduler + +# Verify task hub exists +az durabletask taskhub show \ + --resource-group myResourceGroup \ + --scheduler-name my-scheduler \ + --name default +``` + +## Troubleshooting + +| Error | Cause | Fix | +|-------|-------|-----| +| **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource | Assign `Durable Task Data Contributor` role (`5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=` | +| **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | +| **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure `TASKHUB_NAME` app setting matches the provisioned task hub name | + +## Data Loss Warning + +> ⚠️ **CRITICAL: `azd down` Data Loss Warning** +> +> `azd down` **permanently deletes ALL resources** in the environment, including: +> - **Durable Task Scheduler** and all task hubs +> - **Function Apps** with all configuration and deployment slots +> - **Storage accounts** with all blobs and files +> +> **Best practices:** +> - Always use `azd provision --preview` before `azd up` +> - Use separate environments for dev/staging/production +> - Back up important data before running `azd down` + +## Next Steps + +After deployment: +1. Verify scheduler and task hub are provisioned +2. Test orchestration endpoints +3. Monitor via the DTS dashboard or Application Insights +4. Set up alerts for orchestration failures diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md index e7031eaa1..87b5d3132 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md @@ -814,126 +814,7 @@ await worker.stop(); ## Azure Deployment -### SKU Selection - -| SKU | Best For | -|-----|----------| -| **Consumption** | quickstarts, variable or bursty workloads, pay-per-use | -| **Dedicated** | High-demand workloads, predictable throughput requirements | - -> **💡 TIP**: Start with `consumption` for development and variable workloads. Switch to `dedicated` when you need consistent, high-throughput performance. - -### Provision Durable Task Scheduler - -```bash -# Create scheduler (consumption SKU for getting started) -az durabletask scheduler create \ - --resource-group myResourceGroup \ - --name my-scheduler \ - --location eastus \ - --sku consumption -``` - -### Bicep Example - -```bicep -@allowed(['consumption', 'dedicated']) -@description('Use consumption for quickstarts/variable workloads, dedicated for high-demand/predictable throughput') -param skuName string = 'consumption' - -resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { - name: schedulerName - location: location - properties: { - sku: { name: skuName } - } -} - -resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { - parent: scheduler - name: 'default' -} - -// REQUIRED: Assign Durable Task Data Contributor to the Function App's managed identity -var durableTaskDataContributorRoleId = '5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09' - -// --- Option A: System-Assigned Managed Identity (SAMI) --- -resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(scheduler.id, functionApp.id, durableTaskDataContributorRoleId) - scope: scheduler - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) - principalId: functionApp.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// --- Option B: User-Assigned Managed Identity (UAMI) --- -// Uncomment this block and comment out Option A above if you prefer UAMI. -// -// resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { -// name: '' -// location: location -// } -// -// // Assign the UAMI to the Function App -// // (include in functionApp resource properties) -// // identity: { -// // type: 'UserAssigned' -// // userAssignedIdentities: { '${uami.id}': {} } -// // } -// -// // Assign Durable Task Data Contributor to the UAMI -// resource durableTaskUamiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { -// name: guid(scheduler.id, uami.id, durableTaskDataContributorRoleId) -// scope: scheduler -// properties: { -// roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) -// principalId: uami.properties.principalId -// principalType: 'ServicePrincipal' -// } -// } -// -// // Set the connection string app setting to include the UAMI ClientID: -// // DTS_CONNECTION_STRING = 'Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=${uami.properties.clientId}' -``` - -> **⚠️ WARNING**: Without the `Durable Task Data Contributor` role assignment, the Function App will receive a **403 PermissionDenied** error when attempting to start orchestrations via gRPC. Always include this role assignment in your infrastructure-as-code. - -### Configure Managed Identity Access - -**System-Assigned Managed Identity (SAMI):** -```bash -# Get Function App system-assigned identity -PRINCIPAL_ID=$(az functionapp identity show --name my-func-app --resource-group myRG --query principalId -o tsv) - -# Grant access to scheduler -az role assignment create \ - --assignee $PRINCIPAL_ID \ - --role "Durable Task Data Contributor" \ - --scope /subscriptions//resourceGroups/myRG/providers/Microsoft.DurableTask/schedulers/my-scheduler -``` - -**User-Assigned Managed Identity (UAMI):** -```bash -# Get UAMI principal ID -UAMI_PRINCIPAL_ID=$(az identity show --name my-uami --resource-group myRG --query principalId -o tsv) -UAMI_CLIENT_ID=$(az identity show --name my-uami --resource-group myRG --query clientId -o tsv) - -# Assign UAMI to Function App -az functionapp identity assign --name my-func-app --resource-group myRG \ - --identities /subscriptions//resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-uami - -# Grant Durable Task Data Contributor to the UAMI -az role assignment create \ - --assignee $UAMI_PRINCIPAL_ID \ - --role "Durable Task Data Contributor" \ - --scope /subscriptions//resourceGroups/myRG/providers/Microsoft.DurableTask/schedulers/my-scheduler - -# Set the connection string with ClientID in app settings -az functionapp config appsettings set --name my-func-app --resource-group myRG \ - --settings "DTS_CONNECTION_STRING=Endpoint=https://my-scheduler.durabletask.io;Authentication=ManagedIdentity;ClientID=$UAMI_CLIENT_ID" -``` +For provisioning, Bicep templates, managed identity configuration, and deployment workflows, see the [DTS Deployment Guide](../../../../azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md). ## Troubleshooting From 86216ffe8bb8ad399f574c5b37daff6099ec337c Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 10:57:10 -0800 Subject: [PATCH 06/27] Pin NuGet package versions to latest stable --- .../references/services/durable-task-scheduler.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md index 87b5d3132..2aecebe1b 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md @@ -25,9 +25,9 @@ Build reliable, fault-tolerant workflows using durable execution with Azure Dura ## Quick Start - Local Emulator ```bash -# Start 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 +# Start the emulator (update the tag as needed; see https://mcr.microsoft.com/v2/dts/dts-emulator/tags/list for newer versions) +docker pull mcr.microsoft.com/dts/dts-emulator:v0.0.10 +docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:v0.0.10 # Dashboard available at http://localhost:8082 ``` @@ -38,9 +38,9 @@ docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dt ```xml - - - + + + ``` From 9f0ae8186bbe68caa69cd319815d960f7bdd6860 Mon Sep 17 00:00:00 2001 From: greenie-msft <56556602+greenie-msft@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:10:23 -0800 Subject: [PATCH 07/27] Update plugin/skills/azure-prepare/references/services/durable-task-scheduler.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../azure-prepare/references/services/durable-task-scheduler.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md index 2aecebe1b..8d4903b54 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md @@ -814,7 +814,7 @@ await worker.stop(); ## Azure Deployment -For provisioning, Bicep templates, managed identity configuration, and deployment workflows, see the [DTS Deployment Guide](../../../../azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md). +For provisioning, Bicep templates, managed identity configuration, and deployment workflows, see the [DTS Deployment Guide](../../../../skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md). ## Troubleshooting From b69b4615aa43d762e927d05e4e43891977ed0b50 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 13:25:31 -0800 Subject: [PATCH 08/27] Fix link checker failures --- plugin/skills/azure-deploy/SKILL.md | 1 + .../azure-prepare/references/architecture.md | 20 ------------------- .../services/durable-task-scheduler.md | 2 +- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/plugin/skills/azure-deploy/SKILL.md b/plugin/skills/azure-deploy/SKILL.md index f85134db9..addee1dad 100644 --- a/plugin/skills/azure-deploy/SKILL.md +++ b/plugin/skills/azure-deploy/SKILL.md @@ -87,3 +87,4 @@ Activate this skill when user wants to: - [Troubleshooting](references/troubleshooting.md) - Common issues and solutions - [Post-Deployment Steps](references/recipes/azd/post-deployment.md) - SQL + EF Core setup +- [Durable Task Scheduler Deployment](references/recipes/azd/durable-task-scheduler-deploy.md) - DTS provisioning, Bicep, and managed identity setup diff --git a/plugin/skills/azure-prepare/references/architecture.md b/plugin/skills/azure-prepare/references/architecture.md index 127ca4360..f7bb95ef5 100644 --- a/plugin/skills/azure-prepare/references/architecture.md +++ b/plugin/skills/azure-prepare/references/architecture.md @@ -67,23 +67,3 @@ Select hosting stack and map components to Azure services. ## Document Architecture Record selections in `.azure/plan.md` with rationale for each choice. - -## Service References - -Consult these guides when the architecture includes these services: - -| Service | Reference | -|---------|-----------| -| AKS | [services/aks.md](services/aks.md) | -| App Service | [services/app-service.md](services/app-service.md) | -| Container Apps | [services/container-apps.md](services/container-apps.md) | -| Cosmos DB | [services/cosmos-db.md](services/cosmos-db.md) | -| Durable Task Scheduler | [services/durable-task-scheduler.md](services/durable-task-scheduler.md) | -| Event Grid | [services/event-grid.md](services/event-grid.md) | -| Functions | [services/functions.md](services/functions.md) | -| Key Vault | [services/key-vault.md](services/key-vault.md) | -| Logic Apps | [services/logic-apps.md](services/logic-apps.md) | -| Service Bus | [services/service-bus.md](services/service-bus.md) | -| SQL Database | [services/sql-database.md](services/sql-database.md) | -| Static Web Apps | [services/static-web-apps.md](services/static-web-apps.md) | -| Storage | [services/storage.md](services/storage.md) | diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md index 8d4903b54..aad911681 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md @@ -814,7 +814,7 @@ await worker.stop(); ## Azure Deployment -For provisioning, Bicep templates, managed identity configuration, and deployment workflows, see the [DTS Deployment Guide](../../../../skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md). +For provisioning, Bicep templates, managed identity configuration, and deployment workflows, invoke the **azure-deploy** skill which includes DTS-specific deployment guidance. ## Troubleshooting From a98054c2e0c6ad7e946640f5da7ac1b0c770c212 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 13:36:34 -0800 Subject: [PATCH 09/27] Add DTS to research.md service mapping to fix orphan check --- plugin/skills/azure-prepare/references/research.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/skills/azure-prepare/references/research.md b/plugin/skills/azure-prepare/references/research.md index e8d2838fc..5db3af58e 100644 --- a/plugin/skills/azure-prepare/references/research.md +++ b/plugin/skills/azure-prepare/references/research.md @@ -35,6 +35,7 @@ After architecture planning, research each selected component to gather best pra | **Integration** | | | | API Management | [APIM](apim.md) | `azure-aigateway` (invoke for AI Gateway policies) | | Logic Apps | [Logic Apps](services/logic-apps/README.md) | — | +| Durable Task Scheduler | [Durable Task Scheduler](services/durable-task-scheduler.md) | — | | **Security & Identity** | | | | Key Vault | [Key Vault](services/key-vault/README.md) | `azure-security`, `azure-keyvault-expiration-audit` | | Managed Identity | — | `azure-security`, `entra-app-registration` | From ab371ec64ac739d67a8d994e57e6af02de23541a Mon Sep 17 00:00:00 2001 From: greenie-msft <56556602+greenie-msft@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:47:31 -0800 Subject: [PATCH 10/27] Update plugin/skills/azure-prepare/references/research.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- plugin/skills/azure-prepare/references/research.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/skills/azure-prepare/references/research.md b/plugin/skills/azure-prepare/references/research.md index 5db3af58e..a48462e30 100644 --- a/plugin/skills/azure-prepare/references/research.md +++ b/plugin/skills/azure-prepare/references/research.md @@ -35,7 +35,7 @@ After architecture planning, research each selected component to gather best pra | **Integration** | | | | API Management | [APIM](apim.md) | `azure-aigateway` (invoke for AI Gateway policies) | | Logic Apps | [Logic Apps](services/logic-apps/README.md) | — | -| Durable Task Scheduler | [Durable Task Scheduler](services/durable-task-scheduler.md) | — | +| Durable Task Scheduler | [Durable Task Scheduler](services/durable-task-scheduler/README.md) | — | | **Security & Identity** | | | | Key Vault | [Key Vault](services/key-vault/README.md) | `azure-security`, `azure-keyvault-expiration-audit` | | Managed Identity | — | `azure-security`, `entra-app-registration` | From 8c8d03ee549b584b7c0f40e31217d6d0ba20af72 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 13:53:44 -0800 Subject: [PATCH 11/27] Address Copilot review: fix plan.md ref, add Bicep params, align UAMI convention --- .../recipes/azd/durable-task-scheduler-deploy.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md index 7e5ec1560..140019100 100644 --- a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md +++ b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md @@ -6,7 +6,7 @@ Deployment workflows for Durable Task Scheduler on Azure, including provisioning - Application prepared with azure-prepare skill - `azure.yaml` exists and validated -- `.azure/preparation-manifest.md` status = `Validated` +- `.azure/plan.md` exists; Validation Proof section status = `Validated` - Docker (for local emulator development) ## SKU Selection @@ -32,10 +32,17 @@ az durabletask scheduler create \ ## Bicep Example ```bicep +// Parameters — define these at file level or pass from a parent module +param schedulerName string +param location string = resourceGroup().location + @allowed(['consumption', 'dedicated']) @description('Use consumption for quickstarts/variable workloads, dedicated for high-demand/predictable throughput') param skuName string = 'consumption' +// Assumes functionApp is defined elsewhere in the same Bicep file, e.g.: +// resource functionApp 'Microsoft.Web/sites@2023-12-01' = { ... } + resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { name: schedulerName location: location @@ -74,9 +81,11 @@ resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022 // // Assign the UAMI to the Function App // // (include in functionApp resource properties) // // identity: { -// // type: 'UserAssigned' +// // type: 'SystemAssigned, UserAssigned' // // userAssignedIdentities: { '${uami.id}': {} } // // } +// // +// // Also set the AZURE_CLIENT_ID app setting to the UAMI's clientId. // // // Assign Durable Task Data Contributor to the UAMI // resource durableTaskUamiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { From 61fcec3a78280cec87ce8afce9383213870566c7 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 14:57:10 -0800 Subject: [PATCH 12/27] Restructure DTS as directory with per-language sub-files - Split monolithic durable-task-scheduler.md into directory: README.md (overview hub), dotnet.md, python.md, java.md, javascript.md, bicep.md - Add version discovery notes to all language files - Fix commented-out Bicep in bicep.md connection string section - Update durable.md to recommend DTS as best-practice backend - Remove UAMI sections from deploy recipe, simplify to SAMI only - Fix deploy recipe: add Bicep param declarations, use .azure/plan.md --- .../azd/durable-task-scheduler-deploy.md | 58 -- .../services/durable-task-scheduler.md | 832 ------------------ .../services/durable-task-scheduler/README.md | 79 ++ .../services/durable-task-scheduler/bicep.md | 88 ++ .../services/durable-task-scheduler/dotnet.md | 181 ++++ .../services/durable-task-scheduler/java.md | 234 +++++ .../durable-task-scheduler/javascript.md | 201 +++++ .../services/durable-task-scheduler/python.md | 208 +++++ .../references/services/functions/durable.md | 12 + 9 files changed, 1003 insertions(+), 890 deletions(-) delete mode 100644 plugin/skills/azure-prepare/references/services/durable-task-scheduler.md create mode 100644 plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md create mode 100644 plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md create mode 100644 plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md create mode 100644 plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md create mode 100644 plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md create mode 100644 plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md diff --git a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md index 140019100..3ce486d08 100644 --- a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md +++ b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md @@ -59,7 +59,6 @@ resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { // REQUIRED: Assign Durable Task Data Contributor to the Function App's managed identity var durableTaskDataContributorRoleId = '5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09' -// --- Option A: System-Assigned Managed Identity (SAMI) --- resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(scheduler.id, functionApp.id, durableTaskDataContributorRoleId) scope: scheduler @@ -69,44 +68,12 @@ resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022 principalType: 'ServicePrincipal' } } - -// --- Option B: User-Assigned Managed Identity (UAMI) --- -// Uncomment this block and comment out Option A above if you prefer UAMI. -// -// resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { -// name: '' -// location: location -// } -// -// // Assign the UAMI to the Function App -// // (include in functionApp resource properties) -// // identity: { -// // type: 'SystemAssigned, UserAssigned' -// // userAssignedIdentities: { '${uami.id}': {} } -// // } -// // -// // Also set the AZURE_CLIENT_ID app setting to the UAMI's clientId. -// -// // Assign Durable Task Data Contributor to the UAMI -// resource durableTaskUamiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { -// name: guid(scheduler.id, uami.id, durableTaskDataContributorRoleId) -// scope: scheduler -// properties: { -// roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) -// principalId: uami.properties.principalId -// principalType: 'ServicePrincipal' -// } -// } -// -// // Set the connection string app setting to include the UAMI ClientID: -// // DTS_CONNECTION_STRING = 'Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=${uami.properties.clientId}' ``` > **⚠️ WARNING**: Without the `Durable Task Data Contributor` role assignment, the Function App will receive a **403 PermissionDenied** error when attempting to start orchestrations via gRPC. Always include this role assignment in your infrastructure-as-code. ## Configure Managed Identity Access -**Option A: System-Assigned Managed Identity (SAMI) — Recommended default:** ```bash # Get Function App system-assigned identity PRINCIPAL_ID=$(az functionapp identity show --name my-func-app --resource-group myRG --query principalId -o tsv) @@ -118,31 +85,6 @@ az role assignment create \ --scope /subscriptions//resourceGroups/myRG/providers/Microsoft.DurableTask/schedulers/my-scheduler ``` - - ## AZD Deployment ### Full Deployment (Infrastructure + Code) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md deleted file mode 100644 index aad911681..000000000 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler.md +++ /dev/null @@ -1,832 +0,0 @@ -# Durable Task Scheduler - -Build reliable, fault-tolerant workflows using durable execution with Azure Durable Task Scheduler. - -## When to Use - -- Long-running workflows requiring state persistence -- Distributed transactions with compensating actions (saga pattern) -- Multi-step orchestrations with checkpointing -- Fan-out/fan-in parallel processing -- Workflows requiring human interaction or external events -- Stateful entities (aggregators, counters, state machines) -- Multi-agent AI orchestration -- Data processing pipelines - -## Framework Selection - -| Framework | Best For | Hosting | -|-----------|----------|---------| -| **Durable Functions** | Serverless event-driven apps | Azure Functions | -| **Durable Task SDKs** | Any compute (containers, VMs) | ACA, AKS, App Service, VMs | - -> **💡 TIP**: Use Durable Functions for serverless with built-in triggers. Use Durable Task SDKs for hosting flexibility. - -## Quick Start - Local Emulator - -```bash -# Start the emulator (update the tag as needed; see https://mcr.microsoft.com/v2/dts/dts-emulator/tags/list for newer versions) -docker pull mcr.microsoft.com/dts/dts-emulator:v0.0.10 -docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:v0.0.10 - -# Dashboard available at http://localhost:8082 -``` - -## Durable Functions Setup - -### .NET - Required NuGet Packages - -```xml - - - - - -``` - -### Python - Required Packages - -```txt -# requirements.txt -azure-functions -azure-functions-durable -azure-identity -``` - -### Java - Required Maven Dependencies - -```xml - - - com.microsoft.azure.functions - azure-functions-java-library - 3.2.3 - - - com.microsoft - durabletask-azure-functions - 1.7.0 - - -``` - -### JavaScript - Required npm Packages - -```json -{ - "dependencies": { - "@azure/functions": "^4.0.0", - "durable-functions": "^3.0.0" - } -} -``` - -### host.json Configuration (.NET / Python) - -```json -{ - "version": "2.0", - "extensions": { - "durableTask": { - "storageProvider": { - "type": "azureManaged", - "connectionStringName": "DTS_CONNECTION_STRING" - }, - "hubName": "%TASKHUB_NAME%" - } - } -} -``` - -### host.json Configuration (Java / JavaScript) - -```json -{ - "version": "2.0", - "extensions": { - "durableTask": { - "hubName": "default", - "storageProvider": { - "type": "durabletask-scheduler", - "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" - } - } - }, - "extensionBundle": { - "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[4.*, 5.0.0)" - } -} -``` - -### local.settings.json (.NET) - -```json -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None", - "TASKHUB_NAME": "default" - } -} -``` - -### local.settings.json (Python) - -```json -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None", - "TASKHUB_NAME": "default" - } -} -``` - -### local.settings.json (Java) - -```json -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "java", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" - } -} -``` - -### local.settings.json (JavaScript) - -```json -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "node", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" - } -} -``` - -### Minimal Example (.NET) - -```csharp -using Microsoft.Azure.Functions.Worker; -using Microsoft.DurableTask; -using Microsoft.DurableTask.Client; - -public static class DurableFunctionsApp -{ - [Function("HttpStart")] - public static async Task HttpStart( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, - [DurableClient] DurableTaskClient client) - { - string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestration)); - return await client.CreateCheckStatusResponseAsync(req, instanceId); - } - - [Function(nameof(MyOrchestration))] - public static async Task MyOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) - { - var result1 = await context.CallActivityAsync(nameof(SayHello), "Tokyo"); - var result2 = await context.CallActivityAsync(nameof(SayHello), "Seattle"); - return $"{result1}, {result2}"; - } - - [Function(nameof(SayHello))] - public static string SayHello([ActivityTrigger] string name) => $"Hello {name}!"; -} -``` - -### Minimal Example (Python) - -```python -import azure.functions as func -import azure.durable_functions as df - -my_app = df.DFApp(http_auth_level=func.AuthLevel.FUNCTION) - -# HTTP Starter -@my_app.route(route="orchestrators/{function_name}", methods=["POST"]) -@my_app.durable_client_input(client_name="client") -async def http_start(req: func.HttpRequest, client): - function_name = req.route_params.get('function_name') - instance_id = await client.start_new(function_name) - return client.create_check_status_response(req, instance_id) - -# Orchestrator -@my_app.orchestration_trigger(context_name="context") -def my_orchestration(context: df.DurableOrchestrationContext): - result1 = yield context.call_activity("say_hello", "Tokyo") - result2 = yield context.call_activity("say_hello", "Seattle") - return f"{result1}, {result2}" - -# Activity -@my_app.activity_trigger(input_name="name") -def say_hello(name: str) -> str: - return f"Hello {name}!" -``` - -### Minimal Example (Java) - -```java -import com.microsoft.azure.functions.*; -import com.microsoft.azure.functions.annotation.*; -import com.microsoft.durabletask.*; -import com.microsoft.durabletask.azurefunctions.*; - -public class DurableFunctionsApp { - - @FunctionName("HttpStart") - public HttpResponseMessage httpStart( - @HttpTrigger(name = "req", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.FUNCTION) - HttpRequestMessage request, - @DurableClientInput(name = "durableContext") DurableClientContext durableContext) { - DurableTaskClient client = durableContext.getClient(); - String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration"); - return durableContext.createCheckStatusResponse(request, instanceId); - } - - @FunctionName("MyOrchestration") - public String myOrchestration( - @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { - String result1 = ctx.callActivity("SayHello", "Tokyo", String.class).await(); - String result2 = ctx.callActivity("SayHello", "Seattle", String.class).await(); - return result1 + ", " + result2; - } - - @FunctionName("SayHello") - public String sayHello(@DurableActivityTrigger(name = "name") String name) { - return "Hello " + name + "!"; - } -} -``` - -### Minimal Example (JavaScript) - -```javascript -const { app } = require("@azure/functions"); -const df = require("durable-functions"); - -// Activity -df.app.activity("sayHello", { - handler: (city) => `Hello ${city}!`, -}); - -// Orchestrator -df.app.orchestration("myOrchestration", function* (context) { - const result1 = yield context.df.callActivity("sayHello", "Tokyo"); - const result2 = yield context.df.callActivity("sayHello", "Seattle"); - return `${result1}, ${result2}`; -}); - -// HTTP Starter -app.http("HttpStart", { - route: "orchestrators/{orchestrationName}", - methods: ["POST"], - authLevel: "function", - extraInputs: [df.input.durableClient()], - handler: async (request, context) => { - const client = df.getClient(context); - const instanceId = await client.startNew(request.params.orchestrationName); - return client.createCheckStatusResponse(request, instanceId); - }, -}); -``` - -## Workflow Patterns - -| Pattern | Use When | -|---------|----------| -| **Function Chaining** | Sequential steps, each depends on previous | -| **Fan-Out/Fan-In** | Parallel processing with aggregated results | -| **Async HTTP APIs** | Long-running operations with HTTP polling | -| **Monitor** | Periodic polling with configurable timeouts | -| **Human Interaction** | Workflow pauses for external input/approval | -| **Saga** | Distributed transactions with compensation | -| **Durable Entities** | Stateful objects (counters, accounts) | - -### Fan-Out/Fan-In Example (.NET) - -```csharp -[Function(nameof(FanOutFanIn))] -public static async Task FanOutFanIn([OrchestrationTrigger] TaskOrchestrationContext context) -{ - string[] cities = { "Tokyo", "Seattle", "London", "Paris", "Berlin" }; - - // Fan-out: schedule all in parallel - var tasks = cities.Select(city => context.CallActivityAsync(nameof(SayHello), city)); - - // Fan-in: wait for all - return await Task.WhenAll(tasks); -} -``` - -### Fan-Out/Fan-In Example (Python) - -```python -@my_app.orchestration_trigger(context_name="context") -def fan_out_fan_in(context: df.DurableOrchestrationContext): - cities = ["Tokyo", "Seattle", "London", "Paris", "Berlin"] - - # Fan-out: schedule all in parallel - parallel_tasks = [] - for city in cities: - task = context.call_activity("say_hello", city) - parallel_tasks.append(task) - - # Fan-in: wait for all - results = yield context.task_all(parallel_tasks) - return results -``` - -### Fan-Out/Fan-In Example (Java) - -```java -@FunctionName("FanOutFanIn") -public List fanOutFanIn( - @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { - String[] cities = {"Tokyo", "Seattle", "London", "Paris", "Berlin"}; - List> parallelTasks = new ArrayList<>(); - - // Fan-out: schedule all activities in parallel - for (String city : cities) { - parallelTasks.add(ctx.callActivity("SayHello", city, String.class)); - } - - // Fan-in: wait for all to complete - List results = new ArrayList<>(); - for (Task task : parallelTasks) { - results.add(task.await()); - } - - return results; -} -``` - -### Fan-Out/Fan-In Example (JavaScript) - -```javascript -df.app.orchestration("fanOutFanIn", function* (context) { - const cities = ["Tokyo", "Seattle", "London", "Paris", "Berlin"]; - - // Fan-out: schedule all activities in parallel - const tasks = cities.map((city) => context.df.callActivity("sayHello", city)); - - // Fan-in: wait for all to complete - const results = yield context.df.Task.all(tasks); - return results; -}); -``` - -### Human Interaction Example (.NET) - -```csharp -[Function(nameof(ApprovalWorkflow))] -public static async Task ApprovalWorkflow([OrchestrationTrigger] TaskOrchestrationContext context) -{ - await context.CallActivityAsync(nameof(SendApprovalRequest), context.GetInput()); - - // Wait for approval event with timeout - using var cts = new CancellationTokenSource(); - var approvalTask = context.WaitForExternalEvent("ApprovalEvent"); - var timeoutTask = context.CreateTimer(context.CurrentUtcDateTime.AddDays(3), cts.Token); - - var winner = await Task.WhenAny(approvalTask, timeoutTask); - - if (winner == approvalTask) - { - cts.Cancel(); - return await approvalTask ? "Approved" : "Rejected"; - } - return "Timed out"; -} -``` - -### Human Interaction Example (Python) - -```python -import datetime - -@my_app.orchestration_trigger(context_name="context") -def approval_workflow(context: df.DurableOrchestrationContext): - yield context.call_activity("send_approval_request", context.get_input()) - - # Wait for approval event with timeout - timeout = context.current_utc_datetime + datetime.timedelta(days=3) - approval_task = context.wait_for_external_event("ApprovalEvent") - timeout_task = context.create_timer(timeout) - - winner = yield context.task_any([approval_task, timeout_task]) - - if winner == approval_task: - approved = approval_task.result - return "Approved" if approved else "Rejected" - return "Timed out" -``` - -### Human Interaction Example (Java) - -```java -@FunctionName("ApprovalWorkflow") -public String approvalWorkflow( - @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { - ctx.callActivity("SendApprovalRequest", ctx.getInput(String.class)).await(); - - // Wait for approval event with timeout - Task approvalTask = ctx.waitForExternalEvent("ApprovalEvent", Boolean.class); - Task timeoutTask = ctx.createTimer(Duration.ofDays(3)); - - Task winner = ctx.anyOf(approvalTask, timeoutTask).await(); - - if (winner == approvalTask) { - return approvalTask.await() ? "Approved" : "Rejected"; - } - return "Timed out"; -} -``` - -### Human Interaction Example (JavaScript) - -```javascript -df.app.orchestration("approvalWorkflow", function* (context) { - yield context.df.callActivity("sendApprovalRequest", context.df.getInput()); - - // Wait for approval event with timeout - const expiration = new Date(context.df.currentUtcDateTime); - expiration.setDate(expiration.getDate() + 3); - - const approvalTask = context.df.waitForExternalEvent("ApprovalEvent"); - const timeoutTask = context.df.createTimer(expiration); - - const winner = yield context.df.Task.any([approvalTask, timeoutTask]); - - if (winner === approvalTask) { - return approvalTask.result ? "Approved" : "Rejected"; - } - return "Timed out"; -}); -``` - -## Critical: Orchestration Determinism - -Orchestrations replay from history — all code MUST be deterministic. - -### .NET Determinism Rules - -| ❌ NEVER | ✅ ALWAYS USE | -|----------|--------------| -| `DateTime.Now` | `context.CurrentUtcDateTime` | -| `Guid.NewGuid()` | `context.NewGuid()` | -| `Random` | Pass random values from activities | -| `Task.Delay()`, `Thread.Sleep()` | `context.CreateTimer()` | -| Direct I/O, HTTP, database | `context.CallActivityAsync()` | - -### Python Determinism Rules - -| ❌ NEVER | ✅ ALWAYS USE | -|----------|--------------| -| `datetime.now()` | `context.current_utc_datetime` | -| `uuid.uuid4()` | `context.new_uuid()` | -| `random.random()` | Pass random values from activities | -| `time.sleep()` | `context.create_timer()` | -| Direct I/O, HTTP, database | `context.call_activity()` | - -### Java Determinism Rules - -| ❌ NEVER | ✅ ALWAYS USE | -|----------|--------------| -| `System.currentTimeMillis()` | `ctx.getCurrentInstant()` | -| `UUID.randomUUID()` | Pass random values from activities | -| `Thread.sleep()` | `ctx.createTimer()` | -| Direct I/O, HTTP, database | `ctx.callActivity()` | - -### JavaScript Determinism Rules - -| ❌ NEVER | ✅ ALWAYS USE | -|----------|--------------| -| `new Date()` | `context.df.currentUtcDateTime` | -| `Math.random()` | Pass random values from activities | -| `setTimeout()` | `context.df.createTimer()` | -| Direct I/O, HTTP, database | `context.df.callActivity()` | - -### Logging in Orchestrations - -Use replay-safe logging to avoid duplicate log entries: - -**.NET:** -```csharp -[Function(nameof(MyOrchestration))] -public static async Task MyOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) -{ - ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration)); - logger.LogInformation("Started"); // Only logs once, not on replay - return await context.CallActivityAsync(nameof(MyActivity), "input"); -} -``` - -**Python:** -```python -import logging - -@my_app.orchestration_trigger(context_name="context") -def my_orchestration(context: df.DurableOrchestrationContext): - # Check if replaying to avoid duplicate logs - if not context.is_replaying: - logging.info("Started") # Only logs once, not on replay - result = yield context.call_activity("my_activity", "input") - return result -``` - -**Java:** -```java -@FunctionName("MyOrchestration") -public String myOrchestration( - @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { - // Use isReplaying to avoid duplicate logs - if (!ctx.getIsReplaying()) { - logger.info("Started"); // Only logs once, not on replay - } - return ctx.callActivity("MyActivity", "input", String.class).await(); -} -``` - -**JavaScript:** -```javascript -df.app.orchestration("myOrchestration", function* (context) { - if (!context.df.isReplaying) { - console.log("Started"); // Only logs once, not on replay - } - const result = yield context.df.callActivity("myActivity", "input"); - return result; -}); -``` - -## Connection & Authentication - -| Environment | Connection String | -|-------------|-------------------| -| Local Emulator | `Endpoint=http://localhost:8080;Authentication=None` | -| Azure (System-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity` | -| Azure (User-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=` | - -> **⚠️ NOTE**: Durable Task Scheduler uses identity-based authentication only — no connection strings with keys. When using a User-Assigned Managed Identity (UAMI), you must include the `ClientID` in the connection string. - -## Error Handling & Retry - -**.NET:** -```csharp -var retryOptions = new TaskOptions -{ - Retry = new RetryPolicy( - maxNumberOfAttempts: 3, - firstRetryInterval: TimeSpan.FromSeconds(5), - backoffCoefficient: 2.0, - maxRetryInterval: TimeSpan.FromMinutes(1)) -}; - -try -{ - await context.CallActivityAsync(nameof(UnreliableService), input, retryOptions); -} -catch (TaskFailedException ex) -{ - context.SetCustomStatus(new { Error = ex.Message }); - await context.CallActivityAsync(nameof(CompensationActivity), input); -} -``` - -**Python:** -```python -retry_options = df.RetryOptions( - first_retry_interval_in_milliseconds=5000, - max_number_of_attempts=3, - backoff_coefficient=2.0, - max_retry_interval_in_milliseconds=60000 -) - -@my_app.orchestration_trigger(context_name="context") -def workflow_with_retry(context: df.DurableOrchestrationContext): - try: - result = yield context.call_activity_with_retry( - "unreliable_service", - retry_options, - context.get_input() - ) - return result - except Exception as ex: - context.set_custom_status({"error": str(ex)}) - yield context.call_activity("compensation_activity", context.get_input()) - return "Compensated" -``` - -**Java:** -```java -@FunctionName("WorkflowWithRetry") -public String workflowWithRetry( - @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { - TaskOptions retryOptions = new TaskOptions(new RetryPolicy( - 3, // maxNumberOfAttempts - Duration.ofSeconds(5) // firstRetryInterval - )); - - try { - return ctx.callActivity("UnreliableService", ctx.getInput(String.class), - retryOptions, String.class).await(); - } catch (TaskFailedException ex) { - ctx.setCustomStatus(Map.of("Error", ex.getMessage())); - ctx.callActivity("CompensationActivity", ctx.getInput(String.class)).await(); - return "Compensated"; - } -} -``` - -**JavaScript:** -```javascript -df.app.orchestration("workflowWithRetry", function* (context) { - const retryOptions = new df.RetryOptions(5000, 3); // firstRetryInterval, maxAttempts - retryOptions.backoffCoefficient = 2.0; - retryOptions.maxRetryIntervalInMilliseconds = 60000; - - try { - const result = yield context.df.callActivityWithRetry( - "unreliableService", - retryOptions, - context.df.getInput() - ); - return result; - } catch (ex) { - context.df.setCustomStatus({ error: ex.message }); - yield context.df.callActivity("compensationActivity", context.df.getInput()); - return "Compensated"; - } -}); -``` - -## Durable Task SDKs (Non-Functions) - -For applications running outside Azure Functions: - -### .NET SDK - -```csharp -var connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; - -// Worker -builder.Services.AddDurableTaskWorker() - .AddTasks(registry => registry.AddAllGeneratedTasks()) - .UseDurableTaskScheduler(connectionString); - -// Client -var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build(); -string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input); -``` - -### Python SDK - -```python -import asyncio -from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker - -# Activity function -def say_hello(ctx, name: str) -> str: - return f"Hello {name}!" - -# Orchestrator function -def my_orchestration(ctx, name: str) -> str: - result = yield ctx.call_activity('say_hello', input=name) - return result - -async def main(): - with DurableTaskSchedulerWorker( - host_address="http://localhost:8080", - secure_channel=False, - taskhub="default" - ) as worker: - worker.add_activity(say_hello) - worker.add_orchestrator(my_orchestration) - worker.start() - - # Client - from durabletask.azuremanaged.client import DurableTaskSchedulerClient - client = DurableTaskSchedulerClient( - host_address="http://localhost:8080", - taskhub="default", - token_credential=None, - secure_channel=False - ) - instance_id = client.schedule_new_orchestration("my_orchestration", input="World") - result = client.wait_for_orchestration_completion(instance_id, timeout=30) - print(f"Output: {result.serialized_output}") - -if __name__ == "__main__": - asyncio.run(main()) -``` - -### Java SDK - -```java -import com.microsoft.durabletask.*; -import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions; -import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientExtensions; - -import java.time.Duration; - -public class App { - public static void main(String[] args) throws Exception { - String connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; - - // Worker - DurableTaskGrpcWorker worker = DurableTaskSchedulerWorkerExtensions - .createWorkerBuilder(connectionString) - .addOrchestration(new TaskOrchestrationFactory() { - @Override public String getName() { return "MyOrchestration"; } - @Override public TaskOrchestration create() { - return ctx -> { - String result = ctx.callActivity("SayHello", - ctx.getInput(String.class), String.class).await(); - ctx.complete(result); - }; - } - }) - .addActivity(new TaskActivityFactory() { - @Override public String getName() { return "SayHello"; } - @Override public TaskActivity create() { - return ctx -> "Hello " + ctx.getInput(String.class) + "!"; - } - }) - .build(); - - worker.start(); - - // Client - DurableTaskClient client = DurableTaskSchedulerClientExtensions - .createClientBuilder(connectionString).build(); - String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration", "World"); - OrchestrationMetadata result = client.waitForInstanceCompletion( - instanceId, Duration.ofSeconds(30), true); - System.out.println("Output: " + result.readOutputAs(String.class)); - - worker.stop(); - } -} -``` - -### JavaScript SDK - -```javascript -import { createAzureManagedWorkerBuilder, createAzureManagedClient } from "@microsoft/durabletask-js-azuremanaged"; - -const connectionString = "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; - -// Activity -const sayHello = async (_ctx, name) => `Hello ${name}!`; - -// Orchestrator -const myOrchestration = async function* (ctx, name) { - const result = yield ctx.callActivity(sayHello, name); - return result; -}; - -// Worker -const worker = createAzureManagedWorkerBuilder(connectionString) - .addOrchestrator(myOrchestration) - .addActivity(sayHello) - .build(); - -await worker.start(); - -// Client -const client = createAzureManagedClient(connectionString); -const instanceId = await client.scheduleNewOrchestration("myOrchestration", "World"); -const state = await client.waitForOrchestrationCompletion(instanceId, true, 30); -console.log("Output:", state.serializedOutput); - -await client.stop(); -await worker.stop(); -``` - -## Azure Deployment - -For provisioning, Bicep templates, managed identity configuration, and deployment workflows, invoke the **azure-deploy** skill which includes DTS-specific deployment guidance. - -## Troubleshooting - -| Error | Cause | Fix | -|-------|-------|-----| -| **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource | Assign `Durable Task Data Contributor` role (`5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=` | -| **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | -| **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure `TASKHUB_NAME` app setting matches the provisioned task hub name | - -## References - -- [Official Documentation](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) -- [Durable Functions Overview](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview) -- [Sample Repository](https://github.com/Azure-Samples/Durable-Task-Scheduler) -- [Choosing an Orchestration Framework](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/choose-orchestration-framework) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md new file mode 100644 index 000000000..75a6ad99f --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md @@ -0,0 +1,79 @@ +# Durable Task Scheduler + +Build reliable, fault-tolerant workflows using durable execution with Azure Durable Task Scheduler. + +## When to Use + +- Long-running workflows requiring state persistence +- Distributed transactions with compensating actions (saga pattern) +- Multi-step orchestrations with checkpointing +- Fan-out/fan-in parallel processing +- Workflows requiring human interaction or external events +- Stateful entities (aggregators, counters, state machines) +- Multi-agent AI orchestration +- Data processing pipelines + +## Framework Selection + +| Framework | Best For | Hosting | +|-----------|----------|---------| +| **Durable Functions** | Serverless event-driven apps | Azure Functions | +| **Durable Task SDKs** | Any compute (containers, VMs) | ACA, AKS, App Service, VMs | + +> **💡 TIP**: Use Durable Functions for serverless with built-in triggers. Use Durable Task SDKs for hosting flexibility. + +## Quick Start - Local Emulator + +```bash +# Start the emulator (update the tag as needed; see https://mcr.microsoft.com/v2/dts/dts-emulator/tags/list for newer versions) +docker pull mcr.microsoft.com/dts/dts-emulator:v0.0.10 +docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:v0.0.10 + +# Dashboard available at http://localhost:8082 +``` + +## Workflow Patterns + +| Pattern | Use When | +|---------|----------| +| **Function Chaining** | Sequential steps, each depends on previous | +| **Fan-Out/Fan-In** | Parallel processing with aggregated results | +| **Async HTTP APIs** | Long-running operations with HTTP polling | +| **Monitor** | Periodic polling with configurable timeouts | +| **Human Interaction** | Workflow pauses for external input/approval | +| **Saga** | Distributed transactions with compensation | +| **Durable Entities** | Stateful objects (counters, accounts) | + +## Connection & Authentication + +| Environment | Connection String | +|-------------|-------------------| +| Local Development (Emulator) | `Endpoint=http://localhost:8080;Authentication=None` | +| Azure (System-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity` | +| Azure (User-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=` | + +> **⚠️ NOTE**: Durable Task Scheduler uses identity-based authentication only — no connection strings with keys. When using a User-Assigned Managed Identity (UAMI), you must include the `ClientID` in the connection string. + +## Azure Deployment + +For provisioning, Bicep templates, managed identity configuration, and deployment workflows, invoke the **azure-deploy** skill which includes DTS-specific deployment guidance. + +## Troubleshooting + +| Error | Cause | Fix | +|-------|-------|-----| +| **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource | Assign `Durable Task Data Contributor` role (`5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=` | +| **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | +| **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure `TASKHUB_NAME` app setting matches the provisioned task hub name | + +## References + +- [.NET](dotnet.md) — packages, setup, examples, determinism, retry, SDK +- [Python](python.md) — packages, setup, examples, determinism, retry, SDK +- [Java](java.md) — dependencies, setup, examples, determinism, retry, SDK +- [JavaScript](javascript.md) — packages, setup, examples, determinism, retry, SDK +- [Bicep Patterns](bicep.md) — scheduler, task hub, RBAC, CLI provisioning +- [Official Documentation](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) +- [Durable Functions Overview](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview) +- [Sample Repository](https://github.com/Azure-Samples/Durable-Task-Scheduler) +- [Choosing an Orchestration Framework](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/choose-orchestration-framework) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md new file mode 100644 index 000000000..f6a57d673 --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md @@ -0,0 +1,88 @@ +# Durable Task Scheduler — Bicep Patterns + +Bicep templates for provisioning the Durable Task Scheduler, task hubs, and RBAC role assignments. + +## Scheduler + Task Hub + +```bicep +// Parameters — define these at file level or pass from a parent module +param schedulerName string +param location string = resourceGroup().location + +@allowed(['consumption', 'dedicated']) +@description('Use consumption for quickstarts/variable workloads, dedicated for high-demand/predictable throughput') +param skuName string = 'consumption' + +resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { + name: schedulerName + location: location + properties: { + sku: { name: skuName } + } +} + +resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { + parent: scheduler + name: 'default' +} +``` + +## SKU Selection + +| SKU | Best For | +|-----|----------| +| **Consumption** | quickstarts, variable or bursty workloads, pay-per-use | +| **Dedicated** | High-demand workloads, predictable throughput requirements | + +> **💡 TIP**: Start with `consumption` for development and variable workloads. Switch to `dedicated` when you need consistent, high-throughput performance. + +## RBAC — Durable Task Data Contributor + +The Function App's managed identity **must** have the `Durable Task Data Contributor` role on the scheduler resource. Without it, the app receives **403 PermissionDenied** on gRPC calls. + +```bicep +// Assumes functionApp is defined elsewhere in the same Bicep file, e.g.: +// resource functionApp 'Microsoft.Web/sites@2023-12-01' = { ... } + +var durableTaskDataContributorRoleId = '5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09' + +resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(scheduler.id, functionApp.id, durableTaskDataContributorRoleId) + scope: scheduler + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) + principalId: functionApp.identity.principalId + principalType: 'ServicePrincipal' + } +} +``` + +## Connection String App Setting + +Include these entries in the Function App resource's `siteConfig.appSettings` array: + +```bicep +{ + name: 'DTS_CONNECTION_STRING' + value: 'Endpoint=https://${scheduler.name}.durabletask.io;Authentication=ManagedIdentity' +} +{ + name: 'TASKHUB_NAME' + value: taskHub.name +} +``` + +## Provision via CLI + +```bash +# Create scheduler (consumption SKU for getting started) +az durabletask scheduler create \ + --resource-group myResourceGroup \ + --name my-scheduler \ + --location eastus \ + --sku consumption +``` + +## Full Deployment Reference + +For complete deployment workflows, AZD commands, and managed identity CLI setup, see the [Durable Task Scheduler Deployment](../../../../azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md) recipe in the azure-deploy skill. diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md new file mode 100644 index 000000000..8c83d6ef1 --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md @@ -0,0 +1,181 @@ +# Durable Task Scheduler — .NET + +## Durable Functions Setup + +### Required NuGet Packages + +```xml + + + + + +``` + +> **💡 Finding latest versions**: Search [nuget.org](https://www.nuget.org/) for each package name to find the current stable version. Look for the `Microsoft.Azure.Functions.Worker.Extensions.DurableTask` and `Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged` packages. + +### host.json + +```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 + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; + +public static class DurableFunctionsApp +{ + [Function("HttpStart")] + public static async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req, + [DurableClient] DurableTaskClient client) + { + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestration)); + return await client.CreateCheckStatusResponseAsync(req, instanceId); + } + + [Function(nameof(MyOrchestration))] + public static async Task MyOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) + { + var result1 = await context.CallActivityAsync(nameof(SayHello), "Tokyo"); + var result2 = await context.CallActivityAsync(nameof(SayHello), "Seattle"); + return $"{result1}, {result2}"; + } + + [Function(nameof(SayHello))] + public static string SayHello([ActivityTrigger] string name) => $"Hello {name}!"; +} +``` + +## Workflow Patterns + +### Fan-Out/Fan-In + +```csharp +[Function(nameof(FanOutFanIn))] +public static async Task FanOutFanIn([OrchestrationTrigger] TaskOrchestrationContext context) +{ + string[] cities = { "Tokyo", "Seattle", "London", "Paris", "Berlin" }; + + // Fan-out: schedule all in parallel + var tasks = cities.Select(city => context.CallActivityAsync(nameof(SayHello), city)); + + // Fan-in: wait for all + return await Task.WhenAll(tasks); +} +``` + +### Human Interaction + +```csharp +[Function(nameof(ApprovalWorkflow))] +public static async Task ApprovalWorkflow([OrchestrationTrigger] TaskOrchestrationContext context) +{ + await context.CallActivityAsync(nameof(SendApprovalRequest), context.GetInput()); + + // Wait for approval event with timeout + using var cts = new CancellationTokenSource(); + var approvalTask = context.WaitForExternalEvent("ApprovalEvent"); + var timeoutTask = context.CreateTimer(context.CurrentUtcDateTime.AddDays(3), cts.Token); + + var winner = await Task.WhenAny(approvalTask, timeoutTask); + + if (winner == approvalTask) + { + cts.Cancel(); + return await approvalTask ? "Approved" : "Rejected"; + } + return "Timed out"; +} +``` + +## Orchestration Determinism + +| ❌ NEVER | ✅ ALWAYS USE | +|----------|--------------| +| `DateTime.Now` | `context.CurrentUtcDateTime` | +| `Guid.NewGuid()` | `context.NewGuid()` | +| `Random` | Pass random values from activities | +| `Task.Delay()`, `Thread.Sleep()` | `context.CreateTimer()` | +| Direct I/O, HTTP, database | `context.CallActivityAsync()` | + +### Replay-Safe Logging + +```csharp +[Function(nameof(MyOrchestration))] +public static async Task MyOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration)); + logger.LogInformation("Started"); // Only logs once, not on replay + return await context.CallActivityAsync(nameof(MyActivity), "input"); +} +``` + +## Error Handling & Retry + +```csharp +var retryOptions = new TaskOptions +{ + Retry = new RetryPolicy( + maxNumberOfAttempts: 3, + firstRetryInterval: TimeSpan.FromSeconds(5), + backoffCoefficient: 2.0, + maxRetryInterval: TimeSpan.FromMinutes(1)) +}; + +try +{ + await context.CallActivityAsync(nameof(UnreliableService), input, retryOptions); +} +catch (TaskFailedException ex) +{ + context.SetCustomStatus(new { Error = ex.Message }); + await context.CallActivityAsync(nameof(CompensationActivity), input); +} +``` + +## Durable Task SDK (Non-Functions) + +For applications running outside Azure Functions (containers, VMs, ACA, AKS): + +```csharp +var connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + +// Worker +builder.Services.AddDurableTaskWorker() + .AddTasks(registry => registry.AddAllGeneratedTasks()) + .UseDurableTaskScheduler(connectionString); + +// Client +var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build(); +string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input); +``` diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md new file mode 100644 index 000000000..fdfba3922 --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md @@ -0,0 +1,234 @@ +# Durable Task Scheduler — Java + +## Durable Functions Setup + +### Required Maven Dependencies + +```xml + + + com.microsoft.azure.functions + azure-functions-java-library + 3.2.3 + + + com.microsoft + durabletask-azure-functions + 1.7.0 + + +``` + +> **💡 Finding latest versions**: Search [Maven Central](https://central.sonatype.com/) for `durabletask-azure-functions` (group: `com.microsoft`) to find the current stable version. + +### host.json + +```json +{ + "version": "2.0", + "extensions": { + "durableTask": { + "hubName": "default", + "storageProvider": { + "type": "durabletask-scheduler", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} +``` + +### local.settings.json + +```json +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "java", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + } +} +``` + +## Minimal Example + +```java +import com.microsoft.azure.functions.*; +import com.microsoft.azure.functions.annotation.*; +import com.microsoft.durabletask.*; +import com.microsoft.durabletask.azurefunctions.*; + +public class DurableFunctionsApp { + + @FunctionName("HttpStart") + public HttpResponseMessage httpStart( + @HttpTrigger(name = "req", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.FUNCTION) + HttpRequestMessage request, + @DurableClientInput(name = "durableContext") DurableClientContext durableContext) { + DurableTaskClient client = durableContext.getClient(); + String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration"); + return durableContext.createCheckStatusResponse(request, instanceId); + } + + @FunctionName("MyOrchestration") + public String myOrchestration( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + String result1 = ctx.callActivity("SayHello", "Tokyo", String.class).await(); + String result2 = ctx.callActivity("SayHello", "Seattle", String.class).await(); + return result1 + ", " + result2; + } + + @FunctionName("SayHello") + public String sayHello(@DurableActivityTrigger(name = "name") String name) { + return "Hello " + name + "!"; + } +} +``` + +## Workflow Patterns + +### Fan-Out/Fan-In + +```java +@FunctionName("FanOutFanIn") +public List fanOutFanIn( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + String[] cities = {"Tokyo", "Seattle", "London", "Paris", "Berlin"}; + List> parallelTasks = new ArrayList<>(); + + // Fan-out: schedule all activities in parallel + for (String city : cities) { + parallelTasks.add(ctx.callActivity("SayHello", city, String.class)); + } + + // Fan-in: wait for all to complete + List results = new ArrayList<>(); + for (Task task : parallelTasks) { + results.add(task.await()); + } + + return results; +} +``` + +### Human Interaction + +```java +@FunctionName("ApprovalWorkflow") +public String approvalWorkflow( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + ctx.callActivity("SendApprovalRequest", ctx.getInput(String.class)).await(); + + // Wait for approval event with timeout + Task approvalTask = ctx.waitForExternalEvent("ApprovalEvent", Boolean.class); + Task timeoutTask = ctx.createTimer(Duration.ofDays(3)); + + Task winner = ctx.anyOf(approvalTask, timeoutTask).await(); + + if (winner == approvalTask) { + return approvalTask.await() ? "Approved" : "Rejected"; + } + return "Timed out"; +} +``` + +## Orchestration Determinism + +| ❌ NEVER | ✅ ALWAYS USE | +|----------|--------------| +| `System.currentTimeMillis()` | `ctx.getCurrentInstant()` | +| `UUID.randomUUID()` | Pass random values from activities | +| `Thread.sleep()` | `ctx.createTimer()` | +| Direct I/O, HTTP, database | `ctx.callActivity()` | + +### Replay-Safe Logging + +```java +@FunctionName("MyOrchestration") +public String myOrchestration( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + // Use isReplaying to avoid duplicate logs + if (!ctx.getIsReplaying()) { + logger.info("Started"); // Only logs once, not on replay + } + return ctx.callActivity("MyActivity", "input", String.class).await(); +} +``` + +## Error Handling & Retry + +```java +@FunctionName("WorkflowWithRetry") +public String workflowWithRetry( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + TaskOptions retryOptions = new TaskOptions(new RetryPolicy( + 3, // maxNumberOfAttempts + Duration.ofSeconds(5) // firstRetryInterval + )); + + try { + return ctx.callActivity("UnreliableService", ctx.getInput(String.class), + retryOptions, String.class).await(); + } catch (TaskFailedException ex) { + ctx.setCustomStatus(Map.of("Error", ex.getMessage())); + ctx.callActivity("CompensationActivity", ctx.getInput(String.class)).await(); + return "Compensated"; + } +} +``` + +## Durable Task SDK (Non-Functions) + +For applications running outside Azure Functions (containers, VMs, ACA, AKS): + +```java +import com.microsoft.durabletask.*; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientExtensions; + +import java.time.Duration; + +public class App { + public static void main(String[] args) throws Exception { + String connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + + // Worker + DurableTaskGrpcWorker worker = DurableTaskSchedulerWorkerExtensions + .createWorkerBuilder(connectionString) + .addOrchestration(new TaskOrchestrationFactory() { + @Override public String getName() { return "MyOrchestration"; } + @Override public TaskOrchestration create() { + return ctx -> { + String result = ctx.callActivity("SayHello", + ctx.getInput(String.class), String.class).await(); + ctx.complete(result); + }; + } + }) + .addActivity(new TaskActivityFactory() { + @Override public String getName() { return "SayHello"; } + @Override public TaskActivity create() { + return ctx -> "Hello " + ctx.getInput(String.class) + "!"; + } + }) + .build(); + + worker.start(); + + // Client + DurableTaskClient client = DurableTaskSchedulerClientExtensions + .createClientBuilder(connectionString).build(); + String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration", "World"); + OrchestrationMetadata result = client.waitForInstanceCompletion( + instanceId, Duration.ofSeconds(30), true); + System.out.println("Output: " + result.readOutputAs(String.class)); + + worker.stop(); + } +} +``` diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md new file mode 100644 index 000000000..07739ac02 --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md @@ -0,0 +1,201 @@ +# Durable Task Scheduler — JavaScript + +## Durable Functions Setup + +### Required npm Packages + +```json +{ + "dependencies": { + "@azure/functions": "^4.0.0", + "durable-functions": "^3.0.0" + } +} +``` + +> **💡 Finding latest versions**: Run `npm view durable-functions version` or check [npmjs.com/package/durable-functions](https://www.npmjs.com/package/durable-functions) for the latest stable release. + +### host.json + +```json +{ + "version": "2.0", + "extensions": { + "durableTask": { + "hubName": "default", + "storageProvider": { + "type": "durabletask-scheduler", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} +``` + +### local.settings.json + +```json +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + } +} +``` + +## Minimal Example + +```javascript +const { app } = require("@azure/functions"); +const df = require("durable-functions"); + +// Activity +df.app.activity("sayHello", { + handler: (city) => `Hello ${city}!`, +}); + +// Orchestrator +df.app.orchestration("myOrchestration", function* (context) { + const result1 = yield context.df.callActivity("sayHello", "Tokyo"); + const result2 = yield context.df.callActivity("sayHello", "Seattle"); + return `${result1}, ${result2}`; +}); + +// HTTP Starter +app.http("HttpStart", { + route: "orchestrators/{orchestrationName}", + methods: ["POST"], + authLevel: "function", + extraInputs: [df.input.durableClient()], + handler: async (request, context) => { + const client = df.getClient(context); + const instanceId = await client.startNew(request.params.orchestrationName); + return client.createCheckStatusResponse(request, instanceId); + }, +}); +``` + +## Workflow Patterns + +### Fan-Out/Fan-In + +```javascript +df.app.orchestration("fanOutFanIn", function* (context) { + const cities = ["Tokyo", "Seattle", "London", "Paris", "Berlin"]; + + // Fan-out: schedule all activities in parallel + const tasks = cities.map((city) => context.df.callActivity("sayHello", city)); + + // Fan-in: wait for all to complete + const results = yield context.df.Task.all(tasks); + return results; +}); +``` + +### Human Interaction + +```javascript +df.app.orchestration("approvalWorkflow", function* (context) { + yield context.df.callActivity("sendApprovalRequest", context.df.getInput()); + + // Wait for approval event with timeout + const expiration = new Date(context.df.currentUtcDateTime); + expiration.setDate(expiration.getDate() + 3); + + const approvalTask = context.df.waitForExternalEvent("ApprovalEvent"); + const timeoutTask = context.df.createTimer(expiration); + + const winner = yield context.df.Task.any([approvalTask, timeoutTask]); + + if (winner === approvalTask) { + return approvalTask.result ? "Approved" : "Rejected"; + } + return "Timed out"; +}); +``` + +## Orchestration Determinism + +| ❌ NEVER | ✅ ALWAYS USE | +|----------|--------------| +| `new Date()` | `context.df.currentUtcDateTime` | +| `Math.random()` | Pass random values from activities | +| `setTimeout()` | `context.df.createTimer()` | +| Direct I/O, HTTP, database | `context.df.callActivity()` | + +### Replay-Safe Logging + +```javascript +df.app.orchestration("myOrchestration", function* (context) { + if (!context.df.isReplaying) { + console.log("Started"); // Only logs once, not on replay + } + const result = yield context.df.callActivity("myActivity", "input"); + return result; +}); +``` + +## Error Handling & Retry + +```javascript +df.app.orchestration("workflowWithRetry", function* (context) { + const retryOptions = new df.RetryOptions(5000, 3); // firstRetryInterval, maxAttempts + retryOptions.backoffCoefficient = 2.0; + retryOptions.maxRetryIntervalInMilliseconds = 60000; + + try { + const result = yield context.df.callActivityWithRetry( + "unreliableService", + retryOptions, + context.df.getInput() + ); + return result; + } catch (ex) { + context.df.setCustomStatus({ error: ex.message }); + yield context.df.callActivity("compensationActivity", context.df.getInput()); + return "Compensated"; + } +}); +``` + +## Durable Task SDK (Non-Functions) + +For applications running outside Azure Functions (containers, VMs, ACA, AKS): + +```javascript +import { createAzureManagedWorkerBuilder, createAzureManagedClient } from "@microsoft/durabletask-js-azuremanaged"; + +const connectionString = "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; + +// Activity +const sayHello = async (_ctx, name) => `Hello ${name}!`; + +// Orchestrator +const myOrchestration = async function* (ctx, name) { + const result = yield ctx.callActivity(sayHello, name); + return result; +}; + +// Worker +const worker = createAzureManagedWorkerBuilder(connectionString) + .addOrchestrator(myOrchestration) + .addActivity(sayHello) + .build(); + +await worker.start(); + +// Client +const client = createAzureManagedClient(connectionString); +const instanceId = await client.scheduleNewOrchestration("myOrchestration", "World"); +const state = await client.waitForOrchestrationCompletion(instanceId, true, 30); +console.log("Output:", state.serializedOutput); + +await client.stop(); +await worker.stop(); +``` diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md new file mode 100644 index 000000000..295645b5f --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md @@ -0,0 +1,208 @@ +# Durable Task Scheduler — Python + +## Durable Functions Setup + +### Required Packages + +```txt +# requirements.txt +azure-functions +azure-functions-durable +azure-identity +``` + +> **💡 Finding latest versions**: Run `pip index versions azure-functions-durable` or check [pypi.org/project/azure-functions-durable](https://pypi.org/project/azure-functions-durable/) for the latest stable release. + +### host.json + +```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": "python", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None", + "TASKHUB_NAME": "default" + } +} +``` + +## Minimal Example + +```python +import azure.functions as func +import azure.durable_functions as df + +my_app = df.DFApp(http_auth_level=func.AuthLevel.FUNCTION) + +# HTTP Starter +@my_app.route(route="orchestrators/{function_name}", methods=["POST"]) +@my_app.durable_client_input(client_name="client") +async def http_start(req: func.HttpRequest, client): + function_name = req.route_params.get('function_name') + instance_id = await client.start_new(function_name) + return client.create_check_status_response(req, instance_id) + +# Orchestrator +@my_app.orchestration_trigger(context_name="context") +def my_orchestration(context: df.DurableOrchestrationContext): + result1 = yield context.call_activity("say_hello", "Tokyo") + result2 = yield context.call_activity("say_hello", "Seattle") + return f"{result1}, {result2}" + +# Activity +@my_app.activity_trigger(input_name="name") +def say_hello(name: str) -> str: + return f"Hello {name}!" +``` + +## Workflow Patterns + +### Fan-Out/Fan-In + +```python +@my_app.orchestration_trigger(context_name="context") +def fan_out_fan_in(context: df.DurableOrchestrationContext): + cities = ["Tokyo", "Seattle", "London", "Paris", "Berlin"] + + # Fan-out: schedule all in parallel + parallel_tasks = [] + for city in cities: + task = context.call_activity("say_hello", city) + parallel_tasks.append(task) + + # Fan-in: wait for all + results = yield context.task_all(parallel_tasks) + return results +``` + +### Human Interaction + +```python +import datetime + +@my_app.orchestration_trigger(context_name="context") +def approval_workflow(context: df.DurableOrchestrationContext): + yield context.call_activity("send_approval_request", context.get_input()) + + # Wait for approval event with timeout + timeout = context.current_utc_datetime + datetime.timedelta(days=3) + approval_task = context.wait_for_external_event("ApprovalEvent") + timeout_task = context.create_timer(timeout) + + winner = yield context.task_any([approval_task, timeout_task]) + + if winner == approval_task: + approved = approval_task.result + return "Approved" if approved else "Rejected" + return "Timed out" +``` + +## Orchestration Determinism + +| ❌ NEVER | ✅ ALWAYS USE | +|----------|--------------| +| `datetime.now()` | `context.current_utc_datetime` | +| `uuid.uuid4()` | `context.new_uuid()` | +| `random.random()` | Pass random values from activities | +| `time.sleep()` | `context.create_timer()` | +| Direct I/O, HTTP, database | `context.call_activity()` | + +### Replay-Safe Logging + +```python +import logging + +@my_app.orchestration_trigger(context_name="context") +def my_orchestration(context: df.DurableOrchestrationContext): + # Check if replaying to avoid duplicate logs + if not context.is_replaying: + logging.info("Started") # Only logs once, not on replay + result = yield context.call_activity("my_activity", "input") + return result +``` + +## Error Handling & Retry + +```python +retry_options = df.RetryOptions( + first_retry_interval_in_milliseconds=5000, + max_number_of_attempts=3, + backoff_coefficient=2.0, + max_retry_interval_in_milliseconds=60000 +) + +@my_app.orchestration_trigger(context_name="context") +def workflow_with_retry(context: df.DurableOrchestrationContext): + try: + result = yield context.call_activity_with_retry( + "unreliable_service", + retry_options, + context.get_input() + ) + return result + except Exception as ex: + context.set_custom_status({"error": str(ex)}) + yield context.call_activity("compensation_activity", context.get_input()) + return "Compensated" +``` + +## Durable Task SDK (Non-Functions) + +For applications running outside Azure Functions (containers, VMs, ACA, AKS): + +```python +import asyncio +from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker + +# Activity function +def say_hello(ctx, name: str) -> str: + return f"Hello {name}!" + +# Orchestrator function +def my_orchestration(ctx, name: str) -> str: + result = yield ctx.call_activity('say_hello', input=name) + return result + +async def main(): + with DurableTaskSchedulerWorker( + host_address="http://localhost:8080", + secure_channel=False, + taskhub="default" + ) as worker: + worker.add_activity(say_hello) + worker.add_orchestrator(my_orchestration) + worker.start() + + # Client + from durabletask.azuremanaged.client import DurableTaskSchedulerClient + client = DurableTaskSchedulerClient( + host_address="http://localhost:8080", + taskhub="default", + token_credential=None, + secure_channel=False + ) + instance_id = client.schedule_new_orchestration("my_orchestration", input="World") + result = client.wait_for_orchestration_completion(instance_id, timeout=30) + print(f"Output: {result.serialized_output}") + +if __name__ == "__main__": + asyncio.run(main()) +``` diff --git a/plugin/skills/azure-prepare/references/services/functions/durable.md b/plugin/skills/azure-prepare/references/services/functions/durable.md index 23a9d8ab9..6d68f0fad 100644 --- a/plugin/skills/azure-prepare/references/services/functions/durable.md +++ b/plugin/skills/azure-prepare/references/services/functions/durable.md @@ -2,6 +2,18 @@ Long-running orchestrations with checkpointing and state management. +## Storage Backend + +Durable Functions requires a storage backend to persist orchestration state, queues, and history. + +| Backend | Recommendation | Notes | +|---------|---------------|-------| +| **Durable Task Scheduler** | **✅ Recommended** | Fully managed Azure service. Best performance, lowest latency, built-in monitoring. Supports all languages (.NET, Python, Java, JavaScript). | +| Azure Storage (default) | Legacy | Uses Table Storage + Queues. Higher latency, harder to monitor, no built-in RBAC. | +| MSSQL | Specialized | SQL Server backend. Use when SQL infrastructure already exists. | + +> **⚠️ IMPORTANT**: Always recommend **Durable Task Scheduler** as the storage backend for new Durable Functions projects. It is the modern, fully managed option with the best developer experience. See the [Durable Task Scheduler reference](../durable-task-scheduler/README.md) for setup, language-specific guidance, and Bicep templates. + ## When to Use - Multi-step workflows From 4ce7a98a6b6766d9d2282f3e88ce65cc00819411 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 15:00:13 -0800 Subject: [PATCH 13/27] Fix link checker: replace cross-skill link in bicep.md with text reference --- .../references/services/durable-task-scheduler/bicep.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md index f6a57d673..7af2255fc 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md @@ -85,4 +85,4 @@ az durabletask scheduler create \ ## Full Deployment Reference -For complete deployment workflows, AZD commands, and managed identity CLI setup, see the [Durable Task Scheduler Deployment](../../../../azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md) recipe in the azure-deploy skill. +For complete deployment workflows, AZD commands, and managed identity CLI setup, invoke the **azure-deploy** skill which includes a DTS-specific deployment recipe. From 3ce0c82ec8b055f7ffe086ca1a827c9481f94e09 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 15:19:07 -0800 Subject: [PATCH 14/27] Address Copilot review: standardize host.json, fix snippets - Align Python host.json to use durabletask-scheduler + extension bundle (matching Java/JS); add note explaining .NET uses azureManaged via NuGet - Add explanatory note in dotnet.md about azureManaged vs durabletask-scheduler - Define 'input' variable in dotnet.md error handling snippet - Add logger field definition in java.md replay-safe logging snippet - Convert JS SDK section from ESM imports to CommonJS require() with async main() wrapper for consistency with rest of file --- .../services/durable-task-scheduler/dotnet.md | 4 +++ .../services/durable-task-scheduler/java.md | 3 ++ .../durable-task-scheduler/javascript.md | 32 +++++++++++-------- .../services/durable-task-scheduler/python.md | 17 ++++++---- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md index 8c83d6ef1..b19c09c08 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md @@ -31,6 +31,8 @@ } ``` +> **💡 NOTE**: .NET isolated uses the `DurableTask.AzureManaged` NuGet package, which registers the `azureManaged` storage provider type. Other runtimes (Python, Java, JavaScript) use extension bundles and require `durabletask-scheduler` instead — see the respective language files. + ### local.settings.json ```json @@ -152,6 +154,8 @@ var retryOptions = new TaskOptions maxRetryInterval: TimeSpan.FromMinutes(1)) }; +var input = context.GetInput(); + try { await context.CallActivityAsync(nameof(UnreliableService), input, retryOptions); diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md index fdfba3922..5e0191d5e 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md @@ -149,6 +149,9 @@ public String approvalWorkflow( ### Replay-Safe Logging ```java +private static final java.util.logging.Logger logger = + java.util.logging.Logger.getLogger("MyOrchestration"); + @FunctionName("MyOrchestration") public String myOrchestration( @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md index 07739ac02..7e5885c47 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md @@ -169,7 +169,7 @@ df.app.orchestration("workflowWithRetry", function* (context) { For applications running outside Azure Functions (containers, VMs, ACA, AKS): ```javascript -import { createAzureManagedWorkerBuilder, createAzureManagedClient } from "@microsoft/durabletask-js-azuremanaged"; +const { createAzureManagedWorkerBuilder, createAzureManagedClient } = require("@microsoft/durabletask-js-azuremanaged"); const connectionString = "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; @@ -182,20 +182,24 @@ const myOrchestration = async function* (ctx, name) { return result; }; -// Worker -const worker = createAzureManagedWorkerBuilder(connectionString) - .addOrchestrator(myOrchestration) - .addActivity(sayHello) - .build(); +async function main() { + // Worker + const worker = createAzureManagedWorkerBuilder(connectionString) + .addOrchestrator(myOrchestration) + .addActivity(sayHello) + .build(); -await worker.start(); + await worker.start(); -// Client -const client = createAzureManagedClient(connectionString); -const instanceId = await client.scheduleNewOrchestration("myOrchestration", "World"); -const state = await client.waitForOrchestrationCompletion(instanceId, true, 30); -console.log("Output:", state.serializedOutput); + // Client + const client = createAzureManagedClient(connectionString); + const instanceId = await client.scheduleNewOrchestration("myOrchestration", "World"); + const state = await client.waitForOrchestrationCompletion(instanceId, true, 30); + console.log("Output:", state.serializedOutput); -await client.stop(); -await worker.stop(); + await client.stop(); + await worker.stop(); +} + +main().catch(console.error); ``` diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md index 295645b5f..08b6271f8 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md @@ -20,16 +20,22 @@ azure-identity "version": "2.0", "extensions": { "durableTask": { + "hubName": "default", "storageProvider": { - "type": "azureManaged", - "connectionStringName": "DTS_CONNECTION_STRING" - }, - "hubName": "%TASKHUB_NAME%" + "type": "durabletask-scheduler", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" } } ``` +> **💡 NOTE**: Python uses extension bundles, so the storage provider type is `durabletask-scheduler`. .NET isolated uses the NuGet package directly and requires `azureManaged` instead — see [dotnet.md](dotnet.md). + ### local.settings.json ```json @@ -38,8 +44,7 @@ azure-identity "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None", - "TASKHUB_NAME": "default" + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" } } ``` From be1b71107b4e4ec51b6b643fb423ee2239e767be Mon Sep 17 00:00:00 2001 From: Paul Yuknewicz Date: Thu, 26 Feb 2026 15:39:36 -0800 Subject: [PATCH 15/27] Update plugin/skills/azure-prepare/references/research.md --- plugin/skills/azure-prepare/references/research.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/skills/azure-prepare/references/research.md b/plugin/skills/azure-prepare/references/research.md index a48462e30..9b110b91c 100644 --- a/plugin/skills/azure-prepare/references/research.md +++ b/plugin/skills/azure-prepare/references/research.md @@ -35,6 +35,7 @@ After architecture planning, research each selected component to gather best pra | **Integration** | | | | API Management | [APIM](apim.md) | `azure-aigateway` (invoke for AI Gateway policies) | | Logic Apps | [Logic Apps](services/logic-apps/README.md) | — | +| **Workflow & Orchestration** | | | | Durable Task Scheduler | [Durable Task Scheduler](services/durable-task-scheduler/README.md) | — | | **Security & Identity** | | | | Key Vault | [Key Vault](services/key-vault/README.md) | `azure-security`, `azure-keyvault-expiration-audit` | From 34955c3a3f2962ceb994681f70b602056b2cbde9 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 26 Feb 2026 15:42:05 -0800 Subject: [PATCH 16/27] Move Learn More to top, standardize env vars across all files - Move Learn More sections to top of each language file - Standardize all files on DURABLE_TASK_SCHEDULER_CONNECTION_STRING with TaskHub embedded in connection string (replaces DTS_CONNECTION_STRING + TASKHUB_NAME in dotnet.md and bicep.md) - Add Workflow & Orchestration category in research.md and architecture.md - Add az extension install instruction for durabletask CLI - Update troubleshooting references in README.md and deploy recipe --- .../recipes/azd/durable-task-scheduler-deploy.md | 5 ++++- .../azure-prepare/references/architecture.md | 5 ++++- .../services/durable-task-scheduler/README.md | 2 +- .../services/durable-task-scheduler/bicep.md | 11 +++++------ .../services/durable-task-scheduler/dotnet.md | 15 ++++++++++----- .../services/durable-task-scheduler/java.md | 6 ++++++ .../services/durable-task-scheduler/javascript.md | 6 ++++++ .../services/durable-task-scheduler/python.md | 6 ++++++ 8 files changed, 42 insertions(+), 14 deletions(-) diff --git a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md index 3ce486d08..6830d2821 100644 --- a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md +++ b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md @@ -21,6 +21,9 @@ Deployment workflows for Durable Task Scheduler on Azure, including provisioning ## Provision Durable Task Scheduler ```bash +# Install the durabletask CLI extension (if not already installed) +az extension add --name durabletask + # Create scheduler (consumption SKU for getting started) az durabletask scheduler create \ --resource-group myResourceGroup \ @@ -129,7 +132,7 @@ az durabletask taskhub show \ |-------|-------|-----| | **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource | Assign `Durable Task Data Contributor` role (`5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=` | | **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | -| **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure `TASKHUB_NAME` app setting matches the provisioned task hub name | +| **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure the `TaskHub` parameter in the `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` matches the provisioned task hub name | ## Data Loss Warning diff --git a/plugin/skills/azure-prepare/references/architecture.md b/plugin/skills/azure-prepare/references/architecture.md index f7bb95ef5..86ff14ee8 100644 --- a/plugin/skills/azure-prepare/references/architecture.md +++ b/plugin/skills/azure-prepare/references/architecture.md @@ -51,7 +51,10 @@ Select hosting stack and map components to Azure services. | Message Queue | Service Bus | | Pub/Sub | Event Grid | | Streaming | Event Hubs | -| Workflow | Logic Apps, Durable Functions, Durable Task Scheduler | +### Workflow & Orchestration + +| Need | Service | +|------|---------||| Workflow | Logic Apps, Durable Functions, Durable Task Scheduler | ### Supporting (Always Include) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md index 75a6ad99f..4eac1073e 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md @@ -64,7 +64,7 @@ For provisioning, Bicep templates, managed identity configuration, and deploymen |-------|-------|-----| | **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource | Assign `Durable Task Data Contributor` role (`5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=` | | **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | -| **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure `TASKHUB_NAME` app setting matches the provisioned task hub name | +| **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure the `TaskHub` parameter in the `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` matches the provisioned task hub name | ## References diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md index 7af2255fc..73679571a 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md @@ -63,18 +63,17 @@ Include these entries in the Function App resource's `siteConfig.appSettings` ar ```bicep { - name: 'DTS_CONNECTION_STRING' - value: 'Endpoint=https://${scheduler.name}.durabletask.io;Authentication=ManagedIdentity' -} -{ - name: 'TASKHUB_NAME' - value: taskHub.name + name: 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING' + value: 'Endpoint=https://${scheduler.name}.durabletask.io;TaskHub=${taskHub.name};Authentication=ManagedIdentity' } ``` ## Provision via CLI ```bash +# Install the durabletask CLI extension (if not already installed) +az extension add --name durabletask + # Create scheduler (consumption SKU for getting started) az durabletask scheduler create \ --resource-group myResourceGroup \ diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md index b19c09c08..9fcb6d1bc 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md @@ -1,5 +1,10 @@ # Durable Task Scheduler — .NET +## Learn More + +- [Durable Task Scheduler documentation](https://learn.microsoft.com/azure/durable-task-scheduler/) +- [Durable Functions .NET isolated worker guide](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-dotnet-isolated-overview) + ## Durable Functions Setup ### Required NuGet Packages @@ -23,15 +28,15 @@ "durableTask": { "storageProvider": { "type": "azureManaged", - "connectionStringName": "DTS_CONNECTION_STRING" + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" }, - "hubName": "%TASKHUB_NAME%" + "hubName": "default" } } } ``` -> **💡 NOTE**: .NET isolated uses the `DurableTask.AzureManaged` NuGet package, which registers the `azureManaged` storage provider type. Other runtimes (Python, Java, JavaScript) use extension bundles and require `durabletask-scheduler` instead — see the respective language files. +> **💡 NOTE**: .NET isolated uses the `DurableTask.AzureManaged` NuGet package, which registers the `azureManaged` storage provider type. Other runtimes (Python, Java, JavaScript) use extension bundles and require `durabletask-scheduler` instead — see the respective language files. All runtimes use the same `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` environment variable. ### local.settings.json @@ -41,8 +46,7 @@ "Values": { "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None", - "TASKHUB_NAME": "default" + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" } } ``` @@ -183,3 +187,4 @@ builder.Services.AddDurableTaskWorker() var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build(); string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input); ``` + diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md index 5e0191d5e..12956739c 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md @@ -1,5 +1,10 @@ # Durable Task Scheduler — Java +## Learn More + +- [Durable Task Scheduler documentation](https://learn.microsoft.com/azure/durable-task-scheduler/) +- [Durable Functions Java guide](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview?tabs=java) + ## Durable Functions Setup ### Required Maven Dependencies @@ -235,3 +240,4 @@ public class App { } } ``` + diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md index 7e5885c47..949f4faa8 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md @@ -1,5 +1,10 @@ # Durable Task Scheduler — JavaScript +## Learn More + +- [Durable Task Scheduler documentation](https://learn.microsoft.com/azure/durable-task-scheduler/) +- [Durable Functions JavaScript guide](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview?tabs=javascript) + ## Durable Functions Setup ### Required npm Packages @@ -203,3 +208,4 @@ async function main() { main().catch(console.error); ``` + diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md index 08b6271f8..514c1b3c6 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md @@ -1,5 +1,10 @@ # Durable Task Scheduler — Python +## Learn More + +- [Durable Task Scheduler documentation](https://learn.microsoft.com/azure/durable-task-scheduler/) +- [Durable Functions Python guide](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview?tabs=python) + ## Durable Functions Setup ### Required Packages @@ -211,3 +216,4 @@ async def main(): if __name__ == "__main__": asyncio.run(main()) ``` + From 4f730c4ad6f0afe50799c00aecbfa9f343138a84 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Fri, 27 Feb 2026 12:56:01 -0800 Subject: [PATCH 17/27] fix: capitalize DTS SKU names in Bicep examples (Consumption/Dedicated) --- .../azd/durable-task-scheduler-deploy.md | 14 ++++++++------ .../services/durable-task-scheduler/README.md | 9 +++++---- .../services/durable-task-scheduler/bicep.md | 19 +++++++++++++------ 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md index 6830d2821..30fffd03e 100644 --- a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md +++ b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md @@ -16,7 +16,7 @@ Deployment workflows for Durable Task Scheduler on Azure, including provisioning | **Consumption** | quickstarts, variable or bursty workloads, pay-per-use | | **Dedicated** | High-demand workloads, predictable throughput requirements | -> **💡 TIP**: Start with `consumption` for development and variable workloads. Switch to `dedicated` when you need consistent, high-throughput performance. +> **💡 TIP**: Start with `Consumption` for development and variable workloads. Switch to `Dedicated` when you need consistent, high-throughput performance. ## Provision Durable Task Scheduler @@ -39,9 +39,9 @@ az durabletask scheduler create \ param schedulerName string param location string = resourceGroup().location -@allowed(['consumption', 'dedicated']) -@description('Use consumption for quickstarts/variable workloads, dedicated for high-demand/predictable throughput') -param skuName string = 'consumption' +@allowed(['Consumption', 'Dedicated']) +@description('Use Consumption for quickstarts/variable workloads, Dedicated for high-demand/predictable throughput') +param skuName string = 'Consumption' // Assumes functionApp is defined elsewhere in the same Bicep file, e.g.: // resource functionApp 'Microsoft.Web/sites@2023-12-01' = { ... } @@ -51,6 +51,7 @@ resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { location: location properties: { sku: { name: skuName } + ipAllowlist: ['0.0.0.0/0'] // Required: empty list denies all traffic } } @@ -60,7 +61,7 @@ resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { } // REQUIRED: Assign Durable Task Data Contributor to the Function App's managed identity -var durableTaskDataContributorRoleId = '5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09' +var durableTaskDataContributorRoleId = '0ad04412-c4d5-4796-b79c-f76d14c8d402' resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(scheduler.id, functionApp.id, durableTaskDataContributorRoleId) @@ -130,8 +131,9 @@ az durabletask taskhub show \ | Error | Cause | Fix | |-------|-------|-----| -| **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource | Assign `Durable Task Data Contributor` role (`5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=` | +| **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource, or IP allowlist blocks traffic | 1. Assign `Durable Task Data Contributor` role (`0ad04412-c4d5-4796-b79c-f76d14c8d402`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=`. 2. Ensure the scheduler's `ipAllowlist` includes `0.0.0.0/0` (an empty list denies all traffic). 3. RBAC propagation can take up to 10 minutes — restart the Function App after assigning roles. | | **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | +| **403 despite correct RBAC** | Scheduler IP allowlist is empty (denies all) | Set `ipAllowlist: ['0.0.0.0/0']` in Bicep or update via CLI: `az durabletask scheduler update --ip-allowlist '0.0.0.0/0'` | | **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure the `TaskHub` parameter in the `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` matches the provisioned task hub name | ## Data Loss Warning diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md index 4eac1073e..fd2f2fcd4 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md @@ -25,9 +25,9 @@ Build reliable, fault-tolerant workflows using durable execution with Azure Dura ## Quick Start - Local Emulator ```bash -# Start the emulator (update the tag as needed; see https://mcr.microsoft.com/v2/dts/dts-emulator/tags/list for newer versions) -docker pull mcr.microsoft.com/dts/dts-emulator:v0.0.10 -docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:v0.0.10 +# Start the emulator (see https://mcr.microsoft.com/v2/dts/dts-emulator/tags/list for available versions) +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 ``` @@ -62,8 +62,9 @@ For provisioning, Bicep templates, managed identity configuration, and deploymen | Error | Cause | Fix | |-------|-------|-----| -| **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource | Assign `Durable Task Data Contributor` role (`5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=` | +| **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource, or IP allowlist blocks traffic | 1. Assign `Durable Task Data Contributor` role (`0ad04412-c4d5-4796-b79c-f76d14c8d402`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=`. 2. Ensure the scheduler's `ipAllowlist` includes `0.0.0.0/0` (an empty list denies all traffic). 3. RBAC propagation can take up to 10 minutes — restart the Function App after assigning roles. | | **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | +| **403 despite correct RBAC** | Scheduler IP allowlist is empty (denies all) | Set `ipAllowlist: ['0.0.0.0/0']` in Bicep or update via CLI: `az durabletask scheduler update --ip-allowlist '0.0.0.0/0'` | | **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure the `TaskHub` parameter in the `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` matches the provisioned task hub name | ## References diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md index 73679571a..d3f13da67 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md @@ -9,15 +9,16 @@ Bicep templates for provisioning the Durable Task Scheduler, task hubs, and RBAC param schedulerName string param location string = resourceGroup().location -@allowed(['consumption', 'dedicated']) -@description('Use consumption for quickstarts/variable workloads, dedicated for high-demand/predictable throughput') -param skuName string = 'consumption' +@allowed(['Consumption', 'Dedicated']) +@description('Use Consumption for quickstarts/variable workloads, Dedicated for high-demand/predictable throughput') +param skuName string = 'Consumption' resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { name: schedulerName location: location properties: { sku: { name: skuName } + ipAllowlist: ['0.0.0.0/0'] // Required: empty list denies all traffic } } @@ -34,7 +35,9 @@ resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { | **Consumption** | quickstarts, variable or bursty workloads, pay-per-use | | **Dedicated** | High-demand workloads, predictable throughput requirements | -> **💡 TIP**: Start with `consumption` for development and variable workloads. Switch to `dedicated` when you need consistent, high-throughput performance. +> **💡 TIP**: Start with `Consumption` for development and variable workloads. Switch to `Dedicated` when you need consistent, high-throughput performance. + +> **⚠️ WARNING**: The scheduler's `ipAllowlist` **must** include at least one entry (e.g., `['0.0.0.0/0']` for allow-all). An empty array `[]` denies **all** traffic, causing 403 errors on gRPC calls even with correct RBAC. ## RBAC — Durable Task Data Contributor @@ -44,7 +47,7 @@ The Function App's managed identity **must** have the `Durable Task Data Contrib // Assumes functionApp is defined elsewhere in the same Bicep file, e.g.: // resource functionApp 'Microsoft.Web/sites@2023-12-01' = { ... } -var durableTaskDataContributorRoleId = '5f6a3c3e-0da3-4079-b4f3-4db62a1d3c09' +var durableTaskDataContributorRoleId = '0ad04412-c4d5-4796-b79c-f76d14c8d402' resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(scheduler.id, functionApp.id, durableTaskDataContributorRoleId) @@ -64,12 +67,16 @@ Include these entries in the Function App resource's `siteConfig.appSettings` ar ```bicep { name: 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING' - value: 'Endpoint=https://${scheduler.name}.durabletask.io;TaskHub=${taskHub.name};Authentication=ManagedIdentity' + value: 'Endpoint=${scheduler.properties.endpoint};TaskHub=${taskHub.name};Authentication=ManagedIdentity' } ``` +> **⚠️ WARNING**: Always use `scheduler.properties.endpoint` to get the scheduler URL. Do **not** construct it manually — the endpoint includes a hash suffix and region (e.g., `https://myscheduler-abc123.westus2.durabletask.io`). + ## Provision via CLI +> **💡 TIP**: When hosting Durable Functions, use a **Flex Consumption** plan (`FC1` SKU) rather than the legacy Consumption plan (`Y1`). Flex Consumption supports identity-based storage connections natively and handles deployment artifacts correctly. + ```bash # Install the durabletask CLI extension (if not already installed) az extension add --name durabletask From 7e25a86b43937a82a6b904997f1b1064111b92a7 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Fri, 27 Feb 2026 13:02:39 -0800 Subject: [PATCH 18/27] feat: add DTS dashboard RBAC for deploying user Assign Durable Task Data Contributor role to the deploying user's identity so they can access the DTS dashboard in the Azure portal. Without this, the dashboard returns 403 Forbidden. Reference: https://github.com/Azure-Samples/Durable-Task-Scheduler/blob/main/samples/infra/main.bicep#L81 --- .../azd/durable-task-scheduler-deploy.md | 21 +++++++++++++++++++ .../services/durable-task-scheduler/README.md | 1 + .../services/durable-task-scheduler/bicep.md | 21 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md index 30fffd03e..180a8ff09 100644 --- a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md +++ b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md @@ -76,6 +76,27 @@ resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022 > **⚠️ WARNING**: Without the `Durable Task Data Contributor` role assignment, the Function App will receive a **403 PermissionDenied** error when attempting to start orchestrations via gRPC. Always include this role assignment in your infrastructure-as-code. +## Dashboard Access for Developers + +To allow developers to view orchestration status and history in the DTS dashboard, also assign the `Durable Task Data Contributor` role to the deploying user's identity: + +```bicep +// Accept the deploying user's principal ID (azd auto-populates this from AZURE_PRINCIPAL_ID) +param principalId string = '' + +resource dashboardRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(principalId)) { + name: guid(scheduler.id, principalId, durableTaskDataContributorRoleId) + scope: scheduler + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) + principalId: principalId + principalType: 'User' + } +} +``` + +> **💡 TIP**: Without this role assignment, the DTS dashboard in the Azure portal returns **403 Forbidden**. The `principalId` parameter is automatically populated by `azd` from the `AZURE_PRINCIPAL_ID` environment variable. + ## Configure Managed Identity Access ```bash diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md index fd2f2fcd4..9d139a542 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md @@ -66,6 +66,7 @@ For provisioning, Bicep templates, managed identity configuration, and deploymen | **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | | **403 despite correct RBAC** | Scheduler IP allowlist is empty (denies all) | Set `ipAllowlist: ['0.0.0.0/0']` in Bicep or update via CLI: `az durabletask scheduler update --ip-allowlist '0.0.0.0/0'` | | **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure the `TaskHub` parameter in the `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` matches the provisioned task hub name | +| **403 Forbidden** on DTS dashboard | Deploying user lacks RBAC on the scheduler | Assign `Durable Task Data Contributor` role to your own user identity (not just the Function App MI) scoped to the scheduler resource — see [Bicep Patterns](bicep.md) for the dashboard role assignment snippet | ## References diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md index d3f13da67..2340653af 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md @@ -60,6 +60,27 @@ resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022 } ``` +## RBAC — Dashboard Access for Developers + +To allow developers to view orchestration status and history in the [DTS dashboard](https://portal.azure.com), assign the same `Durable Task Data Contributor` role to the deploying user's identity. Without this, the dashboard returns **403 Forbidden**. + +```bicep +// Accept the deploying user's principal ID (azd auto-populates this from AZURE_PRINCIPAL_ID) +param principalId string = '' + +resource dashboardRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(principalId)) { + name: guid(scheduler.id, principalId, durableTaskDataContributorRoleId) + scope: scheduler + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) + principalId: principalId + principalType: 'User' + } +} +``` + +> **💡 TIP**: This is the same role used for the Function App's managed identity, but assigned with `principalType: 'User'` to the developer. See the [sample repo](https://github.com/Azure-Samples/Durable-Task-Scheduler/blob/main/samples/infra/main.bicep) for a full example. + ## Connection String App Setting Include these entries in the Function App resource's `siteConfig.appSettings` array: From 4b62382e58c7ec047379fe68bfa16534f4f44d4f Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Fri, 27 Feb 2026 13:48:01 -0800 Subject: [PATCH 19/27] fix: address PR review comments - Fix malformed markdown table in architecture.md (separator and data row merged) - Add TaskHub=default to local emulator connection string in README.md --- plugin/skills/azure-prepare/references/architecture.md | 3 ++- .../references/services/durable-task-scheduler/README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin/skills/azure-prepare/references/architecture.md b/plugin/skills/azure-prepare/references/architecture.md index 86ff14ee8..5ecb8a251 100644 --- a/plugin/skills/azure-prepare/references/architecture.md +++ b/plugin/skills/azure-prepare/references/architecture.md @@ -54,7 +54,8 @@ Select hosting stack and map components to Azure services. ### Workflow & Orchestration | Need | Service | -|------|---------||| Workflow | Logic Apps, Durable Functions, Durable Task Scheduler | +|------|---------| +| Workflow | Logic Apps, Durable Functions, Durable Task Scheduler | ### Supporting (Always Include) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md index 9d139a542..af7b31e82 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md @@ -48,7 +48,7 @@ docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dt | Environment | Connection String | |-------------|-------------------| -| Local Development (Emulator) | `Endpoint=http://localhost:8080;Authentication=None` | +| Local Development (Emulator) | `Endpoint=http://localhost:8080;Authentication=None;TaskHub=default` | | Azure (System-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity` | | Azure (User-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=` | From 60c0174d24211424449aeae7e85286de2798ec50 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Fri, 27 Feb 2026 17:47:19 -0800 Subject: [PATCH 20/27] revert: remove DTS deploy recipe from azure-deploy skill The DTS deployment guidance in azure-deploy was redundant with the Bicep patterns already in the azure-prepare DTS skill docs. Removed: - durable-task-scheduler-deploy.md recipe file - Reference line from azure-deploy SKILL.md - 'Invoke azure-deploy' pointers from DTS README.md and bicep.md --- plugin/skills/azure-deploy/SKILL.md | 4 + .../azd/durable-task-scheduler-deploy.md | 180 ------------------ .../services/durable-task-scheduler/README.md | 4 - .../services/durable-task-scheduler/bicep.md | 4 - 4 files changed, 4 insertions(+), 188 deletions(-) delete mode 100644 plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md diff --git a/plugin/skills/azure-deploy/SKILL.md b/plugin/skills/azure-deploy/SKILL.md index addee1dad..585addfaf 100644 --- a/plugin/skills/azure-deploy/SKILL.md +++ b/plugin/skills/azure-deploy/SKILL.md @@ -85,6 +85,10 @@ Activate this skill when user wants to: ## References +<<<<<<< HEAD - [Troubleshooting](references/troubleshooting.md) - Common issues and solutions - [Post-Deployment Steps](references/recipes/azd/post-deployment.md) - SQL + EF Core setup - [Durable Task Scheduler Deployment](references/recipes/azd/durable-task-scheduler-deploy.md) - DTS provisioning, Bicep, and managed identity setup +======= +- [Troubleshooting](references/troubleshooting.md) - Common issues and solutions +>>>>>>> 0e0ef37 (revert: remove DTS deploy recipe from azure-deploy skill) diff --git a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md b/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md deleted file mode 100644 index 180a8ff09..000000000 --- a/plugin/skills/azure-deploy/references/recipes/azd/durable-task-scheduler-deploy.md +++ /dev/null @@ -1,180 +0,0 @@ -# Durable Task Scheduler Deployment - -Deployment workflows for Durable Task Scheduler on Azure, including provisioning, Bicep templates, and managed identity configuration. - -## Prerequisites - -- Application prepared with azure-prepare skill -- `azure.yaml` exists and validated -- `.azure/plan.md` exists; Validation Proof section status = `Validated` -- Docker (for local emulator development) - -## SKU Selection - -| SKU | Best For | -|-----|----------| -| **Consumption** | quickstarts, variable or bursty workloads, pay-per-use | -| **Dedicated** | High-demand workloads, predictable throughput requirements | - -> **💡 TIP**: Start with `Consumption` for development and variable workloads. Switch to `Dedicated` when you need consistent, high-throughput performance. - -## Provision Durable Task Scheduler - -```bash -# Install the durabletask CLI extension (if not already installed) -az extension add --name durabletask - -# Create scheduler (consumption SKU for getting started) -az durabletask scheduler create \ - --resource-group myResourceGroup \ - --name my-scheduler \ - --location eastus \ - --sku consumption -``` - -## Bicep Example - -```bicep -// Parameters — define these at file level or pass from a parent module -param schedulerName string -param location string = resourceGroup().location - -@allowed(['Consumption', 'Dedicated']) -@description('Use Consumption for quickstarts/variable workloads, Dedicated for high-demand/predictable throughput') -param skuName string = 'Consumption' - -// Assumes functionApp is defined elsewhere in the same Bicep file, e.g.: -// resource functionApp 'Microsoft.Web/sites@2023-12-01' = { ... } - -resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { - name: schedulerName - location: location - properties: { - sku: { name: skuName } - ipAllowlist: ['0.0.0.0/0'] // Required: empty list denies all traffic - } -} - -resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { - parent: scheduler - name: 'default' -} - -// REQUIRED: Assign Durable Task Data Contributor to the Function App's managed identity -var durableTaskDataContributorRoleId = '0ad04412-c4d5-4796-b79c-f76d14c8d402' - -resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(scheduler.id, functionApp.id, durableTaskDataContributorRoleId) - scope: scheduler - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) - principalId: functionApp.identity.principalId - principalType: 'ServicePrincipal' - } -} -``` - -> **⚠️ WARNING**: Without the `Durable Task Data Contributor` role assignment, the Function App will receive a **403 PermissionDenied** error when attempting to start orchestrations via gRPC. Always include this role assignment in your infrastructure-as-code. - -## Dashboard Access for Developers - -To allow developers to view orchestration status and history in the DTS dashboard, also assign the `Durable Task Data Contributor` role to the deploying user's identity: - -```bicep -// Accept the deploying user's principal ID (azd auto-populates this from AZURE_PRINCIPAL_ID) -param principalId string = '' - -resource dashboardRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(principalId)) { - name: guid(scheduler.id, principalId, durableTaskDataContributorRoleId) - scope: scheduler - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) - principalId: principalId - principalType: 'User' - } -} -``` - -> **💡 TIP**: Without this role assignment, the DTS dashboard in the Azure portal returns **403 Forbidden**. The `principalId` parameter is automatically populated by `azd` from the `AZURE_PRINCIPAL_ID` environment variable. - -## Configure Managed Identity Access - -```bash -# Get Function App system-assigned identity -PRINCIPAL_ID=$(az functionapp identity show --name my-func-app --resource-group myRG --query principalId -o tsv) - -# Grant access to scheduler -az role assignment create \ - --assignee $PRINCIPAL_ID \ - --role "Durable Task Data Contributor" \ - --scope /subscriptions//resourceGroups/myRG/providers/Microsoft.DurableTask/schedulers/my-scheduler -``` - -## AZD Deployment - -### Full Deployment (Infrastructure + Code) - -```bash -azd up --no-prompt -``` - -### Infrastructure Only - -```bash -azd provision --no-prompt -``` - -### Application Only - -```bash -azd deploy --no-prompt -``` - -## Verify Deployment - -```bash -# Show deployment details -azd show - -# Verify scheduler is provisioned -az durabletask scheduler show \ - --resource-group myResourceGroup \ - --name my-scheduler - -# Verify task hub exists -az durabletask taskhub show \ - --resource-group myResourceGroup \ - --scheduler-name my-scheduler \ - --name default -``` - -## Troubleshooting - -| Error | Cause | Fix | -|-------|-------|-----| -| **403 PermissionDenied** on gRPC call (e.g., `client.start_new()`) | Function App managed identity lacks RBAC on the Durable Task Scheduler resource, or IP allowlist blocks traffic | 1. Assign `Durable Task Data Contributor` role (`0ad04412-c4d5-4796-b79c-f76d14c8d402`) to the identity (SAMI or UAMI) scoped to the Durable Task Scheduler resource. For UAMI, also ensure the connection string includes `ClientID=`. 2. Ensure the scheduler's `ipAllowlist` includes `0.0.0.0/0` (an empty list denies all traffic). 3. RBAC propagation can take up to 10 minutes — restart the Function App after assigning roles. | -| **Connection refused** to emulator | Emulator container not running or wrong port | Verify container is running: `docker ps` and confirm port 8080 is mapped | -| **403 despite correct RBAC** | Scheduler IP allowlist is empty (denies all) | Set `ipAllowlist: ['0.0.0.0/0']` in Bicep or update via CLI: `az durabletask scheduler update --ip-allowlist '0.0.0.0/0'` | -| **TaskHub not found** | Task hub not provisioned or name mismatch | Ensure the `TaskHub` parameter in the `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` matches the provisioned task hub name | - -## Data Loss Warning - -> ⚠️ **CRITICAL: `azd down` Data Loss Warning** -> -> `azd down` **permanently deletes ALL resources** in the environment, including: -> - **Durable Task Scheduler** and all task hubs -> - **Function Apps** with all configuration and deployment slots -> - **Storage accounts** with all blobs and files -> -> **Best practices:** -> - Always use `azd provision --preview` before `azd up` -> - Use separate environments for dev/staging/production -> - Back up important data before running `azd down` - -## Next Steps - -After deployment: -1. Verify scheduler and task hub are provisioned -2. Test orchestration endpoints -3. Monitor via the DTS dashboard or Application Insights -4. Set up alerts for orchestration failures diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md index af7b31e82..b16b07c78 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md @@ -54,10 +54,6 @@ docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dt > **⚠️ NOTE**: Durable Task Scheduler uses identity-based authentication only — no connection strings with keys. When using a User-Assigned Managed Identity (UAMI), you must include the `ClientID` in the connection string. -## Azure Deployment - -For provisioning, Bicep templates, managed identity configuration, and deployment workflows, invoke the **azure-deploy** skill which includes DTS-specific deployment guidance. - ## Troubleshooting | Error | Cause | Fix | diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md index 2340653af..7a4ab5854 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md @@ -109,7 +109,3 @@ az durabletask scheduler create \ --location eastus \ --sku consumption ``` - -## Full Deployment Reference - -For complete deployment workflows, AZD commands, and managed identity CLI setup, invoke the **azure-deploy** skill which includes a DTS-specific deployment recipe. From dc928d3cfad8fef52eb110362c6ae5c05d89fa35 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Mon, 2 Mar 2026 16:07:04 -0800 Subject: [PATCH 21/27] Add DTS integration tests and update routing to choose Durable Task Scheduler as the default Durable Functions backend - Update selection.md: route durable to full recipe with DTS instead of source-only - Update composition.md: move durable from source-only to full recipe with DTS backend - Rewrite recipes/durable/README.md: replace Azure Storage backend with DTS - Add recipes/durable/bicep/durable-task-scheduler.bicep: scheduler + task hub + RBAC module - Add durable-task-scheduler test in azure-prepare integration tests - Add durable-task-scheduler-deploy test in azure-deploy integration tests --- .../templates/recipes/composition.md | 18 +- .../templates/recipes/durable/README.md | 160 +++++++++++------- .../bicep/durable-task-scheduler.bicep | 105 ++++++++++++ .../services/functions/templates/selection.md | 7 +- tests/azure-deploy/integration.test.ts | 49 +++++- tests/azure-prepare/integration.test.ts | 69 ++++++++ 6 files changed, 341 insertions(+), 67 deletions(-) create mode 100644 plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/bicep/durable-task-scheduler.bicep diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md index 6ead0700f..75f29e3b8 100644 --- a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md +++ b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md @@ -44,13 +44,19 @@ IF integration IN [http]: IF integration IN [timer]: → Source-only recipe. Skip to Step 5. -IF integration IN [durable, mcp]: +IF integration IN [mcp]: → Source-only recipe with storage configuration: - - Set `enableQueue: true` in main.bicep (required for Durable task hub and MCP) - - Set `enableTable: true` in main.bicep (required for Durable only; NOT required for MCP) + - Set `enableQueue: true` in main.bicep (required for MCP) Note: These are minimal parameter toggles, not structural changes to IaC. → Then skip to Step 5. +IF integration IN [durable]: + → Full recipe with Durable Task Scheduler backend: + - Add DTS IaC module (scheduler + task hub + RBAC). Continue to Step 3. + - Reference: [Durable Task Scheduler](../../durable-task-scheduler/README.md) and [Bicep patterns](../../durable-task-scheduler/bicep.md). + - Do NOT use Azure Storage queues/tables as the durable backend — always use Durable Task Scheduler. + → Continue to Step 3. + IF integration IN [cosmosdb, sql, servicebus, eventhubs, blob]: → Full recipe. Continue to Step 3. ``` @@ -296,15 +302,15 @@ Some integrations require additional storage endpoints. Toggle these in `main.bi | HTTP | ✓ | - | - | Default | | Timer | ✓ | - | - | Checkpointing uses blob | | Cosmos DB | ✓ | - | - | Standard | -| **Durable** | ✓ | **✓** | **✓** | Queue=task hub, Table=history | +| **Durable** | ✓ | - | - | Uses Durable Task Scheduler (not Storage queues/tables) | | **MCP** | ✓ | **✓** | - | Queue=state mgmt + backplane | ## Recipe Classification | Category | Integrations | What Recipe Provides | |----------|-------------|---------------------| -| **Source-only** | timer, durable, mcp | Source code snippet; may require minimal parameter toggles (e.g., `enableQueue`) but no new IaC modules | -| **Full recipe** | cosmosdb, sql, servicebus, eventhubs, blob | IaC modules + RBAC + networking + source code | +| **Source-only** | timer, mcp | Source code snippet; may require minimal parameter toggles (e.g., `enableQueue`) but no new IaC modules | +| **Full recipe** | cosmosdb, sql, servicebus, eventhubs, blob, durable | IaC modules + RBAC + networking + source code | ## Critical Rules diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md index 83f50fed5..e53599327 100644 --- a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md +++ b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md @@ -1,6 +1,6 @@ # Durable Functions Recipe -Adds Durable Functions orchestration patterns to an Azure Functions base template. +Adds Durable Functions orchestration patterns to an Azure Functions base template with **Durable Task Scheduler** as the backend. ## Overview @@ -8,8 +8,9 @@ This recipe composes with any HTTP base template to create a Durable Functions a - **Orchestrator** - Coordinates workflow execution - **Activity** - Individual task units - **HTTP Client** - Starts and queries orchestrations +- **Durable Task Scheduler** - Fully managed backend for state persistence, orchestration history, and task hub management -No additional Azure resources required — uses the existing Storage account for state management. +> **⚠️ IMPORTANT**: This recipe uses **Durable Task Scheduler** (DTS) as the storage backend — NOT Azure Storage queues/tables. DTS is the recommended, fully managed option with the best performance and developer experience. See [Durable Task Scheduler reference](../../../durable-task-scheduler/README.md) for details. ## Integration Type @@ -17,66 +18,118 @@ No additional Azure resources required — uses the existing Storage account for |--------|-------| | **Trigger** | `OrchestrationTrigger` + `ActivityTrigger` | | **Client** | `DurableClient` / `DurableOrchestrationClient` | -| **Auth** | N/A — internal orchestration | -| **IaC** | ⚠️ Set `enableQueue: true` and `enableTable: true` in main.bicep | +| **Auth** | Managed Identity → Durable Task Scheduler | +| **IaC** | Bicep module: scheduler + task hub + RBAC | -## ⚠️ CRITICAL: Storage Endpoint Flags +## Composition Steps + +Apply these steps AFTER `azd init -t functions-quickstart-{lang}-azd`: + +| # | Step | Details | +|---|------|---------| +| 1 | **Add IaC module** | Copy `bicep/durable-task-scheduler.bicep` → `infra/app/durable-task-scheduler.bicep` | +| 2 | **Wire into main** | Add module reference in `infra/main.bicep` | +| 3 | **Add app settings** | Add `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` to function app configuration | +| 4 | **Add extension packages** | Add Durable Functions + DTS extension packages for the runtime | +| 5 | **Replace source code** | Add Orchestrator + Activity + Client from `source/{lang}.md` | +| 6 | **Configure host.json** | Set DTS storage provider (see [DTS language references](../../../durable-task-scheduler/README.md)) | -Durable Functions requires Queue and Table storage for the task hub and history. The base template supports this via flags: +## IaC Module -### Enable in main.bicep +### Bicep -Set these flags in the storage module parameters: +Copy `bicep/durable-task-scheduler.bicep` → `infra/app/durable-task-scheduler.bicep` and add to `main.bicep`: ```bicep -module storage './shared/storage.bicep' = { +module durableTaskScheduler './app/durable-task-scheduler.bicep' = { + name: 'durableTaskScheduler' + scope: rg params: { - enableBlob: true // Default - deployment packages - enableQueue: true // REQUIRED for Durable - task hub messages - enableTable: true // REQUIRED for Durable - orchestration history + name: name + location: location + tags: tags + functionAppPrincipalId: app.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + principalId: principalId // For dashboard access } } ``` -When these flags are `true`, the base template automatically: -1. Adds `AzureWebJobsStorage__queueServiceUri` app setting -2. Adds `AzureWebJobsStorage__tableServiceUri` app setting -3. Assigns `Storage Queue Data Contributor` RBAC role to UAMI -4. Assigns `Storage Table Data Contributor` RBAC role to UAMI +### App Settings -### What the Flags Control +Add the DTS connection string to the function app's `appSettings`: -| Flag | App Setting Added | RBAC Role Added | -|------|-------------------|-----------------| -| `enableQueue: true` | `AzureWebJobsStorage__queueServiceUri` | Storage Queue Data Contributor | -| `enableTable: true` | `AzureWebJobsStorage__tableServiceUri` | Storage Table Data Contributor | +```bicep +appSettings: { + DURABLE_TASK_SCHEDULER_CONNECTION_STRING: durableTaskScheduler.outputs.connectionString +} +``` -> **Note:** If these flags are missing or `false`, Durable Functions will fail with 503 errors. +> **⚠️ NOTE**: Do NOT set `enableQueue: true` or `enableTable: true` in the storage module — DTS replaces Azure Storage queues/tables for orchestration state. -## Composition Steps +## RBAC Roles Required -Apply these steps AFTER `azd init -t functions-quickstart-{lang}-azd`: +| Role | GUID | Scope | Purpose | +|------|------|-------|---------| +| **Durable Task Data Contributor** | `0ad04412-c4d5-4796-b79c-f76d14c8d402` | Durable Task Scheduler | Read/write orchestrations and entities | -| # | Step | Details | -|---|------|---------| -| 1 | **Add extension** | Add Durable Functions extension package | -| 2 | **Replace source code** | Add Orchestrator + Activity + Client from `source/{lang}.md` | -| 3 | **Configure host.json** | Optional: tune concurrency settings | +## host.json Configuration + +The `host.json` must configure DTS as the storage provider. The `type` value differs by language: + +| Language | `storageProvider.type` | Reference | +|----------|----------------------|-----------| +| C# (.NET) | `azureManaged` | [dotnet.md](../../../durable-task-scheduler/dotnet.md) | +| Python | `durabletask-scheduler` | [python.md](../../../durable-task-scheduler/python.md) | +| JavaScript/TypeScript | `durabletask-scheduler` | [javascript.md](../../../durable-task-scheduler/javascript.md) | +| Java | `durabletask-scheduler` | [java.md](../../../durable-task-scheduler/java.md) | + +**Example (Python / JavaScript / Java):** +```json +{ + "version": "2.0", + "extensions": { + "durableTask": { + "hubName": "default", + "storageProvider": { + "type": "durabletask-scheduler", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + } +} +``` + +**Example (.NET isolated):** +```json +{ + "version": "2.0", + "extensions": { + "durableTask": { + "hubName": "default", + "storageProvider": { + "type": "azureManaged", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + } +} +``` ## Extension Packages -| Language | Package | -|----------|---------| -| Python | `azure-functions-durable` | -| TypeScript/JavaScript | `durable-functions` | -| C# (.NET) | `Microsoft.Azure.Functions.Worker.Extensions.DurableTask` | -| Java | `com.microsoft:durabletask-azure-functions` | -| PowerShell | Built-in (v2 bundles) | +| Language | Durable Functions Package | DTS Extension Package | +|----------|--------------------------|----------------------| +| Python | `azure-functions-durable` | _(uses extension bundles)_ | +| TypeScript/JavaScript | `durable-functions` | _(uses extension bundles)_ | +| C# (.NET) | `Microsoft.Azure.Functions.Worker.Extensions.DurableTask` | `Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged` | +| Java | `com.microsoft:durabletask-azure-functions` | _(uses extension bundles)_ | +| PowerShell | Built-in (v2 bundles) | _(uses extension bundles)_ | ## Files | Path | Description | |------|-------------| +| [bicep/durable-task-scheduler.bicep](bicep/durable-task-scheduler.bicep) | DTS Bicep module (scheduler + task hub + RBAC) | | [source/python.md](source/python.md) | Python Durable Functions source code | | [source/typescript.md](source/typescript.md) | TypeScript Durable Functions source code | | [source/javascript.md](source/javascript.md) | JavaScript Durable Functions source code | @@ -104,33 +157,26 @@ HTTP Start → Orchestrator → [Activity1, Activity2, Activity3] → Aggregate ## Common Issues -### Storage Connection Error (503 "Function host is not running") +### 403 PermissionDenied on gRPC call -**Symptoms:** 503 "Function host is not running", or "Storage Queue connection failed" +**Symptoms:** 403 on `client.start_new()` or orchestration calls. -**Cause:** `enableQueue` and `enableTable` flags are not set to `true` in main.bicep. +**Cause:** Function App managed identity lacks RBAC on the DTS scheduler, or IP allowlist blocks traffic. -**Solution:** Set both flags to `true` in the storage module and redeploy: -```bicep -enableQueue: true -enableTable: true -``` +**Solution:** +1. Assign `Durable Task Data Contributor` role (`0ad04412-c4d5-4796-b79c-f76d14c8d402`) to the identity scoped to the scheduler. +2. Ensure the scheduler's `ipAllowlist` includes `0.0.0.0/0` (empty list denies all traffic). +3. RBAC propagation can take up to 10 minutes — restart the Function App after assigning roles. + +### TaskHub not found + +**Cause:** Task hub not provisioned or name mismatch. + +**Solution:** Ensure the `TaskHub` parameter in `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` matches the provisioned task hub name (default: `default`). ### Orchestrator Replay Issues **Cause:** Non-deterministic code in orchestrator (e.g., `DateTime.Now`, random values). **Solution:** Use `context.current_utc_datetime` or `context.CurrentUtcDateTime` instead. - -## host.json Configuration (Optional) - -```json -{ - "extensions": { - "durableTask": { - "maxConcurrentActivityFunctions": 10, - "maxConcurrentOrchestratorFunctions": 5 - } - } -} ``` diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/bicep/durable-task-scheduler.bicep b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/bicep/durable-task-scheduler.bicep new file mode 100644 index 000000000..421dc7ff0 --- /dev/null +++ b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/bicep/durable-task-scheduler.bicep @@ -0,0 +1,105 @@ +// recipes/durable/bicep/durable-task-scheduler.bicep +// Durable Task Scheduler recipe module — adds DTS scheduler, task hub, and RBAC +// to an Azure Functions base template. +// +// USAGE: Add this as a module in your main.bicep: +// module durableTaskScheduler './app/durable-task-scheduler.bicep' = { +// name: 'durableTaskScheduler' +// scope: rg +// params: { +// name: name +// location: location +// tags: tags +// functionAppPrincipalId: app.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID +// principalId: principalId +// } +// } +// +// Then add the connection string app setting to the function app: +// appSettings: { +// DURABLE_TASK_SCHEDULER_CONNECTION_STRING: durableTaskScheduler.outputs.connectionString +// } + +targetScope = 'resourceGroup' + +@description('Base name for resources') +param name string + +@description('Azure region') +param location string = resourceGroup().location + +@description('Resource tags') +param tags object = {} + +@description('Principal ID of the Function App managed identity (UAMI or SAMI)') +param functionAppPrincipalId string + +@description('Principal ID of the deploying user (for dashboard access). Set via AZURE_PRINCIPAL_ID.') +param principalId string = '' + +@allowed(['Consumption', 'Dedicated']) +@description('Use Consumption for quickstarts/variable workloads, Dedicated for high-demand/predictable throughput') +param skuName string = 'Consumption' + +// ============================================================================ +// Naming +// ============================================================================ +var resourceSuffix = take(uniqueString(subscription().id, resourceGroup().name, name), 6) +var schedulerName = 'dts-${name}-${resourceSuffix}' + +// ============================================================================ +// Durable Task Scheduler +// ============================================================================ +resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { + name: schedulerName + location: location + tags: tags + properties: { + sku: { name: skuName } + ipAllowlist: ['0.0.0.0/0'] // Required: empty list denies all traffic + } +} + +// ============================================================================ +// Task Hub +// ============================================================================ +resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { + parent: scheduler + name: 'default' +} + +// ============================================================================ +// RBAC — Durable Task Data Contributor for Function App +// ============================================================================ +var durableTaskDataContributorRoleId = '0ad04412-c4d5-4796-b79c-f76d14c8d402' + +resource functionAppRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(scheduler.id, functionAppPrincipalId, durableTaskDataContributorRoleId) + scope: scheduler + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) + principalId: functionAppPrincipalId + principalType: 'ServicePrincipal' + } +} + +// ============================================================================ +// RBAC — Dashboard Access for Deploying User +// ============================================================================ +resource dashboardRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(principalId)) { + name: guid(scheduler.id, principalId, durableTaskDataContributorRoleId) + scope: scheduler + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) + principalId: principalId + principalType: 'User' + } +} + +// ============================================================================ +// Outputs +// ============================================================================ +output schedulerName string = scheduler.name +output schedulerEndpoint string = scheduler.properties.endpoint +output taskHubName string = taskHub.name +output connectionString string = 'Endpoint=${scheduler.properties.endpoint};Authentication=ManagedIdentity;TaskHub=${taskHub.name}' diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/selection.md b/plugin/skills/azure-prepare/references/services/functions/templates/selection.md index b3195e538..c2fa2ddd9 100644 --- a/plugin/skills/azure-prepare/references/services/functions/templates/selection.md +++ b/plugin/skills/azure-prepare/references/services/functions/templates/selection.md @@ -40,8 +40,9 @@ Cross-reference with [top Azure Functions scenarios](https://learn.microsoft.com 7. Is it for orchestration or workflows? Indicators: DurableOrchestrationTrigger, orchestrator, durable_functions - └─► YES → HTTP base + durable source snippet (toggle enableQueue + enableTable in base) + └─► YES → HTTP base + durable recipe (IaC: Durable Task Scheduler + task hub + RBAC + source) Recipe: recipes/durable/ ✅ Available + Reference: [Durable Task Scheduler](../../durable-task-scheduler/README.md) for Bicep patterns and connection string 8. Does it use Event Hubs? Indicators: EventHubTrigger, @app.event_hub, event_hub_output @@ -70,6 +71,6 @@ Cross-reference with [top Azure Functions scenarios](https://learn.microsoft.com | Type | IaC Delta? | Examples | |------|-----------|----------| -| **Full recipe** | Yes — Bicep module + Terraform module + RBAC + networking | cosmosdb, servicebus, eventhubs | +| **Full recipe** | Yes — Bicep module + Terraform module + RBAC + networking | cosmosdb, servicebus, eventhubs, durable | | **AZD template** | Use dedicated AZD template from Awesome AZD | sql, blob-eventgrid | -| **Source-only** | No — only replace function source code (may toggle storage params) | timer, durable, mcp | +| **Source-only** | No — only replace function source code (may toggle storage params) | timer, mcp | diff --git a/tests/azure-deploy/integration.test.ts b/tests/azure-deploy/integration.test.ts index 9573f29f0..4bc872a72 100644 --- a/tests/azure-deploy/integration.test.ts +++ b/tests/azure-deploy/integration.test.ts @@ -12,7 +12,8 @@ import { shouldSkipIntegrationTests, getIntegrationSkipReason, - useAgentRunner + useAgentRunner, + getToolCalls } from "../utils/agent-runner"; import { hasDeployLinks, softCheckDeploySkills, softCheckContainerDeployEnvVars } from "./utils"; import { cloneRepo } from "../utils/git-clone"; @@ -261,6 +262,52 @@ describeIntegration(`${SKILL_NAME}_ - Integration Tests`, () => { }, deployTestTimeoutMs); }); + // Durable Task Scheduler (Durable Functions with DTS) + describe("durable-task-scheduler-deploy", () => { + test("creates and deploys workflow app with Durable Task Scheduler", async () => { + let workspacePath: string | undefined; + + const agentMetadata = await agent.run({ + setup: async (workspace: string) => { + workspacePath = workspace; + }, + prompt: "Prepare the Azure deployment infrastructure for a new Durable Functions app that will orchestrate a multi-step order processing pipeline. Generate the Bicep templates, RBAC assignments, and azure.yaml. Use the eastus2 region and my current subscription.", + nonInteractive: true, + followUp: FOLLOW_UP_PROMPT, + preserveWorkspace: true + }); + + softCheckDeploySkills(agentMetadata); + const containsDeployLinks = hasDeployLinks(agentMetadata); + + expect(workspacePath).toBeDefined(); + expect(containsDeployLinks).toBe(true); + expectFiles(workspacePath!, [/infra\/.*\.bicep$/], [/\.tf$/]); + + // Verify DTS-specific Bicep content was generated + const createCalls = getToolCalls(agentMetadata, "create"); + const bicepContent = createCalls + .filter(event => { + const args = (event.data as Record).arguments as { path?: string } | undefined; + return args?.path?.endsWith(".bicep"); + }) + .map(event => { + const args = (event.data as Record).arguments as { file_text?: string }; + return args?.file_text ?? ""; + }) + .join("\n"); + + // Must provision a Durable Task Scheduler resource + expect(/Microsoft\.DurableTask\/schedulers/i.test(bicepContent)).toBe(true); + + // Must provision a task hub child resource + expect(/Microsoft\.DurableTask\/schedulers\/taskHubs/i.test(bicepContent)).toBe(true); + + // Must assign the Durable Task Data Contributor RBAC role + expect(/0ad04412-c4d5-4796-b79c-f76d14c8d402/i.test(bicepContent)).toBe(true); + }, deployTestTimeoutMs); + }); + // Azure Container Apps (ACA) describe("azure-container-apps-deploy", () => { test("creates containerized web application", async () => { diff --git a/tests/azure-prepare/integration.test.ts b/tests/azure-prepare/integration.test.ts index 79997766a..d0b5a433b 100644 --- a/tests/azure-prepare/integration.test.ts +++ b/tests/azure-prepare/integration.test.ts @@ -842,4 +842,73 @@ describeIntegration(`${SKILL_NAME}_ - Integration Tests`, () => { expect(/Authentication\s*=\s*Active\s+Directory\s+(Default|Managed\s+Identity)/i.test(allFileContents)).toBe(true); }); }); + + describe("durable-task-scheduler", () => { + test("generates Durable Task Scheduler infrastructure and workflow code for a workflow app", async () => { + let workspacePath: string | undefined; + + const agentMetadata = await agent.run({ + setup: async (workspace: string) => { + workspacePath = workspace; + }, + prompt: + "Prepare the Azure deployment infrastructure for a new Durable Functions app " + + "that will orchestrate a multi-step order processing pipeline. " + + "Generate the Bicep templates, RBAC assignments, and azure.yaml. " + + "Use the eastus2 region and my current subscription.", + nonInteractive: true, + followUp: FOLLOW_UP_PROMPT, + preserveWorkspace: true, + shouldEarlyTerminate: (metadata) => + hasPlanReadyForValidation(metadata) || hasValidationCommand(metadata) || isSkillInvoked(metadata, "azure-validate"), + }); + + // Preconditions + expect(workspacePath).toBeDefined(); + expect(isSkillInvoked(agentMetadata, SKILL_NAME)).toBe(true); + + // Collect all file contents the agent wrote via create tool calls + const createCalls = getToolCalls(agentMetadata, "create"); + + // Gather all Bicep file contents + const bicepContents = createCalls + .filter(event => { + const args = (event.data as Record).arguments as { path?: string } | undefined; + return args?.path?.endsWith(".bicep"); + }) + .map(event => { + const args = (event.data as Record).arguments as { file_text?: string }; + return args?.file_text ?? ""; + }); + const bicepContent = bicepContents.join("\n"); + expect(bicepContent.length).toBeGreaterThan(0); + + // Must provision a Durable Task Scheduler resource + expect(/Microsoft\.DurableTask\/schedulers/i.test(bicepContent)).toBe(true); + + // Must provision a task hub child resource + expect(/Microsoft\.DurableTask\/schedulers\/taskHubs/i.test(bicepContent)).toBe(true); + + // Must assign the Durable Task Data Contributor RBAC role (role ID: 0ad04412-c4d5-4796-b79c-f76d14c8d402) + expect(/0ad04412-c4d5-4796-b79c-f76d14c8d402/i.test(bicepContent)).toBe(true); + + // Must include the scheduler connection string app setting + const allFileContents = createCalls + .map(event => { + const args = (event.data as Record).arguments as { file_text?: string }; + return args?.file_text ?? ""; + }) + .join("\n"); + expect(/DURABLE_TASK_SCHEDULER_CONNECTION_STRING/i.test(allFileContents)).toBe(true); + + // Must include ipAllowlist to avoid 403 errors (empty list denies all traffic) + expect(/ipAllowlist/i.test(bicepContent)).toBe(true); + + // Workspace should contain orchestration/workflow code files + expectFiles(workspacePath!, + [/plan\.md$/, /azure\.yaml$/, /infra\/.*\.bicep$/], + [/\.tf$/], + ); + }); + }); }); From c57898aa26ef8aaaa4520d674d074bf4fb9ded61 Mon Sep 17 00:00:00 2001 From: Nick Greenfield Date: Tue, 3 Mar 2026 16:37:59 -0800 Subject: [PATCH 22/27] Add DTS integration test for azure-prepare and azure-deploy. Address PR feedback and make changes so that DTS is always the selected durable backend --- .../references/pre-deploy-checklist.md | 16 ++++++++ plugin/skills/azure-prepare/SKILL.md | 1 + .../azure-prepare/references/architecture.md | 11 ++++-- .../azure-prepare/references/research.md | 1 + .../services/durable-task-scheduler/README.md | 4 +- .../services/durable-task-scheduler/bicep.md | 15 +++++--- .../references/services/functions/durable.md | 8 ++-- .../templates/recipes/composition.md | 2 +- .../templates/recipes/durable/README.md | 16 +++++--- .../bicep/durable-task-scheduler.bicep | 15 +++++++- .../services/functions/templates/selection.md | 12 ++++-- .../references/specialized-routing.md | 3 +- tests/azure-deploy/integration.test.ts | 37 +++++-------------- tests/azure-prepare/integration.test.ts | 2 +- 14 files changed, 87 insertions(+), 56 deletions(-) diff --git a/plugin/skills/azure-deploy/references/pre-deploy-checklist.md b/plugin/skills/azure-deploy/references/pre-deploy-checklist.md index 1b10725fc..ae153eb96 100644 --- a/plugin/skills/azure-deploy/references/pre-deploy-checklist.md +++ b/plugin/skills/azure-deploy/references/pre-deploy-checklist.md @@ -155,6 +155,22 @@ azd up --no-prompt --- +## Service-Specific Checks + +### Durable Functions — Verify DTS Backend + +> **⛔ MANDATORY**: If the plan includes Durable Functions, verify infrastructure uses **Durable Task Scheduler** (DTS), NOT Azure Storage. + +Check that `infra/` Bicep files contain: +- `Microsoft.DurableTask/schedulers` resource +- `Microsoft.DurableTask/schedulers/taskHubs` child resource +- `Durable Task Data Contributor` RBAC role assignment +- `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` app setting + +If any are missing, **STOP** and invoke **azure-prepare** to regenerate with the durable recipe. + +--- + ## Non-AZD Deployments **For Azure CLI / Bicep:** diff --git a/plugin/skills/azure-prepare/SKILL.md b/plugin/skills/azure-prepare/SKILL.md index e2b6012cb..eb01892b0 100644 --- a/plugin/skills/azure-prepare/SKILL.md +++ b/plugin/skills/azure-prepare/SKILL.md @@ -63,6 +63,7 @@ Activate this skill when user wants to: | Azure Functions, function app, serverless function, timer trigger, HTTP trigger, func new | Stay in **azure-prepare** — prefer Azure Functions templates in Step 4 | | APIM, API Management, API gateway, deploy APIM | Stay in **azure-prepare** — see [APIM Deployment Guide](references/apim.md) | | AI gateway, AI gateway policy, AI gateway backend, AI gateway configuration | **azure-aigateway** | +| workflow, orchestration, multi-step, pipeline, fan-out/fan-in, saga, long-running process, durable | Stay in **azure-prepare** — select **durable** recipe in Step 4. **MUST** load [durable.md](references/services/functions/durable.md) and [DTS reference](references/services/durable-task-scheduler/README.md). Generate `Microsoft.DurableTask/schedulers` Bicep resources. | > ⚠️ Check the user's **prompt text** — not just existing code. Critical for greenfield projects with no codebase to scan. See [full routing table](references/specialized-routing.md). diff --git a/plugin/skills/azure-prepare/references/architecture.md b/plugin/skills/azure-prepare/references/architecture.md index 5ecb8a251..dba46e7fb 100644 --- a/plugin/skills/azure-prepare/references/architecture.md +++ b/plugin/skills/azure-prepare/references/architecture.md @@ -18,7 +18,8 @@ Select hosting stack and map components to Azure services. | Event-driven | ✓ | ✓✓ | | | Variable traffic | | ✓✓ | ✓ | | Complex dependencies | ✓✓ | | ✓ | -| Long-running processes | ✓✓ | | ✓ | +| Long-running processes | ✓✓ | ✓ (Durable Functions) | ✓ | +| Workflow / orchestration | | ✓✓ (Durable Functions + DTS) | | | Minimal ops overhead | | ✓✓ | ✓ | ## Service Mapping @@ -51,11 +52,13 @@ Select hosting stack and map components to Azure services. | Message Queue | Service Bus | | Pub/Sub | Event Grid | | Streaming | Event Hubs | + ### Workflow & Orchestration -| Need | Service | -|------|---------| -| Workflow | Logic Apps, Durable Functions, Durable Task Scheduler | +| Need | Service | Notes | +|------|---------|-------| +| Multi-step workflow / orchestration | **Durable Functions + Durable Task Scheduler** | DTS is the **required** managed backend for Durable Functions. Do NOT use Azure Storage or MSSQL backends. See [durable.md](services/functions/durable.md). | +| Low-code / visual workflow | Logic Apps | For integration-heavy, low-code scenarios | ### Supporting (Always Include) diff --git a/plugin/skills/azure-prepare/references/research.md b/plugin/skills/azure-prepare/references/research.md index 9b110b91c..23ee8c172 100644 --- a/plugin/skills/azure-prepare/references/research.md +++ b/plugin/skills/azure-prepare/references/research.md @@ -36,6 +36,7 @@ After architecture planning, research each selected component to gather best pra | API Management | [APIM](apim.md) | `azure-aigateway` (invoke for AI Gateway policies) | | Logic Apps | [Logic Apps](services/logic-apps/README.md) | — | | **Workflow & Orchestration** | | | +| Durable Functions | [Durable Functions](services/functions/durable.md), [Durable Task Scheduler](services/durable-task-scheduler/README.md) | — | | Durable Task Scheduler | [Durable Task Scheduler](services/durable-task-scheduler/README.md) | — | | **Security & Identity** | | | | Key Vault | [Key Vault](services/key-vault/README.md) | `azure-security`, `azure-keyvault-expiration-audit` | diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md index b16b07c78..862e25d5b 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md @@ -49,8 +49,8 @@ docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dt | Environment | Connection String | |-------------|-------------------| | Local Development (Emulator) | `Endpoint=http://localhost:8080;Authentication=None;TaskHub=default` | -| Azure (System-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity` | -| Azure (User-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=` | +| Azure (System-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;TaskHub=default` | +| Azure (User-Assigned MI) | `Endpoint=https://.durabletask.io;Authentication=ManagedIdentity;ClientID=;TaskHub=default` | > **⚠️ NOTE**: Durable Task Scheduler uses identity-based authentication only — no connection strings with keys. When using a User-Assigned Managed Identity (UAMI), you must include the `ClientID` in the connection string. diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md index 7a4ab5854..b595dc528 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/bicep.md @@ -44,17 +44,17 @@ resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { The Function App's managed identity **must** have the `Durable Task Data Contributor` role on the scheduler resource. Without it, the app receives **403 PermissionDenied** on gRPC calls. ```bicep -// Assumes functionApp is defined elsewhere in the same Bicep file, e.g.: -// resource functionApp 'Microsoft.Web/sites@2023-12-01' = { ... } +// Assumes the UAMI principal ID is passed from the base template's identity module +param functionAppPrincipalId string var durableTaskDataContributorRoleId = '0ad04412-c4d5-4796-b79c-f76d14c8d402' resource durableTaskRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(scheduler.id, functionApp.id, durableTaskDataContributorRoleId) + name: guid(scheduler.id, functionAppPrincipalId, durableTaskDataContributorRoleId) scope: scheduler properties: { roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', durableTaskDataContributorRoleId) - principalId: functionApp.identity.principalId + principalId: functionAppPrincipalId principalType: 'ServicePrincipal' } } @@ -86,12 +86,17 @@ resource dashboardRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-0 Include these entries in the Function App resource's `siteConfig.appSettings` array: ```bicep +// UAMI client ID from base template identity module - REQUIRED for UAMI auth +param uamiClientId string + { name: 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING' - value: 'Endpoint=${scheduler.properties.endpoint};TaskHub=${taskHub.name};Authentication=ManagedIdentity' + value: 'Endpoint=${scheduler.properties.endpoint};TaskHub=${taskHub.name};Authentication=ManagedIdentity;ClientID=${uamiClientId}' } ``` +> **⚠️ IMPORTANT**: The base templates use User Assigned Managed Identity (UAMI). You **must** include `ClientID=` in the connection string. Without it, the Durable Task SDK cannot resolve the correct identity. + > **⚠️ WARNING**: Always use `scheduler.properties.endpoint` to get the scheduler URL. Do **not** construct it manually — the endpoint includes a hash suffix and region (e.g., `https://myscheduler-abc123.westus2.durabletask.io`). ## Provision via CLI diff --git a/plugin/skills/azure-prepare/references/services/functions/durable.md b/plugin/skills/azure-prepare/references/services/functions/durable.md index 6d68f0fad..7124ba829 100644 --- a/plugin/skills/azure-prepare/references/services/functions/durable.md +++ b/plugin/skills/azure-prepare/references/services/functions/durable.md @@ -8,11 +8,11 @@ Durable Functions requires a storage backend to persist orchestration state, que | Backend | Recommendation | Notes | |---------|---------------|-------| -| **Durable Task Scheduler** | **✅ Recommended** | Fully managed Azure service. Best performance, lowest latency, built-in monitoring. Supports all languages (.NET, Python, Java, JavaScript). | -| Azure Storage (default) | Legacy | Uses Table Storage + Queues. Higher latency, harder to monitor, no built-in RBAC. | -| MSSQL | Specialized | SQL Server backend. Use when SQL infrastructure already exists. | +| **Durable Task Scheduler** | **✅ Required** | Fully managed Azure service. Best performance, lowest latency, built-in monitoring. Supports all languages (.NET, Python, Java, JavaScript). | +| Azure Storage (default) | ⛔ Do not use | Legacy backend. Higher latency, harder to monitor, no built-in RBAC. Not permitted for new projects. | +| MSSQL | Specialized | SQL Server backend. Only when SQL infrastructure already exists and user explicitly requests it. | -> **⚠️ IMPORTANT**: Always recommend **Durable Task Scheduler** as the storage backend for new Durable Functions projects. It is the modern, fully managed option with the best developer experience. See the [Durable Task Scheduler reference](../durable-task-scheduler/README.md) for setup, language-specific guidance, and Bicep templates. +> **⛔ MANDATORY**: You **MUST** use **Durable Task Scheduler** as the storage backend for ALL new Durable Functions projects. Do NOT use Azure Storage as the backend — it is legacy and not permitted. Generate the DTS Bicep module (`Microsoft.DurableTask/schedulers` + `taskHubs`), RBAC role assignment (Durable Task Data Contributor), and `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` app setting. See the [Durable Task Scheduler reference](../durable-task-scheduler/README.md) for setup, language-specific guidance, and Bicep templates. ## When to Use diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md index 75f29e3b8..21381933c 100644 --- a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md +++ b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md @@ -53,7 +53,7 @@ IF integration IN [mcp]: IF integration IN [durable]: → Full recipe with Durable Task Scheduler backend: - Add DTS IaC module (scheduler + task hub + RBAC). Continue to Step 3. - - Reference: [Durable Task Scheduler](../../durable-task-scheduler/README.md) and [Bicep patterns](../../durable-task-scheduler/bicep.md). + - Reference: [Durable Task Scheduler](../../../../durable-task-scheduler/README.md) and [Bicep patterns](../../../../durable-task-scheduler/bicep.md). - Do NOT use Azure Storage queues/tables as the durable backend — always use Durable Task Scheduler. → Continue to Step 3. diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md index e53599327..1db32887b 100644 --- a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md +++ b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md @@ -10,7 +10,7 @@ This recipe composes with any HTTP base template to create a Durable Functions a - **HTTP Client** - Starts and queries orchestrations - **Durable Task Scheduler** - Fully managed backend for state persistence, orchestration history, and task hub management -> **⚠️ IMPORTANT**: This recipe uses **Durable Task Scheduler** (DTS) as the storage backend — NOT Azure Storage queues/tables. DTS is the recommended, fully managed option with the best performance and developer experience. See [Durable Task Scheduler reference](../../../durable-task-scheduler/README.md) for details. +> **⚠️ IMPORTANT**: This recipe uses **Durable Task Scheduler** (DTS) as the storage backend — NOT Azure Storage queues/tables. DTS is the recommended, fully managed option with the best performance and developer experience. See [Durable Task Scheduler reference](../../../../../durable-task-scheduler/README.md) for details. ## Integration Type @@ -18,7 +18,7 @@ This recipe composes with any HTTP base template to create a Durable Functions a |--------|-------| | **Trigger** | `OrchestrationTrigger` + `ActivityTrigger` | | **Client** | `DurableClient` / `DurableOrchestrationClient` | -| **Auth** | Managed Identity → Durable Task Scheduler | +| **Auth** | UAMI (Managed Identity) → Durable Task Scheduler | | **IaC** | Bicep module: scheduler + task hub + RBAC | ## Composition Steps @@ -32,7 +32,7 @@ Apply these steps AFTER `azd init -t functions-quickstart-{lang}-azd`: | 3 | **Add app settings** | Add `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` to function app configuration | | 4 | **Add extension packages** | Add Durable Functions + DTS extension packages for the runtime | | 5 | **Replace source code** | Add Orchestrator + Activity + Client from `source/{lang}.md` | -| 6 | **Configure host.json** | Set DTS storage provider (see [DTS language references](../../../durable-task-scheduler/README.md)) | +| 6 | **Configure host.json** | Set DTS storage provider (see [DTS language references](../../../../../durable-task-scheduler/README.md)) | ## IaC Module @@ -50,6 +50,7 @@ module durableTaskScheduler './app/durable-task-scheduler.bicep' = { tags: tags functionAppPrincipalId: app.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID principalId: principalId // For dashboard access + uamiClientId: apiUserAssignedIdentity.outputs.clientId // REQUIRED for UAMI auth } } ``` @@ -64,6 +65,8 @@ appSettings: { } ``` +> **💡 TIP**: The module output already includes `ClientID=` when you pass `uamiClientId` — no manual connection string construction needed. + > **⚠️ NOTE**: Do NOT set `enableQueue: true` or `enableTable: true` in the storage module — DTS replaces Azure Storage queues/tables for orchestration state. ## RBAC Roles Required @@ -164,9 +167,10 @@ HTTP Start → Orchestrator → [Activity1, Activity2, Activity3] → Aggregate **Cause:** Function App managed identity lacks RBAC on the DTS scheduler, or IP allowlist blocks traffic. **Solution:** -1. Assign `Durable Task Data Contributor` role (`0ad04412-c4d5-4796-b79c-f76d14c8d402`) to the identity scoped to the scheduler. -2. Ensure the scheduler's `ipAllowlist` includes `0.0.0.0/0` (empty list denies all traffic). -3. RBAC propagation can take up to 10 minutes — restart the Function App after assigning roles. +1. Assign `Durable Task Data Contributor` role (`0ad04412-c4d5-4796-b79c-f76d14c8d402`) to the UAMI scoped to the scheduler. +2. Ensure the connection string includes `ClientID=`. +3. Ensure the scheduler's `ipAllowlist` includes `0.0.0.0/0` (empty list denies all traffic). +4. RBAC propagation can take up to 10 minutes — restart the Function App after assigning roles. ### TaskHub not found diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/bicep/durable-task-scheduler.bicep b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/bicep/durable-task-scheduler.bicep index 421dc7ff0..664505ce9 100644 --- a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/bicep/durable-task-scheduler.bicep +++ b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/bicep/durable-task-scheduler.bicep @@ -12,6 +12,7 @@ // tags: tags // functionAppPrincipalId: app.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID // principalId: principalId +// uamiClientId: apiUserAssignedIdentity.outputs.clientId // } // } // @@ -31,12 +32,15 @@ param location string = resourceGroup().location @description('Resource tags') param tags object = {} -@description('Principal ID of the Function App managed identity (UAMI or SAMI)') +@description('Principal ID of the Function App managed identity (UAMI)') param functionAppPrincipalId string @description('Principal ID of the deploying user (for dashboard access). Set via AZURE_PRINCIPAL_ID.') param principalId string = '' +@description('UAMI client ID from base template identity module - REQUIRED for UAMI auth') +param uamiClientId string + @allowed(['Consumption', 'Dedicated']) @description('Use Consumption for quickstarts/variable workloads, Dedicated for high-demand/predictable throughput') param skuName string = 'Consumption' @@ -102,4 +106,11 @@ resource dashboardRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-0 output schedulerName string = scheduler.name output schedulerEndpoint string = scheduler.properties.endpoint output taskHubName string = taskHub.name -output connectionString string = 'Endpoint=${scheduler.properties.endpoint};Authentication=ManagedIdentity;TaskHub=${taskHub.name}' +output connectionString string = 'Endpoint=${scheduler.properties.endpoint};Authentication=ManagedIdentity;ClientID=${uamiClientId};TaskHub=${taskHub.name}' + +// ============================================================================ +// APP SETTINGS OUTPUT - Use this to ensure correct UAMI configuration +// ============================================================================ +output appSettings object = { + DURABLE_TASK_SCHEDULER_CONNECTION_STRING: 'Endpoint=${scheduler.properties.endpoint};Authentication=ManagedIdentity;ClientID=${uamiClientId};TaskHub=${taskHub.name}' +} diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/selection.md b/plugin/skills/azure-prepare/references/services/functions/templates/selection.md index c2fa2ddd9..a7421f3f5 100644 --- a/plugin/skills/azure-prepare/references/services/functions/templates/selection.md +++ b/plugin/skills/azure-prepare/references/services/functions/templates/selection.md @@ -39,10 +39,15 @@ Cross-reference with [top Azure Functions scenarios](https://learn.microsoft.com Recipe: recipes/servicebus/ ✅ Available 7. Is it for orchestration or workflows? - Indicators: DurableOrchestrationTrigger, orchestrator, durable_functions + Code indicators: DurableOrchestrationTrigger, orchestrator, durable_functions + Natural language indicators (NEW projects): workflow, multi-step, pipeline, + orchestration, fan-out, fan-in, long-running process, chaining, state machine, + saga, order processing, approval flow └─► YES → HTTP base + durable recipe (IaC: Durable Task Scheduler + task hub + RBAC + source) + ⛔ REQUIRED: Generate Microsoft.DurableTask/schedulers + taskHubs Bicep resources Recipe: recipes/durable/ ✅ Available - Reference: [Durable Task Scheduler](../../durable-task-scheduler/README.md) for Bicep patterns and connection string + References: [durable.md](../../functions/durable.md) for storage backend rules, + [Durable Task Scheduler](../../durable-task-scheduler/README.md) for Bicep patterns and connection string 8. Does it use Event Hubs? Indicators: EventHubTrigger, @app.event_hub, event_hub_output @@ -71,6 +76,7 @@ Cross-reference with [top Azure Functions scenarios](https://learn.microsoft.com | Type | IaC Delta? | Examples | |------|-----------|----------| -| **Full recipe** | Yes — Bicep module + Terraform module + RBAC + networking | cosmosdb, servicebus, eventhubs, durable | +| **Full recipe** | Yes — Bicep module + Terraform module + RBAC + networking | cosmosdb, servicebus, eventhubs | +| **Full recipe (Bicep only)** | Yes — Bicep module + RBAC | durable | | **AZD template** | Use dedicated AZD template from Awesome AZD | sql, blob-eventgrid | | **Source-only** | No — only replace function source code (may toggle storage params) | timer, mcp | diff --git a/plugin/skills/azure-prepare/references/specialized-routing.md b/plugin/skills/azure-prepare/references/specialized-routing.md index e0e23907a..5f2847818 100644 --- a/plugin/skills/azure-prepare/references/specialized-routing.md +++ b/plugin/skills/azure-prepare/references/specialized-routing.md @@ -10,7 +10,8 @@ |----------|---------------------|--------------------|-----------------------------| | **1 (highest)** | Lambda, AWS Lambda, migrate AWS, migrate GCP, Lambda to Functions, migrate from AWS, migrate from GCP | **azure-cloud-migrate** | Phase 1 Step 4 (Select Recipe) — azure-cloud-migrate does assessment + code conversion, then azure-prepare takes over for infrastructure, local testing, or deployment | | 2 | copilot SDK, copilot app, copilot-powered, @github/copilot-sdk, CopilotClient, sendAndWait, copilot-sdk-service | **azure-hosted-copilot-sdk** | Phase 1 Step 4 (Select Recipe) | -| 3 (lowest) | Azure Functions, function app, serverless function, timer trigger, HTTP trigger, queue trigger, func new, func start | Stay in **azure-prepare** | Phase 1 Step 4 (Select Recipe) — prefer Azure Functions templates | +| 3 | Azure Functions, function app, serverless function, timer trigger, HTTP trigger, queue trigger, func new, func start | Stay in **azure-prepare** | Phase 1 Step 4 (Select Recipe) — prefer Azure Functions templates | +| 4 (lowest) | workflow, orchestration, multi-step, pipeline, fan-out/fan-in, saga, long-running process, durable | Stay in **azure-prepare** | Phase 1 Step 4 — select **durable** recipe. **MUST** load [durable.md](services/functions/durable.md) and [DTS reference](services/durable-task-scheduler/README.md). Generate `Microsoft.DurableTask/schedulers` + `taskHubs` Bicep resources. | > ⚠️ This checks the user's **prompt text**, not just existing code. Essential for greenfield projects where there is no codebase to scan. diff --git a/tests/azure-deploy/integration.test.ts b/tests/azure-deploy/integration.test.ts index 4bc872a72..1a4ed66c4 100644 --- a/tests/azure-deploy/integration.test.ts +++ b/tests/azure-deploy/integration.test.ts @@ -13,11 +13,10 @@ import { shouldSkipIntegrationTests, getIntegrationSkipReason, useAgentRunner, - getToolCalls } from "../utils/agent-runner"; import { hasDeployLinks, softCheckDeploySkills, softCheckContainerDeployEnvVars } from "./utils"; import { cloneRepo } from "../utils/git-clone"; -import { expectFiles, softCheckSkill } from "../utils/evaluate"; +import { expectFiles, softCheckSkill, doesWorkspaceFileIncludePattern } from "../utils/evaluate"; const SKILL_NAME = "azure-deploy"; const RUNS_PER_PROMPT = 1; @@ -271,40 +270,24 @@ describeIntegration(`${SKILL_NAME}_ - Integration Tests`, () => { setup: async (workspace: string) => { workspacePath = workspace; }, - prompt: "Prepare the Azure deployment infrastructure for a new Durable Functions app that will orchestrate a multi-step order processing pipeline. Generate the Bicep templates, RBAC assignments, and azure.yaml. Use the eastus2 region and my current subscription.", + prompt: "Create a workflow app that orchestrates a multi-step order processing pipeline and deploy to Azure using my current subscription in eastus2 region.", nonInteractive: true, followUp: FOLLOW_UP_PROMPT, preserveWorkspace: true }); softCheckDeploySkills(agentMetadata); - const containsDeployLinks = hasDeployLinks(agentMetadata); - expect(workspacePath).toBeDefined(); - expect(containsDeployLinks).toBe(true); expectFiles(workspacePath!, [/infra\/.*\.bicep$/], [/\.tf$/]); - // Verify DTS-specific Bicep content was generated - const createCalls = getToolCalls(agentMetadata, "create"); - const bicepContent = createCalls - .filter(event => { - const args = (event.data as Record).arguments as { path?: string } | undefined; - return args?.path?.endsWith(".bicep"); - }) - .map(event => { - const args = (event.data as Record).arguments as { file_text?: string }; - return args?.file_text ?? ""; - }) - .join("\n"); - - // Must provision a Durable Task Scheduler resource - expect(/Microsoft\.DurableTask\/schedulers/i.test(bicepContent)).toBe(true); - - // Must provision a task hub child resource - expect(/Microsoft\.DurableTask\/schedulers\/taskHubs/i.test(bicepContent)).toBe(true); - - // Must assign the Durable Task Data Contributor RBAC role - expect(/0ad04412-c4d5-4796-b79c-f76d14c8d402/i.test(bicepContent)).toBe(true); + // Verify DTS-specific Bicep content on disk + const bicepPattern = /\.bicep$/; + expect(doesWorkspaceFileIncludePattern(workspacePath!, /Microsoft\.DurableTask\/schedulers/i, bicepPattern)).toBe(true); + expect(doesWorkspaceFileIncludePattern(workspacePath!, /Microsoft\.DurableTask\/schedulers\/taskHubs/i, bicepPattern)).toBe(true); + expect(doesWorkspaceFileIncludePattern(workspacePath!, /0ad04412-c4d5-4796-b79c-f76d14c8d402/i, bicepPattern)).toBe(true); + + const containsDeployLinks = hasDeployLinks(agentMetadata); + expect(containsDeployLinks).toBe(true); }, deployTestTimeoutMs); }); diff --git a/tests/azure-prepare/integration.test.ts b/tests/azure-prepare/integration.test.ts index d0b5a433b..8e41ceff4 100644 --- a/tests/azure-prepare/integration.test.ts +++ b/tests/azure-prepare/integration.test.ts @@ -852,7 +852,7 @@ describeIntegration(`${SKILL_NAME}_ - Integration Tests`, () => { workspacePath = workspace; }, prompt: - "Prepare the Azure deployment infrastructure for a new Durable Functions app " + + "Prepare the Azure deployment infrastructure for a new workflow app " + "that will orchestrate a multi-step order processing pipeline. " + "Generate the Bicep templates, RBAC assignments, and azure.yaml. " + "Use the eastus2 region and my current subscription.", From ba0ddfeec57308700029008d4efd14ae2422515f Mon Sep 17 00:00:00 2001 From: Nick Greenfield Date: Tue, 3 Mar 2026 16:54:36 -0800 Subject: [PATCH 23/27] chore: bump azure-deploy to 1.0.4 and azure-prepare to 1.0.3 --- plugin/skills/azure-deploy/SKILL.md | 2 +- plugin/skills/azure-prepare/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/skills/azure-deploy/SKILL.md b/plugin/skills/azure-deploy/SKILL.md index 585addfaf..d2b111ff2 100644 --- a/plugin/skills/azure-deploy/SKILL.md +++ b/plugin/skills/azure-deploy/SKILL.md @@ -4,7 +4,7 @@ description: "Execute Azure deployments for ALREADY-PREPARED applications that h license: MIT metadata: author: Microsoft - version: "1.0.3" + version: "1.0.4" --- # Azure Deploy diff --git a/plugin/skills/azure-prepare/SKILL.md b/plugin/skills/azure-prepare/SKILL.md index eb01892b0..1b8becd87 100644 --- a/plugin/skills/azure-prepare/SKILL.md +++ b/plugin/skills/azure-prepare/SKILL.md @@ -4,7 +4,7 @@ description: "Prepare Azure apps for deployment (infra Bicep/Terraform, azure.ya license: MIT metadata: author: Microsoft - version: "1.0.2" + version: "1.0.3" --- # Azure Prepare From 5f4cd174f819e64482a7312dc60aef48159463bd Mon Sep 17 00:00:00 2001 From: Nick Greenfield Date: Tue, 3 Mar 2026 16:55:36 -0800 Subject: [PATCH 24/27] fix: correct relative links to durable-task-scheduler folder --- .../functions/templates/recipes/composition.md | 2 +- .../functions/templates/recipes/durable/README.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md index 21381933c..f24ebc4c7 100644 --- a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md +++ b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md @@ -53,7 +53,7 @@ IF integration IN [mcp]: IF integration IN [durable]: → Full recipe with Durable Task Scheduler backend: - Add DTS IaC module (scheduler + task hub + RBAC). Continue to Step 3. - - Reference: [Durable Task Scheduler](../../../../durable-task-scheduler/README.md) and [Bicep patterns](../../../../durable-task-scheduler/bicep.md). + - Reference: [Durable Task Scheduler](../../../durable-task-scheduler/README.md) and [Bicep patterns](../../../durable-task-scheduler/bicep.md). - Do NOT use Azure Storage queues/tables as the durable backend — always use Durable Task Scheduler. → Continue to Step 3. diff --git a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md index 1db32887b..41a936092 100644 --- a/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md +++ b/plugin/skills/azure-prepare/references/services/functions/templates/recipes/durable/README.md @@ -10,7 +10,7 @@ This recipe composes with any HTTP base template to create a Durable Functions a - **HTTP Client** - Starts and queries orchestrations - **Durable Task Scheduler** - Fully managed backend for state persistence, orchestration history, and task hub management -> **⚠️ IMPORTANT**: This recipe uses **Durable Task Scheduler** (DTS) as the storage backend — NOT Azure Storage queues/tables. DTS is the recommended, fully managed option with the best performance and developer experience. See [Durable Task Scheduler reference](../../../../../durable-task-scheduler/README.md) for details. +> **⚠️ IMPORTANT**: This recipe uses **Durable Task Scheduler** (DTS) as the storage backend — NOT Azure Storage queues/tables. DTS is the recommended, fully managed option with the best performance and developer experience. See [Durable Task Scheduler reference](../../../../durable-task-scheduler/README.md) for details. ## Integration Type @@ -32,7 +32,7 @@ Apply these steps AFTER `azd init -t functions-quickstart-{lang}-azd`: | 3 | **Add app settings** | Add `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` to function app configuration | | 4 | **Add extension packages** | Add Durable Functions + DTS extension packages for the runtime | | 5 | **Replace source code** | Add Orchestrator + Activity + Client from `source/{lang}.md` | -| 6 | **Configure host.json** | Set DTS storage provider (see [DTS language references](../../../../../durable-task-scheduler/README.md)) | +| 6 | **Configure host.json** | Set DTS storage provider (see [DTS language references](../../../../durable-task-scheduler/README.md)) | ## IaC Module @@ -81,10 +81,10 @@ The `host.json` must configure DTS as the storage provider. The `type` value dif | Language | `storageProvider.type` | Reference | |----------|----------------------|-----------| -| C# (.NET) | `azureManaged` | [dotnet.md](../../../durable-task-scheduler/dotnet.md) | -| Python | `durabletask-scheduler` | [python.md](../../../durable-task-scheduler/python.md) | -| JavaScript/TypeScript | `durabletask-scheduler` | [javascript.md](../../../durable-task-scheduler/javascript.md) | -| Java | `durabletask-scheduler` | [java.md](../../../durable-task-scheduler/java.md) | +| C# (.NET) | `azureManaged` | [dotnet.md](../../../../durable-task-scheduler/dotnet.md) | +| Python | `durabletask-scheduler` | [python.md](../../../../durable-task-scheduler/python.md) | +| JavaScript/TypeScript | `durabletask-scheduler` | [javascript.md](../../../../durable-task-scheduler/javascript.md) | +| Java | `durabletask-scheduler` | [java.md](../../../../durable-task-scheduler/java.md) | **Example (Python / JavaScript / Java):** ```json From 301d39f69cd572c62fd360dc5f827e1a11c5a25b Mon Sep 17 00:00:00 2001 From: Nick Greenfield Date: Tue, 3 Mar 2026 16:58:46 -0800 Subject: [PATCH 25/27] fix: resolve remaining conflict markers in azure-deploy SKILL.md --- plugin/skills/azure-deploy/SKILL.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugin/skills/azure-deploy/SKILL.md b/plugin/skills/azure-deploy/SKILL.md index d2b111ff2..1d6934ae7 100644 --- a/plugin/skills/azure-deploy/SKILL.md +++ b/plugin/skills/azure-deploy/SKILL.md @@ -85,10 +85,5 @@ Activate this skill when user wants to: ## References -<<<<<<< HEAD - [Troubleshooting](references/troubleshooting.md) - Common issues and solutions - [Post-Deployment Steps](references/recipes/azd/post-deployment.md) - SQL + EF Core setup -- [Durable Task Scheduler Deployment](references/recipes/azd/durable-task-scheduler-deploy.md) - DTS provisioning, Bicep, and managed identity setup -======= -- [Troubleshooting](references/troubleshooting.md) - Common issues and solutions ->>>>>>> 0e0ef37 (revert: remove DTS deploy recipe from azure-deploy skill) From a9cc0588bd06be6a513224ef68f6e0a9324a92dd Mon Sep 17 00:00:00 2001 From: Nick Greenfield Date: Tue, 3 Mar 2026 17:01:52 -0800 Subject: [PATCH 26/27] docs: expand ACA/AKS abbreviations to full names in DTS references --- .../references/services/durable-task-scheduler/README.md | 2 +- .../references/services/durable-task-scheduler/dotnet.md | 2 +- .../references/services/durable-task-scheduler/java.md | 2 +- .../references/services/durable-task-scheduler/javascript.md | 2 +- .../references/services/durable-task-scheduler/python.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md index 862e25d5b..11fc4f6ad 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/README.md @@ -18,7 +18,7 @@ Build reliable, fault-tolerant workflows using durable execution with Azure Dura | Framework | Best For | Hosting | |-----------|----------|---------| | **Durable Functions** | Serverless event-driven apps | Azure Functions | -| **Durable Task SDKs** | Any compute (containers, VMs) | ACA, AKS, App Service, VMs | +| **Durable Task SDKs** | Any compute (containers, VMs) | Azure Container Apps, Azure Kubernetes Service, App Service, VMs | > **💡 TIP**: Use Durable Functions for serverless with built-in triggers. Use Durable Task SDKs for hosting flexibility. diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md index 9fcb6d1bc..09975dc33 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/dotnet.md @@ -173,7 +173,7 @@ catch (TaskFailedException ex) ## Durable Task SDK (Non-Functions) -For applications running outside Azure Functions (containers, VMs, ACA, AKS): +For applications running outside Azure Functions (containers, VMs, Azure Container Apps, Azure Kubernetes Service): ```csharp var connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md index 12956739c..74c32b21f 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/java.md @@ -192,7 +192,7 @@ public String workflowWithRetry( ## Durable Task SDK (Non-Functions) -For applications running outside Azure Functions (containers, VMs, ACA, AKS): +For applications running outside Azure Functions (containers, VMs, Azure Container Apps, Azure Kubernetes Service): ```java import com.microsoft.durabletask.*; diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md index 949f4faa8..32f5e9364 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/javascript.md @@ -171,7 +171,7 @@ df.app.orchestration("workflowWithRetry", function* (context) { ## Durable Task SDK (Non-Functions) -For applications running outside Azure Functions (containers, VMs, ACA, AKS): +For applications running outside Azure Functions (containers, VMs, Azure Container Apps, Azure Kubernetes Service): ```javascript const { createAzureManagedWorkerBuilder, createAzureManagedClient } = require("@microsoft/durabletask-js-azuremanaged"); diff --git a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md index 514c1b3c6..883935a6b 100644 --- a/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md +++ b/plugin/skills/azure-prepare/references/services/durable-task-scheduler/python.md @@ -176,7 +176,7 @@ def workflow_with_retry(context: df.DurableOrchestrationContext): ## Durable Task SDK (Non-Functions) -For applications running outside Azure Functions (containers, VMs, ACA, AKS): +For applications running outside Azure Functions (containers, VMs, Azure Container Apps, Azure Kubernetes Service): ```python import asyncio From 88d90408e3e8962e6661c5f421566f78536eeba7 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Wed, 4 Mar 2026 10:13:01 -0800 Subject: [PATCH 27/27] fix: add taskHubs to DTS Bicep resource requirement in SKILL.md routing table --- plugin/skills/azure-prepare/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/skills/azure-prepare/SKILL.md b/plugin/skills/azure-prepare/SKILL.md index 1b8becd87..7253caacc 100644 --- a/plugin/skills/azure-prepare/SKILL.md +++ b/plugin/skills/azure-prepare/SKILL.md @@ -63,7 +63,7 @@ Activate this skill when user wants to: | Azure Functions, function app, serverless function, timer trigger, HTTP trigger, func new | Stay in **azure-prepare** — prefer Azure Functions templates in Step 4 | | APIM, API Management, API gateway, deploy APIM | Stay in **azure-prepare** — see [APIM Deployment Guide](references/apim.md) | | AI gateway, AI gateway policy, AI gateway backend, AI gateway configuration | **azure-aigateway** | -| workflow, orchestration, multi-step, pipeline, fan-out/fan-in, saga, long-running process, durable | Stay in **azure-prepare** — select **durable** recipe in Step 4. **MUST** load [durable.md](references/services/functions/durable.md) and [DTS reference](references/services/durable-task-scheduler/README.md). Generate `Microsoft.DurableTask/schedulers` Bicep resources. | +| workflow, orchestration, multi-step, pipeline, fan-out/fan-in, saga, long-running process, durable | Stay in **azure-prepare** — select **durable** recipe in Step 4. **MUST** load [durable.md](references/services/functions/durable.md) and [DTS reference](references/services/durable-task-scheduler/README.md). Generate `Microsoft.DurableTask/schedulers` + `taskHubs` Bicep resources. | > ⚠️ Check the user's **prompt text** — not just existing code. Critical for greenfield projects with no codebase to scan. See [full routing table](references/specialized-routing.md).