| author | hhunter-ms | |
|---|---|---|
| ms.author | hannahhunter | |
| title | Quickstart: Host a Durable Task SDK app on Azure Container Apps | |
| titleSuffix | Durable Task | |
| description | Learn how to configure a container app for the Durable Task Scheduler using the Durable Task SDKs and deploy using Azure Developer CLI. | |
| ms.subservice | durable-task-sdks | |
| ms.topic | quickstart | |
| ms.service | durable-task | |
| ms.date | 02/25/2026 | |
| zone_pivot_groups | df-languages | |
| ms.custom |
|
::: zone pivot="powershell"
[!INCLUDE preview-sample-limitations]
::: zone-end
::: zone pivot="csharp,python,java,javascript"
In this quickstart, you learn how to:
[!div class="checklist"]
- Set up and run the Durable Task Scheduler emulator for local development.
- Run the worker and client projects.
- Check the Azure Container Apps logs.
- Review orchestration status and history via the Durable Task Scheduler dashboard.
Before you begin:
::: zone-end
::: zone pivot="csharp"
- Make sure you have .NET 8 SDK or later.
- Install Docker for running the emulator.
- Install Azure Developer CLI
- Clone the Durable Task Scheduler GitHub repository to use the quickstart sample.
::: zone-end
::: zone pivot="python"
- Make sure you have Python 3.9+ or later.
- Install Docker for running the emulator.
- Install Azure Developer CLI
- Clone the Durable Task Scheduler GitHub repository to use the quickstart sample.
::: zone-end
::: zone pivot="java"
- Make sure you have Java 8 or 11.
- Install Docker for running the emulator.
- Install Azure Developer CLI
- Clone the Durable Task Scheduler GitHub repository to use the quickstart sample.
::: zone-end
::: zone pivot="javascript"
- Make sure you have Node.js 22 or later.
- Install Docker for running the emulator.
- Install Azure Developer CLI
- Clone the Durable Task Scheduler GitHub repository to use the quickstart sample.
::: zone-end
::: zone pivot="csharp,python,java,javascript"
In a new terminal window, from the Azure-Samples/Durable-Task-Scheduler directory, navigate into the sample directory.
::: zone-end
::: zone pivot="csharp"
cd /samples/durable-task-sdks/dotnet/FunctionChaining::: zone-end
::: zone pivot="python"
cd /samples/durable-task-sdks/python/function-chaining::: zone-end
::: zone pivot="java"
cd /samples/durable-task-sdks/java/function-chaining::: zone-end
::: zone pivot="javascript"
cd /samples/durable-task-sdks/javascript/function-chaining::: zone-end
::: zone pivot="csharp,python,java,javascript"
-
Run
azd upto provision the infrastructure and deploy the application to Azure Container Apps in a single command.azd up -
When prompted in the terminal, provide the following parameters.
Parameter Description Environment Name Prefix for the resource group created to hold all Azure resources. Azure Location The Azure location for your resources. Azure Subscription The Azure subscription for your resources. This process may take some time to complete. As the
azd upcommand completes, the CLI output displays two Azure portal links to monitor the deployment progress. The output also demonstrates howazd up:- Creates and configures all necessary Azure resources via the provided Bicep files in the
./infradirectory usingazd provision. Once provisioned by Azure Developer CLI, you can access these resources via the Azure portal. The files that provision the Azure resources include:main.parameters.jsonmain.bicep- An
appresources directory organized by functionality - A
corereference library that contains the Bicep modules used by theazdtemplate
- Deploys the code using
azd deploy
Packaging services (azd package) (✓) Done: Packaging service client - Image Hash: {IMAGE_HASH} - Target Image: {TARGET_IMAGE} (✓) Done: Packaging service worker - Image Hash: {IMAGE_HASH} - Target Image: {TARGET_IMAGE} Provisioning Azure resources (azd provision) Provisioning Azure resources can take some time. Subscription: SUBSCRIPTION_NAME (SUBSCRIPTION_ID) Location: West US 2 You can view detailed progress in the Azure portal: https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/%2Fsubscriptions%SUBSCRIPTION_ID%2Fproviders%2FMicrosoft.Resources%2Fdeployments%2FCONTAINER_APP_ENVIRONMENT (✓) Done: Resource group: GENERATED_RESOURCE_GROUP (1.385s) (✓) Done: Container Apps Environment: GENERATED_CONTAINER_APP_ENVIRONMENT (54.125s) (✓) Done: Container Registry: GENERATED_REGISTRY (1m27.747s) (✓) Done: Container App: SAMPLE_CLIENT_APP (21.39s) (✓) Done: Container App: SAMPLE_WORKER_APP (24.136s) Deploying services (azd deploy) (✓) Done: Deploying service client - Endpoint: https://SAMPLE_CLIENT_APP.westus2.azurecontainerapps.io/ (✓) Done: Deploying service worker - Endpoint: https://SAMPLE_WORKER_APP.westus2.azurecontainerapps.io/ SUCCESS: Your up workflow to provision and deploy to Azure completed in 10 minutes 34 seconds. - Creates and configures all necessary Azure resources via the provided Bicep files in the
::: zone-end
::: zone pivot="csharp,python,java,javascript"
In the Azure portal, verify the orchestrations are running successfully.
::: zone-end
::: zone pivot="java"
-
Copy the resource group name from the terminal output.
-
Sign in to the Azure portal and search for that resource group name.
-
From the resource group overview page, click on the client container app resource.
-
Select Monitoring > Log stream.
-
Confirm the sample container app is logging the function chaining tasks.
:::image type="content" source="../scheduler/media/quickstart-container-apps-durable-task-sdk/java-sample-app-log-stream.png" alt-text="Screenshot of the Java sample app's log stream in the Azure portal.":::
::: zone-end
::: zone pivot="csharp,python,javascript"
-
Copy the resource group name from the terminal output.
-
Sign in to the Azure portal and search for that resource group name.
-
From the resource group overview page, click on the client container app resource.
-
Select Monitoring > Log stream.
-
Confirm the client container is logging the function chaining tasks.
:::image type="content" source="../scheduler/media/quickstart-container-apps-durable-task-sdk/client-app-log-stream.png" alt-text="Screenshot of the client container's log stream in the Azure portal.":::
-
Navigate back to the resource group page to select the
workercontainer. -
Select Monitoring > Log stream.
-
Confirm the worker container is logging the function chaining tasks.
:::image type="content" source="../scheduler/media/quickstart-container-apps-durable-task-sdk/worker-app-log-stream.png" alt-text="Screenshot of the worker container's log stream in the Azure portal.":::
::: zone-end
::: zone pivot="csharp,python,java,javascript"
When you're done testing, remove the deployed resources:
azd down
::: zone-end
::: zone pivot="csharp,python,java,javascript"
::: zone-end
::: zone pivot="csharp"
The Client project:
- Uses the same connection string logic as the worker
- Implements a sequential orchestration scheduler that:
- Schedules 20 orchestration instances, one at a time
- Waits 5 seconds between scheduling each orchestration
- Tracks all orchestration instances in a list
- Waits for all orchestrations to complete before exiting
- Uses standard logging to show progress and results
// Schedule 20 orchestrations sequentially
for (int i = 0; i < TotalOrchestrations; i++)
{
// Create a unique instance ID
string instanceName = $"{name}_{i+1}";
// Schedule the orchestration
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
"GreetingOrchestration",
instanceName);
// Wait 5 seconds before scheduling the next one
await Task.Delay(TimeSpan.FromSeconds(IntervalSeconds));
}
// Wait for all orchestrations to complete
foreach (string id in allInstanceIds)
{
OrchestrationMetadata instance = await client.WaitForInstanceCompletionAsync(
id, getInputsAndOutputs: false, CancellationToken.None);
}The Worker project contains:
- GreetingOrchestration.cs: Defines the orchestrator and activity functions in a single file
- Program.cs: Sets up the worker host with proper connection string handling
The orchestration directly calls each activity in sequence using the standard CallActivityAsync method:
public override async Task<string> RunAsync(TaskOrchestrationContext context, string name)
{
// Step 1: Say hello to the person
string greeting = await context.CallActivityAsync<string>(nameof(SayHelloActivity), name);
// Step 2: Process the greeting
string processedGreeting = await context.CallActivityAsync<string>(nameof(ProcessGreetingActivity), greeting);
// Step 3: Finalize the response
string finalResponse = await context.CallActivityAsync<string>(nameof(FinalizeResponseActivity), processedGreeting);
return finalResponse;
}Each activity is implemented as a separate class decorated with the [DurableTask] attribute:
[DurableTask]
public class SayHelloActivity : TaskActivity<string, string>
{
// Implementation details
}The worker uses Microsoft.Extensions.Hosting for proper lifecycle management:
var builder = Host.CreateApplicationBuilder();
builder.Services.AddDurableTaskWorker()
.AddTasks(registry => {
registry.AddAllGeneratedTasks();
})
.UseDurableTaskScheduler(connectionString);
var host = builder.Build();
await host.StartAsync();::: zone-end
::: zone pivot="python"
The Client project:
- Uses the same connection string logic as the worker
- Implements a sequential orchestration scheduler that:
- Schedules 20 orchestration instances, one at a time
- Waits 5 seconds between scheduling each orchestration
- Tracks all orchestration instances in a list
- Waits for all orchestrations to complete before exiting
- Uses standard logging to show progress and results
# Schedule all orchestrations first
instance_ids = []
for i in range(TOTAL_ORCHESTRATIONS):
try:
# Create a unique instance name
instance_name = f"{name}_{i+1}"
logger.info(f"Scheduling orchestration #{i+1} ({instance_name})")
# Schedule the orchestration
instance_id = client.schedule_new_orchestration(
"function_chaining_orchestrator",
input=instance_name
)
instance_ids.append(instance_id)
logger.info(f"Orchestration #{i+1} scheduled with ID: {instance_id}")
# Wait before scheduling next orchestration (except for the last one)
if i < TOTAL_ORCHESTRATIONS - 1:
logger.info(f"Waiting {INTERVAL_SECONDS} seconds before scheduling next orchestration...")
await asyncio.sleep(INTERVAL_SECONDS)
# ...
# Wait for all orchestrations to complete
for idx, instance_id in enumerate(instance_ids):
try:
logger.info(f"Waiting for orchestration {idx+1}/{len(instance_ids)} (ID: {instance_id})...")
result = client.wait_for_orchestration_completion(
instance_id,
timeout=120
)The orchestration directly calls each activity in sequence using the standard call_activity function:
# Orchestrator function
def function_chaining_orchestrator(ctx, name: str) -> str:
"""Orchestrator that demonstrates function chaining pattern."""
logger.info(f"Starting function chaining orchestration for {name}")
# Call first activity - passing input directly without named parameter
greeting = yield ctx.call_activity('say_hello', input=name)
# Call second activity with the result from first activity
processed_greeting = yield ctx.call_activity('process_greeting', input=greeting)
# Call third activity with the result from second activity
final_response = yield ctx.call_activity('finalize_response', input=processed_greeting)
return final_responseEach activity is implemented as a separate function:
# Activity functions
def say_hello(ctx, name: str) -> str:
"""First activity that greets the user."""
logger.info(f"Activity say_hello called with name: {name}")
return f"Hello {name}!"
def process_greeting(ctx, greeting: str) -> str:
"""Second activity that processes the greeting."""
logger.info(f"Activity process_greeting called with greeting: {greeting}")
return f"{greeting} How are you today?"
def finalize_response(ctx, response: str) -> str:
"""Third activity that finalizes the response."""
logger.info(f"Activity finalize_response called with response: {response}")
return f"{response} I hope you're doing well!"The worker uses DurableTaskSchedulerWorker for proper lifecycle management:
with DurableTaskSchedulerWorker(
host_address=host_address,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential
) as worker:
# Register activities and orchestrators
worker.add_activity(say_hello)
worker.add_activity(process_greeting)
worker.add_activity(finalize_response)
worker.add_orchestrator(function_chaining_orchestrator)
# Start the worker (without awaiting)
worker.start()::: zone-end
::: zone pivot="java"
The sample container app contains both the worker and client code.
The client code:
- Uses the same connection string logic as the worker
- Implements a sequential orchestration scheduler that:
- Schedules 20 orchestration instances, one at a time
- Waits 5 seconds between scheduling each orchestration
- Tracks all orchestration instances in a list
- Waits for all orchestrations to complete before exiting
- Uses standard logging to show progress and results
// Create client using Azure-managed extensions
DurableTaskClient client = (credential != null
? DurableTaskSchedulerClientExtensions.createClientBuilder(endpoint, taskHubName, credential)
: DurableTaskSchedulerClientExtensions.createClientBuilder(connectionString)).build();
// Start a new instance of the registered "ActivityChaining" orchestration
String instanceId = client.scheduleNewOrchestrationInstance(
"ActivityChaining",
new NewOrchestrationInstanceOptions().setInput("Hello, world!"));
logger.info("Started new orchestration instance: {}", instanceId);
// Block until the orchestration completes. Then print the final status, which includes the output.
OrchestrationMetadata completedInstance = client.waitForInstanceCompletion(
instanceId,
Duration.ofSeconds(30),
true);
logger.info("Orchestration completed: {}", completedInstance);
logger.info("Output: {}", completedInstance.readOutputAs(String.class))The orchestration directly calls each activity in sequence using the standard callActivity method:
DurableTaskGrpcWorker worker = (credential != null
? DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(endpoint, taskHubName, credential)
: DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(connectionString))
.addOrchestration(new TaskOrchestrationFactory() {
@Override
public String getName() { return "ActivityChaining"; }
@Override
public TaskOrchestration create() {
return ctx -> {
String input = ctx.getInput(String.class);
String x = ctx.callActivity("Reverse", input, String.class).await();
String y = ctx.callActivity("Capitalize", x, String.class).await();
String z = ctx.callActivity("ReplaceWhitespace", y, String.class).await();
ctx.complete(z);
};
}
})
.addActivity(new TaskActivityFactory() {
@Override
public String getName() { return "Reverse"; }
@Override
public TaskActivity create() {
return ctx -> {
String input = ctx.getInput(String.class);
StringBuilder builder = new StringBuilder(input);
builder.reverse();
return builder.toString();
};
}
})
.addActivity(new TaskActivityFactory() {
@Override
public String getName() { return "Capitalize"; }
@Override
public TaskActivity create() {
return ctx -> ctx.getInput(String.class).toUpperCase();
}
})
.addActivity(new TaskActivityFactory() {
@Override
public String getName() { return "ReplaceWhitespace"; }
@Override
public TaskActivity create() {
return ctx -> {
String input = ctx.getInput(String.class);
return input.trim().replaceAll("\\s", "-");
};
}
})
.build();
// Start the worker
worker.start();::: zone-end
::: zone pivot="javascript"
The client code:
- Uses the same connection string logic as the worker
- Implements a sequential orchestration scheduler that:
- Schedules 20 orchestration instances, one at a time
- Waits 5 seconds between scheduling each orchestration
- Tracks all orchestration instances in a list
- Waits for all orchestrations to complete before exiting
- Uses standard logging to show progress and results
const TOTAL_ORCHESTRATIONS = Number(process.env.TOTAL_ORCHESTRATIONS ?? 20);
const INTERVAL_SECONDS = Number(process.env.ORCHESTRATION_INTERVAL ?? 5);
const orchestrationIds = [];
for (let index = 0; index < TOTAL_ORCHESTRATIONS; index += 1) {
const orchestrationInput = `${baseName}_${index + 1}`;
const instanceId = await client.scheduleNewOrchestration(
"functionChainingOrchestrator",
orchestrationInput
);
orchestrationIds.push(instanceId);
if (index < TOTAL_ORCHESTRATIONS - 1) {
await sleep(INTERVAL_SECONDS * 1000);
}
}
for (const instanceId of orchestrationIds) {
const state = await client.waitForOrchestrationCompletion(instanceId, true, 120);
}The orchestration directly calls each activity in sequence using the standard callActivity method:
const functionChainingOrchestrator = async function* functionChainingOrchestrator(ctx, name) {
const greeting = yield ctx.callActivity(sayHello, name);
const processedGreeting = yield ctx.callActivity(processGreeting, greeting);
const finalResponse = yield ctx.callActivity(finalizeResponse, processedGreeting);
return finalResponse;
};Each activity is implemented as a separate function:
const sayHello = async (_ctx, name) => {
const safeName = typeof name === "string" && name.length ? name : "User";
return `Hello ${safeName}!`;
};
const processGreeting = async (_ctx, greeting) => {
const value = typeof greeting === "string" ? greeting : "Hello User!";
return `${value} How are you today?`;
};
const finalizeResponse = async (_ctx, response) => {
const value = typeof response === "string" ? response : "Hello User! How are you today?";
return `${value} I hope you're doing well!`;
};The worker uses createAzureManagedWorkerBuilder for proper lifecycle management:
worker = getWorkerBuilder()
.addOrchestrator(functionChainingOrchestrator)
.addActivity(sayHello)
.addActivity(processGreeting)
.addActivity(finalizeResponse)
.build();
await worker.start();::: zone-end
[!div class="nextstepaction"] Configure autoscaling