dialogs

Use this skill when the user is building dialogs (task modules) in a Microsoft Teams bot on Teams SDK 2.1 — opening a dialog from a card with `OpenDialogData`, handling `dialog.open.<id>` and `dialog.

SKILL.md

GitHub

Dialogs (task modules)

A dialog is a modal the Teams client opens over a chat. Its body is an Adaptive Card or a web page; when the user submits, control returns to the bot. Two routes and two helpers cover it: dialog.open.<id> renders the dialog, dialog.submit.<action> receives the result, OpenDialogData names the dialog on the button that opens it, SubmitData names the action on the button inside it. Both helpers come from @microsoft/teams.cards.

1. Open a dialog from a card

import { MessageActivityInput } from '@microsoft/teams.api';
import { AdaptiveCard, OpenDialogData, SubmitAction, TextBlock } from '@microsoft/teams.cards';

app.on('message', async ({ send }) => {
  const card = new AdaptiveCard(
    new TextBlock('Need details? Open the form.', { size: 'Large', weight: 'Bolder' }),
  ).withActions(
    new SubmitAction().withTitle('Simple form').withData(new OpenDialogData('simple_form')),
    new SubmitAction().withTitle('Web page').withData(new OpenDialogData('webpage_dialog')),
  );
  await send(new MessageActivityInput('Pick one').addCard('adaptive', card));
});

OpenDialogData marks the action as a task/fetch request and carries the dialog id used for routing. It is attached to a SubmitAction; that is what the Teams client requires.

2. dialog.open.<id> — return a card dialog

import { cardAttachment } from '@microsoft/teams.api';
import { AdaptiveCard, SubmitAction, SubmitData, TextBlock, TextInput } from '@microsoft/teams.cards';

app.on('dialog.open.simple_form', async () => {
  const dialogCard = new AdaptiveCard(
    new TextBlock('This is a simple form', { size: 'Large', weight: 'Bolder' }),
    new TextInput().withId('name').withLabel('Name').withIsRequired().withPlaceholder('Enter your name'),
  ).withActions(
    new SubmitAction().withTitle('Submit').withData(new SubmitData('simple_form')),
  );

  return {
    task: {
      type: 'continue',
      value: {
        title: 'Simple form',
        card: cardAttachment('adaptive', dialogCard),
      },
    },
  };
});

The buttons inside a dialog are Action.Submit too. SubmitData('simple_form') makes the click arrive at dialog.submit.simple_form. dialog.open (without an id) is a catch-all; prefer one handler per dialog id.

3. dialog.submit.<action> — collect the result

app.on('dialog.submit.simple_form', async ({ activity, send }) => {
  const name = activity.value.data.name;
  await send(`Hi ${name}, thanks for submitting the form!`);
  return { task: { type: 'message', value: 'Form was submitted' } };   // closes the dialog with a final message
});

Return values:

  • { task: { type: 'message', value: '…' } } closes the dialog and shows the text.
  • { status: 200 } closes it silently.
  • { task: { type: 'continue', value: { title, card } } } swaps the body — a multi-step flow.

4. Multi-step dialogs

Carry earlier answers forward through the extra data of SubmitData:

import { cardAttachment } from '@microsoft/teams.api';
import { AdaptiveCard, SubmitAction, SubmitData, TextBlock, TextInput } from '@microsoft/teams.cards';

app.on('dialog.submit.step_one', async ({ activity }) => {
  const name = activity.value.data.name;
  const next = new AdaptiveCard(
    new TextBlock('Email', { size: 'Large', weight: 'Bolder' }),
    new TextInput().withId('email').withLabel('Email').withIsRequired(),
  ).withActions(
    new SubmitAction().withTitle('Submit').withData(new SubmitData('step_two', { name })),
  );
  return { task: { type: 'continue', value: { title: `Thanks ${name}`, card: cardAttachment('adaptive', next) } } };
});

app.on('dialog.submit.step_two', async ({ activity, send }) => {
  const { name, email } = activity.value.data;
  await send(`Hi ${name}, we will write to ${email}.`);
  return { status: 200 };
});

5. Web-page dialogs

Serve the page from the bot with app.tab(...) and return its URL. The page must be public, load @microsoft/teams-js, and its domain must be in the manifest's validDomains.

import path from 'node:path';

app.tab('dialog-form', path.resolve('dist/dialog-form'));          // served at /tabs/dialog-form

app.on('dialog.open.webpage_dialog', async () => ({
  task: {
    type: 'continue',
    value: {
      title: 'Web page dialog',
      url: `${process.env['BOT_ENDPOINT']}/tabs/dialog-form`,    // your public https origin, set in .env
      width: 1000,
      height: 800,
    },
  },
}));

app.on('dialog.submit.webpage_dialog', async ({ activity, send }) => {
  await send(`Got ${activity.value.data.email}`);
  return { status: 200 };                                          // closes the dialog
});

Inside the page, submit with TeamsJS and include the action field so the router can find the handler:

import * as microsoftTeams from '@microsoft/teams-js';

async function submitForm(email: string) {
  await microsoftTeams.app.initialize();
  microsoftTeams.dialog.url.submit({ action: 'webpage_dialog', email });
}
void submitForm;

Add the domain to the manifest through the CLI, for example teams app manifest update <teamsAppId> --set-json validDomains='["bot.example.com"]'. (teams app update --endpoint adds the bot's own domain on its own.)

6. Dialogs from message extensions

An action command with fetchTask: true does not use dialog.open.*. The route is message.ext.open; the response has the same task envelope. See message-extensions.

Common pitfalls

  • The dialog never opens — OpenDialogData sits on an ExecuteAction or on a card that was not sent through the SDK; use a SubmitAction.
  • Submit reaches no handler — the route is dialog.submit.<action> where <action> is the first argument of SubmitData; it is not the dialog id of OpenDialogData unless you reuse the same string.
  • The web page is blank or 404 — its host is not in validDomains, or the page is not publicly reachable.
  • activity.value.data is empty — the inputs have no id, or a web page submitted without an action field.
  • The dialog stays open — the handler returned nothing; return { status: 200 } or a task response.