Teams Features
Send mentions, adaptive cards, and suggested actions using TeamsActivity and TeamsActivityBuilder.
Overview
While CoreActivity handles basic Bot Service messaging, Microsoft Teams adds rich features — mentions, adaptive cards, channel metadata, quick-reply buttons — that bots frequently use.
TeamsActivity extends CoreActivity with strongly-typed Teams-specific properties. TeamsActivityBuilder provides a fluent API for constructing replies with mentions, cards, and suggested actions.
For the full specification, see specs/teams-activity.md.
Mentions
Mention a user in a reply by combining withText() (with <at>Name</at> markup) and addMention() (which creates the mention entity).
WARNING
addMention() does not modify the activity text — you must include the <at>Name</at> markup yourself. This is intentional: explicit is better than magic.
var sender = ctx.Activity.From!;
var reply = new TeamsActivityBuilder()
.WithConversationReference(ctx.Activity)
.WithText($"<at>{sender.Name}</at> said: {text}")
.AddMention(sender)
.Build();
await ctx.SendAsync(reply, ct);import { TeamsActivityBuilder } from 'botas-core'
const sender = ctx.activity.from
const reply = new TeamsActivityBuilder()
.withConversationReference(ctx.activity)
.withText(`<at>${sender.name}</at> said: ${text}`)
.addMention(sender)
.build()
await ctx.send(reply)from botas import TeamsActivityBuilder
sender = ctx.activity.from_account
reply = (
TeamsActivityBuilder()
.with_conversation_reference(ctx.activity)
.with_text(f"<at>{sender.name}</at> said: {text}")
.add_mention(sender)
.build()
)
await ctx.send(reply)Adaptive Cards
Send rich interactive cards using addAdaptiveCardAttachment() (appends) or withAdaptiveCardAttachment() (replaces all attachments with one card).
Both methods accept a JSON string or a pre-parsed object (to avoid double serialization), and wrap it in an attachment with contentType: "application/vnd.microsoft.card.adaptive".
We recommend using FluentCards to build Adaptive Cards with a fluent, strongly-typed API instead of raw JSON. FluentCards is available for all three languages: NuGet FluentCards, npm fluent-cards, and PyPI fluent-cards.
using FluentCards;
var card = AdaptiveCardBuilder.Create()
.WithVersion(AdaptiveCardVersion.V1_5)
.AddTextBlock(tb => tb
.WithText("Hello from TeamsSample!")
.WithSize(TextSize.Large)
.WithWeight(TextWeight.Bolder))
.AddTextBlock(tb => tb
.WithText("Click the button below to trigger an invoke action.")
.WithWrap(true))
.AddInputText(it => it
.WithId("userInput")
.WithPlaceholder("Type something here..."))
.AddAction(a => a
.Execute()
.WithTitle("Submit")
.WithVerb("submitAction")
.WithData("{\"action\":\"submit\"}"))
.Build();
var reply = new TeamsActivityBuilder()
.WithConversationReference(ctx.Activity)
.WithAdaptiveCardAttachment(card.ToJsonElement())
.Build();
await ctx.SendAsync(reply, ct);import { AdaptiveCardBuilder, TextSize, TextWeight, toObject } from 'fluent-cards'
const card = AdaptiveCardBuilder.create()
.withVersion('1.5')
.addTextBlock(tb => tb
.withText('Hello from TeamsSample!')
.withSize(TextSize.Large)
.withWeight(TextWeight.Bolder))
.addTextBlock(tb => tb
.withText('Click the button below to trigger an invoke action.')
.withWrap(true))
.addInputText(it => it
.withId('userInput')
.withPlaceholder('Type something here...'))
.addAction(a => a
.execute()
.withTitle('Submit')
.withVerb('submitAction')
.withData({ action: 'submit' }))
.build()
const reply = new TeamsActivityBuilder()
.withConversationReference(ctx.activity)
.withAdaptiveCardAttachment(toObject(card))
.build()
await ctx.send(reply)from botas import TeamsActivityBuilder
from fluent_cards import AdaptiveCardBuilder, TextSize, TextWeight, to_dict
card = (
AdaptiveCardBuilder.create()
.with_version("1.5")
.add_text_block(
lambda tb: tb.with_text("Hello from TeamsSample!")
.with_size(TextSize.Large)
.with_weight(TextWeight.Bolder)
)
.add_text_block(
lambda tb: tb.with_text("Click the button below to trigger an invoke action.")
.with_wrap(True)
)
.add_input_text(lambda it: it.with_id("userInput").with_placeholder("Type something here..."))
.add_action(
lambda a: a.execute().with_title("Submit").with_verb("submitAction").with_data({"action": "submit"})
)
.build()
)
reply = (
TeamsActivityBuilder()
.with_conversation_reference(ctx.activity)
.with_adaptive_card_attachment(to_dict(card))
.build()
)
await ctx.send(reply)Invoke Handling (Action.Execute)
When a user clicks an Action.Execute button on an Adaptive Card, Teams sends an invoke activity with name: "adaptiveCard/action". The invoke payload contains the verb and data you specified when building the card.
Register an invoke handler to process the action and return a response card:
using FluentCards;
app.OnInvoke("adaptiveCard/action", async (ctx, ct) =>
{
var valueJson = ctx.Activity.Value?.ToString() ?? "{}";
var valueDoc = System.Text.Json.JsonDocument.Parse(valueJson);
var verb = valueDoc.RootElement.TryGetProperty("action", out var actionProp)
? actionProp.TryGetProperty("verb", out var verbProp) ? verbProp.GetString() ?? "unknown" : "unknown"
: "unknown";
var responseCard = AdaptiveCardBuilder.Create()
.WithVersion(AdaptiveCardVersion.V1_5)
.AddTextBlock(tb => tb
.WithText("✅ Action received!")
.WithSize(TextSize.Large)
.WithWeight(TextWeight.Bolder)
.WithColor(TextColor.Good))
.AddTextBlock(tb => tb
.WithText($"Verb: {verb}")
.WithWrap(true))
.Build();
return new InvokeResponse
{
Status = 200,
Body = new
{
statusCode = 200,
type = "application/vnd.microsoft.card.adaptive",
value = responseCard.ToJsonElement()
}
};
});import { AdaptiveCardBuilder, TextSize, TextWeight, TextColor, toJson, toObject } from 'fluent-cards'
app.onInvoke('adaptiveCard/action', async (ctx) => {
const value = ctx.activity.value as any
const verb = value?.action?.verb ?? 'unknown'
const card = AdaptiveCardBuilder.create()
.withVersion('1.5')
.addTextBlock(tb => tb
.withText('✅ Action received!')
.withSize(TextSize.Large)
.withWeight(TextWeight.Bolder)
.withColor(TextColor.Good))
.addTextBlock(tb => tb
.withText(`Verb: ${verb}`)
.withWrap(true))
.build()
return {
status: 200,
body: {
statusCode: 200,
type: 'application/vnd.microsoft.card.adaptive',
value: toObject(card)
}
}
})from botas import InvokeResponse
from fluent_cards import AdaptiveCardBuilder, TextSize, TextWeight, TextColor, to_dict
@app.on_invoke("adaptiveCard/action")
async def on_card_action(ctx):
value = ctx.activity.value or {}
action_info = value.get("action", {})
verb = action_info.get("verb", "unknown")
response_card = (
AdaptiveCardBuilder.create()
.with_version("1.5")
.add_text_block(
lambda tb: tb.with_text("✅ Action received!")
.with_size(TextSize.Large)
.with_weight(TextWeight.Bolder)
.with_color(TextColor.Good)
)
.add_text_block(lambda tb: tb.with_text(f"Verb: {verb}").with_wrap(True))
.build()
)
return InvokeResponse(
status=200,
body={
"statusCode": 200,
"type": "application/vnd.microsoft.card.adaptive",
"value": to_dict(response_card),
},
)Invoke flow
Card (Action.Execute with verb + data)
→ User clicks button in Teams
→ Teams sends invoke activity (name="adaptiveCard/action")
→ Bot invoke handler reads verb + data from activity.value.action
→ Handler returns an Adaptive Card responseSuggested Actions
Offer quick-reply buttons to the user with withSuggestedActions(). Each button is a CardAction with a type (typically "imBack"), a display title, and a value sent back when clicked.
var reply = new TeamsActivityBuilder()
.WithConversationReference(ctx.Activity)
.WithText("Pick an option:")
.WithSuggestedActions(new SuggestedActions
{
Actions =
[
new CardAction { Type = "imBack", Title = "Option A", Value = "a" },
new CardAction { Type = "imBack", Title = "Option B", Value = "b" },
]
})
.Build();
await ctx.SendAsync(reply, ct);const reply = new TeamsActivityBuilder()
.withConversationReference(ctx.activity)
.withText('Pick an option:')
.withSuggestedActions({
actions: [
{ type: 'imBack', title: 'Option A', value: 'a' },
{ type: 'imBack', title: 'Option B', value: 'b' },
]
})
.build()
await ctx.send(reply)from botas import TeamsActivityBuilder
from botas.suggested_actions import SuggestedActions, CardAction
reply = (
TeamsActivityBuilder()
.with_conversation_reference(ctx.activity)
.with_text("Pick an option:")
.with_suggested_actions(SuggestedActions(
actions=[
CardAction(type="imBack", title="Option A", value="a"),
CardAction(type="imBack", title="Option B", value="b"),
]
))
.build()
)
await ctx.send(reply)TeamsActivity — reading Teams data
Use TeamsActivity.fromActivity() to access Teams-specific metadata from an incoming activity:
var teamsActivity = TeamsActivity.FromActivity(ctx.Activity);
Console.WriteLine($"Tenant: {teamsActivity.ChannelData?.Tenant?.Id}");
Console.WriteLine($"Locale: {teamsActivity.Locale}");import { TeamsActivity } from 'botas-core'
const teamsActivity = TeamsActivity.fromActivity(ctx.activity)
console.log(`Tenant: ${teamsActivity.channelData?.tenant?.id}`)
console.log(`Locale: ${teamsActivity.locale}`)from botas import TeamsActivity
teams_activity = TeamsActivity.from_activity(ctx.activity)
print(f"Tenant: {teams_activity.channel_data.tenant.id}")
print(f"Locale: {teams_activity.locale}")TeamsChannelData
TeamsActivity.channelData (or channel_data in Python) provides typed access to Teams metadata:
| Property | Type | Description |
|---|---|---|
tenant | TenantInfo | Azure AD tenant (id) |
channel | ChannelInfo | Teams channel (id, name) |
team | TeamInfo | Teams team (id, name, aadGroupId) |
meeting | MeetingInfo | Meeting context (id) |
notification | NotificationInfo | Alert settings (alert) |
Unknown fields in channelData are preserved as extension data, so new Teams features won't break existing code.
Running the TeamsSample
Each language has a complete sample in the repository using FluentCards for Adaptive Card construction. The sample responds to three commands:
| Command | Response |
|---|---|
cards | Sends an Adaptive Card with an Action.Execute button |
actions | Sends Suggested Actions (quick-reply buttons) |
| (anything else) | Echoes back with an @mention of the sender |
When you click the Submit button on the Adaptive Card, Teams sends an invoke activity. The bot processes the verb and data, then responds with a confirmation card — demonstrating the full card → invoke → response round-trip.
cd dotnet && dotnet run --project samples/TeamsSamplecd node && npx tsx samples/teams-sample/index.tscd python/samples/teams-sample && python main.pyTyping Indicators
Show the user that your bot is working on a reply. Typing activities are part of the core Bot Service protocol (not Teams-specific), but they are especially useful in Teams where users expect real-time feedback.
Sending a typing indicator
Use sendTyping() / SendTypingAsync() / send_typing() on TurnContext when processing takes a few seconds:
app.On("message", async (ctx, ct) =>
{
await ctx.SendTypingAsync(ct);
await Task.Delay(3000, ct); // simulate long-running work
await ctx.SendAsync("Processing complete!", ct);
});app.on('message', async (ctx) => {
await ctx.sendTyping()
await new Promise(resolve => setTimeout(resolve, 3000))
await ctx.send('Processing complete!')
})@app.on("message")
async def on_message(ctx):
await ctx.send_typing()
await asyncio.sleep(3) # simulate long-running work
await ctx.send("Processing complete!")TIP
- Reserve typing indicators for operations that genuinely take 1–3+ seconds.
- You can send typing multiple times in a single turn if the operation has distinct phases.
Next steps
- Full TeamsActivity spec — complete type definitions and design decisions
- Middleware — add RemoveMentionMiddleware to strip bot @mentions from incoming text
- Language guides — deeper coverage of each implementation