ui.adaptive-cards-ts
purpose
Adaptive Cards construction, sending as attachments, handling Action.Submit via card.action handlers, and Teams-specific constraints.
rules
- Always set
"type": "AdaptiveCard"and"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"at the card root; Teams requires"version": "1.5"or lower (1.6+ features are silently ignored). adaptivecards.io/designer - Wrap every card in a
CardFactory.adaptiveCard(cardJson)attachment -- never send raw JSON as message text. The attachmentcontentTypeis"application/vnd.microsoft.card.adaptive". learn.microsoft.com -- Cards reference - Register card action handlers with
app.adaptiveCards.actionSubmit(actionVerb, handler)whereactionVerbmatches thedata.verb(ordata.action) string you embed in the card'sAction.Submit. The handler receives(ctx, state, data)wheredatais the mergedactivity.valueobject. github.com/microsoft/teams-ai - Every
Action.Submitmust include adataobject with a routing identifier (e.g.,{ "verb": "approve", ...inputValues }). Without it, Teams merges only input field values intoactivity.valueand there is no way to distinguish which button was pressed. - Input element
idvalues become keys inactivity.value. For example,Input.Textwith"id": "comment"yieldsactivity.value.comment. Keep IDs short and unique within a card. - To update an existing message (e.g., replacing a card after action), return an updated card from the handler or call
await ctx.updateActivity({ ...activity, attachments: [newCard] }). To send a new message instead, callawait ctx.sendActivity(MessageFactory.attachment(newCard)). - Teams Adaptive Cards do NOT support
Action.Http,Action.ToggleVisibility(partial -- works for simple show/hide but not nested),backgroundImageon mobile,Mediaelement playback, orhostConfigoverrides. Always test on desktop + mobile. learn.microsoft.com -- Cards reference - Card payload size must be under 28 KB (after JSON serialization). Larger cards are rejected silently. learn.microsoft.com -- Card size limit
- For
Action.Execute(Universal Actions), the handler isapp.adaptiveCards.actionExecute(verb, handler)and the return must be an Adaptive Card (used for automatic card refresh / user-specific views). PreferAction.Submitfor standard form flows; useAction.Executeonly when you need per-user card refresh. learn.microsoft.com -- Universal Actions - Always validate
activity.valueserver-side -- clients can tamper with the JSON payload. Use a schema validator (e.g., zod) before trusting input data.
patterns
Confirm / Cancel card with action routing
import { App, TurnState } from "@microsoft/teams-ai";
import { CardFactory, MessageFactory } from "botbuilder";
// -- Card JSON ---------------------------------------------------------------
const confirmCard = {
type: "AdaptiveCard",
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.5",
body: [
{
type: "TextBlock",
text: "Delete this item?",
weight: "Bolder",
size: "Medium",
},
{
type: "TextBlock",
text: "This action cannot be undone.",
wrap: true,
isSubtle: true,
},
],
actions: [
{
type: "Action.Submit",
title: "Confirm",
style: "destructive",
data: { verb: "deleteConfirm", itemId: "abc-123" },
},
{
type: "Action.Submit",
title: "Cancel",
data: { verb: "deleteCancel", itemId: "abc-123" },
},
],
};
// -- Handlers ----------------------------------------------------------------
export function registerConfirmHandlers(app: App<TurnState>): void {
app.adaptiveCards.actionSubmit("deleteConfirm", async (ctx, _state, data) => {
const itemId = (data as Record<string, string>).itemId;
// ... perform deletion logic ...
// Replace the card with a confirmation message
const doneCard = CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.5",
body: [{ type: "TextBlock", text: `Item ${itemId} deleted.` }],
});
await ctx.updateActivity({
type: "message",
id: ctx.activity.replyToId,
attachments: [doneCard],
});
return undefined;
});
app.adaptiveCards.actionSubmit("deleteCancel", async (ctx) => {
const cancelCard = CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.5",
body: [{ type: "TextBlock", text: "Deletion cancelled." }],
});
await ctx.updateActivity({
type: "message",
id: ctx.activity.replyToId,
attachments: [cancelCard],
});
return undefined;
});
}Form submission with input extraction
import { App, TurnState } from "@microsoft/teams-ai";
import { CardFactory, MessageFactory } from "botbuilder";
const feedbackFormCard = {
type: "AdaptiveCard",
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.5",
body: [
{ type: "TextBlock", text: "Submit Feedback", weight: "Bolder", size: "Large" },
{
type: "Input.Text",
id: "userName",
label: "Your name",
isRequired: true,
errorMessage: "Name is required",
},
{
type: "Input.ChoiceSet",
id: "rating",
label: "Rating",
style: "compact",
value: "3",
choices: [
{ title: "1 - Poor", value: "1" },
{ title: "2 - Fair", value: "2" },
{ title: "3 - Good", value: "3" },
{ title: "4 - Great", value: "4" },
{ title: "5 - Excellent", value: "5" },
],
},
{
type: "Input.Text",
id: "comments",
label: "Comments",
isMultiline: true,
placeholder: "Tell us more...",
},
{
type: "Input.Toggle",
id: "followUp",
title: "Contact me for follow-up",
value: "false",
valueOn: "true",
valueOff: "false",
},
],
actions: [
{
type: "Action.Submit",
title: "Submit",
data: { verb: "submitFeedback" },
},
],
};
interface FeedbackData {
verb: string;
userName: string;
rating: string;
comments?: string;
followUp: string;
}
export function registerFeedbackHandlers(app: App<TurnState>): void {
app.adaptiveCards.actionSubmit("submitFeedback", async (ctx, _state, data) => {
const fd = data as FeedbackData;
// Input values are merged into activity.value alongside the data object
const msg = `Thanks ${fd.userName}! Rating: ${fd.rating}/5.`;
await ctx.sendActivity(MessageFactory.text(msg));
return undefined;
});
}
// -- Sending the card --------------------------------------------------------
// Inside any handler or proactive flow:
// await ctx.sendActivity(MessageFactory.attachment(
// CardFactory.adaptiveCard(feedbackFormCard)
// ));Dynamic choices via Action.Execute refresh
import { App, TurnState } from "@microsoft/teams-ai";
import { CardFactory } from "botbuilder";
// Card with Action.Execute for per-user refresh (Universal Actions)
function buildTicketCard(tickets: { id: string; title: string }[]): object {
return {
type: "AdaptiveCard",
version: "1.4",
refresh: {
action: {
type: "Action.Execute",
title: "Refresh",
verb: "refreshTickets",
},
userIds: [], // empty = refresh for all users
},
body: [
{ type: "TextBlock", text: "Open Tickets", weight: "Bolder" },
{
type: "Input.ChoiceSet",
id: "selectedTicket",
label: "Pick a ticket",
choices: tickets.map((t) => ({ title: t.title, value: t.id })),
},
],
actions: [
{
type: "Action.Execute",
title: "Claim",
verb: "claimTicket",
data: {},
},
],
};
}
export function registerTicketHandlers(app: App<TurnState>): void {
// Action.Execute handler -- must return an Adaptive Card
app.adaptiveCards.actionExecute("refreshTickets", async (_ctx, _state) => {
const tickets = [
{ id: "T-1", title: "Login page broken" },
{ id: "T-2", title: "Report export fails" },
]; // replace with real DB call
return CardFactory.adaptiveCard(buildTicketCard(tickets));
});
app.adaptiveCards.actionExecute("claimTicket", async (ctx, _state, data) => {
const ticketId = (data as Record<string, string>).selectedTicket;
// ... assign ticket ...
return CardFactory.adaptiveCard({
type: "AdaptiveCard",
version: "1.4",
body: [{ type: "TextBlock", text: `Ticket ${ticketId} claimed by you.` }],
});
});
}pitfalls
- Missing
verbin data: IfAction.Submithas nodataobject (or no routing key), theactionSubmithandler cannot route by verb. Always include{ verb: "myAction" }in thedataproperty. - Input IDs collide with data keys: If an
Input.Texthasid: "verb", it overwrites thedata.verbrouting key when merged intoactivity.value. Use prefixes (e.g.,input_name) or avoid reserved keys. - Updating the wrong activity:
ctx.activity.replyToIdis the ID of the message containing the card. Use this forupdateActivity. Usingctx.activity.idtargets the invoke activity itself, not the card message. - Card version too high: Teams desktop/mobile silently drops elements from schema versions above what the client supports. Stick to version
"1.5"for broadest compatibility. Test version"1.6"features explicitly before shipping. Action.ExecutevsAction.Submit:Action.Executerequires the handler to return a card (for automatic replacement).Action.Submithandlers are fire-and-forget from the card's perspective. Mixing them up causes silent failures or empty card replacements.- Card not rendering: Forgetting
CardFactory.adaptiveCard()and instead passing raw JSON toattachmentsresults in a blank message. Always wrap withCardFactory. - ChoiceSet value types: All
Input.ChoiceSetvalues arrive as strings inactivity.value, even if they look numeric. Parse explicitly withparseInt()or a validation library. - 28 KB limit: Large dynamically generated cards (e.g., long lists) can exceed the Teams payload limit. Paginate or truncate before serializing.
references
- Adaptive Cards Schema Explorer
- Adaptive Cards Designer
- Teams: Cards and card actions
- Teams: Adaptive Card for bots
- Teams: Universal Actions for Adaptive Cards
- Teams AI SDK GitHub -- samples
- Teams: Format cards
- Teams: Card size limits
instructions
This expert covers building, sending, and handling Adaptive Cards in Microsoft Teams bots using the Teams AI SDK v2 (@microsoft/teams-ai) in TypeScript. Use it when you need to:
- Construct an Adaptive Card JSON payload (body elements, inputs, actions)
- Send a card as a bot attachment via
CardFactory.adaptiveCard()+MessageFactory.attachment() - Handle
Action.Submitbutton presses withapp.adaptiveCards.actionSubmit(verb, handler) - Handle
Action.Execute(Universal Actions) withapp.adaptiveCards.actionExecute(verb, handler) - Extract user input from
activity.value(merged input IDs + action data) - Update an existing card message vs. sending a new reply
- Avoid Teams-specific limitations (version caps, unsupported elements, size limits)
Pair with ui.dialogs-task-modules-ts.md for modal/dialog card flows and runtime.routing-handlers-ts.md for broader handler registration context.
research
Deep Research prompt:
"Write a micro expert on Adaptive Cards in Teams (TypeScript). Cover card anatomy, input elements, Action.Submit payloads, sending attachments, handling app.on('card.action'), extracting action identifiers from activity.value, updating messages vs sending new, and Teams-specific card limitations. Include 2-3 canonical card patterns (confirm/cancel, form submit, dynamic choices)."