.NETAI AgentsMicrosoft Agent FrameworkTutorial

Build Your First Tool-Using AI Agent in C#

A step-by-step tutorial: build an AI agent in C# with the Microsoft Agent Framework that calls your own tools, remembers the conversation, and chains multiple tool calls.

An LLM on its own can only talk. The moment you want it to do something — look up an order, check inventory, send a notification — it needs tools: functions it can call. This tutorial builds a small but complete tool-using agent in C# with the Microsoft Agent Framework, starting from an empty console app and ending with an agent that chains multiple tool calls and remembers the conversation.

If you’re new to the framework itself, skim our Microsoft Agent Framework guide first. Otherwise, let’s build.

The setup

Create a console app and add the two packages you need — the framework and the Azure OpenAI client (any IChatClient-compatible provider works; Azure OpenAI is used here):

dotnet new console -n AgentDemo
cd AgentDemo
dotnet add package Microsoft.Agents.AI
dotnet add package Azure.AI.OpenAI

You’ll need an Azure OpenAI resource with a chat model deployed (for example gpt-4o-mini). Set the endpoint and use DefaultAzureCredential so you’re not hard-coding keys.

Step 1: A bare agent

Start with an agent that can only talk — no tools yet. This confirms your connection works:

using Microsoft.Agents.AI;
using Azure.AI.OpenAI;
using Azure.Identity;

var chatClient = new AzureOpenAIClient(
        new Uri("https://<your-resource>.openai.azure.com"),
        new DefaultAzureCredential())
    .GetChatClient("gpt-4o-mini");

AIAgent agent = chatClient.CreateAIAgent(
    name: "SupportBot",
    instructions: "You are a concise customer-support assistant.");

Console.WriteLine(await agent.RunAsync("Hi, can you help me track an order?"));

Run it and you’ll get a friendly but useless reply — it has no way to actually track anything. Let’s fix that.

Step 2: Define a tool

A tool is just a C# method. Annotate it and its parameters with [Description] so the model knows what it does and when to use it — the model reads these descriptions to decide whether to call it:

using System.ComponentModel;

[Description("Looks up the delivery status of a customer order by its ID.")]
static string GetOrderStatus(
    [Description("The order ID, e.g. 'ORD-4821'")] string orderId)
{
    // In a real app this would hit your database or an API.
    return orderId == "ORD-4821"
        ? "Order ORD-4821 shipped on Jul 12 and arrives Jul 15."
        : $"No order found with ID {orderId}.";
}

The description quality matters more than you’d expect: vague descriptions lead to the model calling the wrong tool or not calling it at all. Be specific about what it does and when to use it.

Step 3: Give the tool to the agent

Register the method as a tool when you create the agent. The framework turns your model’s tool-call requests into actual C# invocations and feeds the results back automatically — you don’t write the branching logic:

AIAgent agent = chatClient.CreateAIAgent(
    name: "SupportBot",
    instructions: "You help customers track orders. Use your tools to look up real data; never guess an order's status.",
    tools: [AIFunctionFactory.Create(GetOrderStatus)]);

var reply = await agent.RunAsync("Where is my order ORD-4821?");
Console.WriteLine(reply);

Now the agent recognizes the intent, calls GetOrderStatus("ORD-4821"), and answers with the real result. That instruction — “never guess” — is doing real work: it pushes the model toward the tool instead of hallucinating a status.

Step 4: Multiple tools and chaining

Real agents pick from several tools and sometimes chain them. Add a second tool and let the model coordinate:

[Description("Starts a return for a delivered order and returns a return label URL.")]
static string StartReturn(
    [Description("The order ID")] string orderId,
    [Description("Reason for the return")] string reason)
    => $"Return started for {orderId} ({reason}). Label: https://returns.example/{orderId}";

AIAgent agent = chatClient.CreateAIAgent(
    name: "SupportBot",
    instructions: "Help customers with orders and returns using your tools.",
    tools:
    [
        AIFunctionFactory.Create(GetOrderStatus),
        AIFunctionFactory.Create(StartReturn),
    ]);

// The model may call GetOrderStatus first to confirm delivery,
// then StartReturn — across a single RunAsync call.
var reply = await agent.RunAsync(
    "I want to return ORD-4821 because it's the wrong size.");
Console.WriteLine(reply);

The framework runs the tool-call loop for you: model requests a tool, framework executes it, result goes back to the model, repeat until the model produces a final answer.

Step 5: Remembering the conversation

By default each RunAsync is stateless. To hold a multi-turn conversation, create a thread and pass it on every call:

var thread = agent.GetNewThread();

Console.WriteLine(await agent.RunAsync("Track ORD-4821", thread));
// Follow-up — the agent remembers which order you meant:
Console.WriteLine(await agent.RunAsync("Actually, start a return for it — wrong size", thread));

The thread carries the history, so the second message resolves “it” to ORD-4821 without you repeating the ID.

Note: the Agent Framework is young and exact method names (CreateAIAgent, AIFunctionFactory.Create, GetNewThread) can shift between releases. Treat this as the shape of the flow — define tools, register them, run, thread state — and check the official samples against the version you install.

What you built

In a few dozen lines you have an agent that understands intent, calls real C# functions, chains multiple tools in one turn, and remembers context across turns — with none of the tool-dispatch plumbing written by hand. From here, the natural next steps are connecting standardized tools over MCP, and deploying the agent so it runs somewhere other than your laptop.


Have a correction or a topic you want covered? Email mani.bc72@gmail.com.