An AI agent in Microsoft Teams without rewriting the agent
Microsoft's documentation assumes you are starting from nothing: a new project in their SDK, or a new agent in Copilot Studio. Most people asking already have an agent that works on the website or in Slack and a customer who runs on Microsoft 365. How to bring that agent into Teams with a thin adapter, what the protocol actually asks of you, and the tenant-admin reality the docs gloss over.
The agent already works. It answers questions on your website, or it lives in your customers' Slack workspaces, and it has the parts that took the real effort: identity, tools, state, evals. Then a customer says they are a Microsoft shop, and the deal depends on the agent being in Teams.
Search for how to integrate an AI agent into Teams and the results are almost entirely Microsoft: Learn articles, the Teams SDK documentation in three languages, Copilot Studio guidance, plus a few platforms that would like you to move the agent onto them. Every path starts with a new project. If you follow them you end up maintaining two agents, one of which is written in somebody else's framework.
You do not need to. Teams is a channel, and the agent is not. What you need is an adapter that translates between the two, which is a few hundred lines of code and a much longer list of things to ask your customer's IT department.
What Teams actually needs from you
Strip away the SDKs and Teams asks for four things, none of which touch the agent itself:
- 1An identity for the bot. An app registration in Microsoft Entra ID and an Azure Bot resource that points at your endpoint. This is what Teams trusts, and what your customer's administrator will look up.
- 2An HTTPS endpoint that receives activities: JSON messages in the Bot Framework protocol, each signed with a token you must validate.
- 3A way to reply, either in the response to the activity or by calling back to the service URL it came from.
- 4An app package: a
manifest.jsonand two icons in a zip file, which is what gets uploaded, approved and installed.
User messages the bot in Teams
Personal chat, group chat, or an @mention in a channel
Azure Bot Service posts an activity
Signed JSON to your endpoint
Adapter verifies the token and the tenant
Only tenants you have onboarded get past here
↳ bad signature or unknown tenant: reject, log, stop
Map the Entra user to your user
Tenant id plus object id, looked up in your own table
Call the agent core you already have
Same tools, same state, same evals
Format and send the reply
Markdown or an Adaptive Card, back to the conversation
Everything above the agent core is the adapter. Everything inside it is the agent you already shipped.
If you built the agent with the core and adapter split described in where your AI agent should live, Teams is one more adapter. If the agent is tangled up with your website's request handling, separating that out is the real first task, and it pays for itself on the next channel too.
The adapter: four translations
The adapter's job is to turn a Teams activity into the input your agent already accepts, and the agent's output into something Teams renders. In practice that is four translations:
// What your agent core already accepts from every other channel.
type AgentInput = { userId: string; accountId: string; conversationKey: string; text: string };
export async function onTeamsActivity(activity: Activity) {
if (activity.type !== "message") return;
// 1. Tenant: only organisations you have onboarded.
const tenantId = activity.conversation.tenantId;
const account = await accounts.byEntraTenant(tenantId);
if (!account) return reply(activity, notOnboardedCard());
// 2. Identity: the Entra object id, mapped in your own table, scoped to that tenant.
const user = await users.byEntraObjectId(account.id, activity.from.aadObjectId);
if (!user) return reply(activity, signInCard(account));
// 3. Conversation: one key per thread, so state does not bleed between chats.
const conversationKey = "teams:" + tenantId + ":" + activity.conversation.id;
// 4. Text: remove the <at>Bot</at> mention Teams leaves in channel messages.
const text = stripMentions(activity.text ?? "", activity.entities).trim();
const input: AgentInput = { userId: user.id, accountId: account.id, conversationKey, text };
await respondAsync(activity, () => agent.run(input));
}Token validation happens before this function runs, in middleware, and is not optional: the endpoint is public, and without it anyone can post a convincing activity claiming to be anyone. Every SDK does this for you, which is the strongest reason to use one for the transport even when you keep the agent separate.
The fifteen-second problem
Your agent probably takes longer to answer than Teams wants to wait. The bot service expects your endpoint to respond quickly, roughly fifteen seconds, and an agent that retrieves documents, calls two tools and writes a careful answer will regularly take longer than that. When it does, the user sees an error or a retry and you may process the same message twice.
- Acknowledge immediately. Return from the HTTP request at once, send a typing indicator, and run the agent in the background.
- Reply proactively. When the agent finishes, send the answer to the stored conversation using the service URL from the original activity. This is the same mechanism you will need later for anything the agent starts on its own.
- Make inbound handling idempotent on the activity id, so a retried delivery does not produce a second answer. The same rule as every other channel in agents that finish the job.
- Stream where Teams supports it. Teams now supports streamed bot responses in one-to-one chats, which turns a twenty-second wait into text appearing after two. Group chats and channels still get the complete message, so the background pattern is needed either way.
Formatting: your agent writes markdown, Teams renders some of it
Teams renders a subset of markdown in bot messages, and the subset is not the one your website renderer handles. Rather than changing the prompt for Teams, which forks the agent, convert the output in the adapter:
| The agent produces | In Teams | What the adapter does |
|---|---|---|
| Paragraphs, bold, italics, links | Render well | Pass through |
| Bulleted and numbered lists | Render, with less control over nesting | Flatten anything nested more than one level |
| Markdown tables | Unreliable | Convert to an Adaptive Card with a table or fact set |
| Code blocks | Render | Pass through, keep them short |
| Citations to sources | Plain links work | List sources at the end, or use a card with buttons |
| A choice for the user | No native buttons in markdown | Adaptive Card actions, which come back as a new activity |
Adaptive Cards are worth learning for approvals in particular. An agent that wants to do something consequential can show a card with Approve and Cancel, and the click comes back to your adapter as a message you handle like any other, with the same identity checks.
Chats, channels and what the bot can hear
Teams has three conversation types and the bot hears different things in each. In a personal chat it receives every message. In a group chat or a channel, by default, it only receives messages that @mention it. Reading every channel message requires resource-specific consent, where a team owner grants the app permission to read messages in that team.
Stay with mentions unless the feature genuinely needs more. An agent that reads every message in a channel is a harder conversation with the customer's security team, costs more to run, and has the same problem as an over-eager Slack bot: it starts answering questions nobody asked it. The design questions are the same ones covered in threads, mentions and context in Slack, and the permission reasoning is the same as the scopes you should refuse to request.
The tenant-admin reality
This is the section Microsoft's documentation is shortest on, because it is not an engineering problem. Your code can be finished in two weeks and the agent can still be four weeks from anyone in your customer's organisation using it.
| Gate | Who controls it | What to do about it |
|---|---|---|
| Uploading a custom app | Teams admin policy, often turned off | Ask on the first call whether custom apps are allowed, and who can publish one to the organisation's app catalogue |
| App permission and setup policies | Teams administrator | Agree which users get the app first. A pilot group is easier to approve than the whole company |
| Microsoft Graph permissions | Entra administrator, through admin consent | Request as little as possible. Every Graph permission is read line by line, and adding one later means consenting again |
| Security review | The customer's security or procurement team | Have a document ready: where data goes, which model provider, retention, sub-processors, and how to revoke access |
| Public listing in the Teams Store | Microsoft validation through Partner Center | Only needed if you distribute widely. For a handful of enterprise customers, per-tenant upload is faster |
The sequencing that works: send the security document and the list of permissions in the same week the customer says yes, not after the code is done. The adapter is the short part of the project. Approval runs on someone else's calendar, so start it while you are still writing code.
Teams SDK, Agents SDK or Copilot Studio?
Microsoft has renamed and reorganised these tools several times, so check the current documentation before you commit. The choice itself is stable:
- The Teams SDK is the most convenient transport if Teams is the only Microsoft surface you need. Use it for the protocol, auth and cards, and keep your agent behind it.
- The Microsoft 365 Agents SDK is the better fit if the customer also wants the agent available in Microsoft 365 Copilot, since it targets several Microsoft surfaces from one adapter.
- Copilot Studio is the right answer when there is no agent yet and the customer wants to own and change it themselves. For an agent you have already built and tested, it means building it a second time.
The order I would do it in
- 1Separate the agent core from your first channel if it is not already, with one input type and one output type.
- 2Send the customer the permission list and security document, and find out who approves custom apps.
- 3Register the bot and build the adapter: token validation, tenant check, identity mapping, conversation key, mention stripping.
- 4Acknowledge fast and reply proactively, with idempotency on the activity id.
- 5Convert output in the adapter, with Adaptive Cards for tables and approvals.
- 6Run your existing evals through the Teams adapter, plus tests for an unknown tenant, an unlinked user and a duplicated delivery.
Teams does not need a new agent. It needs a translator for the one you have, and a head start on the people who have to say yes.
If you have an agent working on one channel and a customer waiting for it in Teams, tell me what it does and who has to approve it on their side. I will come back with the adapter design, the permission list your customer's administrator will actually approve, and a fixed price. Start that conversation here, or see the engineering behind it on the AI capabilities page.
Questions people ask
How do I integrate an existing AI agent into Microsoft Teams?
Register a bot with an Entra ID app and an Azure Bot resource, then write an adapter endpoint that validates each incoming activity, checks the tenant is one you have onboarded, maps the Teams user to your own user, strips the @mention from the text and calls your existing agent. Package a manifest and two icons so the customer's administrator can install it.
Do I need Copilot Studio to put an AI agent in Teams?
No. Copilot Studio suits teams with no agent yet who want to build and edit one in a low-code tool. If you already have a working, tested agent, use the Teams SDK or the Microsoft 365 Agents SDK for the transport and keep your agent behind an adapter, so you do not maintain two versions of it.
Why does my Teams bot time out or reply twice?
The bot service expects your endpoint to respond within roughly fifteen seconds, and an agent that retrieves documents and calls tools often takes longer, so Teams retries. Return from the request immediately, send a typing indicator, run the agent in the background and send the answer proactively to the stored conversation, with idempotency on the activity id.
Can a Teams bot read every message in a channel?
Not by default. In group chats and channels a bot only receives messages that @mention it. Reading every channel message needs resource-specific consent granted by a team owner. Stay with mentions unless a feature genuinely needs more, because it is easier for a security team to approve and stops the agent answering questions nobody asked it.
Building something like this?
I design and ship these systems for clients: retrieval over private data, agents that complete real tasks, and the Laravel platforms underneath them.