Skip to content
Agent Integration10 min read·

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.

AI AgentsAgent IntegrationMicrosoft TeamsMicrosoft 365Entra IDArchitecture

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:

  1. 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.
  2. 2An HTTPS endpoint that receives activities: JSON messages in the Bot Framework protocol, each signed with a token you must validate.
  3. 3A way to reply, either in the response to the activity or by calling back to the service URL it came from.
  4. 4An app package: a manifest.json and two icons in a zip file, which is what gets uploaded, approved and installed.
Flowchart: a Teams message reaching an existing agent
  1. User messages the bot in Teams

    Personal chat, group chat, or an @mention in a channel

  2. Azure Bot Service posts an activity

    Signed JSON to your endpoint

  3. Adapter verifies the token and the tenant

    Only tenants you have onboarded get past here

    ↳ bad signature or unknown tenant: reject, log, stop

  4. Map the Entra user to your user

    Tenant id plus object id, looked up in your own table

  5. Call the agent core you already have

    Same tools, same state, same evals

  6. 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:

ts
// 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 producesIn TeamsWhat the adapter does
Paragraphs, bold, italics, linksRender wellPass through
Bulleted and numbered listsRender, with less control over nestingFlatten anything nested more than one level
Markdown tablesUnreliableConvert to an Adaptive Card with a table or fact set
Code blocksRenderPass through, keep them short
Citations to sourcesPlain links workList sources at the end, or use a card with buttons
A choice for the userNo native buttons in markdownAdaptive 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.

GateWho controls itWhat to do about it
Uploading a custom appTeams admin policy, often turned offAsk on the first call whether custom apps are allowed, and who can publish one to the organisation's app catalogue
App permission and setup policiesTeams administratorAgree which users get the app first. A pilot group is easier to approve than the whole company
Microsoft Graph permissionsEntra administrator, through admin consentRequest as little as possible. Every Graph permission is read line by line, and adding one later means consenting again
Security reviewThe customer's security or procurement teamHave a document ready: where data goes, which model provider, retention, sub-processors, and how to revoke access
Public listing in the Teams StoreMicrosoft validation through Partner CenterOnly 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

  1. 1Separate the agent core from your first channel if it is not already, with one input type and one output type.
  2. 2Send the customer the permission list and security document, and find out who approves custom apps.
  3. 3Register the bot and build the adapter: token validation, tenant check, identity mapping, conversation key, mention stripping.
  4. 4Acknowledge fast and reply proactively, with idempotency on the activity id.
  5. 5Convert output in the adapter, with Adaptive Cards for tables and approvals.
  6. 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.

Keep reading