Use Teams Activity parsing, Bot Connector calls, Graph reads, formatting, Adaptive Cards, and Task Module helpers without the full Chat runtime.
The Teams adapter is the right default for most bots. It validates Bot Framework requests through the Microsoft Teams SDK, parses Activities, stores conversation context, renders Adaptive Cards, reads Graph history, and routes events through Chat.
Use the low-level Teams subpaths when your app already owns routing, state, sessions, or workflow execution and only needs Teams-specific primitives.
| Subpath | Use for |
|---|---|
@chat-adapter/teams/webhook | Parse Bot Framework Activity JSON, classify common payloads, and extract continuation data |
@chat-adapter/teams/api | Fetch-based Bot Connector calls for messages, updates, deletes, typing, and conversations |
@chat-adapter/teams/graph | Fetch-based Microsoft Graph reads for chats, channels, channel messages, and replies |
@chat-adapter/teams/format | Teams HTML, mention, Markdown-ish, and emoji string helpers |
@chat-adapter/teams/cards | Runtime-free conversion from simple card objects and input requests to Adaptive Cards |
@chat-adapter/teams/modals | Runtime-free Task Module Adaptive Card helpers and submit parsing |
The webhook subpath parses Activities only. It does not verify Microsoft Bot Framework JWTs. For production request validation, use createTeamsAdapter or the Microsoft Teams SDK request pipeline before handing the Activity to these helpers.
Teams sends Bot Framework Activity JSON. readTeamsWebhook reads the request body and classifies the Activity, but it intentionally does not perform JWT validation.
import { postTeamsMessage } from "@chat-adapter/teams/api";
import { readTeamsWebhook } from "@chat-adapter/teams/webhook";
export async function POST(request: Request) {
const payload = await readTeamsWebhook(request, {
botAppId: process.env.TEAMS_APP_ID,
});
if (payload.kind === "message") {
await postTeamsMessage({
conversationId: payload.continuation.conversationId,
credentials: {
appId: process.env.TEAMS_APP_ID!,
appPassword: process.env.TEAMS_APP_PASSWORD!,
tenantId: payload.continuation.tenantId,
},
markdownText: `received: ${payload.text}`,
serviceUrl: payload.continuation.serviceUrl,
});
}
return new Response(null, { status: 200 });
}parseTeamsWebhookBody returns typed payloads:
| Kind | Teams surface |
|---|---|
message | Message activities |
message_reaction | Reaction activities |
card_action | Adaptive Card actions and Action.Submit message activities |
dialog_open | Task Module task/fetch invokes |
dialog_submit | Task Module task/submit invokes |
conversation_update | Conversation membership and install context updates |
installation_update | App installation updates |
unsupported | Valid Activities not normalized by this helper yet |
Message-like payloads include continuation, which contains provider-native reply context:
type TeamsContinuation = {
activityId?: string;
channelId?: string;
conversationId: string;
replyToId?: string;
serviceUrl: string;
teamId?: string;
tenantId?: string;
};This is not a Chat SDK Thread. It is the durable Teams data you need to reply later with @chat-adapter/teams/api.
The API subpath calls the Bot Framework Connector REST API with fetch. It does not import @microsoft/teams.apps.
import {
deleteTeamsMessage,
postTeamsMessage,
sendTeamsTyping,
updateTeamsMessage,
} from "@chat-adapter/teams/api";
const credentials = {
appId: process.env.TEAMS_APP_ID!,
appPassword: process.env.TEAMS_APP_PASSWORD!,
tenantId: process.env.TEAMS_APP_TENANT_ID!,
};
const posted = await postTeamsMessage({
conversationId: "19:abc@thread.tacv2",
credentials,
markdownText: "**hello**",
serviceUrl: "https://smba.trafficmanager.net/teams/",
});
await updateTeamsMessage({
conversationId: "19:abc@thread.tacv2",
credentials,
messageId: posted.id,
serviceUrl: "https://smba.trafficmanager.net/teams/",
text: "updated",
});
await sendTeamsTyping({
conversationId: "19:abc@thread.tacv2",
credentials,
serviceUrl: "https://smba.trafficmanager.net/teams/",
});
await deleteTeamsMessage({
conversationId: "19:abc@thread.tacv2",
credentials,
messageId: posted.id,
serviceUrl: "https://smba.trafficmanager.net/teams/",
});Use accessToken in credentials when your runtime already owns Microsoft token acquisition. A direct accessToken must be scoped for the API you call it against — the Bot Connector subpath (/api) needs a https://api.botframework.com/.default token, while the Graph subpath (/graph) needs a https://graph.microsoft.com/.default token. Passing the same token to both will fail against one of them. When you supply appId/appPassword instead, each subpath requests the correct scope for you.
The Graph subpath reads Teams history with explicit Graph IDs. Unlike TeamsAdapter, it does not use the adapter state cache to infer teamId, channelId, or chatId.
import { listTeamsChannelMessages } from "@chat-adapter/teams/graph";
const messages = await listTeamsChannelMessages({
channelId: "19:channel@thread.tacv2",
credentials: {
appId: process.env.TEAMS_APP_ID!,
appPassword: process.env.TEAMS_APP_PASSWORD!,
tenantId: process.env.TEAMS_APP_TENANT_ID!,
},
limit: 25,
teamId: "19:team@thread.tacv2",
});
const latestText = messages.items[0]?.text;Graph reads require the same Microsoft Graph permissions as the full adapter. Channel and group-chat reads can use RSC permissions; DM reads require Azure AD application permissions such as Chat.Read.All.
Teams renders message text as HTML. The format subpath provides small helpers for custom runtimes:
import {
formatTeamsMention,
markdownToTeamsHtml,
teamsHtmlToMarkdown,
} from "@chat-adapter/teams/format";
const html = markdownToTeamsHtml(
`${formatTeamsMention("Ada")} approved **deploy v2.4.1**`
);
const markdown = teamsHtmlToMarkdown("<p>Hello <strong>world</strong></p>");Use the full TeamsFormatConverter from @chat-adapter/teams when you need mdast conversion inside Chat SDK.
The cards subpath converts simple card objects into Adaptive Card JSON without importing the full chat JSX runtime.
import {
cardToAdaptiveCard,
cardToTeamsFallbackText,
} from "@chat-adapter/teams/cards";
import { postTeamsMessage } from "@chat-adapter/teams/api";
const card = {
children: [
{ content: "deploy v2.4.1?", type: "text" },
{
children: [
{ id: "approve", label: "Approve", style: "primary", type: "button" },
{ id: "deny", label: "Deny", style: "danger", type: "button" },
],
type: "actions",
},
],
title: "Deployment",
type: "card",
} as const;
await postTeamsMessage({
adaptiveCard: cardToAdaptiveCard(card),
conversationId: payload.continuation.conversationId,
credentials,
serviceUrl: payload.continuation.serviceUrl,
text: cardToTeamsFallbackText(card),
});Use the full Chat SDK card JSX when you want cross-platform rendering. Use @chat-adapter/teams/cards when you are building a Teams-only runtime and want Adaptive Card output directly.
Teams Task Modules are invoke-based dialogs backed by Adaptive Cards. The modals subpath builds those cards and parses submit data.
import {
modalToAdaptiveCard,
parseTeamsDialogSubmitValues,
toTeamsTaskModuleResponse,
} from "@chat-adapter/teams/modals";
const modal = {
callbackId: "deploy",
children: [
{ content: "Why deploy now?", type: "text" },
{ id: "reason", label: "Reason", type: "text_input" },
],
title: "Deploy",
type: "modal",
} as const;
const card = modalToAdaptiveCard(modal, { contextId: "deploy-1" });
const values = parseTeamsDialogSubmitValues(payload.value);
return Response.json(
toTeamsTaskModuleResponse({ action: "update", modal }, { contextId: "deploy-1" })
);The low-level Teams subpaths are designed to avoid the full runtime import graph:
- no
chatimport - no
@chat-adapter/sharedimport - no
@microsoft/teams.appsimport - no full adapter import
The package still installs the full Teams adapter dependencies. The subpaths keep your source and bundle imports clean, but they are not a package-size split.



















