Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
The Teams extension enables you to build agents that go beyond basic messaging in Microsoft Teams. It provides structured, event-driven access to the full range of Teams platform capabilities, including meetings, message extensions, task modules, channels, and team lifecycle events. The extension lets you create deeply integrated, interactive agent experiences without managing low-level protocol details.
Why use the Teams extension?
Teams is a rich platform. Without the extension, your agent handles generic activities and must manually inspect raw payloads to detect Teams-specific events. The Teams extension eliminates that friction:
- Purpose-built APIs for each Teams feature area: Write handlers for exactly what you care about
- Typed models for Teams data: No manual JSON parsing or guesswork
- Automatic routing: Incoming activities are matched to the right handler by event type, command ID, or pattern
- Built-in invoke handling: Teams requires specific response shapes for interactive invocations. The extension manages those details for you.
- Fluent and attribute-based registration: Configure handlers in the style that fits your architecture
- Full Teams API access: The Teams SDK
ApiClientis available on every turn for advanced scenarios beyond what built-in helpers cover
Feature areas
The Teams extension groups capabilities into submodules:
| Feature Area | Description |
|---|---|
| Message extensions | Search, action, and link-unfurl commands |
| Task Modules | Modal dialogs (adaptive card or webpage) |
| Meetings | Meeting start/end, participants join/leave |
| Messages | Message edit/delete/undelete, read receipts, Office 365 connector |
| File Consent | File upload consent flow |
| Configuration | Agent settings page |
| Channels | Channel create/rename/delete and member events |
| Teams | Team archive/rename/delete lifecycle events |
Set up the Teams extension
Use the TeamsExtension attribute (recommended)
Apply TeamsExtension to a partial subclass of AgentApplication. A source generator adds a TeamsExtension property of type TeamsAgentExtension and registers the extension automatically.
Register handlers by decorating methods with route attributes. Each feature area has its own set of attributes, such as TeamsMeetingStartRoute, TeamsQueryRoute, and TeamsTaskFetchRoute.
using Microsoft.Agents.Builder.App;
using Microsoft.Agents.Extensions.MSTeams;
using Microsoft.Agents.Extensions.MSTeams.App;
using Microsoft.Agents.Extensions.MSTeams.Meetings;
[TeamsExtension]
public partial class MyAgent(AgentApplicationOptions options) : AgentApplication(options)
{
[TeamsMeetingStartRoute]
public async Task OnMeetingStartAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.Meetings.MeetingDetails meeting,
CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Meeting started: {meeting.Id}", cancellationToken: cancellationToken);
}
[TeamsMeetingEndRoute]
public async Task OnMeetingEndAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.Meetings.MeetingDetails meeting,
CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Meeting ended: {meeting.Id}", cancellationToken: cancellationToken);
}
}
The handler method signatures are identical whether you register handlers via route attributes or use the TeamsExtension fluent methods (for example, TeamsExtension.Meetings.OnStart). You can use either style.
Manual registration
If you can't use a partial class or prefer explicit control, construct a TeamsAgentExtension and register it by using RegisterExtension:
public class MyAgent : AgentApplication
{
public MyAgent(AgentApplicationOptions options) : base(options)
{
var teams = new TeamsAgentExtension(this);
RegisterExtension(teams);
teams.Meetings.OnStart(OnMeetingStartAsync);
}
}
Create a TeamsAgentExtension instance and register it with your AgentApplication. Register handlers inside the registerExtension callback:
import { AgentApplication, MemoryStorage, TurnState } from '@microsoft/agents-hosting'
import { startServer } from '@microsoft/agents-hosting-express'
import { TeamsAgentExtension } from '@microsoft/agents-hosting-extensions-msteams'
const app = new AgentApplication({ storage: new MemoryStorage() })
const teamsExt = new TeamsAgentExtension(app)
app.registerExtension(teamsExt, (teams) => {
// Register Teams-specific handlers here
teams.meetings.onStart(async (context, state, details) => {
await context.sendActivity(`Meeting started: ${details.id}`)
})
})
startServer(app)
All handler methods return the submodule instance for fluent chaining:
teams.meetings
.onStart(handleStart)
.onEnd(handleEnd)
.onParticipantsJoin(handleJoin)
.onParticipantsLeave(handleLeave)
Construct a TeamsAgentExtension from your AgentApplication. The extension automatically wires its before-turn hook, and you register handlers by using the decorator on each feature-area property:
from microsoft_agents.hosting.core import AgentApplication, MemoryStorage, TurnState
from microsoft_agents.hosting.msteams import TeamsAgentExtension
from microsoft_teams.api.models import MeetingDetails
AGENT_APP = AgentApplication[TurnState](storage=MemoryStorage())
teams = TeamsAgentExtension[TurnState](AGENT_APP)
@teams.meetings.start
async def on_meeting_start(context, state, meeting: MeetingDetails):
await context.send_activity(f"Meeting started: {meeting.id}")
@teams.meetings.end
async def on_meeting_end(context, state, meeting: MeetingDetails):
await context.send_activity(f"Meeting ended: {meeting.id}")
Every handler receives a Teams-aware context (a TeamsTurnContext), the turn state, and a typed payload. The extension exposes feature areas as properties: message_extensions, task_modules, meetings, messages, file_consent, config, channels, and teams.
Message extensions
Message extensions surface as commands in the Teams compose box, the action menu on messages, or link previews. Each command is identified by its commandId as defined in the Teams app manifest.
Query and SelectItem
Users can query your service and pick results to insert into a message.
Add this command to the Teams manifest:
"composeExtensions": [
{
"commands": [
{
"id": "searchQuery",
"type": "query",
"title": "searchQuery",
"description": "searchQuery",
"initialRun": true,
"fetchTask": false,
"context": ["commandBox", "compose"],
"parameters": [
{
"name": "searchQuery",
"title": "Search Query",
"description": "Search Query",
"inputType": "text"
}
]
}
]
}
]
[TeamsExtension]
public partial class SearchAgent(AgentApplicationOptions options) : AgentApplication(options)
{
[TeamsQueryRoute("searchQuery")]
public Task<Microsoft.Teams.Api.MessageExtensions.Response> OnSearchAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.MessageExtensions.Query query,
CancellationToken cancellationToken)
{
string searchText = query.Parameters?
.FirstOrDefault(p => p.Name == "searchQuery")?.Value?.ToString() ?? "";
var attachments = new List<Microsoft.Teams.Api.MessageExtensions.Attachment>();
for (int i = 1; i <= 5; i++)
{
var preview = new ThumbnailCard
{
Title = searchText,
Tap = new CardAction { Type = "invoke", Value = $"{{\"index\": \"{i}\", \"query\":\"{searchText}\" }}" }
};
var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment
{
ContentType = Microsoft.Teams.Api.ContentType.AdaptiveCard,
Content = $"{{\"type\":\"AdaptiveCard\",\"version\":\"1.4\",\"body\":[{{\"type\":\"TextBlock\",\"text\":\"Result {i} for: {searchText}\"}}]}}",
Preview = new Microsoft.Teams.Api.MessageExtensions.Attachment
{
ContentType = Microsoft.Teams.Api.ContentType.ThumbnailCard,
Content = preview
}
};
attachments.Add(attachment);
}
return Task.FromResult(new Microsoft.Teams.Api.MessageExtensions.Response
{
ComposeExtension = new Microsoft.Teams.Api.MessageExtensions.Result
{
Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Result,
AttachmentLayout = Microsoft.Teams.Api.Attachment.Layout.List,
Attachments = [.. attachments]
}
});
}
[TeamsSelectItemRoute]
public Task<Microsoft.Teams.Api.MessageExtensions.Response> OnSelectItemAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Dictionary<string, string> items,
CancellationToken cancellationToken)
{
var index = items.TryGetValue("index", out string? value) ? value : "No Index";
var card = new AdaptiveCard([new TextBlock($"You selected item: {index}")]);
var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment
{
ContentType = Microsoft.Teams.Api.ContentType.AdaptiveCard,
Content = card
};
return Task.FromResult(new Microsoft.Teams.Api.MessageExtensions.Response
{
ComposeExtension = new Microsoft.Teams.Api.MessageExtensions.Result
{
Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Result,
AttachmentLayout = Microsoft.Teams.Api.Attachment.Layout.List,
Attachments = [attachment]
}
});
}
}
import { MessagingExtensionQuery, MessagingExtensionResult } from '@microsoft/teams.api'
teams.messageExtensions
.onQuery('searchQuery', async (context, state, query) => {
const searchText = query.parameters?.[0]?.value ?? ''
return {
attachmentLayout: 'list',
type: 'result',
attachments: [
{
preview: {
contentType: 'application/vnd.microsoft.card.thumbnail',
content: {
title: `Result for: ${searchText}`,
text: 'Click to select',
tap: { type: 'invoke', value: { index: '1', query: searchText } }
}
},
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
type: 'AdaptiveCard',
version: '1.4',
body: [
{ type: 'TextBlock', text: `Search Result for: ${searchText}`, weight: 'Bolder' }
]
}
}
]
}
})
.onSelectItem(async (context, state, item) => {
return {
attachmentLayout: 'list',
type: 'result',
attachments: [{
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
type: 'AdaptiveCard',
version: '1.4',
body: [{ type: 'TextBlock', text: `You selected item: ${item.index}` }]
}
}]
}
})
from microsoft_teams.api.models import (
MessagingExtensionQuery,
MessagingExtensionResponse,
MessagingExtensionResult,
MessagingExtensionResultType,
MessagingExtensionAttachment,
MessagingExtensionAttachmentLayout,
)
@teams.message_extensions.query("searchQuery")
async def on_search(context, state, query: MessagingExtensionQuery) -> MessagingExtensionResponse:
params = {p.name: p.value for p in (query.parameters or [])}
search_text = str(params.get("searchQuery", "") or "")
attachments = []
for i in range(1, 6):
card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [{"type": "TextBlock", "text": f"Result {i} for: {search_text}"}],
}
preview = {
"title": f"Result {i}",
"text": "Click to select",
"tap": {"type": "invoke", "value": {"index": str(i), "query": search_text}},
}
attachments.append(
MessagingExtensionAttachment(
content_type="application/vnd.microsoft.card.adaptive",
content=card,
preview=MessagingExtensionAttachment(
content_type="application/vnd.microsoft.card.thumbnail",
content=preview,
),
)
)
return MessagingExtensionResponse(
compose_extension=MessagingExtensionResult(
type=MessagingExtensionResultType.RESULT,
attachment_layout=MessagingExtensionAttachmentLayout.LIST,
attachments=attachments,
)
)
@teams.message_extensions.select_item
async def on_select_item(context, state, item) -> MessagingExtensionResponse:
item = item or {}
index = item.get("index", "No Index")
card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [{"type": "TextBlock", "text": f"You selected item: {index}"}],
}
return MessagingExtensionResponse(
compose_extension=MessagingExtensionResult(
type=MessagingExtensionResultType.RESULT,
attachment_layout=MessagingExtensionAttachmentLayout.LIST,
attachments=[
MessagingExtensionAttachment(
content_type="application/vnd.microsoft.card.adaptive",
content=card,
)
],
)
)
Action commands
A submit action command opens a form, collects input, and returns a card to post into the conversation.
The Teams manifest must contain the following command:
"composeExtensions": [
{
"commands": [
{
"id": "createCard",
"type": "action",
"title": "Create Card",
"description": "Create Card",
"initialRun": false,
"fetchTask": false,
"context": ["commandBox", "compose", "message"],
"parameters": [
{ "name": "title", "title": "Title", "inputType": "text" },
{ "name": "description", "title": "Description", "inputType": "text" }
]
}
]
}
]
[TeamsSubmitActionRoute("createCard")]
public Task<Microsoft.Teams.Api.MessageExtensions.Response> OnCreateCardAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.MessageExtensions.Action action,
CancellationToken cancellationToken)
{
var title = action.GetDataString("title", "Default Title");
var description = action.GetDataString("description", "Default Description");
var cardJson = $$"""
{
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{ "type": "TextBlock", "text": "Custom Card Created", "size": "Large", "weight": "Bolder" },
{ "type": "TextBlock", "text": "{{title}}", "weight": "Bolder" },
{ "type": "TextBlock", "text": "{{description}}", "isSubtle": true }
]
}
""";
return Task.FromResult(new Microsoft.Teams.Api.MessageExtensions.Response
{
ComposeExtension = new Microsoft.Teams.Api.MessageExtensions.Result
{
Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Result,
AttachmentLayout = Microsoft.Teams.Api.Attachment.Layout.List,
Attachments =
[
new Microsoft.Teams.Api.MessageExtensions.Attachment
{
ContentType = Microsoft.Teams.Api.ContentType.AdaptiveCard,
Content = cardJson
}
]
}
});
}
import { MessagingExtensionActionResponse } from '@microsoft/teams.api'
teams.messageExtensions
.onSubmitAction('createCard', async (context, state, data) => {
const title = data?.data?.title ?? 'Default Title'
const description = data?.data?.description ?? 'Default Description'
return {
composeExtension: {
type: 'result',
attachmentLayout: 'list',
attachments: [{
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
type: 'AdaptiveCard',
version: '1.4',
body: [
{ type: 'TextBlock', text: 'Custom Card Created', size: 'Large', weight: 'Bolder' },
{ type: 'TextBlock', text: title, weight: 'Bolder' },
{ type: 'TextBlock', text: description, isSubtle: true }
]
}
}]
}
}
})
The submit action handler receives a MessagingExtensionAction. Read the form fields from action.data:
from microsoft_teams.api.models import (
MessagingExtensionAction,
MessagingExtensionResponse,
MessagingExtensionResult,
MessagingExtensionResultType,
MessagingExtensionAttachment,
MessagingExtensionAttachmentLayout,
)
@teams.message_extensions.submit_action("createCard")
async def on_create_card(context, state, action: MessagingExtensionAction) -> MessagingExtensionResponse:
data = action.data if isinstance(action.data, dict) else {}
title = data.get("title") or "Default Title"
description = data.get("description") or "Default Description"
card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "text": "Custom Card Created", "size": "Large", "weight": "Bolder"},
{"type": "TextBlock", "text": title, "weight": "Bolder"},
{"type": "TextBlock", "text": description, "isSubtle": True},
],
}
return MessagingExtensionResponse(
compose_extension=MessagingExtensionResult(
type=MessagingExtensionResultType.RESULT,
attachment_layout=MessagingExtensionAttachmentLayout.LIST,
attachments=[
MessagingExtensionAttachment(
content_type="application/vnd.microsoft.card.adaptive",
content=card,
)
],
)
)
Link unfurling
Link unfurling generates a rich preview card when a user pastes a URL into the compose box. Teams calls your agent when the URL's domain matches a messageHandlers entry in your app manifest:
"composeExtensions": [{
"botId": "...",
"messageHandlers": [{
"type": "link",
"value": { "domains": ["*.contoso.com"] }
}]
}]
[TeamsQueryLinkRoute]
public Task<Microsoft.Teams.Api.MessageExtensions.Response> OnQueryLinkAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.AppBasedQueryLink? query,
CancellationToken cancellationToken)
{
var url = query?.Url ?? "";
var card = new AdaptiveCard([
new TextBlock("Link Preview") { Weight = TextWeight.Bolder },
new TextBlock(url) { IsSubtle = true, Wrap = true }
]);
var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment
{
ContentType = Microsoft.Teams.Api.ContentType.AdaptiveCard,
Content = card,
Preview = new Microsoft.Teams.Api.MessageExtensions.Attachment
{
ContentType = Microsoft.Teams.Api.ContentType.ThumbnailCard,
Content = new ThumbnailCard { Title = "Contoso Preview", Text = url }
}
};
return Task.FromResult(new Microsoft.Teams.Api.MessageExtensions.Response
{
ComposeExtension = new Microsoft.Teams.Api.MessageExtensions.Result
{
Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Result,
AttachmentLayout = Microsoft.Teams.Api.Attachment.Layout.List,
Attachments = [attachment]
}
});
}
Zero-install/anonymous link unfurling allows Teams to call your agent even before the user installs the app. Use [TeamsAnonQueryLinkRoute]. The handler signature is identical. The agent can't send proactive messages or access member details in anonymous context.
teams.messageExtensions
.onQueryLink(async (context, state, url) => {
return {
attachmentLayout: 'list',
type: 'result',
attachments: [{
contentType: 'application/vnd.microsoft.card.thumbnail',
content: {
title: 'Link Preview',
text: `Preview for: ${url}`,
tap: { type: 'invoke', value: { title: 'Link Clicked', text: url } }
}
}]
}
})
Zero-install/anonymous link unfurling allows Teams to call your agent before the user installs the app. Use onAnonymousQueryLink. The handler signature is identical.
The handler receives an AppBasedLinkQuery. Read the pasted URL from query.url:
from microsoft_teams.api.models import (
AppBasedLinkQuery,
MessagingExtensionResponse,
MessagingExtensionResult,
MessagingExtensionResultType,
MessagingExtensionAttachment,
MessagingExtensionAttachmentLayout,
)
@teams.message_extensions.query_link
async def on_query_link(context, state, query: AppBasedLinkQuery) -> MessagingExtensionResponse:
url = query.url or ""
card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "text": "Link Preview", "weight": "Bolder"},
{"type": "TextBlock", "text": f"URL: {url}", "isSubtle": True, "wrap": True},
],
}
return MessagingExtensionResponse(
compose_extension=MessagingExtensionResult(
type=MessagingExtensionResultType.RESULT,
attachment_layout=MessagingExtensionAttachmentLayout.LIST,
attachments=[
MessagingExtensionAttachment(
content_type="application/vnd.microsoft.card.adaptive",
content=card,
preview=MessagingExtensionAttachment(
content_type="application/vnd.microsoft.card.thumbnail",
content={"title": "Contoso Preview", "text": url},
),
)
],
)
)
Zero-install/anonymous link unfurling allows Teams to call your agent before the user installs the app. Use @teams.message_extensions.anonymous_query_link - the handler signature is identical.
Message preview
When you configure a compose extension with botMessagePreviewAction, Teams shows the user a preview of the card before sending it.
[TeamsMessagePreviewEditRoute("myCommand")]
public Task<Microsoft.Teams.Api.MessageExtensions.Response> OnPreviewEditAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
IActivity previewActivity,
CancellationToken cancellationToken)
{
var editCard = new AdaptiveCard([ /* edit form */ ]);
return Task.FromResult(new Microsoft.Teams.Api.MessageExtensions.Response
{
ComposeExtension = new Microsoft.Teams.Api.MessageExtensions.Result
{
Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Result,
AttachmentLayout = Microsoft.Teams.Api.Attachment.Layout.List,
Attachments =
[
new Microsoft.Teams.Api.MessageExtensions.Attachment
{
ContentType = Microsoft.Teams.Api.ContentType.AdaptiveCard,
Content = editCard
}
]
}
});
}
[TeamsMessagePreviewSendRoute("myCommand")]
public async Task OnPreviewSendAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
IActivity previewActivity,
CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Card sent!"), cancellationToken);
}
import { Activity } from '@microsoft/agents-activity'
import { MessagingExtensionActionResponse } from '@microsoft/teams.api'
teams.messageExtensions
.onMessagePreviewEdit('myCommand', async (context, state, activity) => {
return {
composeExtension: {
type: 'result',
attachmentLayout: 'list',
attachments: [{
contentType: 'application/vnd.microsoft.card.adaptive',
content: { /* edit card */ }
}]
}
}
})
.onMessagePreviewSend('myCommand', async (context, state, activity) => {
await context.sendActivity('Card sent!')
})
The preview handlers receive the parsed preview Activity. Use message_preview_edit to return an editable card and message_preview_send to send it:
from microsoft_agents.activity import Activity
from microsoft_teams.api.models import (
MessagingExtensionResponse,
MessagingExtensionResult,
MessagingExtensionResultType,
MessagingExtensionAttachment,
MessagingExtensionAttachmentLayout,
)
@teams.message_extensions.message_preview_edit("myCommand")
async def on_preview_edit(context, state, activity_preview: Activity) -> MessagingExtensionResponse:
return MessagingExtensionResponse(
compose_extension=MessagingExtensionResult(
type=MessagingExtensionResultType.RESULT,
attachment_layout=MessagingExtensionAttachmentLayout.LIST,
attachments=[
MessagingExtensionAttachment(
content_type="application/vnd.microsoft.card.adaptive",
content={"type": "AdaptiveCard", "version": "1.4", "body": []}, # edit form
)
],
)
)
@teams.message_extensions.message_preview_send("myCommand")
async def on_preview_send(context, state, activity_preview: Activity):
await context.send_activity("Card sent!")
Settings
Expose a settings page that users reach via the gear icon in the message extension flyout.
[TeamsQuerySettingUrlRoute]
public Task<Microsoft.Teams.Api.MessageExtensions.Response> OnQuerySettingsUrlAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
CancellationToken cancellationToken)
=> Task.FromResult(new Microsoft.Teams.Api.MessageExtensions.Response
{
ComposeExtension = new Microsoft.Teams.Api.MessageExtensions.Result
{
Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Config,
SuggestedActions = new Microsoft.Teams.Api.MessageExtensions.SuggestedActions
{
Actions =
[
new Microsoft.Teams.Api.Cards.Action(Microsoft.Teams.Api.Cards.ActionType.OpenUrl)
{
Value = "https://your-agent-host/settings",
Title = "Configure"
}
]
}
}
});
[TeamsSettingRoute]
public Task<Microsoft.Teams.Api.MessageExtensions.Response> OnConfigureSettingsAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.MessageExtensions.Query settings,
CancellationToken cancellationToken)
{
// Persist settings.State or settings.Parameters as needed
return Task.FromResult(new Microsoft.Teams.Api.MessageExtensions.Response());
}
import { MessagingExtensionResponse } from '@microsoft/teams.api'
teams.messageExtensions
.onQueryUrlSetting(async (context, state) => {
return {
composeExtension: {
type: 'config',
suggestedActions: {
actions: [{
type: 'openUrl',
value: 'https://your-agent-host/settings',
title: 'Configure'
}]
}
}
}
})
.onConfigureSettings(async (context, state, settings) => {
// Persist the user's settings
console.log('Settings saved:', settings)
})
Use query_setting_url to return the settings page URL and setting to apply the submitted settings:
from microsoft_teams.api.models import (
MessagingExtensionQuery,
MessagingExtensionResponse,
MessagingExtensionResult,
MessagingExtensionResultType,
)
@teams.message_extensions.query_setting_url
async def on_query_settings_url(context, state, query: MessagingExtensionQuery) -> MessagingExtensionResponse:
return MessagingExtensionResponse(
compose_extension=MessagingExtensionResult(
type=MessagingExtensionResultType.CONFIG,
suggested_actions={
"actions": [
{"type": "openUrl", "title": "Configure", "value": "https://your-agent-host/settings"}
]
},
)
)
@teams.message_extensions.setting
async def on_configure_settings(context, state, query: MessagingExtensionQuery):
# Persist query.state or query.parameters as needed
return MessagingExtensionResponse()
Task modules
Task modules (also called dialogs) open a modal window in Teams. The window can show an adaptive card or load a webpage. The route value corresponds to a key in the activity payload, set by the adaptive card button's data.task field.
Use [TeamsTaskFetchRoute] to display the dialog and [TeamsTaskSubmitRoute] to process the submitted data:
[TeamsExtension]
public partial class TaskModulesAgent(AgentApplicationOptions options) : AgentApplication(options)
{
[TeamsTaskFetchRoute("simple_form")]
public Task<Microsoft.Teams.Api.TaskModules.Response> OnSimpleFormFetchAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.TaskModules.Request request,
CancellationToken cancellationToken)
{
var cardJson = """
{
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{ "type": "TextBlock", "text": "Simple Form", "size": "Large", "weight": "Bolder" },
{ "type": "Input.Text", "id": "name", "label": "Name", "isRequired": true }
],
"actions": [{"type": "Action.Submit", "title": "Submit", "data": {"task": "simple_form"}}]
}
""";
return Task.FromResult(new Microsoft.Teams.Api.TaskModules.Response(
new Microsoft.Teams.Api.TaskModules.ContinueTask(
new Microsoft.Teams.Api.TaskModules.TaskInfo
{
Card = new Microsoft.Teams.Api.Attachment(ContentTypes.AdaptiveCard, cardJson),
Title = "Simple Form",
Height = new Microsoft.Teams.Common.Union<int, Microsoft.Teams.Api.TaskModules.Size>(Microsoft.Teams.Api.TaskModules.Size.Small),
Width = new Microsoft.Teams.Common.Union<int, Microsoft.Teams.Api.TaskModules.Size>(Microsoft.Teams.Api.TaskModules.Size.Small)
})));
}
[TeamsTaskSubmitRoute("simple_form")]
public async Task<Microsoft.Teams.Api.TaskModules.Response> OnSimpleFormSubmitAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.TaskModules.Request request,
CancellationToken cancellationToken)
{
var name = request.GetDataString("name", "Unknown");
await turnContext.SendActivityAsync($"Hi {name}!", cancellationToken: cancellationToken);
return new Microsoft.Teams.Api.TaskModules.Response(
new Microsoft.Teams.Api.TaskModules.MessageTask("Form submitted"));
}
}
Chain task modules by returning a new ContinueTask response from a submit handler instead of a MessageTask. Teams replaces the current dialog with the new card.
Use onFetch to display the dialog and onSubmit to process the submitted data:
import { CardFactory } from '@microsoft/agents-hosting'
teams.taskModules
.onFetch('simple_form', async (context, state, request) => {
const formCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{ type: 'TextBlock', text: 'Simple Form', size: 'Large', weight: 'Bolder' },
{ type: 'Input.Text', id: 'name', label: 'Name', isRequired: true }
],
actions: [{ type: 'Action.Submit', title: 'Submit', data: { task: 'simple_form' } }]
}
return {
task: {
type: 'continue',
value: {
title: 'Simple Form',
height: 'small',
width: 'small',
card: CardFactory.adaptiveCard(formCard)
}
}
}
})
.onSubmit('simple_form', async (context, state, request) => {
const name = typeof request.data?.name === 'string' ? request.data.name : 'Unknown'
await context.sendActivity(`Hi ${name}!`)
return { task: { type: 'message', value: 'Form submitted' } }
})
Chain task modules by returning a continue response from a submit handler instead of a message. Teams replaces the current dialog with the new card.
Use fetch to display the dialog and submit to process the submitted data. Both match on the verb carried in the adaptive card's data:
from microsoft_teams.api.models import TaskModuleRequest, TaskModuleResponse
@teams.task_modules.fetch("simple_form")
async def on_simple_form_fetch(context, state, request: TaskModuleRequest) -> TaskModuleResponse:
card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "text": "Simple Form", "size": "Large", "weight": "Bolder"},
{"type": "Input.Text", "id": "name", "label": "Name", "isRequired": True},
],
"actions": [{"type": "Action.Submit", "title": "Submit", "data": {"verb": "simple_form"}}],
}
return TaskModuleResponse.model_validate(
{
"task": {
"type": "continue",
"value": {
"title": "Simple Form",
"height": "small",
"width": "small",
"card": {
"contentType": "application/vnd.microsoft.card.adaptive",
"content": card,
},
},
}
}
)
@teams.task_modules.submit("simple_form")
async def on_simple_form_submit(context, state, request: TaskModuleRequest) -> TaskModuleResponse:
data = request.data if isinstance(request.data, dict) else {}
name = data.get("name", "Unknown")
await context.send_activity(f"Hi {name}!")
return TaskModuleResponse.model_validate({"task": {"type": "message", "value": "Form submitted"}})
Chain task modules by returning a continue response from a submit handler instead of a message. Teams replaces the current dialog with the new card.
Meetings
Register handlers for Teams meeting lifecycle events. Each handler receives meeting details with the meeting ID, join URL, and other metadata.
Note
Meeting start and end events require the Resource-Specific Consent (RSC) permission OnlineMeeting.ReadBasic.Chat in your app manifest.
[TeamsMeetingStartRoute]
public async Task OnMeetingStartAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.Meetings.MeetingDetails meeting,
CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Meeting started: {meeting.Id}", cancellationToken: cancellationToken);
}
[TeamsMeetingEndRoute]
public async Task OnMeetingEndAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.Meetings.MeetingDetails meeting,
CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Meeting ended: {meeting.Id}", cancellationToken: cancellationToken);
}
[TeamsMeetingParticipantsJoinRoute]
public Task OnParticipantsJoinAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
MeetingParticipantsEventDetails details,
CancellationToken cancellationToken)
{
// details.Members contains the joining participants
return Task.CompletedTask;
}
[TeamsMeetingParticipantsLeaveRoute]
public Task OnParticipantsLeaveAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
MeetingParticipantsEventDetails details,
CancellationToken cancellationToken)
{
// details.Members contains the leaving participants
return Task.CompletedTask;
}
import { MeetingDetails } from '@microsoft/teams.api'
import { MeetingParticipantsEventDetails } from '@microsoft/agents-hosting-extensions-msteams'
teams.meetings
.onStart(async (context, state, details) => {
await context.sendActivity(`Meeting started: ${details.id}`)
})
.onEnd(async (context, state, details) => {
await context.sendActivity(`Meeting ended: ${details.id}`)
})
.onParticipantsJoin(async (context, state, details) => {
for (const member of details.members) {
console.log(`${member.user.name} joined (role: ${member.meeting.role})`)
}
})
.onParticipantsLeave(async (context, state, details) => {
// details.members contains the leaving participants
})
Meeting start and end handlers receive a MeetingDetails object. Participant join and leave handlers receive a MeetingParticipantsEventDetails object.
from microsoft_teams.api.models import MeetingDetails
from microsoft_agents.activity.teams import MeetingParticipantsEventDetails
@teams.meetings.start
async def on_meeting_start(context, state, meeting: MeetingDetails):
await context.send_activity(f"Meeting started: {meeting.id}")
@teams.meetings.end
async def on_meeting_end(context, state, meeting: MeetingDetails):
await context.send_activity(f"Meeting ended: {meeting.id}")
@teams.meetings.participants_join
async def on_participants_join(context, state, details: MeetingParticipantsEventDetails):
for member in details.members or []:
print(f"{member.user.name} joined (role: {member.meeting.role})")
@teams.meetings.participants_leave
async def on_participants_leave(context, state, details: MeetingParticipantsEventDetails):
# details.members contains the leaving participants
...
Messages
Handle Teams-specific message events beyond the standard message activity.
Message edit, delete, and undelete
[TeamsMessageEditRoute]
public async Task OnMessageEditAsync(ITeamsTurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Message was edited: {turnContext.Activity.Text}", cancellationToken: cancellationToken);
}
[TeamsMessageDeleteRoute]
public Task OnMessageDeleteAsync(ITeamsTurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
[TeamsMessageUndeleteRoute]
public Task OnMessageUndeleteAsync(ITeamsTurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
teams.messages
.onMessageEdit(async (context, state) => {
await context.sendActivity(`Message was edited: ${context.activity.text}`)
})
.onMessageDelete(async (context, state) => {
// The message has been soft-deleted
})
.onMessageUndelete(async (context, state) => {
// The deletion was undone
})
The edit, delete, and undelete handlers receive only the context and state:
@teams.messages.edit
async def on_message_edit(context, state):
await context.send_activity(f"Message was edited: {context.activity.text}")
@teams.messages.delete
async def on_message_delete(context, state):
# The message has been soft-deleted
...
@teams.messages.undelete
async def on_message_undelete(context, state):
# The deletion was undone
...
Read receipts
Read receipts are available in personal (1:1) chats only.
[TeamsReadReceiptRoute]
public async Task OnReadReceiptAsync(ITeamsTurnContext turnContext, ITurnState turnState, JsonElement data, CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync("Your message was read!", cancellationToken: cancellationToken);
}
teams.messages
.onReadReceipt(async (context, state, data) => {
console.log('Message was read:', data)
})
The read-receipt handler receives the raw event payload as a dict:
@teams.messages.read_receipt
async def on_read_receipt(context, state, data: dict):
await context.send_activity("Your message was read!")
Office 365 connector card actions
Handle HTTP POST actions from Office 365 connector cards posted to a channel.
[TeamsExecuteActionRoute]
public async Task OnO365ConnectorCardActionAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.O365.ConnectorCardActionQuery query,
CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Connector action received: {query.ActionId}", cancellationToken: cancellationToken);
}
teams.messages
.onO365ConnectorCardAction(async (context, state, query) => {
await context.sendActivity(`Connector action received: ${query.actionId}`)
})
The handler receives an O365ConnectorCardActionQuery:
from microsoft_teams.api.models.o365 import O365ConnectorCardActionQuery
@teams.messages.execute_action
async def on_o365_connector_card_action(context, state, query: O365ConnectorCardActionQuery):
await context.send_activity(f"Connector action received: {query.action_id}")
File consent
When your agent requests permission to upload a file to a user's OneDrive, Teams shows a consent card. Register handlers for the user accepting or declining.
[TeamsFileConsentAcceptRoute]
public async Task OnFileConsentAcceptedAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.FileConsentCardResponse response,
CancellationToken cancellationToken)
{
var uploadUrl = response.UploadInfo?.UploadUrl;
// Upload the file using HttpClient, then notify the user
await turnContext.SendActivityAsync("File uploaded successfully!", cancellationToken: cancellationToken);
}
[TeamsFileConsentDeclineRoute]
public async Task OnFileConsentDeclinedAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
Microsoft.Teams.Api.FileConsentCardResponse response,
CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync("File upload was declined.", cancellationToken: cancellationToken);
}
teams.fileConsent
.onAccept(async (context, state, response) => {
const uploadUrl = response.uploadInfo?.uploadUrl
// Upload the file using fetch/axios, then notify the user
await context.sendActivity('File uploaded successfully!')
})
.onDecline(async (context, state, response) => {
await context.sendActivity('File upload was declined.')
})
Register accept and decline handlers. Each receives a FileConsentCardResponse; the upload URL is on file_consent.upload_info:
from microsoft_teams.api.models import FileConsentCardResponse
@teams.file_consent.accept
async def on_file_consent_accepted(context, state, file_consent: FileConsentCardResponse):
upload_url = file_consent.upload_info.upload_url if file_consent.upload_info else None
# Upload the file using an HTTP client, then notify the user
await context.send_activity("File uploaded successfully!")
@teams.file_consent.decline
async def on_file_consent_declined(context, state, file_consent: FileConsentCardResponse):
await context.send_activity("File upload was declined.")
Agent configuration
The configuration feature opens a settings page when users select the gear icon in the agent's profile card in Teams.
[TeamsConfigFetchRoute]
public Task<Microsoft.Teams.Api.Config.ConfigResponse> OnConfigFetchAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
object configData,
CancellationToken cancellationToken)
{
const string cardJson = """
{
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{ "type": "TextBlock", "text": "Agent Configuration", "weight": "bolder" },
{ "type": "Input.Text", "id": "setting", "label": "Setting", "placeholder": "Enter a value" }
],
"actions": [{ "type": "Action.Submit", "title": "Save" }]
}
""";
var response = new Microsoft.Teams.Api.Config.ConfigTaskResponse(
new Microsoft.Teams.Api.TaskModules.ContinueTask(
new Microsoft.Teams.Api.TaskModules.TaskInfo
{
Title = "Configure Agent",
Height = new Microsoft.Teams.Common.Union<int, Microsoft.Teams.Api.TaskModules.Size>(300),
Width = new Microsoft.Teams.Common.Union<int, Microsoft.Teams.Api.TaskModules.Size>(400),
Card = new Microsoft.Agents.Core.Models.Attachment
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(cardJson)
}
}));
return Task.FromResult<Microsoft.Teams.Api.Config.ConfigResponse>(response);
}
[TeamsConfigSubmitRoute]
public Task<Microsoft.Teams.Api.Config.ConfigResponse> OnConfigSubmitAsync(
ITeamsTurnContext turnContext,
ITurnState turnState,
object configData,
CancellationToken cancellationToken)
{
var response = new Microsoft.Teams.Api.Config.ConfigTaskResponse(
new Microsoft.Teams.Api.TaskModules.MessageTask("Configuration saved!"));
return Task.FromResult<Microsoft.Teams.Api.Config.ConfigResponse>(response);
}
teams.configuration
.onConfigFetch(async (context, state, configData) => {
return {
cacheInfo: { cacheType: 'no-cache' },
responseType: 'task',
config: {
task: {
type: 'continue',
value: {
title: 'Configure Agent',
height: 300,
width: 400,
card: {
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
type: 'AdaptiveCard',
version: '1.4',
body: [
{ type: 'TextBlock', text: 'Agent Configuration', weight: 'bolder' },
{ type: 'Input.Text', id: 'setting', label: 'Setting', placeholder: 'Enter a value' }
],
actions: [{ type: 'Action.Submit', title: 'Save' }]
}
}
}
}
}
}
})
.onConfigSubmit(async (context, state, configData) => {
return {
responseType: 'task',
config: {
task: {
type: 'message',
value: 'Configuration saved!'
}
}
}
})
The fetch and submit handlers receive the raw invoke value as config_data and return a ConfigResponse:
from microsoft_teams.api.models.config import ConfigResponse
@teams.config.fetch
async def on_config_fetch(context, state, config_data) -> ConfigResponse:
card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "text": "Agent Configuration", "weight": "bolder"},
{"type": "Input.Text", "id": "setting", "label": "Setting", "placeholder": "Enter a value"},
],
"actions": [{"type": "Action.Submit", "title": "Save"}],
}
return ConfigResponse.model_validate(
{
"config": {
"type": "continue",
"value": {
"title": "Configure Agent",
"height": 300,
"width": 400,
"card": {
"contentType": "application/vnd.microsoft.card.adaptive",
"content": card,
},
},
}
}
)
@teams.config.submit
async def on_config_submit(context, state, config_data) -> ConfigResponse:
return ConfigResponse.model_validate(
{"config": {"type": "message", "value": "Configuration saved!"}}
)
Channels
Handle channel lifecycle events within a team.
[TeamsChannelCreatedRoute]
public async Task OnChannelCreatedAsync(
ITeamsTurnContext turnContext, ITurnState turnState,
Microsoft.Teams.Api.Channel channel, CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Channel created: {channel.Name}", cancellationToken: cancellationToken);
}
[TeamsChannelRenamedRoute]
public async Task OnChannelRenamedAsync(
ITeamsTurnContext turnContext, ITurnState turnState,
Microsoft.Teams.Api.Channel channel, CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Channel renamed to: {channel.Name}", cancellationToken: cancellationToken);
}
[TeamsChannelDeletedRoute]
public Task OnChannelDeletedAsync(
ITeamsTurnContext turnContext, ITurnState turnState,
Microsoft.Teams.Api.Channel channel, CancellationToken cancellationToken)
=> Task.CompletedTask;
Available route attributes:
| Attribute | Event |
|---|---|
TeamsChannelUpdateRoute |
Any channel event |
TeamsChannelCreatedRoute |
Channel created |
TeamsChannelDeletedRoute |
Channel deleted |
TeamsChannelRenamedRoute |
Channel renamed |
TeamsChannelSharedRoute |
Channel shared with another team |
TeamsChannelUnsharedRoute |
Channel unshared |
TeamsChannelRestoredRoute |
Channel restored |
TeamsChannelMemberAddedRoute |
Member added to channel |
TeamsChannelMemberRemovedRoute |
Member removed from channel |
teams.channels
.onCreated(async (context, state, channel) => {
await context.sendActivity(`Channel created: ${channel.name}`)
})
.onRenamed(async (context, state, channel) => {
await context.sendActivity(`Channel renamed to: ${channel.name}`)
})
.onDeleted(async (context, state, channel) => {
console.log(`Channel deleted: ${channel.id}`)
})
.onRestored(async (context, state, channel) => {
console.log(`Channel restored: ${channel.name}`)
})
.onShared(async (context, state, channel) => {
console.log(`Channel shared: ${channel.name}`)
})
.onUnshared(async (context, state, channel) => {
console.log(`Channel unshared: ${channel.name}`)
})
.onMemberAdded(async (context, state, channel) => {
console.log('Member added to channel')
})
.onMemberRemoved(async (context, state, channel) => {
console.log('Member removed from channel')
})
To handle any channel event with a single handler, use onChannelEventReceived.
Each channel handler receives a ChannelData object. The affected channel is on data.channel.
from microsoft_teams.api.models import ChannelData
@teams.channels.created
async def on_channel_created(context, state, data: ChannelData):
await context.send_activity(f"Channel created: {data.channel.name}")
@teams.channels.renamed
async def on_channel_renamed(context, state, data: ChannelData):
await context.send_activity(f"Channel renamed to: {data.channel.name}")
@teams.channels.deleted
async def on_channel_deleted(context, state, data: ChannelData):
print(f"Channel deleted: {data.channel.id}")
Other channel events are available as restored, shared, unshared, members_added, and members_removed. To handle any channel event with a single handler, use @teams.channels.event.
Team events
Handle team-level lifecycle events such as archiving or renaming.
[TeamsTeamArchivedRoute]
public async Task OnTeamArchivedAsync(
ITeamsTurnContext turnContext, ITurnState turnState,
Microsoft.Teams.Api.Team team, CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Team archived: {team.Name}", cancellationToken: cancellationToken);
}
[TeamsTeamRenamedRoute]
public async Task OnTeamRenamedAsync(
ITeamsTurnContext turnContext, ITurnState turnState,
Microsoft.Teams.Api.Team team, CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync($"Team renamed to: {team.Name}", cancellationToken: cancellationToken);
}
[TeamsTeamUnarchivedRoute]
public Task OnTeamUnarchivedAsync(
ITeamsTurnContext turnContext, ITurnState turnState,
Microsoft.Teams.Api.Team team, CancellationToken cancellationToken)
=> Task.CompletedTask;
[TeamsTeamDeletedRoute]
public Task OnTeamDeletedAsync(
ITeamsTurnContext turnContext, ITurnState turnState,
Microsoft.Teams.Api.Team team, CancellationToken cancellationToken)
=> Task.CompletedTask;
Available route attributes:
| Attribute | Event |
|---|---|
TeamsTeamUpdateRoute |
Any team event |
TeamsTeamArchivedRoute |
Team archived |
TeamsTeamUnarchivedRoute |
Team unarchived |
TeamsTeamRenamedRoute |
Team renamed |
TeamsTeamRestoredRoute |
Team restored |
TeamsTeamDeletedRoute |
Team soft-deleted |
TeamsTeamHardDeletedRoute |
Team permanently deleted |
teams.teams
.onArchived(async (context, state, team) => {
await context.sendActivity(`Team archived: ${team.name}`)
})
.onUnarchived(async (context, state, team) => {
console.log(`Team unarchived: ${team.name}`)
})
.onRenamed(async (context, state, team) => {
await context.sendActivity(`Team renamed to: ${team.name}`)
})
.onRestored(async (context, state, team) => {
console.log(`Team restored: ${team.name}`)
})
.onDeleted(async (context, state, team) => {
console.log(`Team deleted: ${team.id}`)
})
.onHardDeleted(async (context, state, team) => {
console.log(`Team hard deleted: ${team.id}`)
})
To handle any team event with a single handler, use onTeamEventReceived.
Each team handler receives a ChannelData; the affected team is on data.team:
from microsoft_teams.api.models import ChannelData
@teams.teams.archived
async def on_team_archived(context, state, data: ChannelData):
await context.send_activity(f"Team archived: {data.team.name}")
@teams.teams.renamed
async def on_team_renamed(context, state, data: ChannelData):
await context.send_activity(f"Team renamed to: {data.team.name}")
@teams.teams.deleted
async def on_team_deleted(context, state, data: ChannelData):
print(f"Team deleted: {data.team.id}")
Other team events are available as unarchived, restored, and hard_deleted. To handle any team event with a single handler, use @teams.teams.event.
Roster, team, and meeting lookups
From within a handler, you can look up Teams roster data, team details, and meeting information. These lookups require registering the Teams extension so the Teams API client is available on the turn context. In C#, JavaScript, and Python, you call methods on the Teams API client.
Get members
var api = TeamsExtension.GetTeamsClient(turnContext);
// Get a single member
var member = await api.Conversations.Members.GetByIdAsync(
turnContext.Activity.Conversation.Id,
turnContext.Activity.From.Id,
cancellationToken);
// Page through all members
string? continuationToken = null;
do
{
var page = await api.Conversations.Members.GetPagedAsync(turnContext.Activity.Conversation.Id, 100, continuationToken!, cancellationToken);
continuationToken = page.ContinuationToken;
foreach (var teamMember in page.Members)
{
// Process each member
}
}
while (continuationToken != null);
// Query a specific team's roster
var teamId = turnContext.Activity.TeamsGetTeamInfo()?.Id;
var teamPage = await api.Conversations.Members.GetPagedAsync(teamId!, 100, null, cancellationToken);
import { teamsGetTeamInfo } from '@microsoft/agents-hosting-extensions-msteams'
const client = teamsExt.getTeamsClient(context)
const conversationId = context.activity.conversation.id
// Get a single member
const member = await client.conversations.getMemberById(conversationId, context.activity.from.id)
console.log(`Name: ${member.name}`)
// Get all members at once
const allMembers = await client.conversations.getMembers(conversationId)
// Page through members
let continuationToken
do {
const page = await client.conversations.getPagedMembers(conversationId, 100, continuationToken)
continuationToken = page.continuationToken
for (const member of page.members) {
console.log(member.name)
}
} while (continuationToken)
// Query a specific team's roster
const teamId = teamsGetTeamInfo(context.activity)?.id
const teamMembers = await client.conversations.getMembers(teamId)
Note
Inside a Teams extension route handler, context is a TeamsTurnContext, so you can use the context.client shorthand instead of teamsExt.getTeamsClient(context).
Get the Teams API client from the handler context, and then call the conversation member operations. The members(conversation_id) method returns an operations object with get, get_paged, and get_all methods:
api = teams.get_teams_api_client(context)
conversation_id = context.activity.conversation.id
# Get a single member
member = await api.conversations.members(conversation_id).get(context.activity.from_property.id)
# Get all members at once
all_members = await api.conversations.members(conversation_id).get_all()
# Page through members
result = await api.conversations.members(conversation_id).get_paged(page_size=100)
for member in result.members:
print(member.name)
continuation_token = result.continuation_token
# Query a specific team's roster
team_id = context.activity.get_team_info().id
team_members = await api.conversations.members(team_id).get_all()
Note
Within a Teams route handler, you can also use the shorthand context.api_client instead of teams.get_teams_api_client(context).
Team details and channels
var api = TeamsExtension.GetTeamsClient(turnContext);
var teamId = turnContext.Activity.TeamsGetTeamInfo()?.Id;
// Get the Team object (name, aadGroupId, etc.)
var team = await api.Teams.GetByIdAsync(teamId!, cancellationToken);
// Get all channels in the team
var channels = await api.Teams.GetConversationsAsync(teamId!, cancellationToken);
foreach (var channel in channels)
{
Console.WriteLine($"{channel.Name}: {channel.Id}");
}
import { teamsGetTeamInfo } from '@microsoft/agents-hosting-extensions-msteams'
const client = teamsExt.getTeamsClient(context)
const teamId = teamsGetTeamInfo(context.activity)?.id
// Get the Team object (name, aadGroupId, etc.)
const team = await client.teams.getById(teamId)
// Get all channels in the team
const channels = await client.teams.getConversations(teamId)
for (const channel of channels) {
console.log(`${channel.name}: ${channel.id}`)
}
api = teams.get_teams_api_client(context)
team_id = context.activity.get_team_info().id
# Get the Team object (name, aad_group_id, etc.)
team = await api.teams.get_by_id(team_id)
# Get all channels in the team
channels = await api.teams.get_conversations(team_id)
for channel in channels:
print(f"{channel.name}: {channel.id}")
Meeting info
var api = TeamsExtension.GetTeamsClient(turnContext);
// Get details for the current meeting
var meetingId = turnContext.Activity.TeamsGetMeetingInfo()?.Id;
var meeting = await api.Meetings.GetByIdAsync(meetingId!, cancellationToken);
// Get a specific participant's role and in-meeting status
var participant = await api.Meetings.GetParticipantAsync(
meetingId!,
turnContext.Activity.From.AadObjectId,
turnContext.Activity.Conversation.TenantId,
cancellationToken);
import { teamsGetMeetingInfo } from '@microsoft/agents-hosting-extensions-msteams'
const client = teamsExt.getTeamsClient(context)
// Get details for the current meeting
const meetingId = teamsGetMeetingInfo(context.activity)?.id
const meeting = await client.meetings.getById(meetingId)
// Get a specific participant's role and in-meeting status
const participant = await client.meetings.getParticipant(
meetingId,
context.activity.from?.aadObjectId,
context.activity.conversation?.tenantId
)
api = teams.get_teams_api_client(context)
# Get details for the current meeting
meeting_id = context.activity.get_meeting_info().id
meeting = await api.meetings.get_by_id(meeting_id)
# Get a specific participant's role and in-meeting status
participant = await api.meetings.get_participant(
meeting_id,
context.activity.from_property.aad_object_id,
context.activity.conversation.tenant_id,
)
Teams SDK API client
The Teams extension registers an API client on the turn context for any activity arriving on the Teams channel.
Use TeamsExtension.GetTeamsClient(turnContext) to retrieve it:
var client = TeamsExtension.GetTeamsClient(turnContext);
Use the client directly for any Teams API operation, including roster, team, channel, and meeting lookups.
Note
The client is only available for activities on the Teams channel, where Activity.ChannelId == Channels.Msteams.
Call getTeamsClient(context) on the TeamsAgentExtension instance you registered during setup:
const client = teamsExt.getTeamsClient(context)
Inside a Teams extension route handler, context is a TeamsTurnContext, so the context.client shorthand returns the same client.
Use the client directly for any Teams API operation, including roster, team, channel, and meeting lookups.
Note
getTeamsClient() throws an error if the Teams API client isn't available on the turn context. The client is only set for activities where activity.channelId === 'msteams'.
Use teams.get_teams_api_client(context) to retrieve it. Within a Teams route handler, the shorthand context.api_client returns the same client:
client = teams.get_teams_api_client(context)
# or, inside a handler:
client = context.api_client
Use the client directly for any Teams API operation, including roster, team, channel, and meeting lookups.
Note
The client is only available for activities on the Teams channel.