custom-agent
Learn how to build custom agents with Microsoft Agent Framework.
$ npx -y skills add managedcode/dotnet-skills --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
Learn how to build custom agents with Microsoft Agent Framework.
Agent definition
custom-agent.mdtitle: Custom Agents
description: Learn how to build custom agents with Microsoft Agent Framework.
zone_pivot_groups: programming-languages
author: westey-m
ms.topic: tutorial
ms.author: westey
ms.date: 09/25/2025
ms.service: agent-framework
Custom Agents
::: zone pivot="programming-language-csharp"
Microsoft Agent Framework supports building custom agents by inheriting from the `AIAgent` class and implementing the required methods.
This article shows how to build a simple custom agent that parrots back user input in upper case. In most cases building your own agent will involve more complex logic and integration with an AI service.
Getting Started
Add the required NuGet packages to your project.
dotnet add package Microsoft.Agents.AI.Abstractions --prerelease
Create a Custom Agent
The Agent Thread
To create a custom agent you also need a thread, which is used to keep track of the state of a single conversation, including message history, and any other state the agent needs to maintain.
To make it easy to get started, you can inherit from various base classes that implement common thread storage mechanisms.
1. `InMemoryAgentThread` - stores the chat history in memory and can be serialized to JSON. 1. `ServiceIdAgentThread` - doesn't store any chat history, but allows you to associate an ID with the thread, under which the chat history can be stored externally.
For this example, you'll use the `InMemoryAgentThread` as the base class for the custom thread.
internal sealed class CustomAgentThread : InMemoryAgentThread
{
internal CustomAgentThread() : base() { }
internal CustomAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedThreadState, jsonSerializerOptions) { }
}The Agent class
Next, create the agent class itself by inheriting from the `AIAgent` class.
internal sealed class UpperCaseParrotAgent : AIAgent
{
}Constructing threads
Threads are always created via two factory methods on the agent class. This allows for the agent to control how threads are created and deserialized. Agents can therefore attach any additional state or behaviors needed to the thread when constructed.
Two methods are required to be implemented:
public override Task<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> Task.FromResult<AgentThread>(new CustomAgentThread());
public override Task<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> Task.FromResult<AgentThread>(new CustomAgentThread(serializedThread, jsonSerializerOptions));Core agent logic
The core logic of the agent is to take any input messages, convert their text to upper case, and return them as response messages.
Add the following method to contain this logic. The input messages are cloned, since various aspects of the input messages have to be modified to be valid response messages. For example, the role has to be changed to `Assistant`.
private static IEnumerable<ChatMessage> CloneAndToUpperCase(IEnumerable<ChatMessage> messages, string agentName) => messages.Select(x =>
{
var messageClone = x.Clone();
messageClone.Role = ChatRole.Assistant;
messageClone.MessageId = Guid.NewGuid().ToString();
messageClone.AuthorName = agentName;
messageClone.Contents = x.Contents.Select(c => c is TextContent tc ? new TextContent(tc.Text.ToUpperInvariant())
{
AdditionalProperties = tc.AdditionalProperties,
Annotations = tc.Annotations,
RawRepresentation = tc.RawRepresentation
} : c).ToList();
return messageClone;
});Agent run methods
Finally, you need to implement the two core methods that are used to run the agent: one for non-streaming and one for streaming.
For both methods, you need to ensure that a thread is provided, and if not, create a new thread. The thread can then be updated with the new messages by calling `NotifyThreadOfNewMessagesAsync`. If you don't do this, the user won't be able to have a multi-turn conversation with the agent and each run will be a fresh interaction.
public override async Task<AgentResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
thread ??= await this.GetNewThreadAsync(cancellationToken);
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
return new AgentResponse
{
AgentId = this.Id,
ResponseId = Guid.NewGuid().ToString(),
Messages = responseMessages
};
}
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
thread ??= await this.GetNewThreadAsync(cancellationToken);
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
foreach (var message in responseMessages)
{
yield return new AgentResponseUpdate
{
AgentId = this.Id,
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = message.Contents,
ResponseId = Guid.NewGuid().ToString(),Read more
title: Custom Agents description: Learn how to build custom agents with Microsoft Agent Framework. zone_pivot_groups: programming-languages author: westey-m ms.topic: tutorial ms.author: westey ms.date: 09/25/2025 ms.service: agent-framework
Custom Agents
::: zone pivot="programming-language-csharp"
Microsoft Agent Framework supports building custom agents by inheriting from the `AIAgent` class and implementing the required methods.
This article shows how to build a simple custom agent that parrots back user input in upper case. In most cases building your own agent will involve more complex logic and integration with an AI service.
Getting Started
Add the required NuGet packages to your project.
dotnet add package Microsoft.Agents.AI.Abstractions --prerelease
Create a Custom Agent
The Agent Thread
To create a custom agent you also need a thread, which is used to keep track of the state of a single conversation, including message history, and any other state the agent needs to maintain.
To make it easy to get started, you can inherit from various base classes that implement common thread storage mechanisms.
1. `InMemoryAgentThread` - stores the chat history in memory and can be serialized to JSON. 1. `ServiceIdAgentThread` - doesn't store any chat history, but allows you to associate an ID with the thread, under which the chat history can be stored externally.
For this example, you'll use the `InMemoryAgentThread` as the base class for the custom thread.
internal sealed class CustomAgentThread : InMemoryAgentThread
{
internal CustomAgentThread() : base() { }
internal CustomAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedThreadState, jsonSerializerOptions) { }
}The Agent class
Next, create the agent class itself by inheriting from the `AIAgent` class.
internal sealed class UpperCaseParrotAgent : AIAgent
{
}Constructing threads
Threads are always created via two factory methods on the agent class. This allows for the agent to control how threads are created and deserialized. Agents can therefore attach any additional state or behaviors needed to the thread when constructed.
Two methods are required to be implemented:
public override Task<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> Task.FromResult<AgentThread>(new CustomAgentThread());
public override Task<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> Task.FromResult<AgentThread>(new CustomAgentThread(serializedThread, jsonSerializerOptions));Core agent logic
The core logic of the agent is to take any input messages, convert their text to upper case, and return them as response messages.
Add the following method to contain this logic. The input messages are cloned, since various aspects of the input messages have to be modified to be valid response messages. For example, the role has to be changed to `Assistant`.
private static IEnumerable<ChatMessage> CloneAndToUpperCase(IEnumerable<ChatMessage> messages, string agentName) => messages.Select(x =>
{
var messageClone = x.Clone();
messageClone.Role = ChatRole.Assistant;
messageClone.MessageId = Guid.NewGuid().ToString();
messageClone.AuthorName = agentName;
messageClone.Contents = x.Contents.Select(c => c is TextContent tc ? new TextContent(tc.Text.ToUpperInvariant())
{
AdditionalProperties = tc.AdditionalProperties,
Annotations = tc.Annotations,
RawRepresentation = tc.RawRepresentation
} : c).ToList();
return messageClone;
});Agent run methods
Finally, you need to implement the two core methods that are used to run the agent: one for non-streaming and one for streaming.
For both methods, you need to ensure that a thread is provided, and if not, create a new thread. The thread can then be updated with the new messages by calling `NotifyThreadOfNewMessagesAsync`. If you don't do this, the user won't be able to have a multi-turn conversation with the agent and each run will be a fresh interaction.
public override async Task<AgentResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
thread ??= await this.GetNewThreadAsync(cancellationToken);
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
return new AgentResponse
{
AgentId = this.Id,
ResponseId = Guid.NewGuid().ToString(),
Messages = responseMessages
};
}
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
thread ??= await this.GetNewThreadAsync(cancellationToken);
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
foreach (var message in responseMessages)
{
yield return new AgentResponseUpdate
{
AgentId = this.Id,
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = message.Contents,
ResponseId = Guid.NewGuid().ToString(),Stop explaining .NET to your AI. Start building. We've all been there: asking Claude to use Entity Framework, only to get EF6 patterns in a .NET 8 project. Explaining to Copilot that Blazor Server and Blazor WebAssembly aren't the same thing.
Repo: managedcode/dotnet-skills
Other agents on dotnet-skills.
- AGENT
Specialist orchestration agent for .NET Aspire work. Use when the problem is clearly about AppHost design, ServiceDefaults, first-party versus CommunityToolkit/Aspire integrations, dashboard and testing, `DistributedApplicationTestingBuilder`, `WebApplicationFactory`
Open agent - agent-as-function-tool
Legacy tutorial alias retained locally; the live Learn URL now resolves into the broader Function Tools surface
Open agent - agent-as-mcp-tool
Learn how to expose an agent as a tool over the MCP protocol
Open agent - create-and-run-durable-agent
Learn how to create and run a durable AI agent with Azure Functions and the durable task extension for Microsoft Agent Framework
Open agent - enable-observability
Enable OpenTelemetry for an agent so agent interactions are automatically logged
Open agent - function-tools-approvals
Learn how to use function tools with human in the loop approvals
Open agent

