Blog Post

Apps on Azure Blog
8 MIN READ

Building Reliable AI Travel Agents with the Durable Task Extension for Microsoft Agent Framework

greenie-msft's avatar
greenie-msft
Icon for Microsoft rankMicrosoft
Dec 18, 2025

AI agents that remember everything across failures, coordinate like a well-rehearsed team, and cost nothing while waiting for human input.

The durable task extension for Microsoft Agent Framework makes all this possible.

In this post, we'll walk through the AI Travel Planner, a C# application I built that demonstrates how to build reliable, scalable multi-agent applications using the durable task extension for Microsoft agent framework. While I work on the python version, I've included code snippets that show the python equivalent.

If you haven't already seen the announcement post on the durable task extension for Microsoft Agent Framework, I suggest you read that first before continuing with this post: http://aka.ms/durable-extension-for-af-blog.

In brief, production AI agents face real challenges: crashes can lose conversation history, unpredictable behavior makes debugging difficult, human-in-the-loop workflows require waiting without wasting resources, and variable demand needs flexible scaling. The durable task extension addresses each of these:

  • Serverless Hosting: Deploy agents on Azure Functions with auto-scaling from thousands of instances to zero, while retaining full control in a serverless architecture.
  • Automatic Session Management: Agents maintain persistent sessions with full conversation context that survives process crashes, restarts, and distributed execution across instances
  • Deterministic Multi-Agent Orchestrations: Coordinate specialized durable agents with predictable, repeatable, code-driven execution patterns
  • Human-in-the-Loop with Serverless Cost Savings: Pause for human input without consuming compute resources or incurring costs
  • Built-in Observability with Durable Task Scheduler: Deep visibility into agent operations and orchestrations through the Durable Task Scheduler UI dashboard

AI Travel Planner Architecture Overview

The Travel Planner application takes user trip preferences and starts a workflow that orchestrates three specialized agent framework agents (a Destination Recommender, an Itinerary Planner, and a Local Recommender) to build a comprehensive, personalized travel plan.


Once a travel plan is created, the workflow includes human-in-the-loop approval before booking the trip (mocked), showcasing how the durable task extension handles long-running operations easily:

Application Workflow

  1. User Request: User submits travel preferences via React frontend
  2. Orchestration Scheduled: Azure Functions backend receives the request and schedules a deterministic agentic workflow using the Durable Task Extension for Agent Framework.
  3. Destination Recommendation: The orchestrator first coordinates the Destination Recommender agent to analyze preferences and suggest destinations
  4. Itinerary Planning and Local Recommendations: The orchestrator then parallelizes the invocation of the Itinerary Planner agent to create detailed day-by-day plans for the given destination and the Local Recommendations agent to add insider tips and attractions 
  5. Storage: Created travel plan is saved to Azure Blob Storage
  6. Approval: User reviews and approves the plan (human-in-the-loop)
  7. Booking: Upon approval, booking of the trip completes

Key Components

  • Azure Static Web Apps: Hosts the React frontend
  • Azure Functions (.NET 9): Serverless compute hosting the agents and workflow with automatic scaling
  • Durable Task Extension for Microsoft Agent Framework: The AI agent SDK with durable task extension
  • Durable Task Scheduler: Manages state persistence, orchestration, and observability
  • Azure OpenAI (GPT-4o-mini): Powers the AI agents

Now let’s dive into the code. Along the way, I’ll highlight the value the durable task extension brings and patterns you can apply to your own applications.

Creating Durable Agents

Making the standard Agent Framework agents durable agents is simple. Include the durable task extension package and register your agents within the ConfigureDurableAgents extension method and you automatically get:

  • Persistent conversation sessions that survive restarts
  • HTTP endpoints for agent interactions
  • Automatic state checkpointing that survive restarts
  • Distributed execution across instances

C#

FunctionsApplication
    .CreateBuilder(args)
    .ConfigureDurableAgents(configure =>
    {
        configure.AddAIAgentFactory("DestinationRecommenderAgent", sp =>
            chatClient.CreateAIAgent(
                instructions: "You are a travel destination expert...",
                name: "DestinationRecommenderAgent",
                services: sp));

        configure.AddAIAgentFactory("ItineraryPlannerAgent", sp =>
            chatClient.CreateAIAgent(
                instructions: "You are a travel itinerary planner...",
                name: "ItineraryPlannerAgent",
                services: sp,
                tools: [AIFunctionFactory.Create(CurrencyConverterTool.ConvertCurrency)]));

        configure.AddAIAgentFactory("LocalRecommendationsAgent", sp =>
            chatClient.CreateAIAgent(
                instructions: "You are a local expert...",
                name: "LocalRecommendationsAgent",
                services: sp));
    });

Python

# Create the Azure OpenAI chat client
chat_client = AzureOpenAIChatClient(
    endpoint=endpoint,
    deployment_name=deployment_name,
    credential=DefaultAzureCredential()
)

# Destination Recommender Agent
destination_recommender_agent = chat_client.create_agent(
    name="DestinationRecommenderAgent",
    instructions="You are a travel destination expert..."
)

# Itinerary Planner Agent (with tools)
itinerary_planner_agent = chat_client.create_agent(
    name="ItineraryPlannerAgent",
    instructions="You are a travel itinerary planner...",
    tools=[get_exchange_rate, convert_currency]
)

# Local Recommendations Agent
local_recommendations_agent = chat_client.create_agent(
    name="LocalRecommendationsAgent",
    instructions="You are a local expert..."
)

# Configure Function App with Durable Agents. AgentFunctionApp is where the magic happens
app = AgentFunctionApp(agents=[
    destination_recommender_agent,
    itinerary_planner_agent,
    local_recommendations_agent
])

The Orchestration Programming Model

The durable task extension uses an intuitive async/await programming model for deterministic orchestration. You write orchestration logic as ordinary imperative code (if/else, try/catch), and the framework handles all the complexity of coordination, durability, retries, and distributed execution.

The Travel Planner Orchestration

Here's the actual orchestration from the application that coordinates all three agents, runs tasks in parallel, handles human approval, and books the trip:

C#

[Function(nameof(RunTravelPlannerOrchestration))]
public async Task<TravelPlanResult> RunTravelPlannerOrchestration(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var travelRequest = context.GetInput<TravelRequest>()!;
    
    // Get durable agents and create conversation threads
    DurableAIAgent destinationAgent = context.GetAgent("DestinationRecommenderAgent");
    DurableAIAgent itineraryAgent = context.GetAgent("ItineraryPlannerAgent");
    DurableAIAgent localAgent = context.GetAgent("LocalRecommendationsAgent");

    // Step 1: Get destination recommendations
    var destinations = await destinationAgent.RunAsync<DestinationRecommendations>(
        $"Recommend destinations for {travelRequest.Preferences}", 
        destinationAgent.GetNewThread());

    var topDestination = destinations.Result.Recommendations.First();

    // Steps 2 & 3: Run itinerary and local recommendations IN PARALLEL
    var itineraryTask = itineraryAgent.RunAsync<TravelItinerary>(
        $"Create itinerary for {topDestination.Name}", itineraryAgent.GetNewThread());

    var localTask = localAgent.RunAsync<LocalRecommendations>(
        $"Local recommendations for {topDestination.Name}", localAgent.GetNewThread());
    
    await Task.WhenAll(itineraryTask, localTask);

    // Step 4: Save to blob storage
    await context.CallActivityAsync(nameof(SaveTravelPlanToBlob), travelPlan);

    // Step 5: Wait for human approval (NO COMPUTE COSTS while waiting!)
    var approval = await context.WaitForExternalEvent<ApprovalResponse>(
        "ApprovalEvent", TimeSpan.FromDays(7));

    // Step 6: Book if approved
    if (approval.Approved)
        await context.CallActivityAsync(nameof(BookTrip), travelPlan);
    
    return new TravelPlanResult(travelPlan, approval.Approved);
}

Python

app.orchestration_trigger(context_name="context")
def travel_planner_orchestration(context: df.DurableOrchestrationContext):
    travel_request_data = context.get_input()
    travel_request = TravelRequest(**travel_request_data)
    
    # Get durable agents and create conversation threads
    destination_agent = app.get_agent(context, "DestinationRecommenderAgent")
    itinerary_agent = app.get_agent(context, "ItineraryPlannerAgent")
    local_agent = app.get_agent(context, "LocalRecommendationsAgent")

    # Step 1: Get destination recommendations
    destinations_result = yield destination_agent.run(
        messages=f"Recommend destinations for {travel_request.preferences}",
        thread=destination_agent.get_new_thread(),
        response_format=DestinationRecommendations
    )
    destinations = cast(DestinationRecommendations, destinations_result.value)
    top_destination = destinations.recommendations[0]

    # Steps 2 & 3: Run itinerary and local recommendations IN PARALLEL
    itinerary_task = itinerary_agent.run(
        messages=f"Create itinerary for {top_destination.destination_name}",
        thread=itinerary_agent.get_new_thread(),
        response_format=Itinerary
    )
    local_task = local_agent.run(
        messages=f"Local recommendations for {top_destination.destination_name}",
        thread=local_agent.get_new_thread(),
        response_format=LocalRecommendations
    )
    
    results = yield context.task_all([itinerary_task, local_task])
    itinerary = cast(Itinerary, results[0].value)
    local_recs = cast(LocalRecommendations, results[1].value)

    # Step 4: Save to blob storage
    yield context.call_activity("save_travel_plan_to_blob", travel_plan)

    # Step 5: Wait for human approval (NO COMPUTE COSTS while waiting!)
    approval_task = context.wait_for_external_event("ApprovalEvent")
    timeout_task = context.create_timer(
        context.current_utc_datetime + timedelta(days=7))
    
    winner = yield context.task_any([approval_task, timeout_task])
    
    if winner == approval_task:
        timeout_task.cancel()
        approval = approval_task.result
        
        # Step 6: Book if approved
        if approval.get("approved"):
            yield context.call_activity("book_trip", travel_plan)
        
        return TravelPlanResult(plan=travel_plan, approved=approval.get("approved"))
    
    return TravelPlanResult(plan=travel_plan, approved=False)

Notice how the orchestration combines:

  • Agent calls (await agent.RunAsync(...)) for AI-driven decisions
  • Parallel execution (Task.WhenAll) for running multiple agents concurrently
  • Activity calls (await context.CallActivityAsync(...)) for non-intelligent business tasks
  • Human-in-the-loop (await context.WaitForExternalEvent(...)) for approval workflows

The orchestration automatically checkpoints after each step. If a failure occurs, completed steps aren't re-executed. The orchestration resumes exactly where it left off, no need for manual intervention.

Agent Patterns in Action

Agent Chaining: Sequential Handoffs

The Travel Planner demonstrates agent chaining where the Destination Recommender's output feeds into both the Itinerary Planner and Local Recommendations agents:

C#

// Agent 1: Get destination recommendations
var destinations = await destinationAgent.RunAsync<DestinationRecommendations>(prompt, thread);
var topDestination = destinations.Result.Recommendations.First();

// Agent 2: Create itinerary based on Agent 1's output
var itinerary = await itineraryAgent.RunAsync<TravelItinerary>(
    $"Create itinerary for {topDestination.Name}", thread);

Python

# Agent 1: Get destination recommendations
destinations_result = yield destination_agent.run(
    messages=prompt,
    thread=thread,
    response_format=DestinationRecommendations
)
destinations = cast(DestinationRecommendations, destinations_result.value)
top_destination = destinations.recommendations[0]

# Agent 2: Create itinerary based on Agent 1's output
itinerary_result = yield itinerary_agent.run(
    messages=f"Create itinerary for {top_destination.destination_name}",
    thread=thread,
    response_format=Itinerary
)
itinerary = cast(Itinerary, itinerary_result.value)

Agent Parallelization: Concurrent Execution

The app runs the Itinerary Planner and Local Recommendations agents in parallel to reduce latency:

C#

// Launch both agent calls simultaneously
var itineraryTask = itineraryAgent.RunAsync<TravelItinerary>(itineraryPrompt, thread1);
var localTask = localAgent.RunAsync<LocalRecommendations>(localPrompt, thread2);

// Wait for both to complete
await Task.WhenAll(itineraryTask, localTask);

Python

# Launch both agent calls simultaneously
itinerary_task = itinerary_agent.run(
    messages=itinerary_prompt,
    thread=thread1,
    response_format=Itinerary
)
local_task = local_agent.run(
    messages=local_prompt,
    thread=thread2,
    response_format=LocalRecommendations
)

# Wait for both to complete
results = yield context.task_all([itinerary_task, local_task])

itinerary = cast(Itinerary, results[0].value)
local_recs = cast(LocalRecommendations, results[1].value)

Human-in-the-Loop: Approval Workflows

The Travel Planner includes a complete human-in-the-loop pattern. After generating the travel plan, the workflow pauses for user approval:

C#

// Send approval request notification
await context.CallActivityAsync(nameof(RequestApproval), travelPlan);

// Wait for approval - NO COMPUTE COSTS OR LLM TOKENS while waiting!
var approval = await context.WaitForExternalEvent<ApprovalResponse>(
    "ApprovalEvent", TimeSpan.FromDays(7));

if (approval.Approved)
    await context.CallActivityAsync(nameof(BookTrip), travelPlan);

Python

# Send approval request notification
await context.CallActivityAsync(nameof(RequestApproval), travelPlan);

# Wait for approval - NO COMPUTE COSTS OR LLM TOKENS while waiting!
var approval = await context.WaitForExternalEvent<ApprovalResponse>(
    "ApprovalEvent", TimeSpan.FromDays(7));

if (approval.Approved)
    await context.CallActivityAsync(nameof(BookTrip), travelPlan);

 

The API endpoint to handle approval responses:

C#

[Function(nameof(HandleApprovalResponse))]
public async Task HandleApprovalResponse(
    [HttpTrigger("post", Route = "approve/{instanceId}")] HttpRequestData req,
    string instanceId,
    [DurableClient] DurableTaskClient client)
{
    var approval = await req.ReadFromJsonAsync<ApprovalResponse>();
    await client.RaiseEventAsync(instanceId, "ApprovalEvent", approval);
}

Python

app.function_name(name="ApproveTravelPlan")
app.route(route="travel-planner/approve/{instance_id}", methods=["POST"])
app.durable_client_input(client_name="client")
async def approve_travel_plan(req: func.HttpRequest, client) -> func.HttpResponse:
    instance_id = req.route_params.get("instance_id")
    approval = req.get_json()
    await client.raise_event(instance_id, "ApprovalEvent", approval)
    
    return func.HttpResponse(
        json.dumps({"message": "Approval processed"}),
        status_code=200,
        mimetype="application/json"
    )

The workflow generates a complete travel plan and waits up to 7 days for user approval. During this entire waiting period, since we're hosting this application on the Functions Flex Consumption plan, the app scales down and zero compute resources or LLM tokens are consumed. When the user approves (or the timeout expires), the app scales back up and the orchestration automatically resumes with full context intact.

Real-Time Monitoring with the Durable Task Scheduler

Since we're using the Durable Task Scheduler as the backend for our durable agents, we're provided with a built-in dashboard for monitoring our agents and orchestrations in real-time.

Agent Thread Insights

  • Conversation history: View complete conversation threads for each agent session, including all messages, tool calls, and agent decisions
  • Task timing: Monitor how long specific tasks and agent interactions take to complete

Orchestration Insights

  • Multi-agent visualization: See the execution flow across multiple agents with visual representation of parallel executions and branching
  • Real-time monitoring: Track active orchestrations, queued work items, and agent states
  • Performance metrics: Monitor response times, token usage, and orchestration duration

Debugging Capabilities

  • View structured inputs and outputs for activities, agents, and tool calls
  • 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 and workflows are doing, diagnose issues quickly, and optimize performance, all without adding custom logging to your code.

Try The Travel Planner Application

That’s it! That’s the gist of how the AI Travel Planner application is put together and some of the key components that it took to build it.

I'd love for you to try the application out for yourself. It's fully instrumented with the Azure Developer CLI and Bicep, so you can deploy it to Azure with a few simple CLI commands.

Click here to try the AI Travel Planner sample

Python version coming soon!

Demo Video

Learn More

Updated Dec 18, 2025
Version 4.0
No CommentsBe the first to comment