Problem
Approval prompts (plugin approval, exec approval) generate interactive button descriptors via the delivery pipeline. The pipeline checks whether the channel's outbound adapter implements sendPayload and whether the payload contains interactive content. If both conditions are met, it calls sendPayload to render rich approval UX. Otherwise it falls back to sendText.
The Microsoft Teams channel plugin does not implement sendPayload in its outbound adapter. The adapter created via createRuntimeOutboundDelegates only exposes sendText, sendMedia, and sendPoll. As a result, all approval prompts on Teams render as plain text with raw /approve <id> <decision> commands that the user must type manually.
Current behavior
- Gateway generates an approval prompt with
interactive.blocks[0].buttons containing { label, value, style } entries (where value is the /approve <id> <decision> command string).
- Delivery pipeline evaluates:
if (handler.sendPayload && hasReplyPayloadContent({interactive, channelData})).
- Teams outbound adapter has no
sendPayload — condition is false.
- Falls through to
sendText — user sees plain text like "Reply /approve abc123 yes to approve".
Expected behavior
Teams should render approval prompts as Adaptive Cards with clickable action buttons. Clicking a button should send the /approve command string as a user message, triggering the existing command handler. No changes to the approval pipeline itself should be needed.
Proposed solution
Add sendPayload to the Teams outbound adapter. The implementation would:
- Map
interactive.blocks[0].buttons to an Adaptive Card with Action.Submit buttons.
- Each button uses
msteams: { type: "imBack" } so that clicking it sends the button's value string as a regular chat message. This reuses the existing /approve command handler with zero changes to approval logic.
- The card body includes the approval title and description for a clean, non-technical UX.
The Teams runtime already has sendAdaptiveCardMSTeams (used by the message tool's { action: "send", card: {...} } path), so the card-sending infrastructure is in place. This is primarily about wiring the approval interactive descriptors into that existing capability.
Rough shape of the sendPayload addition to the Teams outbound adapter:
// Added to the outbound config alongside sendText/sendMedia/sendPoll
sendPayload: async (ctx) => {
const buttons = ctx.payload?.interactive?.blocks?.[0]?.buttons;
if (!buttons?.length) {
const { sendMessageMSTeams } = await loadMSTeamsChannelRuntime();
return sendMessageMSTeams({ cfg: ctx.cfg, to: ctx.to, text: ctx.text });
}
const raw = ctx.text || "";
const titleMatch = raw.match(/Title:\s*(.+)/);
const descMatch = raw.match(/Description:\s*(.+)/);
const title = titleMatch ? titleMatch[1].trim() : "Approval required";
const desc = descMatch ? descMatch[1].trim() : "";
const card = {
type: "AdaptiveCard",
version: "1.5",
body: [
{ type: "TextBlock", text: title, weight: "bolder", size: "medium" },
...(desc ? [{ type: "TextBlock", text: desc, wrap: true, spacing: "small" }] : [])
],
actions: buttons.map(b => ({
type: "Action.Submit",
title: b.label,
style: b.style === "danger" ? "destructive" : "default",
data: { msteams: { type: "imBack", value: b.value } }
}))
};
const { sendAdaptiveCardMSTeams } = await loadMSTeamsChannelRuntime();
return sendAdaptiveCardMSTeams({ cfg: ctx.cfg, to: ctx.to, card });
}
Notes
- We have a working local prototype that patches the compiled gateway dist and confirms the flow end-to-end on Teams (plugin approvals render as cards, button clicks correctly trigger
/approve via imBack).
- The delivery pipeline already checks
handler.sendPayload && hasReplyPayloadContent({interactive, channelData}) — this is purely a channel adapter addition with zero changes to core approval logic.
- The same pattern could benefit other channel plugins (Feishu, Slack) that support rich interactive messages but currently only implement
sendText.
- Happy to contribute a PR if maintainers are interested in this approach.
Problem
Approval prompts (plugin approval, exec approval) generate interactive button descriptors via the delivery pipeline. The pipeline checks whether the channel's outbound adapter implements
sendPayloadand whether the payload contains interactive content. If both conditions are met, it callssendPayloadto render rich approval UX. Otherwise it falls back tosendText.The Microsoft Teams channel plugin does not implement
sendPayloadin its outbound adapter. The adapter created viacreateRuntimeOutboundDelegatesonly exposessendText,sendMedia, andsendPoll. As a result, all approval prompts on Teams render as plain text with raw/approve <id> <decision>commands that the user must type manually.Current behavior
interactive.blocks[0].buttonscontaining{ label, value, style }entries (wherevalueis the/approve <id> <decision>command string).if (handler.sendPayload && hasReplyPayloadContent({interactive, channelData})).sendPayload— condition is false.sendText— user sees plain text like "Reply /approve abc123 yes to approve".Expected behavior
Teams should render approval prompts as Adaptive Cards with clickable action buttons. Clicking a button should send the
/approvecommand string as a user message, triggering the existing command handler. No changes to the approval pipeline itself should be needed.Proposed solution
Add
sendPayloadto the Teams outbound adapter. The implementation would:interactive.blocks[0].buttonsto an Adaptive Card withAction.Submitbuttons.msteams: { type: "imBack" }so that clicking it sends the button'svaluestring as a regular chat message. This reuses the existing/approvecommand handler with zero changes to approval logic.The Teams runtime already has
sendAdaptiveCardMSTeams(used by the message tool's{ action: "send", card: {...} }path), so the card-sending infrastructure is in place. This is primarily about wiring the approval interactive descriptors into that existing capability.Rough shape of the
sendPayloadaddition to the Teams outbound adapter:Notes
/approvevia imBack).handler.sendPayload && hasReplyPayloadContent({interactive, channelData})— this is purely a channel adapter addition with zero changes to core approval logic.sendText.