| name | Teams App Dev |
| description | Deep expertise in custom Microsoft Teams app development — manifest v1.25 schema, M365 Agents Toolkit CLI, Adaptive Cards 1.6, search/action/link-unfurling message extensions, meeting apps (side panel, stage, content bubble), Custom Engine Agents, Agent 365 blueprints, Nested App Authentication (NAA), dialog namespace (replacing task modules), single-tenant bot registration, and Agents Playground for local testing. Targets professional TypeScript developers building production Teams apps on the Microsoft 365 platform.
|
| allowed-tools | ["Read","Write","Edit","Glob","Grep","Bash"] |
| triggers | ["teams app","m365 agents toolkit","adaptive card","message extension","bot framework","teams bot","teams tab","teams manifest","sideload","teams sso","link unfurling","task module","dialog","meeting app","meeting extension","custom engine agent","agent 365","agents playground","nested app auth","naa"] |
Teams App Dev
1. Teams App Architecture Overview
Microsoft Teams apps extend Teams with custom functionality through several surface areas:
App types:
| Type | Surface | Technology |
|---|
| Bot | Chat, channels, group chats | Bot Framework SDK + TeamsActivityHandler |
| Message extension | Compose box, message actions | Bot Framework search/action handlers |
| Tab (static) | Personal app, channel tab | React/HTML + Teams JS SDK v2 |
| Tab (configurable) | Channel/group chat tab | React/HTML + configuration page |
| Meeting extension | Pre/in/post meeting, side panel, stage | Tabs + bots + content bubble |
| Custom Engine Agent | Conversational AI agent | M365 Agents Toolkit + AI SDK |
| Agent 365 | Declarative agent | Agent 365 manifest + MCP tools |
| Connector | Channel notifications | Incoming/outgoing webhooks or O365 connectors |
Development stack:
- Language: TypeScript (recommended) or C#/.NET
- Bot runtime:
botbuilder + botbuilder-teams npm packages (Bot Framework SDK v4)
- Frontend: React with
@microsoft/teams-js SDK v2
- Auth: MSAL.js + Entra ID app registration (SSO via
getAuthToken) — single-tenant enforced
- Tooling: M365 Agents Toolkit CLI (
m365agents) — replaces Teams Toolkit CLI
- Hosting: Azure App Service, Azure Functions, or any HTTPS endpoint
- Local testing: Agents Playground (no bot registration required)
M365 Agents Toolkit vs legacy Teams Toolkit:
| Aspect | M365 Agents Toolkit (current) | Teams Toolkit (legacy) |
|---|
| CLI package | @microsoft/m365agentstoolkit-cli | @microsoft/teamsapp-cli |
| CLI command | m365agents | teamsapp |
| Config file | m365agents.yml | teamsapp.yml |
| Manifest schema | v1.25 | v1.17 |
| Bot auth | Single-tenant with APP_TENANTID | Multi-tenant |
| Local testing | Agents Playground (no registration) | Dev tunnel + sideload |
| Agent support | Custom Engine Agents, Agent 365 | N/A |
2. M365 Agents Toolkit CLI
M365 Agents Toolkit CLI (m365agents) manages the full app lifecycle. It replaces the legacy teamsapp CLI.
Install:
npm install -g @microsoft/m365agentstoolkit-cli
Core commands:
| Command | Description |
|---|
m365agents new | Scaffold a new Teams app or agent project |
m365agents provision | Create Azure resources defined in m365agents.yml |
m365agents deploy | Deploy code to provisioned Azure resources |
m365agents preview --local | Start local debug with Agents Playground |
m365agents preview --remote | Preview deployed app in Teams |
m365agents validate | Validate manifest against v1.25 schema |
m365agents package | Build the app package (ZIP with manifest + icons) |
m365agents publish | Publish to org app catalog |
m365agents env list | List configured environments |
m365agents env add <name> | Add a new environment |
Project structure (generated by m365agents new):
my-teams-app/
├── m365agents.yml # Lifecycle configuration (replaces teamsapp.yml)
├── m365agents.local.yml # Local debug overrides
├── env/
│ ├── .env.dev # Dev environment variables
│ └── .env.local # Local debug variables
├── appPackage/
│ ├── manifest.json # App manifest v1.25 (with {{VAR}} placeholders)
│ ├── color.png # 192x192 color icon
│ └── outline.png # 32x32 outline icon
├── src/
│ ├── index.ts # Entry point (bot adapter / Express server)
│ └── teamsBot.ts # TeamsActivityHandler implementation
├── infra/
│ └── azure.bicep # Azure resource definitions
└── package.json
m365agents.yml lifecycle hooks:
version: v1.6
provision:
- uses: teamsApp/create
with:
name: my-teams-app-${{TEAMSFX_ENV}}
writeToEnvironmentFile:
teamsAppId: TEAMS_APP_ID
- uses: botAadApp/create
with:
name: my-teams-app-bot-${{TEAMSFX_ENV}}
appTenantId: ${{APP_TENANTID}}
writeToEnvironmentFile:
botId: BOT_ID
botPassword: SECRET_BOT_PASSWORD
- uses: botFramework/create
with:
botId: ${{BOT_ID}}
name: my-teams-app-bot
messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages
appTenantId: ${{APP_TENANTID}}
- uses: teamsApp/validateManifest
with:
manifestPath: ./appPackage/manifest.json
- uses: teamsApp/zipAppPackage
with:
manifestPath: ./appPackage/manifest.json
outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip
- uses: teamsApp/update
with:
appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip
deploy:
- uses: cli/runNpmCommand
with:
args: install
- uses: cli/runNpmCommand
with:
args: run build
Agents Playground
Agents Playground allows local testing of bots and agents without Azure Bot registration or Dev Tunnels.
m365agents preview --local
Key benefits:
- No Azure Bot Service registration required for local dev
- No Dev Tunnel or ngrok setup needed
- Simulates Teams channel data, conversation references, and invoke activities
- Supports Adaptive Card rendering and action testing
3. Adaptive Card Schema
Adaptive Cards are platform-agnostic UI snippets rendered natively in Teams. Teams supports schema version 1.6 on desktop/web and 1.5 on mobile.
Card structure:
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5",
"body": [],
"actions": []
}
Element Types
Containers:
| Type | Purpose | Key Properties |
|---|
Container | Group elements vertically | items[], style, bleed, minHeight |
ColumnSet | Side-by-side columns | columns[] (each has width, items[]) |
Column | Single column in a ColumnSet | width ("auto"/"stretch"/weight), items[] |
FactSet | Key-value pairs | facts[] (title, value) |
ImageSet | Gallery of images | images[], imageSize |
Table | Tabular data (v1.5+) | columns[], rows[], gridStyle |
ActionSet | Inline action buttons | actions[] |
Elements:
| Type | Purpose | Key Properties |
|---|
TextBlock | Display text | text, size, weight, color, wrap, style |
Image | Display an image | url, size, style ("default"/"person"), altText |
RichTextBlock | Formatted inline text | inlines[] (TextRun objects) |
Media | Embedded media (v1.1+) | sources[], poster |
Icon | Fluent icon (v1.6+) | name, size, color |
Input elements:
| Type | Purpose | Key Properties |
|---|
Input.Text | Single/multi-line text | id, placeholder, isMultiline, maxLength, regex |
Input.Number | Numeric input | id, min, max, placeholder |
Input.Date | Date picker | id, min, max, placeholder |
Input.Time | Time picker | id, min, max |
Input.Toggle | On/off switch | id, title, valueOn, valueOff |
Input.ChoiceSet | Dropdown or radio/checkbox | id, choices[], isMultiSelect, style |
Action types:
| Type | Purpose | Key Properties |
|---|
Action.OpenUrl | Open a URL | url |
Action.Submit | Submit input values | data (merged with input values) |
Action.Execute | Universal action (Teams) | verb, data — Teams-preferred over Action.Submit |
Action.ShowCard | Toggle an inline card | card |
Action.ToggleVisibility | Show/hide elements | targetElements[] |
Teams-Specific: Action.Execute
Teams uses Action.Execute (Universal Actions) instead of Action.Submit for bot-powered cards. The bot receives an adaptiveCard/action invoke with the verb and data.
{
"type": "Action.Execute",
"title": "Approve",
"verb": "approveRequest",
"data": {
"requestId": "REQ-001"
}
}
Bot handler for Action.Execute:
async onAdaptiveCardInvoke(
context: TurnContext,
invokeValue: AdaptiveCardInvokeValue
): Promise<AdaptiveCardInvokeResponse> {
const verb = invokeValue.action.verb;
const data = invokeValue.action.data;
switch (verb) {
case "approveRequest":
await this.processApproval(data.requestId);
return {
statusCode: 200,
type: "application/vnd.microsoft.card.adaptive",
value: this.buildApprovedCard(data.requestId),
};
default:
return { statusCode: 200, type: "application/vnd.microsoft.activity.message", value: "Unknown action" };
}
}
Templating
Adaptive Card Templating separates data from layout using $data (data binding) and $when (conditional rendering).
Install:
npm install adaptivecards-templating
Template:
{
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"text": "Hello, ${name}!",
"size": "Large",
"weight": "Bolder"
},
{
"type": "Container",
"$data": "${items}",
"items": [
{
"type": "TextBlock",
"text": "${title} — ${status}",
"$when": "${status != 'hidden'}"
}
]
}
]
}
Render with data:
import { Template } from "adaptivecards-templating";
const template = new Template(cardPayload);
const card = template.expand({
$root: {
name: "Contoso",
items: [
{ title: "Task A", status: "active" },
{ title: "Task B", status: "hidden" },
{ title: "Task C", status: "done" },
],
},
});
Validation Rules
version must be "1.4" or "1.5" for broad Teams client support.
- Every
Input.* element must have a unique id property.
Action.Execute must include a verb string.
- Card payload size limit: 28 KB (compressed).
- Image URLs must be HTTPS.
fallbackText should be set for accessibility when elements use newer schema features.
4. Message Extensions
Message extensions let users search external services, take actions on messages, and unfurl links — all from the Teams compose box or message context menu.
Search-Based Message Extension
Users type a query in the compose box and receive results from an external service.
Manifest fragment (composeExtensions):
{
"composeExtensions": [
{
"botId": "{{BOT_ID}}",
"commands": [
{
"id": "searchProducts",
"type": "query",
"title": "Search Products",
"description": "Find products by name or SKU",
"initialRun": true,
"parameters": [
{
"name": "query",
"title": "Search",
"description": "Product name or SKU",
"inputType": "text"
}
]
}
]
}
]
}
Handler (TeamsActivityHandler):
async handleTeamsMessagingExtensionQuery(
context: TurnContext,
query: MessagingExtensionQuery
): Promise<MessagingExtensionResponse> {
const searchText = query.parameters?.[0]?.value || "";
const products = await this.productService.search(searchText);
const attachments = products.map((product) => ({
contentType: "application/vnd.microsoft.card.adaptive",
content: {
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: product.name, weight: "Bolder", size: "Medium" },
{ type: "TextBlock", text: `SKU: ${product.sku}`, isSubtle: true },
{ type: "TextBlock", text: product.description, wrap: true },
],
},
preview: CardFactory.heroCard(product.name, product.description, [product.imageUrl]),
}));
return {
composeExtension: {
type: "result",
attachmentLayout: "list",
attachments,
},
};
}
Action-Based Message Extension
Users fill out a form (dialog) triggered from the compose box or a message context menu.
Manifest fragment:
{
"composeExtensions": [
{
"botId": "{{BOT_ID}}",
"commands": [
{
"id": "createTicket",
"type": "action",
"title": "Create Ticket",
"description": "Create a support ticket from this message",
"context": ["message", "compose"],
"fetchTask": true
}
]
}
]
}
Fetch task handler (returns the form via dialog.url.open() pattern):
async handleTeamsMessagingExtensionFetchTask(
context: TurnContext,
action: MessagingExtensionAction
): Promise<MessagingExtensionActionResponse> {
const messageText = action.messagePayload?.body?.content || "";
return {
task: {
type: "continue",
value: {
title: "Create Support Ticket",
width: "medium",
height: "medium",
card: CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "Input.Text", id: "title", label: "Title", placeholder: "Ticket title" },
{
type: "Input.Text",
id: "description",
label: "Description",
isMultiline: true,
value: messageText,
},
{
type: "Input.ChoiceSet",
id: "priority",
label: "Priority",
choices: [
{ title: "Low", value: "low" },
{ title: "Medium", value: "medium" },
{ title: "High", value: "high" },
],
value: "medium",
},
],
actions: [{ type: "Action.Submit", title: "Create" }],
}),
},
},
};
}
Submit handler:
async handleTeamsMessagingExtensionSubmitAction(
context: TurnContext,
action: MessagingExtensionAction
): Promise<MessagingExtensionActionResponse> {
const { title, description, priority } = action.data;
const ticket = await this.ticketService.create({ title, description, priority });
return {
composeExtension: {
type: "result",
attachmentLayout: "list",
attachments: [
CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: `Ticket Created: ${ticket.id}`, weight: "Bolder" },
{ type: "FactSet", facts: [
{ title: "Title", value: title },
{ title: "Priority", value: priority },
{ title: "Status", value: "Open" },
]},
],
}),
],
},
};
}
Link Unfurling
When a user pastes a URL matching a registered domain, Teams invokes the bot to provide a preview card.
Manifest fragment:
{
"composeExtensions": [
{
"botId": "{{BOT_ID}}",
"messageHandlers": [
{
"type": "link",
"value": {
"domains": ["contoso.com", "*.contoso.com"]
}
}
]
}
]
}
Handler:
async handleTeamsAppBasedLinkQuery(
context: TurnContext,
query: AppBasedLinkQuery
): Promise<MessagingExtensionResponse> {
const url = query.url;
const metadata = await this.fetchPageMetadata(url);
return {
composeExtension: {
type: "result",
attachmentLayout: "list",
attachments: [
{
contentType: "application/vnd.microsoft.card.thumbnail",
content: {
title: metadata.title,
text: metadata.description,
images: [{ url: metadata.imageUrl }],
buttons: [{ type: "openUrl", title: "View", value: url }],
},
preview: CardFactory.thumbnailCard(metadata.title, metadata.description, [metadata.imageUrl]),
},
],
},
};
}
5. Bot Framework for Teams
Teams bots extend the Bot Framework TeamsActivityHandler, which provides Teams-specific method overrides on top of the standard ActivityHandler.
TeamsActivityHandler Methods
| Method | Trigger | Use Case |
|---|
onMessage | User sends a text message | General conversation, command routing |
onMembersAdded | User/bot joins conversation | Welcome message |
onTeamsMembersAdded | Member added to a team | Team-specific welcome |
onTeamsMembersRemoved | Member removed from team | Cleanup, farewell |
onTeamsChannelCreated | Channel created | Notify team about new channel |
onTeamsChannelDeleted | Channel deleted | Audit logging |
onTeamsChannelRenamed | Channel renamed | Update references |
onTeamsTeamRenamed | Team renamed | Update references |
onTeamsTeamArchived | Team archived | Status update |
onTeamsTeamUnarchived | Team unarchived | Re-enable features |
handleTeamsMessagingExtensionQuery | Search message extension | Return search results |
handleTeamsMessagingExtensionFetchTask | Action extension form | Return dialog card |
handleTeamsMessagingExtensionSubmitAction | Action extension submit | Process form data |
handleTeamsAppBasedLinkQuery | Link unfurling | Return preview card |
onAdaptiveCardInvoke | Action.Execute on card | Process universal action |
handleTeamsTaskModuleFetch | Dialog requested (legacy: task/fetch) | Return dialog content |
handleTeamsTaskModuleSubmit | Dialog submitted (legacy: task/submit) | Process dialog data |
onInstallationUpdate | App installed/uninstalled | Setup or teardown per-user state |
Bot Scaffold
import { TeamsActivityHandler, TurnContext, MessageFactory, CardFactory } from "botbuilder";
export class TeamsBot extends TeamsActivityHandler {
constructor() {
super();
this.onMessage(async (context: TurnContext, next) => {
const text = context.activity.text?.trim().toLowerCase() || "";
if (text === "help") {
await context.sendActivity(
MessageFactory.text("I can help with:\n- **search** — Find items\n- **status** — Check status")
);
} else {
await context.sendActivity(
MessageFactory.text(`You said: "${context.activity.text}"`)
);
}
await next();
});
this.onMembersAdded(async (context, next) => {
for (const member of context.activity.membersAdded || []) {
if (member.id !== context.activity.recipient.id) {
await context.sendActivity(
MessageFactory.text("Welcome! Type **help** to see what I can do.")
);
}
}
await next();
});
}
}
TurnContext Key Methods
| Method | Description |
|---|
context.sendActivity(activity) | Send a reply to the user |
context.activity.text | The user's message text |
context.activity.from | Sender info (id, name, aadObjectId) |
context.activity.conversation | Conversation info (id, tenantId, conversationType) |
context.activity.channelData | Teams-specific data (team, channel, tenant) |
context.activity.value | Data from card actions or dialogs |
Proactive Messaging
Send messages outside the normal request-response flow (e.g., notifications, scheduled updates).
import { ConversationReference, TurnContext } from "botbuilder";
const conversationReferences: Record<string, Partial<ConversationReference>> = {};
const ref = TurnContext.getConversationReference(context.activity);
conversationReferences[ref.conversation!.id] = ref;
async function sendProactiveMessage(adapter: BotFrameworkAdapter, ref: Partial<ConversationReference>, message: string) {
await adapter.continueConversation(ref, async (turnContext) => {
await turnContext.sendActivity(MessageFactory.text(message));
});
}
Conversation State
Bot Framework provides state management via storage providers.
import {
MemoryStorage,
ConversationState,
UserState,
StatePropertyAccessor,
} from "botbuilder";
const memoryStorage = new MemoryStorage();
const conversationState = new ConversationState(memoryStorage);
const userState = new UserState(memoryStorage);
const dialogStateAccessor: StatePropertyAccessor = conversationState.createProperty("DialogState");
const userProfileAccessor: StatePropertyAccessor = userState.createProperty("UserProfile");
this.onTurn(async (context, next) => {
await next();
await conversationState.saveChanges(context, false);
await userState.saveChanges(context, false);
});
Production storage: Replace MemoryStorage with BlobStorage (botbuilder-azure-blobs) or CosmosDbPartitionedStorage (botbuilder-azure) for persistence.
Dialogs (WaterfallDialog)
Multi-step conversation flows using the Dialogs library.
import { WaterfallDialog, WaterfallStepContext, TextPrompt, ChoicePrompt } from "botbuilder-dialogs";
const ORDER_DIALOG = "orderDialog";
const orderDialog = new WaterfallDialog(ORDER_DIALOG, [
async (step: WaterfallStepContext) => {
return step.prompt("textPrompt", "What product are you looking for?");
},
async (step: WaterfallStepContext) => {
step.values["product"] = step.result;
return step.prompt("choicePrompt", "Select quantity:", ["1", "5", "10"]);
},
async (step: WaterfallStepContext) => {
step.values["quantity"] = step.result.value;
await step.context.sendActivity(
`Order placed: ${step.values["quantity"]}x ${step.values["product"]}`
);
return step.endDialog();
},
]);
6. Meeting Apps
Meeting apps extend the Teams meeting experience with pre-meeting, in-meeting, and post-meeting surfaces.
Meeting App Surfaces
| Surface | When Available | Manifest Context | Technology |
|---|
| Pre-meeting tab | Before meeting starts | meetingDetailsTab | Configurable tab |
| Side panel | During meeting | meetingSidePanel | Configurable tab |
| Meeting stage | During meeting (shared) | meetingStage | Configurable tab + shareAppContentToStage |
| Content bubble | During meeting | N/A | Bot notification with targetedMeetingNotification |
| Post-meeting tab | After meeting ends | meetingDetailsTab | Configurable tab |
Meeting App Manifest (v1.25)
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.25/MicrosoftTeams.schema.json",
"manifestVersion": "1.25",
"configurableTabs": [
{
"configurationUrl": "https://{{TAB_DOMAIN}}/config",
"canUpdateConfiguration": true,
"scopes": ["groupChat"],
"context": [
"meetingChatTab",
"meetingDetailsTab",
"meetingSidePanel",
"meetingStage"
],
"supportsChannelFeatures": true
}
],
"authorization": {
"permissions": {
"resourceSpecific": [
{ "name": "OnlineMeeting.ReadBasic.Chat", "type": "Delegated" },
{ "name": "OnlineMeetingParticipant.Read.Chat", "type": "Delegated" },
{ "name": "MeetingStage.Write.Chat", "type": "Delegated" }
]
}
}
}
Share to Meeting Stage
import { meeting, app } from "@microsoft/teams-js";
async function shareToStage() {
await app.initialize();
meeting.shareAppContentToStage(
(err, result) => {
if (err) {
console.error("Failed to share to stage:", err);
return;
}
console.log("Shared to stage successfully");
},
`${window.location.origin}/stage?meetingId=${meetingId}`
);
}
Meeting Side Panel
import { app, meeting } from "@microsoft/teams-js";
async function initSidePanel() {
await app.initialize();
const context = await app.getContext();
if (context.page.frameContext === "sidePanel") {
const meetingId = context.meeting?.id;
meeting.getMeetingDetails((err, details) => {
if (details) {
console.log("Organizer:", details.organizer?.id);
console.log("Scheduled start:", details.details?.scheduledStartTime);
}
});
}
}
Content Bubble (In-Meeting Notification)
Bots can send targeted meeting notifications that appear as content bubbles during a meeting.
async function sendContentBubble(context: TurnContext, meetingId: string) {
const card = CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: "Action Required", weight: "Bolder" },
{ type: "TextBlock", text: "Please vote on the current agenda item", wrap: true },
],
actions: [
{ type: "Action.Execute", title: "Vote Yes", verb: "vote", data: { vote: "yes" } },
{ type: "Action.Execute", title: "Vote No", verb: "vote", data: { vote: "no" } },
],
});
await context.sendActivity({
type: "message",
attachments: [card],
channelData: {
notification: {
alertInMeeting: true,
externalResourceUrl: `https://${process.env.TAB_DOMAIN}/stage?action=vote`,
},
},
});
}
Meeting Message Extensions
Message extensions work within meeting chats. When a meeting is active, extensions can:
- Search and share content relevant to the meeting agenda
- Create action items from the meeting chat context
- Unfurl links shared during the meeting with rich previews
Meeting-aware search extension:
async handleTeamsMessagingExtensionQuery(
context: TurnContext,
query: MessagingExtensionQuery
): Promise<MessagingExtensionResponse> {
const searchText = query.parameters?.[0]?.value || "";
const meetingId = context.activity.channelData?.meeting?.id;
const isMeetingChat = !!meetingId;
let results;
if (isMeetingChat) {
results = await this.searchMeetingAgendaItems(meetingId, searchText);
} else {
results = await this.searchAllItems(searchText);
}
const attachments = results.map((item) => ({
contentType: "application/vnd.microsoft.card.adaptive",
content: {
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: item.title, weight: "Bolder", size: "Medium" },
{ type: "TextBlock", text: item.description, wrap: true },
...(isMeetingChat ? [
{ type: "TextBlock", text: `📋 Agenda: ${item.agendaItem}`, isSubtle: true }
] : []),
],
},
preview: CardFactory.heroCard(item.title, item.description),
}));
return {
composeExtension: {
type: "result",
attachmentLayout: "list",
attachments,
},
};
}
Meeting action extension — create action item from meeting chat:
async handleTeamsMessagingExtensionFetchTask(
context: TurnContext,
action: MessagingExtensionAction
): Promise<MessagingExtensionActionResponse> {
const meetingId = context.activity.channelData?.meeting?.id;
const messageText = action.messagePayload?.body?.content || "";
return {
task: {
type: "continue",
value: {
title: "Create Meeting Action Item",
width: "medium",
height: "medium",
card: CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: "Create Action Item", size: "Large", weight: "Bolder" },
{ type: "Input.Text", id: "title", label: "Action Item", value: messageText, isRequired: true },
{ type: "Input.Text", id: "assignee", label: "Assign To", placeholder: "user@contoso.com" },
{ type: "Input.Date", id: "dueDate", label: "Due Date" },
{
type: "Input.ChoiceSet",
id: "priority",
label: "Priority",
choices: [
{ title: "High", value: "high" },
{ title: "Medium", value: "medium" },
{ title: "Low", value: "low" },
],
value: "medium",
},
],
actions: [{ type: "Action.Submit", title: "Create", data: { meetingId } }],
}),
},
},
};
}
async handleTeamsMessagingExtensionSubmitAction(
context: TurnContext,
action: MessagingExtensionAction
): Promise<MessagingExtensionActionResponse> {
const { title, assignee, dueDate, priority, meetingId } = action.data;
const actionItem = await this.createActionItem({ title, assignee, dueDate, priority, meetingId });
return {
composeExtension: {
type: "result",
attachmentLayout: "list",
attachments: [
CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: `Action Item Created`, weight: "Bolder" },
{ type: "FactSet", facts: [
{ title: "Title", value: title },
{ title: "Assigned To", value: assignee || "Unassigned" },
{ title: "Due", value: dueDate || "No date" },
{ title: "Priority", value: priority },
{ title: "Meeting", value: meetingId ? "Linked" : "None" },
]},
],
}),
],
},
};
}
7. Dialog Namespace (Replaces Task Modules)
The dialog namespace replaces the deprecated tasks namespace. Use dialog.url.open() and dialog.adaptiveCard.open() instead of tasks.startTask().
Opening a Dialog from a Tab
import { dialog, app } from "@microsoft/teams-js";
async function openAdaptiveCardDialog() {
await app.initialize();
dialog.adaptiveCard.open({
card: JSON.stringify({
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: "Submit Feedback", size: "Large", weight: "Bolder" },
{ type: "Input.Text", id: "feedback", label: "Your feedback", isMultiline: true, isRequired: true },
{ type: "Input.ChoiceSet", id: "rating", label: "Rating", choices: [
{ title: "Excellent", value: "5" },
{ title: "Good", value: "4" },
{ title: "Average", value: "3" },
{ title: "Poor", value: "2" },
]},
],
actions: [{ type: "Action.Submit", title: "Submit" }],
}),
title: "Feedback",
size: { width: "medium", height: "medium" },
}, (result) => {
if (result.err) {
console.error("Dialog error:", result.err);
return;
}
const data = JSON.parse(result.result as string);
console.log("Feedback:", data.feedback, "Rating:", data.rating);
});
}
async function openUrlDialog() {
await app.initialize();
dialog.url.open({
url: `${window.location.origin}/dialog/form`,
title: "Create Item",
size: { width: "large", height: "large" },
fallbackUrl: `${window.location.origin}/dialog/form`,
}, (result) => {
if (result.err) {
console.error("Dialog error:", result.err);
return;
}
console.log("Dialog result:", result.result);
});
}
Submitting from Inside a Dialog
import { dialog, app } from "@microsoft/teams-js";
async function submitDialog() {
await app.initialize();
const formData = {
title: document.getElementById("title")?.value,
description: document.getElementById("description")?.value,
};
dialog.url.submit(JSON.stringify(formData));
}
Bot-Side Dialog Handlers
protected async handleTeamsTaskModuleFetch(
context: TurnContext,
taskModuleRequest: { data: Record<string, string> }
) {
return {
task: {
type: "continue",
value: {
title: "Create Item",
height: 450,
width: 500,
card: CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "Input.Text", id: "title", label: "Title", isRequired: true },
{ type: "Input.Text", id: "notes", label: "Notes", isMultiline: true },
],
actions: [{ type: "Action.Submit", title: "Create" }],
}),
},
},
};
}
protected async handleTeamsTaskModuleSubmit(
context: TurnContext,
taskModuleRequest: { data: Record<string, string> }
) {
const { title, notes } = taskModuleRequest.data;
await this.createItem(title, notes);
return { task: { type: "message", value: `Created: ${title}` } };
}
8. Tab Apps
Tabs embed web content as personal apps or channel tabs inside Teams.
Tab Types
| Type | Scope | Configuration |
|---|
| Static (personal) tab | Personal app bar | No config page needed; define contentUrl in manifest |
| Configurable tab | Channel or group chat | Requires a configuration page (configurationUrl) |
Teams JavaScript SDK v2
npm install @microsoft/teams-js
Initialize:
import { app, authentication } from "@microsoft/teams-js";
async function initializeApp() {
await app.initialize();
const context = await app.getContext();
console.log("Theme:", context.app.theme);
console.log("Locale:", context.app.locale);
console.log("User ID:", context.user?.id);
console.log("Team ID:", context.team?.internalId);
console.log("Channel ID:", context.channel?.id);
console.log("Host:", context.app.host.name);
}
SSO Authentication Pattern — Nested App Authentication (NAA)
Manifest v1.25 introduces Nested App Authentication (NAA) for pop-up-free iframe authentication.
Manifest v1.25 nestedAppAuthInfo:
{
"nestedAppAuthInfo": {
"oidcScopes": ["openid", "profile", "email", "offline_access"],
"accessTokenAcceptedVersion": 2
},
"webApplicationInfo": {
"id": "{{AZURE_CLIENT_ID}}",
"resource": "api://{{TAB_DOMAIN}}/{{AZURE_CLIENT_ID}}"
}
}
Client-side with NAA (no popup required):
import { authentication, app } from "@microsoft/teams-js";
async function getTokenWithNAA(): Promise<string> {
await app.initialize();
const token = await authentication.getAuthToken({
silent: true,
});
return token;
}
Traditional SSO (fallback):
import { authentication } from "@microsoft/teams-js";
async function getToken(): Promise<string> {
const token = await authentication.getAuthToken();
return token;
}
Server-side (On-Behalf-Of flow):
import { ConfidentialClientApplication } from "@azure/msal-node";
const msalClient = new ConfidentialClientApplication({
auth: {
clientId: process.env.AZURE_CLIENT_ID!,
clientSecret: process.env.AZURE_CLIENT_SECRET!,
authority: `https://login.microsoftonline.com/${process.env.APP_TENANTID}`,
},
});
async function exchangeToken(ssoToken: string): Promise<string> {
const result = await msalClient.acquireTokenOnBehalfOf({
oboAssertion: ssoToken,
scopes: ["https://graph.microsoft.com/.default"],
});
return result!.accessToken;
}
React Tab Starter
import React, { useEffect, useState } from "react";
import { app } from "@microsoft/teams-js";
import { FluentProvider, teamsLightTheme, teamsDarkTheme, teamsHighContrastTheme, Text } from "@fluentui/react-components";
const themeMap = {
default: teamsLightTheme,
dark: teamsDarkTheme,
contrast: teamsHighContrastTheme,
};
export function Tab() {
const [theme, setTheme] = useState(teamsLightTheme);
const [userName, setUserName] = useState("");
useEffect(() => {
(async () => {
await app.initialize();
const ctx = await app.getContext();
setTheme(themeMap[ctx.app.theme as keyof typeof themeMap] || teamsLightTheme);
setUserName(ctx.user?.userPrincipalName || "Unknown");
app.registerOnThemeChangeHandler((newTheme) => {
setTheme(themeMap[newTheme as keyof typeof themeMap] || teamsLightTheme);
});
})();
}, []);
return (
<FluentProvider theme={theme}>
<Text size={500} weight="bold">Hello, {userName}!</Text>
</FluentProvider>
);
}
9. Teams App Manifest v1.25
The manifest (manifest.json) is the app's contract with Teams. Manifest v1.25 is the current schema version.
What Changed from v1.17 to v1.25
| Feature | v1.17 | v1.25 |
|---|
| Schema URL | v1.17/MicrosoftTeams.schema.json | v1.25/MicrosoftTeams.schema.json |
manifestVersion | "1.17" | "1.25" |
| Bot tenancy | Multi-tenant | Single-tenant with APP_TENANTID |
supportsChannelFeatures | N/A | New boolean on configurable tabs |
nestedAppAuthInfo | N/A | NAA for pop-up-free SSO |
backgroundLoadConfiguration | N/A | Background tab preloading |
agenticUserTemplates | N/A | Custom Engine Agent prompt templates |
| Tooling | teamsapp CLI | m365agents CLI |
| Config file | teamsapp.yml | m365agents.yml |
Known v1.25 Bugs
- Regex validation: The Dev Portal schema validator may reject valid regex patterns in
Input.Text.regex. Workaround: validate locally with m365agents validate.
- Dev Portal save issue: Editing manifests directly in the Teams Developer Portal may silently drop
nestedAppAuthInfo and agenticUserTemplates fields. Workaround: always author manifests in your codebase and use m365agents package.
Required Fields
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.25/MicrosoftTeams.schema.json",
"manifestVersion": "1.25",
"version": "1.0.0",
"id": "{{APP_ID}}",
"developer": {
"name": "Contoso",
"websiteUrl": "https://contoso.com",
"privacyUrl": "https://contoso.com/privacy",
"termsOfUseUrl": "https://contoso.com/terms"
},
"name": {
"short": "My Teams App",
"full": "My Teams App — Full Description"
},
"description": {
"short": "Brief app description (max 80 chars)",
"full": "Detailed description of app capabilities (max 4000 chars)"
},
"icons": {
"color": "color.png",
"outline": "outline.png"
},
"accentColor": "#4F6BED",
"validDomains": ["contoso.com", "*.contoso.com"]
}
Bot Registration (Single-Tenant)
All bot registrations in v1.25 enforce single-tenant with APP_TENANTID.
{
"bots": [
{
"botId": "{{BOT_ID}}",
"scopes": ["personal", "team", "groupChat"],
"supportsFiles": false,
"isNotificationOnly": false,
"commandLists": [
{
"scopes": ["personal"],
"commands": [
{ "title": "help", "description": "Show available commands" },
{ "title": "status", "description": "Check current status" }
]
}
]
}
]
}
Bicep for single-tenant bot:
resource botService 'Microsoft.BotService/botServices@2022-09-15' = {
name: botServiceName
location: 'global'
sku: { name: 'F0' }
kind: 'azurebot'
properties: {
displayName: botServiceName
endpoint: botEndpoint
msaAppId: botAadAppClientId
msaAppType: 'SingleTenant'
msaAppTenantId: appTenantId // Required for v1.25
}
}
Static Tabs
{
"staticTabs": [
{
"entityId": "dashboard",
"name": "Dashboard",
"contentUrl": "https://contoso.com/tabs/dashboard?theme={theme}",
"websiteUrl": "https://contoso.com/dashboard",
"scopes": ["personal"]
}
]
}
Configurable Tabs with supportsChannelFeatures
{
"configurableTabs": [
{
"configurationUrl": "https://contoso.com/tabs/config",
"canUpdateConfiguration": true,
"scopes": ["team", "groupChat"],
"context": ["channelTab", "privateChatTab", "meetingSidePanel", "meetingStage"],
"supportsChannelFeatures": true
}
]
}
Nested App Authentication (NAA)
{
"nestedAppAuthInfo": {
"oidcScopes": ["openid", "profile", "email", "offline_access"],
"accessTokenAcceptedVersion": 2
}
}
Background Load Configuration
{
"backgroundLoadConfiguration": {
"enabled": true,
"preloadOnAppInstall": true
}
}
Agentic User Templates
{
"agenticUserTemplates": [
{
"id": "summarize",
"title": "Summarize conversation",
"description": "Generate a summary of the current conversation",
"prompt": "Summarize the key points discussed in this conversation."
},
{
"id": "actionItems",
"title": "Extract action items",
"description": "List all action items from the conversation",
"prompt": "Extract and list all action items from this conversation, including assignees and deadlines."
}
]
}
Web Application Info (SSO)
{
"webApplicationInfo": {
"id": "{{AZURE_CLIENT_ID}}",
"resource": "api://contoso.com/{{AZURE_CLIENT_ID}}"
}
}
RSC (Resource-Specific Consent) Permissions
{
"authorization": {
"permissions": {
"resourceSpecific": [
{ "name": "ChannelMessage.Read.Group", "type": "Application" },
{ "name": "TeamSettings.ReadWrite.Group", "type": "Application" },
{ "name": "ChatMessage.Read.Chat", "type": "Application" }
]
}
}
}
Complete Manifest v1.25 Example
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.25/MicrosoftTeams.schema.json",
"manifestVersion": "1.25",
"version": "1.0.0",
"id": "{{APP_ID}}",
"developer": {
"name": "Contoso",
"websiteUrl": "https://contoso.com",
"privacyUrl": "https://contoso.com/privacy",
"termsOfUseUrl": "https://contoso.com/terms"
},
"name": { "short": "Contoso Helper", "full": "Contoso Helper — Bot, Tabs, and Meeting App" },
"description": {
"short": "Search products and manage tickets",
"full": "A Teams app with a conversational bot, product search message extension, dashboard tab, and meeting side panel with stage sharing."
},
"icons": { "color": "color.png", "outline": "outline.png" },
"accentColor": "#4F6BED",
"bots": [
{
"botId": "{{BOT_ID}}",
"scopes": ["personal", "team", "groupChat"],
"commandLists": [
{
"scopes": ["personal"],
"commands": [
{ "title": "help", "description": "Show available commands" },
{ "title": "search", "description": "Search for products" }
]
}
]
}
],
"composeExtensions": [
{
"botId": "{{BOT_ID}}",
"commands": [
{
"id": "searchProducts",
"type": "query",
"title": "Search Products",
"description": "Find products by name or SKU",
"initialRun": true,
"parameters": [
{ "name": "query", "title": "Search", "description": "Product name or SKU" }
]
},
{
"id": "createActionItem",
"type": "action",
"title": "Create Action Item",
"description": "Create an action item from this message",
"context": ["message", "compose"],
"fetchTask": true
}
],
"messageHandlers": [
{
"type": "link",
"value": { "domains": ["contoso.com"] }
}
]
}
],
"staticTabs": [
{
"entityId": "dashboard",
"name": "Dashboard",
"contentUrl": "https://contoso.com/tabs/dashboard",
"scopes": ["personal"]
}
],
"configurableTabs": [
{
"configurationUrl": "https://contoso.com/tabs/config",
"canUpdateConfiguration": true,
"scopes": ["team", "groupChat"],
"context": ["channelTab", "meetingSidePanel", "meetingStage"],
"supportsChannelFeatures": true
}
],
"validDomains": ["contoso.com"],
"webApplicationInfo": {
"id": "{{AZURE_CLIENT_ID}}",
"resource": "api://contoso.com/{{AZURE_CLIENT_ID}}"
},
"nestedAppAuthInfo": {
"oidcScopes": ["openid", "profile", "email", "offline_access"],
"accessTokenAcceptedVersion": 2
},
"backgroundLoadConfiguration": {
"enabled": true,
"preloadOnAppInstall": true
},
"authorization": {
"permissions": {
"resourceSpecific": [
{ "name": "OnlineMeeting.ReadBasic.Chat", "type": "Delegated" },
{ "name": "MeetingStage.Write.Chat", "type": "Delegated" },
{ "name": "ChannelMessage.Read.Group", "type": "Application" }
]
}
}
}
10. Custom Engine Agents
Custom Engine Agents are conversational AI agents built with the M365 Agents Toolkit. They combine Bot Framework with AI SDK capabilities.
Scaffold a Custom Engine Agent
m365agents new --capability custom-engine-agent --app-name MyAgent
Agent Structure
import { TeamsActivityHandler, TurnContext, MessageFactory } from "botbuilder";
export class CustomEngineAgent extends TeamsActivityHandler {
private aiClient: AIClient;
constructor(aiClient: AIClient) {
super();
this.aiClient = aiClient;
this.onMessage(async (context: TurnContext, next) => {
const userMessage = context.activity.text?.trim() || "";
const response = await this.aiClient.chat({
messages: [
{ role: "system", content: "You are a helpful assistant for Contoso employees." },
{ role: "user", content: userMessage },
],
});
await context.sendActivity(MessageFactory.text(response.content));
await next();
});
}
}
Agent with Tools (Function Calling)
export class ToolCallingAgent extends TeamsActivityHandler {
private tools = [
{
name: "searchProducts",
description: "Search the product catalog",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
},
required: ["query"],
},
},
{
name: "getOrderStatus",
description: "Get the status of an order",
parameters: {
type: "object",
properties: {
orderId: { type: "string", description: "Order ID" },
},
required: ["orderId"],
},
},
];
private async executeTool(name: string, args: Record<string, string>): Promise<string> {
switch (name) {
case "searchProducts":
return JSON.stringify(await this.productService.search(args.query));
case "getOrderStatus":
return JSON.stringify(await this.orderService.getStatus(args.orderId));
default:
return JSON.stringify({ error: "Unknown tool" });
}
}
}
11. Agent 365
Agent 365 is a declarative agent framework for building agents with manifest-driven configuration and MCP tool integration.
Agent 365 Manifest
{
"agentManifest": {
"id": "contoso-support-agent",
"name": "Contoso Support Agent",
"description": "Handles customer support inquiries using company knowledge base",
"instructions": "You are a support agent for Contoso. Help users with product questions, order status, and troubleshooting. Always be polite and professional.",
"capabilities": {
"tools": [
{
"type": "mcp",
"server": "contoso-api",
"tools": ["searchKnowledgeBase", "getOrderStatus", "createTicket"]
}
],
"knowledge": {
"sources": [
{
"type": "sharepoint",
"siteUrl": "https://contoso.sharepoint.com/sites/knowledge"
}
]
}
}
}
}
Agent 365 Identity Configuration
{
"webApplicationInfo": {
"id": "{{AZURE_CLIENT_ID}}",
"resource": "api://{{TAB_DOMAIN}}/{{AZURE_CLIENT_ID}}"
},
"nestedAppAuthInfo": {
"oidcScopes": ["openid", "profile", "email"],
"accessTokenAcceptedVersion": 2
}
}
12. Authentication Patterns — Single-Tenant
All v1.25 bot registrations enforce single-tenant authentication.
Environment Configuration
BOT_ID=<microsoft-app-id>
BOT_PASSWORD=<client-secret>
BOT_ENDPOINT=https://<your-domain>
APP_TENANTID=<your-tenant-id>
AZURE_CLIENT_ID=<your-client-id>
AZURE_CLIENT_SECRET=<your-client-secret>
Bot Adapter (Single-Tenant)
import { CloudAdapter, ConfigurationServiceClientCredentialFactory, ConfigurationBotFrameworkAuthentication } from "botbuilder";
const credentialsFactory = new ConfigurationServiceClientCredentialFactory({
MicrosoftAppId: process.env.BOT_ID!,
MicrosoftAppPassword: process.env.BOT_PASSWORD!,
MicrosoftAppType: "SingleTenant",
MicrosoftAppTenantId: process.env.APP_TENANTID!,
});
const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication(
{},
credentialsFactory
);
const adapter = new CloudAdapter(botFrameworkAuthentication);
Bot Authentication (OAuthPrompt)
Bot Framework's OAuthPrompt handles sign-in flows for bots, including Teams SSO.
import { OAuthPrompt, OAuthPromptSettings } from "botbuilder-dialogs";
const oauthSettings: OAuthPromptSettings = {
connectionName: process.env.OAUTH_CONNECTION_NAME!,
text: "Please sign in to continue.",
title: "Sign In",
timeout: 300000,
};
const oauthPrompt = new OAuthPrompt("oauthPrompt", oauthSettings);
Azure Bot OAuth connection setup (single-tenant):
- Azure Portal > Bot resource > Configuration > OAuth Connection Settings.
- Add a new connection with service provider
Azure Active Directory v2.
- Set
Client ID, Client Secret, Tenant ID (required for single-tenant).
- Scopes:
openid profile User.Read (add more as needed).
- Use the connection name in
OAuthPromptSettings.connectionName.
13. Graph API for App Management
Manage Teams apps in the organization catalog and per-team/user installations via Microsoft Graph.
App Catalog Operations
| Operation | Method | Endpoint | Permission |
|---|
| List published apps | GET | /appCatalogs/teamsApps?$filter=distributionMethod eq 'organization' | AppCatalog.Read.All |
| Publish app | POST | /appCatalogs/teamsApps (multipart ZIP) | AppCatalog.Submit |
| Update published app | POST | /appCatalogs/teamsApps/{id}/appDefinitions | AppCatalog.Submit |
| Delete published app | DELETE | /appCatalogs/teamsApps/{id} | AppCatalog.Submit |
Per-Team Installation
| Operation | Method | Endpoint | Permission |
|---|
| List apps in team | GET | /teams/{team-id}/installedApps?$expand=teamsAppDefinition | TeamsAppInstallation.ReadForTeam |
| Install app in team | POST | /teams/{team-id}/installedApps | TeamsAppInstallation.ReadWriteForTeam |
| Remove from team | DELETE | /teams/{team-id}/installedApps/{id} | TeamsAppInstallation.ReadWriteForTeam |
14. Permissions and Scopes
Bot Scopes
| Scope | Description |
|---|
personal | Bot available in 1:1 chat with user |
team | Bot available in team channels |
groupChat | Bot available in group chats |
RSC Permissions (Resource-Specific Consent)
| Permission | Type | Scope |
|---|
TeamSettings.Read.Group | Application | Read team settings |
TeamSettings.ReadWrite.Group | Application | Read/write team settings |
ChannelMessage.Read.Group | Application | Read channel messages |
ChatMessage.Read.Chat | Application | Read chat messages |
OnlineMeeting.ReadBasic.Chat | Delegated | Read meeting details |
MeetingStage.Write.Chat | Delegated | Share to meeting stage |
TeamsActivity.Send.Group | Application | Send activity feed notifications |
TeamsActivity.Send.Chat | Application | Send activity feed in chats |
TeamsActivity.Send.User | Application | Send activity feed to users |
15. Error Handling
Bot onTurnError
import { CloudAdapter, TurnContext } from "botbuilder";
const adapter = new CloudAdapter(botFrameworkAuthentication);
adapter.onTurnError = async (context: TurnContext, error: Error) => {
console.error(`[onTurnError] unhandled error: ${error.message}`, error.stack);
await context.sendActivity("Sorry, something went wrong. Please try again later.");
await conversationState.delete(context);
};
Manifest Validation Errors
| Error | Cause | Fix |
|---|
MissingRequiredProperty | Required field not set | Add the missing field |
InvalidGuid | id or botId is not a valid GUID | Use crypto.randomUUID() or Azure-generated GUID |
InvalidDomain | validDomains entry malformed | Use FQDN without protocol |
BotIdMismatch | Bot ID mismatch with Azure registration | Ensure bots[].botId matches Azure Bot's App ID |
IconSizeMismatch | Icon dimensions wrong | color.png = 192x192, outline.png = 32x32 |
DescriptionTooLong | Short >80 chars or full >4000 chars | Shorten text |
RegexValidationBug | v1.25 schema rejects valid regex | Validate locally with m365agents validate |
Sideloading Errors
| Error | Cause | Fix |
|---|
SideloadingNotEnabled | Org policy blocks custom apps | Admin enables "Upload custom apps" in Teams admin center |
AppPackageInvalid | ZIP structure incorrect | ZIP must contain files at root level |
ManifestSchemaError | Manifest doesn't match schema | Run m365agents validate |
BotNotRegistered | Bot ID not registered | Create Azure Bot resource with matching App ID |
16. Common Patterns
Pattern 1: Meeting App with Side Panel and Stage
import { TeamsActivityHandler, TurnContext, CardFactory } from "botbuilder";
export class MeetingBot extends TeamsActivityHandler {
constructor() {
super();
this.onMessage(async (context, next) => {
const meetingId = context.activity.channelData?.meeting?.id;
if (meetingId && context.activity.text?.includes("share")) {
const card = CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: "Ready to share to stage?", weight: "Bolder" },
],
actions: [
{ type: "Action.Execute", title: "Share Now", verb: "shareToStage", data: { meetingId } },
],
});
await context.sendActivity({
type: "message",
attachments: [card],
channelData: {
notification: { alertInMeeting: true },
},
});
}
await next();
});
}
async onAdaptiveCardInvoke(context: TurnContext, invokeValue: any) {
if (invokeValue.action.verb === "shareToStage") {
return {
statusCode: 200,
type: "application/vnd.microsoft.activity.message",
value: "Content shared to stage!",
};
}
return { statusCode: 200, type: "application/vnd.microsoft.activity.message", value: "OK" };
}
}
Pattern 2: Full Meeting Message Extension
A message extension that adapts its behavior based on meeting context.
export class MeetingExtensionBot extends TeamsActivityHandler {
async handleTeamsMessagingExtensionQuery(
context: TurnContext,
query: MessagingExtensionQuery
): Promise<MessagingExtensionResponse> {
const meetingId = context.activity.channelData?.meeting?.id;
const searchText = query.parameters?.[0]?.value || "";
const items = meetingId
? await this.getMeetingAgendaItems(meetingId, searchText)
: await this.getAllItems(searchText);
const attachments = items.map((item) => ({
contentType: "application/vnd.microsoft.card.adaptive",
content: {
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: item.title, weight: "Bolder" },
{ type: "TextBlock", text: item.description, wrap: true },
],
},
preview: CardFactory.heroCard(item.title, item.description),
}));
return { composeExtension: { type: "result", attachmentLayout: "list", attachments } };
}
}
Pattern 3: Search Message Extension with SSO
export class SSOSearchExtension extends TeamsActivityHandler {
async handleTeamsMessagingExtensionQuery(
context: TurnContext,
query: MessagingExtensionQuery
): Promise<MessagingExtensionResponse> {
const searchText = query.parameters?.[0]?.value || "";
const tokenResponse = await this.getGraphToken(context);
if (!tokenResponse) {
return {
composeExtension: {
type: "auth",
suggestedActions: {
actions: [{ type: "openUrl", value: this.getAuthUrl(), title: "Sign in" }],
},
},
};
}
const graphClient = Client.init({
authProvider: (done) => done(null, tokenResponse.token),
});
const results = await graphClient
.api("/search/query")
.post({ requests: [{ entityTypes: ["driveItem"], query: { queryString: searchText } }] });
const attachments = results.value[0]?.hitsContainers?.[0]?.hits?.map((hit: any) => ({
contentType: "application/vnd.microsoft.card.adaptive",
content: {
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: hit.resource.name, weight: "Bolder" },
{ type: "TextBlock", text: hit.summary || "", wrap: true },
],
actions: [{ type: "Action.OpenUrl", title: "Open", url: hit.resource.webUrl }],
},
preview: CardFactory.heroCard(hit.resource.name, hit.summary || ""),
})) || [];
return { composeExtension: { type: "result", attachmentLayout: "list", attachments } };
}
}