Skip to content

Teams channel: implement sendPayload for interactive approval cards #64690

Description

@kwizzlek

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

  1. Gateway generates an approval prompt with interactive.blocks[0].buttons containing { label, value, style } entries (where value is the /approve <id> <decision> command string).
  2. Delivery pipeline evaluates: if (handler.sendPayload && hasReplyPayloadContent({interactive, channelData})).
  3. Teams outbound adapter has no sendPayload — condition is false.
  4. 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:

  1. Map interactive.blocks[0].buttons to an Adaptive Card with Action.Submit buttons.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions