Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
The Durable Extension for Microsoft Agent Framework brings durable execution to agents, multi-agent orchestrations, and Microsoft Agent Framework workflows. You can use it to persist agent sessions, checkpoint orchestration and workflow progress, recover from failures, and scale work across distributed hosts without changing your core agent logic.
The extension supports two hosting models in C# and Python:
- Azure Functions for managed, serverless hosting with the Azure Functions programming model.
- Bring-your-own-compute / self-hosted for running durable agents and workflows in your own worker process, service, container, Kubernetes environment, or existing app infrastructure.
Note
This guide covers C# and Python Durable Extension hosting. For Go hosting, see A2A hosting and the Agent Framework Go repository.
Overview
Durable agents combine the Agent Framework programming model with Durable Task infrastructure, such as the Durable Task Scheduler, to create agents that:
- Persist state automatically across requests and worker executions
- Resume after failures without losing conversation context or repeating completed work
- Scale across distributed, stateless workers based on demand
- Orchestrate multi-agent workflows with reliable execution guarantees
- Checkpoint Agent Framework workflows built with the graph-based workflow model
- Pause for human input or external events without consuming compute or model tokens while waiting
- Resume streamed delivery when configured with a stream broker, such as Redis; confirm durable completion separately
- Manage session lifecycle with session time-to-live (TTL) cleanup and dashboard-based monitoring
When to use durable agents
Choose durable agents when you need:
- Persistent conversation state: Agent sessions survive process crashes, restarts, and scale-out events
- Complex orchestrations: Coordinate multiple agents with deterministic, reliable workflows that can run for days or weeks
- Event-driven orchestration: Integrate with triggers, queues, webhooks, timers, or existing application events
- Automatic conversation state: Agent conversation history is automatically managed and persisted without requiring explicit state handling in your code
- Durable Agent Framework workflows: Make graph-based Microsoft Agent Framework workflows durable so each step can be checkpointed and resumed
- Long-lived sessions: Keep useful conversations available while using session time-to-live (TTL) cleanup to remove idle sessions automatically
- Real-time responses: Deliver incremental token output with broker-based reconnection, while checking the durable result separately
This hosting approach differs from managed service-based agent hosting (such as Foundry Agent Service), which provides fully managed infrastructure without requiring you to deploy or manage worker hosts. Durable agents are ideal when you need the flexibility of code-first deployment combined with durable state management.
Choose a hosting model
| Hosting model | Choose it when you need |
|---|---|
| Azure Functions | A managed, serverless hosting model; built-in scale-out and scale-to-zero; Azure Functions triggers and bindings; HTTP endpoints generated by the Functions programming model; the MCP server trigger; and minimal host infrastructure management. |
| Bring-your-own-compute / self-hosted | More control over the host process, deployment environment, runtime lifecycle, infrastructure, networking, authentication, or integration with an existing app or service. Use this model for containers, Kubernetes, long-running workers, console apps, custom services, or non-Functions hosting environments. |
When hosted in the Azure Functions Flex Consumption hosting plan, agents can scale to thousands of instances or to zero instances when not in use, allowing you to pay only for the compute you need. In self-hosted scenarios, your own host controls process lifetime, scaling, networking, and deployment.
Getting started
Use the installation commands for your hosting model below. Keep the prerelease options shown for preview packages. For complete applications, see the source and samples.
Source-branch documentation can describe capabilities that aren't available in published packages yet. Apply configuration and migration steps only for features included in your installed release.
Important
Before upgrading an existing deployment, review deployment and schema migration. Don't point an upgraded worker at old workflow histories without checking replay compatibility.
In a .NET project, choose the package set for your hosting model.
For Azure Functions hosting, add the Azure Functions integration package and the Functions worker packages.
dotnet add package Microsoft.Agents.AI.Foundry --prerelease
dotnet add package Microsoft.Agents.AI.Hosting.AzureFunctions --prerelease
Note
Use the Functions Worker, DurableTask worker extension, and AzureManaged worker extension versions declared by the agent extension's dependencies. Remove direct package references that downgrade these dependencies. See the .NET dependency references. Side-by-side schema-v2 upgrades also require work-item filtering; see deployment and schema migration.
For bring-your-own-compute hosting, add the base Durable Task integration package and the Durable Task Scheduler worker/client packages used by your host:
dotnet add package Microsoft.Agents.AI.Foundry --prerelease
dotnet add package Microsoft.Agents.AI.DurableTask --prerelease
dotnet add package Microsoft.DurableTask.Client.AzureManaged
dotnet add package Microsoft.DurableTask.Worker.AzureManaged
dotnet add package Microsoft.Extensions.Hosting
In a Python project, choose the package for your hosting model. Python hosts require an explicit deployment strategy.
For Azure Functions hosting, use Python 3.13 or later and install the Azure Functions integration package. The following commands also install the Azure OpenAI client used in this guide.
pip install agent-framework-azurefunctions agent-framework-openai azure-identity --pre
For bring-your-own-compute hosting, use Python 3.10 or later and install the Durable Task integration package.
pip install agent-framework-durabletask agent-framework-openai azure-identity --pre
For a new isolated task hub with upgraded workers and clients, set DURABLE_AGENTS_DEPLOYMENT_MODE=isolated_v2 before starting either host. For an existing hub, choose the migration strategy instead of applying this setting blindly. The setting acknowledges isolation; it doesn't detect other workers or make old histories compatible.
Azure Functions hosting
With the Durable Extension, you can deploy and host Microsoft Agent Framework agents in Azure Functions with built-in HTTP endpoints and orchestration-based invocation. Azure Functions provides event-driven, pay-per-invocation pricing with automatic scaling and minimal infrastructure management.
When you configure a durable agent in Azure Functions, the extension automatically creates HTTP endpoints for your agent and manages the underlying infrastructure for storing conversation state, handling concurrent requests, and coordinating multi-agent workflows. The Azure Functions hosting integration also provides Functions-specific conveniences such as generated REST APIs for sending messages, checking status, and managing sessions, plus triggers such as the MCP server trigger for hosting agents as MCP servers without writing trigger glue.
using System;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o-mini";
// Create an AI agent following the standard Microsoft Agent Framework pattern
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(
model: deploymentName,
instructions: "You are good at telling jokes.",
name: "Joker");
// Configure the function app to host the agent with durable thread management
// This automatically creates HTTP endpoints and manages state persistence
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
options.AddAIAgent(agent)
)
.Build();
app.Run();
Warning
DefaultAzureCredential is convenient for development but requires careful consideration in production. In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
import os
from agent_framework_azurefunctions import AgentFunctionApp
from agent_framework.openai import OpenAIChatCompletionClient
from azure.identity import DefaultAzureCredential
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini")
api_version = os.getenv("AZURE_OPENAI_API_VERSION")
# Create an AI agent following the standard Microsoft Agent Framework pattern
agent = OpenAIChatCompletionClient(
azure_endpoint=endpoint,
model=deployment_name,
api_version=api_version,
credential=DefaultAzureCredential()
).as_agent(
instructions="You are good at telling jokes.",
name="Joker"
)
# Configure the function app to host the agent with durable thread management
# This automatically creates HTTP endpoints and manages state persistence
app = AgentFunctionApp(agents=[agent])
Bring-your-own-compute / self-hosted hosting
Use bring-your-own-compute hosting when you want the Durable Extension capabilities without using the Azure Functions programming model. In this model, your process starts a Durable Task worker, registers durable agents or workflows, and connects to a Durable Task Scheduler backend. Client code can run in the same process or in a separate service.
Self-hosted workers use the same core Durable Extension capabilities as Azure Functions hosting: checkpointing and resumption, deterministic agent orchestration, durable Agent Framework workflows, human-in-the-loop waits, reliable streaming, idle-session cleanup, dashboard visibility, and distributed execution across stateless worker instances. Your host is responsible for exposing its own APIs, lifecycle management, networking, authentication, and deployment model.
Configure your host with the base Durable Task integration package. Use ConfigureDurableAgents for durable agents and ConfigureDurableWorkflows for graph-based Microsoft Agent Framework workflows.
string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.ConfigureDurableAgents(
options => options.AddAIAgent(agent),
workerBuilder: builder => builder.UseDurableTaskScheduler(connectionString),
clientBuilder: builder => builder.UseDurableTaskScheduler(connectionString));
})
.Build();
await host.StartAsync();
See the .NET Durable Agents console samples and .NET Durable Workflows console samples for runnable self-hosted examples.
Use the Durable Task integration package to run a worker process that registers agents and listens for requests. Client code can connect to the same Durable Task Scheduler task hub from another process.
from agent_framework_durabletask import DurableAIAgentWorker
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
worker = DurableTaskSchedulerWorker(
host_address="http://localhost:8080",
secure_channel=False,
taskhub="default",
)
agent_worker = DurableAIAgentWorker(worker)
agent_worker.add_agent(agent)
worker.start()
See the Python Durable Task samples for worker-client examples, including single-agent hosting, multi-agent routing, reliable streaming, orchestration chaining, concurrency, conditionals, and human-in-the-loop patterns.
Durable Agent Framework workflows
Durability is not limited to durable orchestrations. Microsoft Agent Framework workflows built with the graph-based workflow model can also be made durable. The Durable Extension checkpoints workflow execution so completed executor and agent steps are not repeated after a process restart or failure.
Use durable orchestrations when you want imperative coordination with code-based branching, timers, activities, and external events. Use durable Agent Framework workflows when you want a declarative graph of executors and agents with typed routing, fan-out/fan-in, conditional edges, workflow events, shared state, sub-workflows, or human-in-the-loop request ports.
Note
Durable Agent Framework workflows are different from checkpoint storage in standard workflows. Checkpoint storage helps resume a workflow run in the Agent Framework runtime. The Durable Extension runs the workflow on Durable Task infrastructure so workflow progress is checkpointed and recovered across distributed durable workers. For standard workflow checkpointing, see Checkpoints and resuming.
Register graph-based workflows with ConfigureDurableWorkflows for self-hosted apps or ConfigureDurableWorkflows on the Functions app builder for Azure Functions hosting.
See the .NET Durable Workflows Azure Functions samples and .NET Durable Workflows console samples.
Durable workflow samples are available for Azure Functions hosting, including shared state, no shared state, parallel workflow execution, and human-in-the-loop workflows. Python also supports graph-based workflows on a standalone Durable Task worker.
See the Python Azure Functions samples for durable agent, orchestration, MCP server, and workflow examples.
For self-hosted graph execution, see the standalone Python workflow sample.
Samples
| Language | Hosting model | Durable agents and orchestrations | Graph-based workflows |
|---|---|---|---|
| C# | Azure Functions | .NET agent samples | .NET workflow samples |
| C# | Self-hosted | .NET console agent samples | .NET console workflow samples |
| Python | Azure Functions | Python Functions samples | Python Functions workflow samples |
| Python | Self-hosted | Python worker and client samples | Python standalone workflow |
These samples demonstrate support, not identical API contracts across languages. Functions generates HTTP endpoints; self-hosted applications implement and secure their own APIs.
Stateful agent threads with conversation history
Agents maintain persistent sessions that survive across multiple interactions. Each session has a unique session ID and stores conversation state in durable storage managed by Durable Task infrastructure, such as the Durable Task Scheduler.
This pattern enables conversational continuity where agent state is preserved through process crashes and restarts, allowing full conversation history to be maintained across user threads. The durable storage ensures that even if a host process restarts or work resumes on a different worker instance, the conversation seamlessly continues from where it left off.
Configure session cleanup, history retention, and result retention deliberately. These policies have different effects; see state lifecycle and capacity.
The following Azure Functions example demonstrates multiple HTTP requests to the same session. Both language integrations return x-ms-session-id and accept the canonical sessionId query parameter. Treat the returned ID as opaque: the example shows a .NET schema-v2 entity address, while Python session formatting depends on the deployment strategy. In self-hosted apps, use the Durable Task client APIs from your own process or service.
# First interaction - start a new session
curl -i -X POST https://your-function-app.azurewebsites.net/api/agents/Joker/run \
-H "Content-Type: text/plain" \
-d "Tell me a joke about pirates"
# Response includes session ID in x-ms-session-id header and joke as plain text
# HTTP/1.1 200 OK
# Content-Type: text/plain
# x-ms-session-id: @dafx-v2-joker@263fa373-fa01-4705-abf2-5a114c2bb87d
#
# Why don't pirates shower before they walk the plank? Because they'll just wash up on shore later!
# Second interaction - reuse the exact session ID returned by the first request
curl -X POST "https://your-function-app.azurewebsites.net/api/agents/Joker/run?sessionId=@dafx-v2-joker@263fa373-fa01-4705-abf2-5a114c2bb87d" \
-H "Content-Type: text/plain" \
-d "Tell me another one about the same topic"
# Agent remembers the pirate context from the first message and responds with plain text
# What's a pirate's favorite letter? You'd think it's R, but it's actually the C!
Agent state is maintained in durable storage, enabling distributed execution across multiple instances. Any instance can resume an agent's execution after interruptions or failures, ensuring continuous operation.
Use sessionId and waitForResponse in new agent HTTP clients. thread_id is a deprecated request alias, not the response header to capture. For Python JSON responses, read sessionId and correlationId. See the HTTP API migration guide for aliases and conflict handling.
Confirm durable completion
For generated .NET Functions agent endpoints, interpret completion as follows:
| HTTP status | Meaning |
|---|---|
202 Accepted |
Dispatch is accepted. Execution isn't confirmed successful. |
200 OK |
A committed successful result is available. |
500 Internal Server Error |
A supported committed terminal failure is returned. Distinguish this from ordinary host or transport errors. |
410 Gone |
Completion is known, but its result is unavailable. The retained outcome can be succeeded or failed; this isn't necessarily a failed execution or only result expiry. |
Read x-ms-durable-outcome when a completion has no available result. Don't treat 410 as pending, infer a result from conversation history, or automatically resubmit with a new identity. JSON callers use string status and numeric statusCode; the earlier status_code field is a temporary compatibility alias.
Python Functions also distinguishes available success, retained failed results, and unavailable completed results. Its error envelopes and SDK options aren't identical to .NET. See the .NET completion contract and Python HTTP contract.
These statuses describe agent endpoints. The Durable Functions orchestration management API has its own status response and polling contract.
Reliable streaming
The Durable Extension supports reliable streaming for applications that need real-time token delivery alongside durable agent execution. Streaming can be used with the core extension in both hosting models, but distributed hosts need a reliable stream broker, such as Redis, so clients can resume reading retained token streams after reconnects or worker changes, subject to the broker's persistence and retention settings.
Use reliable streaming when the user experience depends on incremental responses, but the workload still needs durable execution semantics. Stream completion does not by itself confirm that the agent's result and state were durably persisted; check the authoritative durable result separately. For runnable examples, see the Python Durable Task samples, which include reliable streaming patterns.
Deterministic multi-agent orchestrations
The Durable Extension supports building deterministic workflows that coordinate multiple agents using Durable Task orchestrations. In Azure Functions, these use Durable Functions orchestrations; in bring-your-own-compute hosts, they run through the Durable Task worker and client you configure.
Orchestrations are code-based workflows that coordinate multiple operations (like agent calls, external API calls, or timers) in a reliable way. Deterministic means the orchestration code executes the same way when replayed after a failure, making workflows reliable and debuggable—when you replay an orchestration's history, you can see exactly what happened at each step.
Orchestrations execute reliably, surviving failures between agent calls, and provide predictable and repeatable processes. This makes them ideal for complex multi-agent scenarios where you need guaranteed execution order and fault tolerance.
Durable checkpointing doesn't make tools or external writes transactional or exactly-once. Make activity and tool side effects idempotent and define retry boundaries. A failure after an external write but before a durable commit can cause the operation to run again.
Sequential orchestrations
In the sequential multi-agent pattern, specialized agents execute in a specific order, where each agent's output can influence the next agent's execution. This pattern supports conditional logic and branching based on agent responses.
When using agents in orchestrations, you must use the context.GetAgent() API to get a DurableAIAgent instance, which is a special subclass of the standard AIAgent type that wraps one of your registered agents. The DurableAIAgent wrapper ensures that agent calls are properly tracked and checkpointed by the durable orchestration framework.
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Agents.AI.DurableTask;
[Function(nameof(SpamDetectionOrchestration))]
public static async Task<string> SpamDetectionOrchestration(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
Email email = context.GetInput<Email>();
// Check if the email is spam
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
AgentSession spamSession = await spamDetectionAgent.CreateSessionAsync();
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
message: $"Analyze this email for spam: {email.EmailContent}",
session: spamSession);
DetectionResult result = spamDetectionResponse.Result;
if (result.IsSpam)
{
return await context.CallActivityAsync<string>(nameof(HandleSpamEmail), result.Reason);
}
// Generate response for legitimate email
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
AgentSession emailSession = await emailAssistantAgent.CreateSessionAsync();
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
message: $"Draft a professional response to: {email.EmailContent}",
session: emailSession);
return await context.CallActivityAsync<string>(nameof(SendEmail), emailAssistantResponse.Result.Response);
}
When using agents in orchestrations, you must use the app.get_agent() method to get a durable agent instance, which is a special wrapper around one of your registered agents. The durable agent wrapper ensures that agent calls are properly tracked and checkpointed by the durable orchestration framework.
from durabletask.task import OrchestrationContext
from agent_framework_azurefunctions import AgentFunctionApp
from pydantic import BaseModel
class SpamDetectionResult(BaseModel):
is_spam: bool
reason: str
class EmailResponse(BaseModel):
response: str
app = AgentFunctionApp(agents=[spam_detection_agent, email_assistant_agent])
@app.orchestration_trigger(context_name="context")
def spam_detection_orchestration(context: OrchestrationContext, email: dict):
# Check if the email is spam
spam_agent = app.get_agent(context, "SpamDetectionAgent")
spam_thread = spam_agent.create_session()
spam_result_raw = yield spam_agent.run(
messages=f"Analyze this email for spam: {email['content']}",
session=spam_thread,
options={"response_format": SpamDetectionResult},
)
spam_result = SpamDetectionResult.model_validate(spam_result_raw.value)
if spam_result.is_spam:
result = yield context.call_activity("handle_spam_email", input=spam_result.reason)
return result
# Generate response for legitimate email
email_agent = app.get_agent(context, "EmailAssistantAgent")
email_thread = email_agent.create_session()
email_response_raw = yield email_agent.run(
messages=f"Draft a professional response to: {email['content']}",
session=email_thread,
options={"response_format": EmailResponse},
)
email_response = EmailResponse.model_validate(email_response_raw.value)
result = yield context.call_activity("send_email", input=email_response.response)
return result
Orchestrations coordinate work across multiple agents, surviving failures between agent calls. The orchestration context provides methods to retrieve and interact with hosted agents within orchestrations.
Parallel orchestrations
In the parallel multi-agent pattern, you execute multiple agents concurrently and then aggregate their results. This pattern is useful for gathering diverse perspectives or processing independent subtasks simultaneously.
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Agents.AI.DurableTask;
[Function(nameof(ResearchOrchestration))]
public static async Task<string> ResearchOrchestration(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
string topic = context.GetInput<string>();
// Execute multiple research agents in parallel
DurableAIAgent technicalAgent = context.GetAgent("TechnicalResearchAgent");
DurableAIAgent marketAgent = context.GetAgent("MarketResearchAgent");
DurableAIAgent competitorAgent = context.GetAgent("CompetitorResearchAgent");
// Start all agent runs concurrently
Task<AgentResponse<TextResponse>> technicalTask =
technicalAgent.RunAsync<TextResponse>($"Research technical aspects of {topic}");
Task<AgentResponse<TextResponse>> marketTask =
marketAgent.RunAsync<TextResponse>($"Research market trends for {topic}");
Task<AgentResponse<TextResponse>> competitorTask =
competitorAgent.RunAsync<TextResponse>($"Research competitors in {topic}");
// Wait for all tasks to complete
await Task.WhenAll(technicalTask, marketTask, competitorTask);
// Aggregate results
string allResearch = string.Join("\n\n",
technicalTask.Result.Result.Text,
marketTask.Result.Result.Text,
competitorTask.Result.Result.Text);
DurableAIAgent summaryAgent = context.GetAgent("SummaryAgent");
AgentResponse<TextResponse> summaryResponse =
await summaryAgent.RunAsync<TextResponse>($"Summarize this research:\n{allResearch}");
return summaryResponse.Result.Text;
}
from durabletask.task import OrchestrationContext, when_all
from agent_framework_azurefunctions import AgentFunctionApp
app = AgentFunctionApp(agents=[technical_agent, market_agent, competitor_agent, summary_agent])
@app.orchestration_trigger(context_name="context")
def research_orchestration(context: OrchestrationContext, topic: str):
# Execute multiple research agents in parallel
technical_agent = app.get_agent(context, "TechnicalResearchAgent")
market_agent = app.get_agent(context, "MarketResearchAgent")
competitor_agent = app.get_agent(context, "CompetitorResearchAgent")
technical_task = technical_agent.run(messages=f"Research technical aspects of {topic}")
market_task = market_agent.run(messages=f"Research market trends for {topic}")
competitor_task = competitor_agent.run(messages=f"Research competitors in {topic}")
# Wait for all tasks to complete
results = yield when_all([technical_task, market_task, competitor_task])
# Aggregate results
all_research = "\n\n".join(r.text for r in results)
summary_agent = app.get_agent(context, "SummaryAgent")
summary = yield summary_agent.run(messages=f"Summarize this research:\n{all_research}")
return summary.text
The parallel execution is tracked using a list of tasks. Automatic checkpointing ensures that completed agent executions are not repeated or lost if a failure occurs during aggregation.
Human-in-the-loop orchestrations
Deterministic agent orchestrations can pause for human input, approval, or review without consuming compute resources. Durable execution enables orchestrations to wait for days or even weeks while waiting for human responses. When combined with serverless hosting, all compute resources are spun down during the wait period, eliminating compute costs until the human provides their input.
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Agents.AI.DurableTask;
[Function(nameof(ContentApprovalWorkflow))]
public static async Task<string> ContentApprovalWorkflow(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
string topic = context.GetInput<string>();
// Generate content using an agent
DurableAIAgent contentAgent = context.GetAgent("ContentGenerationAgent");
AgentResponse<GeneratedContent> contentResponse =
await contentAgent.RunAsync<GeneratedContent>($"Write an article about {topic}");
GeneratedContent draftContent = contentResponse.Result;
// Send for human review
await context.CallActivityAsync(nameof(NotifyReviewer), draftContent);
// Wait for approval with timeout
HumanApprovalResponse approvalResponse;
try
{
approvalResponse = await context.WaitForExternalEvent<HumanApprovalResponse>(
eventName: "ApprovalDecision",
timeout: TimeSpan.FromHours(24));
}
catch (OperationCanceledException)
{
// Timeout occurred - escalate for review
return await context.CallActivityAsync<string>(nameof(EscalateForReview), draftContent);
}
if (approvalResponse.Approved)
{
return await context.CallActivityAsync<string>(nameof(PublishContent), draftContent);
}
return "Content rejected";
}
from durabletask.task import OrchestrationContext, when_any
from datetime import timedelta
from agent_framework_azurefunctions import AgentFunctionApp
app = AgentFunctionApp(agents=[content_agent])
@app.orchestration_trigger(context_name="context")
def content_approval_workflow(context: OrchestrationContext, topic: str):
# Generate content using an agent
content_agent = app.get_agent(context, "ContentGenerationAgent")
draft_content = yield content_agent.run(
messages=f"Write an article about {topic}"
)
# Send for human review
yield context.call_activity("notify_reviewer", input=draft_content.text)
# Wait for approval with timeout
approval_task = context.wait_for_external_event("ApprovalDecision")
timeout_task = context.create_timer(
context.current_utc_datetime + timedelta(hours=24)
)
winner = yield when_any([approval_task, timeout_task])
if winner == approval_task:
timeout_task.cancel()
approval_data = approval_task.result
if approval_data.get("approved"):
result = yield context.call_activity("publish_content", input=draft_content.text)
return result
return "Content rejected"
# Timeout occurred - escalate for review
result = yield context.call_activity("escalate_for_review", input=draft_content.text)
return result
Deterministic agent orchestrations can wait for external events, durably persisting their state while waiting for human feedback, surviving failures, restarts, and extended waiting periods. When the human response arrives, the orchestration automatically resumes with full conversation context and execution state intact.
Providing human input
To send approval or input to a waiting orchestration, raise an external event to the orchestration instance using the Durable Task client SDK or the Azure Functions Durable extension endpoints. For example, a reviewer might approve content through a web form that calls:
await client.RaiseEventAsync(instanceId, "ApprovalDecision", new HumanApprovalResponse
{
Approved = true,
Feedback = "Looks great!"
});
approval_data = {
"approved": True,
"feedback": "Looks great!"
}
await client.raise_event(instance_id, "ApprovalDecision", approval_data)
Cost efficiency
Human-in-the-loop workflows with durable agents are extremely cost-effective when hosted on the Azure Functions Flex Consumption plan. For a workflow waiting 24 hours for approval, you only pay for a few seconds of execution time (the time to generate content, send notification, and process the response)—not the 24 hours of waiting. During the wait period, no compute resources are consumed.
Observability with Durable Task Scheduler
The Durable Task Scheduler (DTS) is the recommended durable backend for your durable agents, offering the best performance, fully managed infrastructure, and built-in observability through a UI dashboard. Azure Functions apps can use other storage backends (like Azure Storage), but DTS is optimized specifically for durable workloads and provides superior performance and monitoring capabilities. Self-hosted workers also use DTS for durable scheduling, state, and dashboard visibility.
Agent session insights
- Conversation history: View complete chat history for each agent session, including all messages, tool calls, and conversation context at any point in time
- Task timing: Monitor how long specific tasks and agent interactions take to complete
Orchestration insights
- Multi-agent visualization: See the execution flow when calling multiple specialized agents with visual representation of parallel executions and conditional branching
- Execution history: Access detailed execution logs
- Real-time monitoring: Track active orchestrations, queued work items, and agent states across your deployment
- Performance metrics: Monitor agent response times, token usage, and orchestration duration
Debugging capabilities
- View structured agent outputs and tool call results
- Trace tool invocations and their outcomes
- Monitor external event handling for human-in-the-loop scenarios
The dashboard enables you to understand exactly what your agents are doing, diagnose issues quickly, and optimize performance based on real execution data.
Deployment and schema migration
Schema-v2 agent state changes both persisted state and dispatch identities. Plan worker coexistence separately from state conversion.
New sessions use the dafx-v2- entity namespace. During a two-phase rollout, leave ProcessLegacyEntities disabled while v1 workers remain active. Stop all v1 workers before enabling ProcessLegacyEntities to process and upgrade eligible legacy entities in place.
Standalone workers register work-item filters. Side-by-side Functions deployments also require compatible Functions Worker, DurableTask worker extension, and AzureManaged worker extension packages that support host-side filtering on every participating host, plus workItemFilteringEnabled in the scheduler storage provider configuration. Without compatible filtering, use an offline cutover instead of concurrent v1 and v2 hosts on one hub. Follow the .NET upgrade guide for the complete configuration.
Choose an explicit strategy before constructing a host:
two_phase: Use Durable Task Scheduler with compatible backend work-item filtering. New versioned names isolate dispatch while eligible legacy entities remain on the old workers. Stop old workers before enabling historical-name processing and in-place entity upgrades. Maintain compatible filtered workers during coexistence; name filters aren't permanent reservations if that fleet disconnects.isolated_v2: Use an isolated task hub with matching upgraded workers and clients. The setting acknowledges isolation but doesn't probe for other workers. Use fresh workflow instances.
A new hub isn't an unconditional requirement for the two-phase entity migration path. See Python entity migration for host-specific setup and prerequisites.
Warning
Entity migration doesn't repair workflow replay compatibility. Affected older workflow histories, including older v2 histories, aren't supported by the upgraded runtime. If they must finish, keep them on their original workers and hub. Start new instances after upgrading; don't rely on the workflow version marker alone to prove compatibility. See Python runtime compatibility.
State lifecycle and capacity
Choose policies for each layer independently:
| Layer | What it controls |
|---|---|
| Session TTL | Deletes the whole entity, including conversation state and completion receipts. |
| Durable history retention | Removes eligible stored transcript content; protected state can still exceed capacity. |
| Model-input compaction | Changes context sent to the model; it doesn't necessarily prune durable history. |
| Result retention | Controls response-payload availability independently of the retained completion outcome. |
| Blob payload lifecycle | Controls SDK-offloaded payload blobs; session deletion or orchestration purge doesn't clean them up. |
In .NET schema-v2 receipt-bearing state, the unconfigured legacy 14-day TTL doesn't automatically delete the entity. Configure a global or per-agent TTL explicitly if you want idle-session cleanup. Passing timeToLive: null to AddAIAgent inherits the configured global default; it doesn't override a seven-day global default with "never expires." Set DefaultTimeToLive = null to disable the global TTL for agents without positive per-agent overrides.
.NET defaults to DurableAgentHistoryRetentionMode.KeepAll; Auto enables pressure-based transcript retention. ResultRetentionPeriod defaults to 60 seconds and is separate from session TTL. Python has distinct retention and budget options, including retention="keep_all" by default. Don't assume the same options or defaults across hosts.
Python supports optional SDK-managed Blob payload offloading in both hosting models. Configure both producer and consumer stores, credentials, and Blob lifecycle. Offloading doesn't remove whole-entity capacity limits or replace retention; keep blobs while referenced state or replay history needs them. See .NET state and retention, Python retention and budgets, and Python Functions Blob offloading.
For scheduler infrastructure, capacity, and operational guidance, see Durable Task integration for Agent Framework.
Tutorial: Create and run a durable agent with Azure Functions
This tutorial shows you how to create and run a durable AI agent using the Azure Functions hosting model for the Durable Extension. You'll build an Azure Functions app that hosts a stateful agent with built-in HTTP endpoints, and learn how to monitor it using the Durable Task Scheduler dashboard. For self-hosted agents, see the samples.
Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 9.0 SDK or later
- Azure Functions Core Tools v4.x
- Azure Developer CLI (azd)
- Azure CLI installed and authenticated
- Docker Desktop installed and running (for local development with Azurite and the Durable Task Scheduler emulator)
- An Azure subscription with permissions to create resources
Note
Microsoft Agent Framework is supported with all actively supported versions of .NET. For the purposes of this sample, we recommend the .NET 9 SDK or a later version.
- Python 3.13 or later for Functions hosting. The standalone package's Python 3.10 minimum doesn't apply to this tutorial.
- uv for creating the virtual environment and installing dependencies
- Azure Functions Core Tools v4.x
- Azure Developer CLI (azd)
- Azure CLI installed and authenticated
- Docker Desktop installed and running (for local development with Azurite and the Durable Task Scheduler emulator)
- An Azure subscription with permissions to create resources
Set DURABLE_AGENTS_DEPLOYMENT_MODE=isolated_v2 only after confirming that the tutorial hub is isolated and all workers and clients use the upgraded runtime.
Use a fresh task hub for this tutorial, separate from existing durable applications. For an existing application, follow deployment and schema migration instead.
Download the quickstart project
Use Azure Developer CLI to initialize a new project from the durable agents quickstart template.
Create a new directory for your project and navigate to it:
mkdir MyDurableAgent cd MyDurableAgent
Initialize the project from the template:
azd init --template durable-agents-quickstart-dotnetWhen prompted for an environment name, enter a name like
my-durable-agent.
This downloads the quickstart project with all necessary files, including the Azure Functions configuration, agent code, and infrastructure as code templates.
Create a new directory for your project and navigate to it:
mkdir MyDurableAgent cd MyDurableAgent
Initialize the project from the template:
azd init --template durable-agents-quickstart-pythonWhen prompted for an environment name, enter a name like
my-durable-agent.Create and activate a virtual environment:
uv venv --python 3.13 .venv source .venv/bin/activate
Note
You can also use python -m venv .venv with an installed Python 3.13 interpreter. If that command hangs on Windows, use the uv venv command above instead. Verify the active interpreter with python --version before installing packages.
Install the required packages:
Update the template's
requirements.txtto the Functions packages in Getting started, including theagent-framework-openaiclient package, before installing. Resolve conflicting dependency pins in the manifest rather than forcing installation over them.uv pip install -r requirements.txt
This downloads the quickstart project with all necessary files, including the Azure Functions configuration, agent code, and infrastructure as code templates. It also prepares a virtual environment with the required dependencies.
Provision Azure resources
Use Azure Developer CLI to create the required Azure resources for your durable agent.
Provision the infrastructure:
azd provisionThis command creates:
- An Azure OpenAI service with a gpt-4o-mini deployment
- An Azure Functions app with Flex Consumption hosting plan
- An Azure Storage account for the Azure Functions runtime and durable storage
- A Durable Task Scheduler instance (Consumption plan) for managing agent state
- Necessary networking and identity configurations
When prompted, select your Azure subscription and choose a location for the resources.
The provisioning process takes a few minutes. Once complete, azd stores the created resource information in your environment.
Review the agent code
Now let's examine the code that defines your durable agent.
Open Program.cs to see the agent configuration:
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT environment variable is not set");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o-mini";
// Create an AI agent following the standard Microsoft Agent Framework pattern
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(
model: deploymentName,
instructions: "You are a helpful assistant that can answer questions and provide information.",
name: "MyDurableAgent");
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
.Build();
app.Run();
This code:
- Retrieves your Azure OpenAI configuration from environment variables.
- Creates an Azure OpenAI client using Azure credentials.
- Creates an AI agent with instructions and a name.
- Configures the Azure Functions app to host the agent with durable thread management.
Open function_app.py to see the agent configuration:
import os
from agent_framework_azurefunctions import AgentFunctionApp
from agent_framework.openai import OpenAIChatCompletionClient
from azure.identity import DefaultAzureCredential
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
if not endpoint:
raise ValueError("AZURE_OPENAI_ENDPOINT is not set.")
deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini")
api_version = os.getenv("AZURE_OPENAI_API_VERSION")
# Create an AI agent following the standard Microsoft Agent Framework pattern
agent = OpenAIChatCompletionClient(
azure_endpoint=endpoint,
model=deployment_name,
api_version=api_version,
credential=DefaultAzureCredential()
).as_agent(
instructions="You are a helpful assistant that can answer questions and provide information.",
name="MyDurableAgent"
)
# Configure the function app to host the agent with durable thread management
app = AgentFunctionApp(agents=[agent])
This code:
- Retrieves your Azure OpenAI configuration from environment variables.
- Creates an Azure OpenAI client using Azure credentials.
- Creates an AI agent with instructions and a name.
- Configures the Azure Functions app to host the agent with durable thread management.
The agent is now ready to be hosted in Azure Functions. The durable task extension automatically creates HTTP endpoints for interacting with your agent and manages conversation state across multiple requests.
Configure local settings
Create a local.settings.json file for local development based on the sample file included in the project.
Copy the sample settings file:
cp local.settings.sample.json local.settings.json
Get your Azure OpenAI endpoint from the provisioned resources:
azd env get-value AZURE_OPENAI_ENDPOINTOpen
local.settings.jsonand replace<your-resource-name>in theAZURE_OPENAI_ENDPOINTvalue with the endpoint from the previous command.
Your local.settings.json should look like this:
{
"IsEncrypted": false,
"Values": {
// ... other settings ...
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"AZURE_OPENAI_ENDPOINT": "https://your-openai-resource.openai.azure.com",
"AZURE_OPENAI_DEPLOYMENT": "gpt-4o-mini",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
"TASKHUB_NAME": "default"
}
}
Note
The local.settings.json file is used for local development only and is not deployed to Azure. For production deployments, these settings are automatically configured in your Azure Functions app by the infrastructure templates.
For Python, add "DURABLE_AGENTS_DEPLOYMENT_MODE": "isolated_v2" to Values only after confirming the tutorial hub is isolated. Configure the equivalent application setting when deploying; local settings aren't uploaded automatically.
For scheduler-backed hosting, use the same task hub in host.json (through %TASKHUB_NAME%), TASKHUB_NAME, DURABLE_TASK_SCHEDULER_CONNECTION_STRING, the emulator, and the dashboard. This tutorial uses default, which the emulator creates automatically. If that hub already contains old histories or is used by other workers, choose a unique, unused hub name, set the emulator's DTS_TASK_HUB_NAMES to that name, and update the application settings and dashboard selection consistently. Merely defining a scheduler connection string doesn't select the scheduler storage provider.
In host.json, configure the scheduler provider under extensions.durableTask, preserving the template's other host settings:
{
"extensions": {
"durableTask": {
"hubName": "%TASKHUB_NAME%",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
Use the scheduler provider dependencies for your hosting language. This configuration selects the backend for a new isolated tutorial deployment; it doesn't enable the filtering required for a side-by-side upgrade.
Start local development dependencies
To run durable agents locally, you need to start two services:
- Azurite: Emulates Azure Storage services (used by Azure Functions for managing triggers and internal state).
- Durable Task Scheduler (DTS) emulator: Manages durable state (conversation history, orchestration state) and scheduling for your agents
These steps assume the Functions host uses the Durable Task Scheduler storage provider. The release's Python Functions samples default to Azure Storage. If you start from those samples, select and configure the scheduler provider before using the DTS dashboard; otherwise Azurite holds the durable state and the scheduler dashboard remains empty. See Durable Task Scheduler setup.
Start Azurite
Azurite emulates Azure Storage services locally. The Azure Functions uses it for managing internal state. You'll need to run this in a new terminal window and keep it running while you develop and test your durable agent.
Open a new terminal window and pull the Azurite Docker image:
docker pull mcr.microsoft.com/azure-storage/azuriteStart Azurite in a terminal window:
docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azuriteAzurite will start and listen on the default ports for Blob (10000), Queue (10001), and Table (10002) services.
Keep this terminal window open while you're developing and testing your durable agent.
Tip
For more information about Azurite, including alternative installation methods, see Use Azurite emulator for local Azure Storage development.
Start the Durable Task Scheduler emulator
The DTS emulator provides the durable backend for managing agent state and orchestrations. It stores conversation history and ensures your agent's state persists across restarts. It also triggers durable orchestrations and agents. You'll need to run this in a separate new terminal window and keep it running while you develop and test your durable agent.
Open another new terminal window and pull the DTS emulator Docker image:
docker pull mcr.microsoft.com/dts/dts-emulator:latestRun the DTS emulator:
docker run -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latestThis command creates the
defaulttask hub and starts the emulator, matching this tutorial's settings. The command exposes:- Port 8080: The gRPC endpoint for the Durable Task Scheduler (used by your Functions app)
- Port 8082: The administrative dashboard
The dashboard will be available at
http://localhost:8082.
Keep this terminal window open while you're developing and testing your durable agent.
Tip
To learn more about the DTS emulator, including how to configure multiple task hubs and access the dashboard, see Develop with Durable Task Scheduler.
Run the function app
Now you're ready to run your Azure Functions app with the durable agent.
In a new terminal window (keeping both Azurite and the DTS emulator running in separate windows), navigate to your project directory.
Start the Azure Functions runtime:
func startYou should see output indicating that your function app is running, including the HTTP endpoints for your agent:
Functions: http-MyDurableAgent: [POST] http://localhost:7071/api/agents/MyDurableAgent/run dafx-v2-MyDurableAgent: entityTrigger
This illustrates .NET schema-v2 registration; Python trigger names depend on the deployment strategy. These endpoints manage conversation state automatically, so you don't need to create session objects yourself.
Test the agent locally
Now you can interact with your durable agent using HTTP requests. The agent maintains conversation state across multiple requests, enabling multi-turn conversations.
Start a new conversation
Create a new session and send your first message:
curl -i -X POST http://localhost:7071/api/agents/MyDurableAgent/run \
-H "Content-Type: text/plain" \
-d "What are three popular programming languages?"
Sample response (the x-ms-session-id header contains the session ID; this example shows a .NET schema-v2 ID):
HTTP/1.1 200 OK
Content-Type: text/plain
x-ms-session-id: @dafx-v2-mydurableagent@263fa373-fa01-4705-abf2-5a114c2bb87d
Content-Length: 189
Three popular programming languages are Python, JavaScript, and Java. Python is known for its simplicity and readability, JavaScript powers web interactivity, and Java is widely used in enterprise applications.
Save the exact session ID from the x-ms-session-id header for the next request. Don't construct or rewrite it. Python also returns this header, but its ID format can differ.
Continue the conversation
Send a follow-up message to the same session by including the returned ID in the canonical sessionId query parameter:
Replace the example ID with the actual value from the previous response's x-ms-session-id header. URL-encode the value if necessary.
curl -X POST "http://localhost:7071/api/agents/MyDurableAgent/run?sessionId=@dafx-v2-mydurableagent@263fa373-fa01-4705-abf2-5a114c2bb87d" \
-H "Content-Type: text/plain" \
-d "Which one is best for beginners?"
Sample response:
Python is often considered the best choice for beginners among those three. Its clean syntax reads almost like English, making it easier to learn programming concepts without getting overwhelmed by complex syntax. It's also versatile and widely used in education.
Notice that the agent remembers the context from the previous message (the three programming languages) without you having to specify them again. Because the conversation state is stored durably by the Durable Task Scheduler, this history persists even if you restart the function app or the conversation is resumed by a different instance.
Monitor with the Durable Task Scheduler dashboard
The Durable Task Scheduler provides a built-in dashboard for monitoring and debugging your durable agents. The dashboard offers deep visibility into agent operations, conversation history, and execution flow.
Access the dashboard
Open the dashboard for your local DTS emulator at
http://localhost:8082in your web browser.Select default, or the same unique hub name you configured in both your application and emulator, to view its details.
Select the gear icon in the top-right corner to open the settings, and ensure that the Enable Agent pages option under Preview Features is selected.
Explore agent conversations
In the dashboard, navigate to the Agents tab.
Select your durable agent thread (e.g.,
mydurableagent - 263fa373-fa01-4705-abf2-5a114c2bb87d) from the list.You'll see a detailed view of the agent thread, including the complete conversation history with all messages and responses.
The dashboard provides a timeline view to help you understand the flow of the conversation. Key information include:
- Timestamps and duration for each interaction
- Prompt and response content
- Number of tokens used
Tip
The DTS dashboard provides real-time updates, so you can watch your agent's behavior as you interact with it through the HTTP endpoints.
Deploy to Azure
Now that you've tested your durable agent locally, deploy it to Azure.
Deploy the application:
azd deployThis command packages your application and deploys it to the Azure Functions app created during provisioning.
Wait for the deployment to complete. The output will confirm when your agent is running in Azure.
Test the deployed agent
After deployment, test your agent running in Azure.
Get the function key
For the generated endpoints' default function-level authorization, include a Functions key in requests to Azure. A Functions key isn't application-user authorization; see production security and troubleshooting.
API_KEY=`az functionapp function keys list --name $(azd env get-value AZURE_FUNCTION_NAME) --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) --function-name http-MyDurableAgent --query default -o tsv`
Start a new conversation in Azure
Create a new session and send your first message to the deployed agent:
curl -i -X POST "https://$(azd env get-value AZURE_FUNCTION_NAME).azurewebsites.net/api/agents/MyDurableAgent/run?code=$API_KEY" \
-H "Content-Type: text/plain" \
-d "What are three popular programming languages?"
Save the session ID returned in the x-ms-session-id response header.
Continue the conversation in Azure
Send a follow-up message in the same session. Use the exact ID returned by the previous response:
SESSION_ID="<session-id>"
curl -X POST "https://$(azd env get-value AZURE_FUNCTION_NAME).azurewebsites.net/api/agents/MyDurableAgent/run?code=$API_KEY" \
--url-query "sessionId=$SESSION_ID" \
-H "Content-Type: text/plain" \
-d "Which is easiest to learn?"
The agent maintains conversation context in Azure just as it did locally, demonstrating the durability of the agent state.
Monitor the deployed agent
You can monitor your deployed agent using the Durable Task Scheduler dashboard in Azure.
Get the name of your Durable Task Scheduler instance:
azd env get-value DTS_NAMEOpen the Azure portal and search for the Durable Task Scheduler name from the previous step.
In the overview blade of the Durable Task Scheduler resource, select the task hub configured for the deployed application.
Select Open Dashboard at the top of the task hub page to open the monitoring dashboard.
View your agent's conversations just as you did with the local emulator.
The Azure-hosted dashboard provides the same debugging and monitoring capabilities as the local emulator, allowing you to inspect conversation history, trace tool calls, and analyze performance in your production environment.
Tutorial: Orchestrate durable agents with Azure Functions
This tutorial shows you how to orchestrate multiple durable AI agents using the Azure Functions hosting model and the fan-out/fan-in pattern. You'll extend the durable agent from the previous tutorial to create a multi-agent system that processes a user's question, then translates the response into multiple languages concurrently. For self-hosted orchestration examples, see the samples.
Understanding the orchestration pattern
The orchestration you'll build follows this flow:
- User input - A question or message from the user
- Main agent - The
MyDurableAgentfrom the first tutorial processes the question - Fan-out - The main agent's response is sent concurrently to both translation agents
- Translation agents - Two specialized agents translate the response (French and Spanish)
- Fan-in - Results are aggregated into a single JSON response with the original response and translations
This pattern enables concurrent processing, reducing total response time compared to sequential translation.
Register agents at startup
To properly use agents in durable orchestrations, register them at application startup. They can be used across orchestration executions.
Update your Program.cs to register the translation agents alongside the existing MyDurableAgent:
using System;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
// Get the Azure OpenAI configuration
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? "gpt-4o-mini";
// Create the Microsoft Foundry client
AIProjectClient client = new(new Uri(endpoint), new DefaultAzureCredential());
// Create the main agent from the first tutorial
AIAgent mainAgent = client.AsAIAgent(
model: deploymentName,
instructions: "You are a helpful assistant that can answer questions and provide information.",
name: "MyDurableAgent");
// Create translation agents
AIAgent frenchAgent = client.AsAIAgent(
model: deploymentName,
instructions: "You are a translator. Translate the following text to French. Return only the translation, no explanations.",
name: "FrenchTranslator");
AIAgent spanishAgent = client.AsAIAgent(
model: deploymentName,
instructions: "You are a translator. Translate the following text to Spanish. Return only the translation, no explanations.",
name: "SpanishTranslator");
// Build and configure the Functions host
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
{
// Register all agents for use in orchestrations and HTTP endpoints
options.AddAIAgent(mainAgent);
options.AddAIAgent(frenchAgent);
options.AddAIAgent(spanishAgent);
})
.Build();
app.Run();
Update your function_app.py to register the translation agents alongside the existing MyDurableAgent:
import os
from azure.identity import DefaultAzureCredential
from agent_framework_azurefunctions import AgentFunctionApp
from agent_framework.openai import OpenAIChatCompletionClient
# Get the Azure OpenAI configuration
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
if not endpoint:
raise ValueError("AZURE_OPENAI_ENDPOINT is not set.")
deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini")
api_version = os.getenv("AZURE_OPENAI_API_VERSION")
# Create the Azure OpenAI client
chat_client = OpenAIChatCompletionClient(
azure_endpoint=endpoint,
model=deployment_name,
api_version=api_version,
credential=DefaultAzureCredential()
)
# Create the main agent from the first tutorial
main_agent = chat_client.as_agent(
instructions="You are a helpful assistant that can answer questions and provide information.",
name="MyDurableAgent"
)
# Create translation agents
french_agent = chat_client.as_agent(
instructions="You are a translator. Translate the following text to French. Return only the translation, no explanations.",
name="FrenchTranslator"
)
spanish_agent = chat_client.as_agent(
instructions="You are a translator. Translate the following text to Spanish. Return only the translation, no explanations.",
name="SpanishTranslator"
)
# Create the function app and register all agents
app = AgentFunctionApp(agents=[main_agent, french_agent, spanish_agent])
Create an orchestration function
An orchestration function coordinates the workflow across multiple agents. It retrieves registered agents from the durable context and orchestrates their execution, first calling the main agent, then fanning out to translation agents concurrently.
Create a new file named AgentOrchestration.cs in your project directory:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
namespace MyDurableAgent;
public static class AgentOrchestration
{
// Define a strongly-typed response structure for agent outputs
public sealed record TextResponse(string Text);
[Function("agent_orchestration_workflow")]
public static async Task<Dictionary<string, string>> AgentOrchestrationWorkflow(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var input = context.GetInput<string>() ?? throw new ArgumentNullException(nameof(context), "Input cannot be null");
// Step 1: Get the main agent's response
DurableAIAgent mainAgent = context.GetAgent("MyDurableAgent");
AgentResponse<TextResponse> mainResponse = await mainAgent.RunAsync<TextResponse>(input);
string agentResponse = mainResponse.Result.Text;
// Step 2: Fan out - get the translation agents and run them concurrently
DurableAIAgent frenchAgent = context.GetAgent("FrenchTranslator");
DurableAIAgent spanishAgent = context.GetAgent("SpanishTranslator");
Task<AgentResponse<TextResponse>> frenchTask = frenchAgent.RunAsync<TextResponse>(agentResponse);
Task<AgentResponse<TextResponse>> spanishTask = spanishAgent.RunAsync<TextResponse>(agentResponse);
// Step 3: Wait for both translation tasks to complete (fan-in)
await Task.WhenAll(frenchTask, spanishTask);
// Get the translation results
TextResponse frenchResponse = (await frenchTask).Result;
TextResponse spanishResponse = (await spanishTask).Result;
// Step 4: Combine results into a dictionary
var result = new Dictionary<string, string>
{
["original"] = agentResponse,
["french"] = frenchResponse.Text,
["spanish"] = spanishResponse.Text
};
return result;
}
}
Add the orchestration function to your function_app.py file:
from durabletask.task import OrchestrationContext, when_all
@app.orchestration_trigger(context_name="context")
def agent_orchestration_workflow(context: OrchestrationContext, input_text: str):
"""
Orchestration function that coordinates multiple agents.
Returns a dictionary with the original response and translations.
"""
# Step 1: Get the main agent's response
main_agent = app.get_agent(context, "MyDurableAgent")
main_response = yield main_agent.run(input_text)
agent_response = main_response.text
# Step 2: Fan out - get the translation agents and run them concurrently
french_agent = app.get_agent(context, "FrenchTranslator")
spanish_agent = app.get_agent(context, "SpanishTranslator")
parallel_tasks = [
french_agent.run(agent_response),
spanish_agent.run(agent_response)
]
# Step 3: Wait for both translation tasks to complete (fan-in)
translations = yield when_all(parallel_tasks)
# Step 4: Combine results into a dictionary
result = {
"original": agent_response,
"french": translations[0].text,
"spanish": translations[1].text
}
return result
Note
This guide covers C# and Python Durable Extension hosting. For Go hosting, see A2A hosting and the Agent Framework Go repository.
Test the orchestration
Ensure your local development dependencies from the first tutorial are still running:
- Azurite in one terminal window
- Durable Task Scheduler emulator in another terminal window
With your local development dependencies running:
Start your Azure Functions app in a new terminal window:
func startThe Durable Functions extension automatically creates built-in HTTP endpoints for managing orchestrations. Start the orchestration using the built-in API:
curl -X POST http://localhost:7071/runtime/webhooks/durabletask/orchestrators/agent_orchestration_workflow \ -H "Content-Type: application/json" \ -d '"\"What are three popular programming languages?\""'
The response includes URLs for managing the orchestration instance:
{ "id": "abc123def456", "statusQueryGetUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456", "sendEventPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456/raiseEvent/{eventName}", "terminatePostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456/terminate", "purgeHistoryDeleteUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456" }Query the orchestration status using the
statusQueryGetUri(replaceabc123def456with your actual instance ID):curl http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456
Poll the status endpoint until
runtimeStatusisCompleted. When complete, you'll see the orchestration output with the main agent's response and its translations:{ "name": "agent_orchestration_workflow", "instanceId": "abc123def456", "runtimeStatus": "Completed", "output": { "original": "Three popular programming languages are Python, JavaScript, and Java. Python is known for its simplicity...", "french": "Trois langages de programmation populaires sont Python, JavaScript et Java. Python est connu pour sa simplicité...", "spanish": "Tres lenguajes de programación populares son Python, JavaScript y Java. Python es conocido por su simplicidad..." } }
Monitor the orchestration in the dashboard
The Durable Task Scheduler dashboard provides visibility into your orchestration:
Open
http://localhost:8082in your browser.Select the task hub configured for your application.
Select the "Orchestrations" tab.
Find your orchestration instance in the list.
Select the instance to see:
- The orchestration timeline
- Main agent execution followed by concurrent translation agents
- Each agent execution (MyDurableAgent, then French and Spanish translators)
- Fan-out and fan-in patterns visualized
- Timing and duration for each step
Deploy the orchestration to Azure
Deploy the updated application using Azure Developer CLI:
azd deploy
This deploys your updated code with the new orchestration function and additional agents to the Azure Functions app created in the first tutorial.
Test the deployed orchestration
After deployment, test your orchestration running in Azure.
Get the system key for the durable extension:
SYSTEM_KEY=$(az functionapp keys list --name $(azd env get-value AZURE_FUNCTION_NAME) --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) --query "systemKeys.durabletask_extension" -o tsv)
Start the orchestration using the built-in API:
curl -X POST "https://$(azd env get-value AZURE_FUNCTION_NAME).azurewebsites.net/runtime/webhooks/durabletask/orchestrators/agent_orchestration_workflow?code=$SYSTEM_KEY" \ -H "Content-Type: application/json" \ -d '"\"What are three popular programming languages?\""'
- Use the
statusQueryGetUrifrom the response to poll for completion and view the results with translations.
Production security and troubleshooting
Treat these access boundaries separately:
- Application users: Authenticate callers and authorize access to each session, workflow, and human-approval response. Session, correlation, and request IDs are identifiers, not credentials.
- Functions endpoints: Function-level keys protect generated HTTP endpoints but don't establish application-user identity or session ownership. Protect and rotate keys; don't expose them in browser-delivered application code.
- Model access: Grant the worker identity the required model permissions. Model credentials don't grant access to the scheduler or to another user's session.
- Scheduler access: Configure worker, client, and dashboard permissions for their intended roles. For infrastructure guidance, see Durable Task integration.
| Symptom | Check |
|---|---|
| Python package installation or Functions startup fails | Use Python 3.13 or later for Functions, install the Functions packages in Getting started, and explicitly select a deployment strategy. The standalone Python minimum is different. |
| Requests succeed but the DTS dashboard is empty | Confirm the Functions storage provider is AzureManaged, not AzureStorage, and that the scheduler endpoint and task hub match the dashboard. |
| A session appears to lose context | Reuse the exact returned x-ms-session-id value with sessionId; check for a different hub, deployment namespace, or configured session cleanup. |
| Old workflows fail after upgrading | Check replay compatibility before resuming historical runs. Entity migration doesn't repair incompatible workflow histories. |
| A stream finishes but durable completion is missing | Treat stream output as provisional and check the authoritative result or retained outcome. A stream completion marker isn't a durable commit acknowledgment. |
A completed operation returns 410 |
Inspect the retained outcome and result availability. Don't assume failure, keep polling as if pending, or automatically submit a new operation. |
| Entity-state writes exceed capacity | Inspect whole-entity state, protected records, retention policy, and payload references. Model-input compaction alone doesn't guarantee that stored state fits. |
Next steps
Additional resources: