Skip to content

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.

csharp
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);
typescript
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)
python
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.

csharp
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);
typescript
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)
python
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:

csharp
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()
        }
    };
});
typescript
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)
    }
  }
})
python
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 response

Suggested 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.

csharp
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);
typescript
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)
python
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:

csharp
var teamsActivity = TeamsActivity.FromActivity(ctx.Activity);
Console.WriteLine($"Tenant: {teamsActivity.ChannelData?.Tenant?.Id}");
Console.WriteLine($"Locale: {teamsActivity.Locale}");
typescript
import { TeamsActivity } from 'botas-core'

const teamsActivity = TeamsActivity.fromActivity(ctx.activity)
console.log(`Tenant: ${teamsActivity.channelData?.tenant?.id}`)
console.log(`Locale: ${teamsActivity.locale}`)
python
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:

PropertyTypeDescription
tenantTenantInfoAzure AD tenant (id)
channelChannelInfoTeams channel (id, name)
teamTeamInfoTeams team (id, name, aadGroupId)
meetingMeetingInfoMeeting context (id)
notificationNotificationInfoAlert 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:

CommandResponse
cardsSends an Adaptive Card with an Action.Execute button
actionsSends 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.

bash
cd dotnet && dotnet run --project samples/TeamsSample
bash
cd node && npx tsx samples/teams-sample/index.ts
bash
cd python/samples/teams-sample && python main.py

Typing 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:

csharp
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);
});
typescript
app.on('message', async (ctx) => {
  await ctx.sendTyping()
  await new Promise(resolve => setTimeout(resolve, 3000))
  await ctx.send('Processing complete!')
})
python
@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

BotAS — Multi-language Microsoft Teams bot library