Tuesday, May 19, 2026

Microsoft Agent Framework: Agents on Azure Functions with .NET

In this post, let's have a look at hosting an Agent on Azure Functions using the Microsoft Agent Framework. In a previous post, I have have written about getting started with Microsoft Agent Framework in .NET. If you haven't read the previous post, I would recommend doing so.

In the previous post, we have been running our agent as a console application. That's great for learning, but it isn't how we would actually run agents in production. Agents typically need an HTTP endpoint to be invoked from a client, and they need to persist conversation state across requests so a user can have a multi-turn conversation.

The Microsoft Agent Framework ships with a hosting package for Azure Functions (Microsoft.Agents.AI.Hosting.AzureFunctions) that gives us both of these out of the box. It builds on top of Durable Functions, so each conversation thread is durably persisted, can survive process restarts, and can be resumed later. Let's see how it works.

Let's start creating an Azure Functions worker project targeting .NET 10 (isolated worker model). On top of the usual Functions packages, we need a couple of extras (versions are the latest as of today, they will change):
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <_FunctionsSkipCleanOutput>true</_FunctionsSkipCleanOutput>
    <RootNamespace>MafSamples.DurableFunctions</RootNamespace>
    <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.2" />
    <PackageReference Include="Azure.Identity" Version="1.21.0" />
    <PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
    <PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" Version="1.6.1-preview.260514.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
  </ItemGroup>

  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>

</Project> 
Microsoft.Agents.AI.Hosting.AzureFunctions does the heavy lifting and plugs the agent into the Functions host, exposes an HTTP endpoint per agent, and wires up Durable Functions backed state for each conversation thread.

local.settings.json

Durable Functions needs a storage backend, and for local development we will be using Azurite storage emulator for simplicity. We could also go with the Durable Task Scheduler (DTS) and get its monitoring features out of the box, but let's leave that for another post.
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AZURE_AI_ENDPOINT": "https://<your-foundry-resource>.services.ai.azure.com/",
    "AZURE_AI_MODEL": "<DEPLOYMENT_NAME>"
  }
}

Program.cs

Here is the entire program. The agent definition is the same one we have used in the previous post, a small weekend-planner with three tools. The new changes are FunctionsApplication.CreateBuilder and ConfigureDurableAgents:
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.DurableTask;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Hosting;
using System.ComponentModel;

var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_ENDPOINT")!;
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL")!;

// For local development, using AzureCliCredential
var credential = new AzureCliCredential();

AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
    .AsAIAgent(
        model: deploymentName,
        name: "weekend-planner",
        instructions: """
            You help users plan their weekends and choose the best activities for the given weather.
            If an activity would be unpleasant in weather, don't suggest it.
            Include date of the weekend in response.
            """,
        tools: [
            AIFunctionFactory.Create(GetWeather),
            AIFunctionFactory.Create(GetActivities),
            AIFunctionFactory.Create(GetCurrentDate)
        ]);

// Automatically creates HTTP endpoints and manages state persistence
using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableAgents(options =>
        options.AddAIAgent(agent)
    )
    .Build();

app.Run();

[Description("Returns weather data for a given city.")]
static WeatherResult GetWeather(
    [Description("The city to get the weather for.")] string city)
{
    Console.WriteLine($"[Tool] Getting weather for '{city}'.");

    return new WeatherResult(18, "Rainy");
}

[Description("Returns a list of leisure activities for a given city and date, each with a name and location.")]
static List<LeisureActivity> GetActivities(
    [Description("The city to get activities for.")] string city,
    [Description("The date to get activities for in format YYYY-MM-DD.")] string date)
{
    Console.WriteLine($"[Tool] Getting activities for '{city}' on '{date}'.");

    return
    [
        new("Hiking", city),
        new("Beach", city),
        new("Museum", city)
    ];
}

[Description("Gets the current date from the system and returns as a string in format YYYY-MM-DD.")]
static string GetCurrentDate()
{
    Console.WriteLine("[Tool] Getting current date");

    return DateTime.Now.ToString("yyyy-MM-dd");
}

record WeatherResult(int Temperature, string Description);

record LeisureActivity(string Name, string Location);

// Dummy orchestrator to satisfy Durable Functions requirement of having at least one orchestration in the assembly.
public static class DummyOrchestrator
{
    [Function(nameof(DummyOrchestrator))]
    public static Task RunOrchestrator(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        return Task.CompletedTask;
    }
}
Notice we don't define any HTTP-triggered functions ourselves. ConfigureDurableAgents registers an HTTP endpoint for every agent we add via AddAIAgent, using the agent's name. For our weekend-planner agent, the endpoint will be:
POST /api/agents/weekend-planner/run  
The one quirky bit is the DummyOrchestrator. The Functions runtime needs at least one [OrchestrationTrigger] to be discovered in the assembly for the Durable extension to bind correctly. (see issue: microsoft/agent-framework/issues/5927)

Now let's run the Azure Function App and we can see something like below:
Function App
Then we can call the agent over HTTP. Here is a request to start a new conversation:
POST http://localhost:7098/api/agents/weekend-planner/run
Content-Type: application/json

{
  "message": "What should I do this weekend in Auckland?"
}  
The response comes back with the agent's reply.
Start a chat
Note the x-ms-thread-id under response Headers. That value is the durable identifier for the conversation. To continue the same conversation, we pass it back as a query parameter:
POST http://localhost:7098/api/agents/weekend-planner/run?thread_id=<x-ms-thread-id>
Content-Type: application/json

{
  "message": "What is the weather like there?"
}
  
Chat Continuation
The agent remembers we were talking about a specific city and answers in context without us writing a single line of state management code. Without a thread, if we ask the same above question as a fresh conversation, it will return something like What is the city etc. The thread, messages, and tool calls are all persisted in the storage account via Durable Functions. If the Functions host restarts, the conversation continues from where it left off.


Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment