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.
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:
- The hosting layer receives the HTTP request and authenticates it.
- The
AgentApplicationprocesses the incoming activity through its pipeline. - 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:
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 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.