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.mdteams-dev/skills/dialogs/SKILL.md
GitHubDialogs (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 —
OpenDialogDatasits on anExecuteActionor on a card that was not sent through the SDK; use aSubmitAction. - Submit reaches no handler — the route is
dialog.submit.<action>where<action>is the first argument ofSubmitData; it is not the dialog id ofOpenDialogDataunless 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.datais empty — the inputs have no id, or a web page submitted without anactionfield.- The dialog stays open — the handler returned nothing; return
{ status: 200 }or ataskresponse.