Dialogs (Task Modules)
A dialog is a modal pop-up the Teams client opens on top of a chat. It receives input, then closes and returns control. The Teams SDK supports two body types: an Adaptive Card or a static web page.
1. Trigger a dialog from a card
import { AdaptiveCard, TextBlock, ExecuteAction } from '@microsoft/teams.cards';
import { OpenDialogData } from '@microsoft/teams.api';
const card = new AdaptiveCard()
.addBody(new TextBlock('Need details? Open the form.'))
.addActions(
new ExecuteAction('open_form')
.withTitle('Open form')
.withData(new OpenDialogData('signup_form')),
);
OpenDialogData('<id>') is the magic key. When the user clicks, Teams invokes dialog.open.signup_form on the bot.
2. Handle dialog.open.<id> — return a card-based dialog
import { AdaptiveCard, TextBlock, TextInput, SubmitAction } from '@microsoft/teams.cards';
app.on('dialog.open.signup_form', async ({ activity }) => {
const card = new AdaptiveCard()
.addBody(new TextBlock('Sign up').withWeight('Bolder'))
.addBody(new TextInput('email').withIsRequired())
.addActions(new SubmitAction().withTitle('Submit'));
return {
task: {
type: 'continue',
value: {
title: 'Sign up',
height: 200,
width: 400,
card: { contentType: 'application/vnd.microsoft.card.adaptive', content: card.toJSON() },
},
},
};
});
Returning the task envelope is what tells Teams to render the dialog. type: 'continue' keeps the dialog open; type: 'message' closes it and posts a final message.
3. Handle dialog.submit.<id> — collect the result
app.on('dialog.submit.signup_form', async ({ activity, send }) => {
const data = activity.value.data as { email: string };
await persist(data);
await send(`Signed up: ${data.email}`);
return { task: { type: 'message', value: 'Thanks!' } };
});
The submission carries the input values keyed by their id. Return:
type: 'message'to close the dialog with a final toast.type: 'continue'to swap the dialog body (multi-step wizards).
4. Web-page dialogs
For richer UIs (your own React/Vue page), serve a static URL and reference it instead of a card:
app.on('dialog.open.profile_editor', async () => ({
task: {
type: 'continue',
value: {
title: 'Edit profile',
height: 600,
width: 800,
url: 'https://yourapp.example.com/profile',
fallbackUrl: 'https://yourapp.example.com/profile',
},
},
}));
The page uses @microsoft/teams.client to call microsoftTeams.dialog.url.submit({ ... }) from the browser, which fires dialog.submit.<id> on the bot. See tabs for client-side SDK details.
5. Multi-step dialogs
Return another continue from dialog.submit.<id> to swap the body without closing:
app.on('dialog.submit.signup_form', async ({ activity }) => {
if (!isComplete(activity.value.data)) {
return {
task: {
type: 'continue',
value: { title: 'One more thing', card: { contentType: '…', content: nextStepCard.toJSON() } },
},
};
}
return { task: { type: 'message', value: 'Done!' } };
});
Manifest
The bot must declare the dialog domain in appPackage/manifest.json:
{
"validDomains": ["yourapp.example.com"],
"webApplicationInfo": {
"id": "<AAD app id>",
"resource": "api://yourapp.example.com/<AAD app id>"
}
}
Web-page dialogs without a validDomains entry are rejected by the Teams client.
Common pitfalls
- Dialog never opens —
OpenDialogDatawas attached to the wrong action type, or the action button was sent withoutcardAttachment('adaptive', card). - Submit hits nothing — handler is
dialog.submit.<id>(with the same id asOpenDialogData(<id>)). A baredialog.submitmatches every id but won't run before a specific handler. - Webpage dialog 404s — the URL is missing from
validDomains. - Dialog body is blank — the returned task envelope is missing
task.value.card.contentortask.value.url.