AgentApplication in Microsoft 365 Agents SDK

AgentApplication is the central building block of an agent built with the Agents SDK. AgentApplication is the entry point for all incoming activity, including messages from users, conversation lifecycle events, adaptive card interactions, OAuth callbacks.

An agent is, at its core, an AgentApplication. You configure it with handlers that describe what your agent does. The SDK takes care of routing, state management, and the infrastructure required to run it.

How AgentApplication works

Every agent has a lifecycle that starts when a channel (Microsoft Teams, a bot service, or a custom client) delivers an activity to your agent's endpoint. AgentApplication sits at the center of that lifecycle:

Channel → Hosting layer → AgentApplication → Your handlers

The layers of processing in an agent built with the Agents SDK work as follows:

  1. The hosting layer receives the HTTP request and authenticates it.
  2. The AgentApplication processes the incoming activity through its pipeline.
  3. Your handlers are called based on matching routes.

Your agent loads turn state before your handlers run. Afterward, the agent saves the turn state.

Core concepts

Activities

Everything in the Agents SDK flows as an activity. An activity is a structured message representing something that happened. An activity has a type, such as message, event, invoke, conversationUpdate, and so on. It carries a payload relevant to that type. AgentApplication receives activities and routes them to the right handler.

Routes

A route pairs a selector with a handler. The selector determines whether a route matches the current activity. The handler runs your logic when the route matches.

Register routes when you configure your agent. They can match:

  • A message containing specific text or matching a regular expression
  • Any activity of a given type
  • Conversation lifecycle events (member added, member removed)
  • Adaptive card actions
  • Custom conditions

When an activity arrives, the system evaluates routes in order until it finds a match. By default, only one route runs.

Turn state

AgentApplication manages _turn state—structured storage partitioned into scopes:

Scope type Description
Conversation Shared across all users in a conversation, persisted between turns
User Scoped to an individual user across all conversations
Temp Current turn only - never persisted

The system automatically loads state before your handlers run and saves it automatically afterward.

Turn context

When a handler runs, it receives a turn context. Turn context is a snapshot of the current activity, the adapter connection, and utilities for sending responses. The turn context is your interface to the current interaction.

Middleware

AgentApplication supports a middleware pipeline. Middleware is a chain of components that process each turn before and after your handlers run. Middleware can inspect, transform, or short-circuit the activity flow. Common uses include logging, authentication checks, and request normalization.

Create an agent

Subclass AgentApplication and register your handlers in the constructor. The hosting framework automatically injects AgentApplicationOptions.

public class MyAgent : AgentApplication
{
    public MyAgent(AgentApplicationOptions options) : base(options)
    {
        OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeAsync);
        OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last);
    }

    private async Task WelcomeAsync(ITurnContext context, ITurnState state, CancellationToken ct)
    {
        foreach (var member in context.Activity.MembersAdded)
        {
            if (member.Id != context.Activity.Recipient.Id)
            {
                await context.SendActivityAsync("Hello! How can I help you?", cancellationToken: ct);
            }
        }
    }

    private async Task OnMessageAsync(ITurnContext context, ITurnState state, CancellationToken ct)
    {
        await context.SendActivityAsync($"You said: {context.Activity.Text}", cancellationToken: ct);
    }
}

Register your agent in Program.cs:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient();
builder.Services.AddSingleton<IStorage, MemoryStorage>();
builder.Services.AddAgent<MyAgent>();
builder.Services.AddAgentAspNetAuthentication(builder.Configuration);

WebApplication app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment());

app.Run();

Register activity handlers

Handle messages

Match messages by exact text (case-insensitive):

OnMessage("help", async (context, state, ct) =>
{
    await context.SendActivityAsync("Here's what I can do...", cancellationToken: ct);
});

Match messages using a regular expression:

OnMessage(new Regex(@"^order\s+\d+$", RegexOptions.IgnoreCase), async (context, state, ct) =>
{
    await context.SendActivityAsync("Looking up your order...", cancellationToken: ct);
});

Handle conversation updates

Register handlers for conversation lifecycle events such as members joining or leaving.

OnConversationUpdate(ConversationUpdateEvents.MembersAdded, async (context, state, ct) =>
{
    foreach (var member in context.Activity.MembersAdded)
    {
        if (member.Id != context.Activity.Recipient.Id)
        {
            await context.SendActivityAsync("Welcome!", cancellationToken: ct);
        }
    }
});

OnConversationUpdate(ConversationUpdateEvents.MembersRemoved, async (context, state, ct) =>
{
    // Called when participants leave the conversation
});

Handle any activity type

Match any activity by its type string for complete control over routing.

OnActivity(ActivityTypes.Message, async (context, state, ct) =>
{
    // Handles all message activities
});

OnActivity(ActivityTypes.Event, async (context, state, ct) =>
{
    // Handles event activities
});

Use ActivityTypes constants instead of hard-coded strings.

Control route evaluation order

The system sorts routes into a fixed evaluation order when you register them, not at runtime. The sort uses two levels:

  1. Route type: The system groups routes by type, and it always evaluates higher-priority types before lower-priority types, regardless of rank:

    Priority Route type
    1 (highest) Agentic invoke routes
    2 Invoke routes (adaptive card actions, OAuth callbacks, and other time-sensitive invokes)
    3 Agentic routes
    4 (lowest) All other routes
  2. Rank: Within each route type group, the system orders routes by their rank value. Lower numeric values are evaluated first.

Use RouteRank constants to set rank when registering a handler:

Constant Value Meaning
RouteRank.First 0 Evaluated before all other routes in its group
RouteRank.Unspecified 32767 Default when no rank is specified
RouteRank.Last 65535 Evaluated after all other routes in its group

By default, evaluation stops at the first matching route. Use RouteRank.Last for a catch-all fallback that handles anything not matched by a more specific route.

// Specific handlers use the default rank
OnMessage("status", HandleStatusAsync);
OnMessage("help", HandleHelpAsync);

// Catch-all — handles anything not matched above
OnActivity(ActivityTypes.Message, HandleUnknownMessageAsync, rank: RouteRank.Last);

Turn lifecycle hooks

Register logic that runs on every turn, before or after route matching. These hooks are useful for logging, cross-cutting concerns, and error handling.

OnBeforeTurn(async (context, state, ct) =>
{
    logger.LogInformation("Turn started: {Type}", context.Activity.Type);
    return true; // Return false to abort the turn
});

OnAfterTurn(async (context, state, ct) =>
{
    logger.LogInformation("Turn completed");
    return true; // Return false to skip state saving
});

OnTurnError(async (context, state, exception, ct) =>
{
    logger.LogError(exception, "Turn error");
    await context.SendActivityAsync("Something went wrong. Please try again.", cancellationToken: ct);
});

When OnBeforeTurn returns false, the turn is aborted and no routes run. When OnAfterTurn returns false, turn state isn't saved.

Use turn state

The agent automatically loads turn state before your handlers run and saves it afterward. The turn state object passed to your handlers gives you access to the different scopes so you can read and write data that persists across turns or is ephemeral for the current turn:

  • Conversation scope: For data shared across all turns in a conversation
  • User scope: For per-user data
  • Temp scope: For data that only needs to exist during the current turn
OnActivity(ActivityTypes.Message, async (context, state, ct) =>
{
    // Conversation scope — persisted per conversation
    var count = state.Conversation.GetValue<int>("messageCount", () => 0);
    state.Conversation.SetValue("messageCount", count + 1);

    // User scope — persisted per user
    var name = state.User.GetValue<string>("displayName");

    // Temp scope — current turn only
    state.Temp.SetValue("parsedInput", context.Activity.Text?.Trim());

    await context.SendActivityAsync($"Message #{count + 1}: {context.Activity.Text}", cancellationToken: ct);
});

Note

Use MemoryStorage for local development and testing. For production deployments, especially deployments running on multiple instances, use a persistent storage provider such as Azure Cosmos DB or Azure Blob Storage. See Use storage providers in your agent.

Next steps