Teams App Developer
MicrosoftOfficialPublished by Microsoft
- Skill
- Tool integration
- Software development
- MIT
- Medium risk
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The teams-app-developer skill provides a structured framework for managing the lifecycle of Microsoft 365 agents and Teams applications. It leverages the ATK CLI to handle project scaffolding, local testing via the Agents Playground, cloud provisioning on Azure, and troubleshooting. The skill includes specialized guidance for migrating Slack bots to Teams, including converting Block Kit UI to Adaptive Cards, and provides access to an extensive library of micro-expert documentation for cross-platform development.
What it does
- Scaffolds new Microsoft 365 agents and Teams applications from templates.
- Facilitates local testing using the Agents Playground or Teams-specific environments.
- Automates Azure resource provisioning and cloud deployment workflows.
- Provides diagnostic procedures for common authentication and configuration errors.
- Supports migration of Slack bots to the Teams platform.
- Manages project configuration and environment variables for ATK-based projects.
When to use it
- When starting a new Teams app, bot, or Declarative Agent project.
- When you need to test an agent locally before deploying to the cloud.
- When migrating existing Slack bots to the Microsoft Teams ecosystem.
- When troubleshooting deployment or authentication issues in a Teams project.
- When you need to provision Azure infrastructure for a Teams application.
Advantages
- Streamlines the development lifecycle with standardized CLI commands.
- Reduces development time by offering a local testing playground.
- Provides expert-level guidance for complex cross-platform migrations.
- Ensures consistent configuration management across development environments.
- Includes robust error-handling protocols for common platform-specific pitfalls.
Usage examples
- “Create a new Teams bot project using the default template.”
- The agent will initialize a new project folder, configure the necessary YAML files, and guide you through the initial setup using the ATK CLI.
- “How do I migrate my Slack bot to Microsoft Teams?”
- The agent will initiate the slack-to-teams workflow, providing access to cross-platform bridging experts and guidance on converting UI components.
- “My Teams bot is returning a 401 error, how do I fix it?”
- The agent will trigger the troubleshooting workflow, checking your environment variables, tunnel status, and authentication configuration.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/microsoft/teams-app-developer/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip teams-app-developer.zip -d .agents/skills
Documentation by Microsoft
Microsoft 365 Agents Toolkit Skill
Build Microsoft 365 agents and Teams apps using the ATK CLI.
AI Behavior Guidelines
Testing Strategy: Recommend Agents Playground first (faster, no M365 needed). Use Teams workflow only if user explicitly requests it.
Environment Variables: NEVER hardcode secrets or make up placeholder values. Always ask users for real values.
Error Handling: Read error messages carefully. Check
env/.env.local,.localConfigs, andatk auth list. Common pitfalls:AADSTS7000229→aadApp/createmissinggenerateServicePrincipal: truein YAML — add it and re-provisionMissing
TENANT_IDin.localConfigs→ SDK uses wrong token authority → 401 from Bot Connector401 persists after auth fix → devtunnel URL may be blacklisted — create a fresh tunnel
See troubleshoot/troubleshoot.md for full diagnostic steps
Long-Running Commands — WAIT for completion:
atk new,atk provision,atk deploycan take several minutesAlways wait for completion before running the next step (timeout 120000ms+)
Local Service Startup — Hangs terminal (expected):
npm run dev,npm start,python app.py,devtunnel host, etc. will hang — the process keeps running indefinitelyALWAYS run as a background process (
isBackground=true) — NEVER useisBackground=falsefor these commandsDo NOT wait for it to "finish" — verify startup by checking output for "listening on port" or tunnel URL
If errors appear, read logs, diagnose, fix, restart
Use a NEW terminal to launch Agents Playground or open Teams sideloading URL
Monitor App Logs: Periodically check background terminal output for runtime errors. If the app crashes, read the error, fix the root cause, and restart.
Telemetry Tagging: Before running any
atkCLI commands, set the session environment variable so all CLI invocations are tagged as skill-initiated:export ATK_CLI_SKILL=trueRun this once at the start of the session. All subsequent
atkcommands in the same terminal will inherit it.
ATK CLI Setup
atk --version # Must be > 1.1.5-betaIf ATK is not found or version is too old:
npm i -g @microsoft/m365agentstoolkit-cli@betaCLI Global Options
| Option | Meaning | Recommendation |
|---|---|---|
-i | Interactive mode | Always use -i false in automation to avoid hanging |
-f | Project folder | Default to be current directory, used when specifying a custom folder. When scaffolding a new project, this is the parent folder where the project folder will be created under. |
-h | Command help | Use atk <command> -h for quick syntax checks |
Sub-Skills
| Sub-Skill | When to Use | Reference |
|---|---|---|
| create-project | Scaffold new project from template, choose template, atk new | create-project/create-project.md |
| test-playground | Test locally with Agents Playground, agentsplayground, quick testing | test-playground/test-playground.md |
| test-teams | Run on Teams, devtunnel, sideload, Teams testing, test in Copilot | test-teams/test-teams.md |
| provision-deploy | Provision Azure resources, deploy to cloud, atk provision, atk deploy | provision-deploy/provision-deploy.md |
| troubleshoot | Fix errors, 401, port conflicts, YAML errors, stale bots | troubleshoot/troubleshoot.md |
| slack-to-teams | Migrate Slack bot to Teams, cross-platform bridging, Block Kit to Adaptive Cards | slack-to-teams/SKILL.md |
MANDATORY: Before executing any workflow, read the corresponding sub-skill document.
Shared References
manifest-and-yaml.md — Project files, YAML config, env vars, .localConfigs flow
commands.md — ATK CLI commands: package, validate, share, collaborate
templates.md — Complete template catalog with language support
experts/ — 100+ micro-expert files: Teams SDK, Slack SDK, cross-platform bridging, deploy, AI models, security, language conversion
docs/ — Platform comparison guides: UI, messaging, identity, infrastructure, feature gaps
Workflow Chains
Match user intent to the smallest valid workflow.
| User Intent | Workflow (read in order) |
|---|---|
| Build new app from scratch | create-project → test-playground |
| Test existing project locally | test-playground (recommended) or test-teams |
| Deploy to Azure | provision-deploy |
| Fix broken bot | troubleshoot → re-test |
| Migrate Slack bot to Teams | slack-to-teams |
MANDATORY: Before executing any slack-to-teams workflow, read slack-to-teams/SKILL.md first. The sub-skill contains a routed expert system with 100+ micro-expert files for cross-platform bot development.
ATK Project Context Resolution
Resolve config values only when missing. If a value is already known in the session, reuse it.
Step 1: Detect ATK Project
If m365agentstoolkit*.yml exists in the current folder, treat it as an ATK project and parse configuration.
Step 2: Resolve Common Configuration
Resolve variables referenced in m365agentstoolkit*.yml. Common variables:
AZURE_OPENAI_API_KEY
AZURE_OPENAI_ENDPOINT
AZURE_OPENAI_DEPLOYMENT_NAME
Step 3: Collect Missing Values
If required values are missing, ask the user for only the missing ones.
Refer to manifest-and-yaml.md for full config-file details.
Files
- ATTRIBUTION.mdAdded by AutoGineer906 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md7.6 KiB
- create-project/create-project.md8.1 KiB
- docs/README.md2.8 KiB
- docs/advanced-features.md7.0 KiB
- docs/feature-gaps.md46.3 KiB
- docs/files-and-links.md4.2 KiB
- docs/identity-and-auth.md4.3 KiB
- docs/infrastructure.md6.7 KiB
- docs/interactive-responses.md4.4 KiB
- docs/messaging-and-commands.md4.5 KiB
- docs/middleware-and-handlers.md4.4 KiB
- docs/ui-components.md6.9 KiB
- docs/workflows.md14.4 KiB
- experts/README.md8.6 KiB
- experts/_expert-ts.md2.2 KiB
- experts/analyzer.md14.0 KiB
- experts/bridge/app-distribution-packaging-ts.md14.2 KiB
- experts/bridge/channel-ops-graph-ts.md15.4 KiB
- experts/bridge/commands-slash-text-ts.md18.5 KiB
- experts/bridge/cross-platform-advisor-ts.md36.4 KiB
- experts/bridge/cross-platform-architecture-ts.md8.3 KiB
- experts/bridge/events-activities-ts.md22.4 KiB
- experts/bridge/files-upload-download-ts.md17.2 KiB
- experts/bridge/identity-oauth-bridge-ts.md19.8 KiB
- experts/bridge/index.md13.0 KiB
- experts/bridge/infra-compute-ts.md14.6 KiB
- experts/bridge/infra-observability-ts.md14.0 KiB
- experts/bridge/infra-secrets-config-ts.md13.2 KiB
- experts/bridge/infra-storage-ts.md14.3 KiB
- experts/bridge/interactive-responses-ts.md19.6 KiB
- experts/bridge/link-unfurl-preview-ts.md17.1 KiB
- experts/bridge/middleware-handlers-ts.md15.4 KiB
- experts/bridge/python-cross-platform.md9.3 KiB
- experts/bridge/rate-limiting-resilience-ts.md17.6 KiB
- experts/bridge/rest-only-integration-ts.md8.4 KiB
- experts/bridge/scheduling-deferred-send-ts.md17.8 KiB
- experts/bridge/shortcuts-extensions-ts.md17.5 KiB
- experts/bridge/transport-socketmode-https-ts.md14.3 KiB
- experts/bridge/ui-app-home-personal-tab-ts.md14.5 KiB
- experts/bridge/ui-block-kit-adaptive-cards-ts.md21.5 KiB
- experts/bridge/ui-legacy-attachments-cards-ts.md11.2 KiB
- experts/bridge/ui-modals-dialogs-ts.md21.4 KiB
- experts/bridge/workflow.composable-platform-ts.md13.7 KiB
- experts/bridge/workflows-automation-ts.md14.3 KiB
- experts/builder.md11.1 KiB
- experts/convert/bulk-conversion-strategy-ts.md12.8 KiB
- experts/convert/dependency-mapping-ts.md8.3 KiB
- experts/convert/index.md3.5 KiB
- experts/convert/java-to-ts-ts.md20.6 KiB
- experts/convert/js-to-ts-ts.md6.2 KiB
- experts/convert/json-serialization-ts.md11.0 KiB
- experts/convert/kotlin-to-ts-ts.md11.2 KiB
- experts/convert/ruby-to-ts-ts.md9.7 KiB
- experts/convert/type-mapping-ts.md10.1 KiB
- experts/deploy/aws-bot-deploy-ts.md14.6 KiB
- experts/deploy/aws-cli-reference-ts.md52.4 KiB
- experts/deploy/azure-bot-deploy-ts.md15.4 KiB
- experts/deploy/azure-cli-reference-ts.md16.9 KiB
- experts/deploy/index.md4.6 KiB
- experts/fallback.md2.1 KiB
- experts/index.md18.9 KiB
- experts/models/anthropic-ts.md7.8 KiB
- experts/models/bedrock-ts.md9.9 KiB
- experts/models/foundry-cloud-ts.md8.1 KiB
- experts/models/foundry-local-ts.md10.6 KiB
- experts/models/index.md5.8 KiB
- experts/models/openai-azure-openai-ts.md8.3 KiB
- experts/models/oss-openai-compatible-ts.md10.7 KiB
- experts/models/transformers-js-ts.md11.6 KiB
- experts/prompt-engineer.md14.7 KiB
- experts/researcher.md2.8 KiB
- experts/security/index.md1.2 KiB
- experts/security/input-validation-ts.md13.8 KiB
- experts/security/secrets-ts.md13.9 KiB
- experts/slack/bolt-assistant-ts.md9.9 KiB
- experts/slack/bolt-events-ts.md9.3 KiB
- experts/slack/bolt-java.md8.9 KiB
- experts/slack/bolt-oauth-distribution-ts.md10.9 KiB
- experts/slack/bolt-python.md9.5 KiB
- experts/slack/cli.app-management.md7.9 KiB
- experts/slack/cli.datastore-env.md8.6 KiB
- experts/slack/cli.getting-started.md8.0 KiB
- experts/slack/cli.local-dev-deploy.md8.4 KiB
- experts/slack/cli.manifest-triggers.md9.9 KiB
- experts/slack/index.md8.9 KiB
- experts/slack/runtime.ack-rules-ts.md9.8 KiB
- experts/slack/runtime.bolt-foundations-ts.md10.7 KiB
- experts/slack/runtime.shortcuts-ts.md11.0 KiB
- experts/slack/runtime.slash-commands-ts.md11.6 KiB
- experts/slack/runtime.socket-mode-ts.md9.5 KiB
- experts/slack/ui.block-kit-ts.md11.5 KiB
- experts/slack/ui.modals-lifecycle-ts.md13.3 KiB
- experts/slack/web-api-proactive-ts.md11.0 KiB
- experts/slack/workflow.slack-automations-ts.md10.2 KiB
- experts/teams/a2a.client-basics-ts.md9.7 KiB
- experts/teams/a2a.orchestrator-patterns-ts.md12.7 KiB
- experts/teams/a2a.server-basics-ts.md11.4 KiB
- experts/teams/ai.chatprompt-basics-ts.md8.1 KiB
- experts/teams/ai.citations-feedback-ts.md6.9 KiB
- experts/teams/ai.conversational-query-ts.md14.2 KiB
- experts/teams/ai.function-calling-design-ts.md9.0 KiB
- experts/teams/ai.function-calling-implementation-ts.md7.7 KiB
- experts/teams/ai.memory-localmemory-ts.md7.3 KiB
- experts/teams/ai.model-setup-ts.md6.6 KiB
- experts/teams/ai.rag-retrieval-ts.md7.4 KiB
- experts/teams/ai.rag-vectorstores-ts.md8.5 KiB
- experts/teams/ai.streaming-ts.md6.9 KiB
- experts/teams/auth.oauth-sso-ts.md10.1 KiB
- experts/teams/compat.botbuilder-interop-ts.md14.6 KiB
- experts/teams/dev.debug-test-ts.md14.1 KiB
- experts/teams/graph.usergraph-appgraph-ts.md9.0 KiB
- experts/teams/index.md9.8 KiB
- experts/teams/mcp.client-basics-ts.md9.5 KiB
- experts/teams/mcp.expose-chatprompt-tools-ts.md9.5 KiB
- experts/teams/mcp.security-ts.md10.4 KiB
- experts/teams/mcp.server-basics-ts.md9.9 KiB
- experts/teams/project.scaffold-files-ts.md12.2 KiB
- experts/teams/runtime.app-init-ts.md10.9 KiB
- experts/teams/runtime.manifest-ts.md12.3 KiB
- experts/teams/runtime.proactive-messaging-ts.md12.6 KiB
- experts/teams/runtime.routing-handlers-ts.md12.3 KiB
- experts/teams/state.storage-patterns-ts.md9.4 KiB
- experts/teams/teams-dotnet.md8.2 KiB
- experts/teams/teams-python.md8.3 KiB
- experts/teams/ui.adaptive-cards-ts.md12.9 KiB
- experts/teams/ui.dialogs-task-modules-ts.md15.1 KiB
- experts/teams/ui.message-extensions-ts.md15.6 KiB
- experts/teams/workflow.approvals-inline-ts.md12.4 KiB
- experts/teams/workflow.message-native-records-ts.md10.6 KiB
- experts/teams/workflow.sharepoint-lists-ts.md9.0 KiB
- experts/teams/workflow.state-driven-events-ts.md12.2 KiB
- experts/teams/workflow.triggers-compose-ts.md10.3 KiB
- experts/update-experts.md10.0 KiB
- provision-deploy/provision-deploy.md4.3 KiB
- test-playground/playground-cli.md4.6 KiB
- test-playground/playground.md4.5 KiB
- test-playground/test-playground.md1.1 KiB
- test-teams/test-teams.md4.9 KiB
- toolkit/README.md4.2 KiB
- toolkit/commands.md2.6 KiB
- toolkit/environments.md12.6 KiB
- toolkit/lifecycle-cli.md17.6 KiB
- toolkit/manifest-and-yaml.md6.0 KiB
- toolkit/playground.md7.6 KiB
- toolkit/publish.md9.3 KiB
- toolkit/templates.md6.7 KiB
- troubleshoot/troubleshoot.md20.2 KiB
experts/bridge/ui-modals-dialogs-ts.md
ui-modals-dialogs-ts
purpose
Bridges Slack modal workflows and Teams task module / dialog flows for cross-platform bots targeting Slack, Teams, or both.
rules
Slack
views.open(trigger_id, view)maps to Teamsdialog.openhandler. In Slack, the app callsctx.client().viewsOpen()with atrigger_idfrom a slash command or interaction. In Teams, the dialog opens when the user clicks anAction.Submitwith{ msteams: { type: 'task/fetch' } }in its data, or from a manifest command. Thedialog.openhandler returns the card form.Slack
app.viewSubmission(callback_id)maps to Teamsapp.on('dialog.submit', handler). Slack provides form data inview.state.values[block_id][action_id]; Teams provides it inactivity.value.dataas a flat object keyed by Adaptive Card inputids.Slack
viewsUpdate(updating the current modal) maps to returning acontinueresponse fromdialog.submitwith a new card. Slack'sctx.ack({ response_action: 'update', view: newView })becomes returning{ status: 200, body: { task: { type: 'continue', value: { title, card } } } }.Slack
views.push(stacking a new modal) has no Teams equivalent. Teams task modules do not support stacking. Flatten multi-modal stacks into a single multi-step dialog with step routing indialog.submit, or redesign as sequential cards in the chat.Slack
app.viewClosed(callback_id)(notify_on_close: true) has no direct Teams equivalent. Teams does not notify the bot when a user closes/cancels a task module. If cleanup is needed, handle it via timeout or the next user interaction. For critical cleanup, consider storing pending state and reconciling on the next bot message.Slack field-level validation with
ctx.ackWithErrors({ block_id: "error message" })(which keeps the modal open and shows inline errors) has no server-side equivalent in Teams. Use Adaptive Card client-side validation (isRequired,errorMessage,regex,min,max) for pre-submit validation. For server-side validation that fails, return acontinueresponse with the form re-rendered including errorTextBlocks, or return amessageresponse with the error text.Slack
private_metadata(arbitrary string stored on the view) maps to embedding hidden state inAction.Submit.datafields. Include any round-trip state (original command args, IDs, step indicators) in the card's submit actiondataobject.Slack
blockSuggestion(typeahead/external data source for selects inside modals) maps to Adaptive CardInput.ChoiceSetwith"style": "filtered"for client-side filtering, orData.Querywith dynamic data source for server-side filtering (schema 1.6+, limited Teams support). For most cases, pre-populate the choices at dialog open time instead of dynamic fetching.Slack
blockActioninside modals (responding to user interactions mid-form without submitting) has no Teams equivalent. Adaptive Card inputs do not fire events untilAction.Submitis clicked. If the Slack modal updated dynamically based on a selection, redesign as: (a) multi-step dialog (submit step 1, return step 2 card), or (b) pre-compute all variants and include conditional data in the initial card.Slack modal
title,submit, andcloselabels map to task moduletitle(in thevalueobject) and Adaptive CardAction.Submitbutton titles. There is no separate close button label — the task module always shows a platform X button.
patterns
Slash command → modal → submit (full flow)
Slack (before):
// Slash command opens a modal
app.command("/meeting") { _, ctx ->
val res = ctx.client().viewsOpen {
it.triggerId(ctx.triggerId).viewAsString(modalJson)
}
if (res.isOk) ctx.ack()
else Response.builder().statusCode(500).body(res.error).build()
}
// Handle submission
app.viewSubmission("meeting-arrangement") { req, ctx ->
val stateValues = req.payload.view.state.values
val agenda = stateValues["agenda"]!!["agenda-input"]!!.value
val errors = mutableMapOf<String, String>()
if (agenda.length <= 10) {
errors["agenda"] = "Agenda needs to be longer than 10 characters."
}
if (errors.isNotEmpty()) {
ctx.ackWithErrors(errors)
} else {
ctx.ack()
}
}
// Handle close
app.viewClosed("meeting-arrangement") { _, ctx -> ctx.ack() }Teams (after):
import { App } from '@microsoft/teams.apps';
import { ConsoleLogger } from '@microsoft/teams.common';
const app = new App({
logger: new ConsoleLogger('meeting-bot'),
});
// Step 1: Send a message with a button that triggers dialog.open
app.on('message', async ({ activity, send }) => {
if (activity.text?.match(/\/meeting/i)) {
await send({
type: 'message',
attachments: [{
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
type: 'AdaptiveCard',
version: '1.5',
body: [{ type: 'TextBlock', text: 'Click below to arrange a meeting.' }],
actions: [{
type: 'Action.Submit',
title: 'Arrange Meeting',
data: { msteams: { type: 'task/fetch' } },
}],
},
}],
});
}
});
// Step 2: dialog.open returns the form card (replaces views.open)
app.on('dialog.open', async () => {
return {
status: 200,
body: {
task: {
type: 'continue',
value: {
title: 'Meeting Arrangement',
width: 'medium',
height: 'medium',
card: {
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
type: 'AdaptiveCard',
version: '1.5',
body: [
{
type: 'Input.Date',
id: 'meetingDate',
label: 'Meeting Date',
},
{
type: 'Input.ChoiceSet',
id: 'topics',
label: 'Topics',
isMultiSelect: true,
style: 'filtered',
choices: [
{ title: 'Schedule', value: 'schedule' },
{ title: 'Budget', value: 'budget' },
{ title: 'Assignment', value: 'assignment' },
],
},
{
type: 'Input.Text',
id: 'agenda',
label: 'Detailed Agenda',
isMultiline: true,
isRequired: true,
errorMessage: 'Agenda is required',
},
],
actions: [{
type: 'Action.Submit',
title: 'Submit',
data: { action: 'meeting-arrangement' },
}],
},
},
},
},
},
};
});
// Step 3: dialog.submit handles form data (replaces viewSubmission)
app.on('dialog.submit', async ({ activity }) => {
const data = activity.value.data;
const agenda: string = data.agenda ?? '';
// Server-side validation (replaces ctx.ackWithErrors)
if (agenda.length <= 10) {
// Return the form again with an error message
return {
status: 200,
body: {
task: {
type: 'continue',
value: {
title: 'Meeting Arrangement',
card: {
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
type: 'AdaptiveCard',
version: '1.5',
body: [
{
type: 'TextBlock',
text: 'Agenda needs to be longer than 10 characters.',
color: 'Attention',
weight: 'Bolder',
},
// ... repeat form fields with previous values pre-filled ...
],
actions: [{
type: 'Action.Submit',
title: 'Submit',
data: { action: 'meeting-arrangement' },
}],
},
},
},
},
},
};
}
// Success — close the dialog
return {
status: 200,
body: {
task: {
type: 'message',
value: `Meeting arranged! Date: ${data.meetingDate}, Topics: ${data.topics}`,
},
},
};
});
// Note: No viewClosed equivalent — Teams does not notify on dialog cancel.
app.start(3978);Mapping reference table
| Slack Modal Concept | Teams Dialog Equivalent | Notes |
|---|---|---|
views.open(trigger_id, view) | dialog.open handler returning continue response | Triggered by Action.Submit with msteams: { type: 'task/fetch' } |
viewSubmission(callback_id) | dialog.submit handler | Form data in activity.value.data (flat object) |
ctx.ack() (close modal) | Return { task: { type: 'message', value } } | Message shown briefly, then dialog closes |
ctx.ack({ response_action: 'update', view }) | Return { task: { type: 'continue', value: { card } } } | Replaces dialog content |
ctx.ack({ response_action: 'push', view }) | (no equivalent) | Flatten into multi-step dialog |
ctx.ackWithErrors(errors) | Return continue with error TextBlocks, or use client-side validation | No native field-level error API |
viewClosed(callback_id) | (no equivalent) | Teams does not notify on cancel |
private_metadata | Action.Submit.data fields | Embed state in submit action |
view.state.values[block_id][action_id] | activity.value.data[inputId] | Flat key-value vs nested structure |
blockSuggestion (typeahead) | Input.ChoiceSet with style: "filtered" | Client-side only; pre-populate choices |
blockAction mid-form | (no equivalent) | Redesign as multi-step dialog |
Modal title / submit / close labels | value.title + Action.Submit.title | No custom close label |
Dynamic selects best practice (Y9)
Pre-populate Input.ChoiceSet with style: "filtered" for datasets under 500 items. For larger datasets, use a two-step dialog.
// Small dataset (<500 items): pre-populate with client-side filtering
function buildSelectCard(users: { name: string; email: string }[]): object {
return {
type: "AdaptiveCard", version: "1.5",
body: [{
type: "Input.ChoiceSet",
id: "user_select",
label: "Assign to",
style: "filtered", // enables client-side typeahead search
choices: users.map(u => ({ title: u.name, value: u.email })),
}],
actions: [{ type: "Action.Submit", title: "Assign", data: { action: "assign" } }],
};
}
// Large dataset (>500 items): two-step dialog
// Step 1: text input for search query
function buildSearchStep(): object {
return {
type: "AdaptiveCard", version: "1.5",
body: [{
type: "Input.Text", id: "search_query",
label: "Search users", placeholder: "Type a name...",
}],
actions: [{ type: "Action.Submit", title: "Search", data: { action: "search_users", step: 1 } }],
};
}
// Step 2: submit handler queries server, returns filtered ChoiceSet
app.on("dialog.submit", async ({ activity }) => {
const data = activity.value.data;
if (data?.action === "search_users" && data.step === 1) {
const results = await searchUsers(data.search_query); // server-side query
return {
status: 200,
body: { task: { type: "continue", value: {
title: "Select User",
card: {
contentType: "application/vnd.microsoft.card.adaptive",
content: buildSelectCard(results), // now a small filtered set
},
}}},
};
}
});Don't: Build a web-based task module just for a searchable dropdown. The effort (16–24 hrs) rarely justifies the marginal UX improvement over two-step.
Reverse (Teams → Slack): Use external_data_source: true on select elements with app.options() for server-side typeahead.
Cancel detection workaround: TTL + Cancel button (R3)
Teams does not notify the bot when a dialog is dismissed. Add an explicit "Cancel" button and a timeout to handle cleanup.
// Track pending dialog state with TTL
const pendingDialogs = new Map<string, { userId: string; lockedResource: string; expiresAt: number }>();
// When opening a dialog, record the pending state
app.on('dialog.open', async ({ activity }) => {
const userId = activity.from?.aadObjectId ?? '';
const dialogId = `dlg_${Date.now()}`;
pendingDialogs.set(dialogId, {
userId,
lockedResource: 'ticket-123',
expiresAt: Date.now() + 5 * 60_000, // 5-minute TTL
});
return {
status: 200,
body: {
task: {
type: 'continue',
value: {
title: 'Edit Ticket',
card: buildFormCard(dialogId), // embed dialogId in Action.Submit.data
},
},
},
};
});
// Handle explicit Cancel button (inside the dialog)
app.on('dialog.submit', async ({ activity }) => {
const data = activity.value.data;
if (data?.action === 'cancel') {
pendingDialogs.delete(data.dialogId);
releaseLock(data.dialogId);
return { status: 200, body: { task: { type: 'message', value: 'Cancelled.' } } };
}
// Handle normal submit...
pendingDialogs.delete(data.dialogId);
return { status: 200, body: { task: { type: 'message', value: 'Saved!' } } };
});
// Periodic cleanup of expired dialogs (user closed without clicking Cancel)
setInterval(() => {
const now = Date.now();
for (const [id, state] of pendingDialogs) {
if (state.expiresAt < now) {
releaseLock(id);
pendingDialogs.delete(id);
}
}
}, 60_000); // check every minuteReverse (Teams → Slack): Use notify_on_close: true in views.open() and handle viewClosed natively.
Multi-step dialog workaround: step routing (R4/R6)
Replace Slack's views.push() stacking and dispatch_action mid-form updates with a single dialog using step routing.
app.on('dialog.submit', async ({ activity }) => {
const data = activity.value.data;
const step = data?.step ?? 1;
if (data?.action === 'back') {
return buildStepResponse(step - 1, data);
}
if (data?.action === 'next') {
// Validate current step
const errors = validateStep(step, data);
if (errors.length > 0) {
return buildStepResponse(step, data, errors); // re-render with errors (R5)
}
if (step >= 3) {
// Final step — process all data
await processWizard(data);
return { status: 200, body: { task: { type: 'message', value: 'Done!' } } };
}
return buildStepResponse(step + 1, data);
}
});
function buildStepResponse(step: number, previousData: Record<string, unknown>, errors: string[] = []) {
return {
status: 200,
body: {
task: {
type: 'continue',
value: {
title: `Step ${step} of 3`,
card: {
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
type: 'AdaptiveCard', version: '1.5',
body: [
// Show validation errors if any (R5 workaround)
...errors.map(e => ({
type: 'TextBlock', text: e, color: 'Attention', weight: 'Bolder',
})),
// Step-specific fields
...getStepFields(step, previousData),
],
actions: [
...(step > 1 ? [{
type: 'Action.Submit', title: 'Back',
data: { ...previousData, step, action: 'back' },
}] : []),
{
type: 'Action.Submit',
title: step === 3 ? 'Finish' : 'Next',
data: { ...previousData, step, action: 'next' },
},
{
type: 'Action.Submit', title: 'Cancel',
data: { ...previousData, step, action: 'cancel' },
},
],
},
},
},
},
},
};
}Key principle: Every step's Action.Submit.data must carry forward ALL data from previous steps, since there's no persistent modal state like Slack's private_metadata.
Reverse (Teams → Slack): Use views.push() for stacking (up to 3 levels) and dispatch_action: true + views.update() for mid-form dynamics.
Reverse direction (Teams → Slack)
For Teams → Slack, map dialog.open to views.open with trigger_id, dialog.submit to viewSubmission, and Adaptive Card inputs to Block Kit inputs. Key reverse mappings:
dialog.openhandler returningcontinue→views.open(trigger_id, view)-- note: Slack requires atrigger_idfrom a preceding interaction (slash command, button click, etc.)dialog.submithandler →app.view('callback_id', ...)withview.state.values[block_id][action_id]activity.value.data[inputId](flat) →view.state.values[block_id][action_id].value(nested)Return
{ task: { type: 'continue', value: { card } } }→ctx.ack({ response_action: 'update', view: newView })Return
{ task: { type: 'message', value } }→ctx.ack()(close modal)Multi-step dialog (routing by
data.step) →views.pushfor stacked modals (Slack supports stacking)Error
TextBlockre-render →ctx.ackWithErrors({ block_id: 'error message' })for inline field-level errorsAction.Submit.datahidden fields →private_metadatastring on the viewInput.ChoiceSetwithstyle: "filtered"→blockSuggestionhandler for server-side typeaheadAdaptive Card
isRequired/errorMessage/regexclient-side validation → server-side validation inviewSubmissionwithackWithErrorsNo cancel notification (Teams) →
viewClosed(callback_id)withnotify_on_close: true(Slack supports cancel callbacks)
pitfalls
No modal stacking: Slack's
views.pushstacks modals. Teams task modules cannot stack. Redesign stacked flows as multi-step forms within a single dialog (route bydata.stepin the submit handler).No cancel notification: Slack's
viewClosedhandler fires when a user clicks Cancel (withnotify_on_close: true). Teams has no equivalent. Do not rely on cancel callbacks for critical state cleanup.Validation UX is different: Slack's
ackWithErrorsshows inline red text under specific fields and keeps the modal open. Teams has no server-side field-level error API. Use Adaptive CardisRequired/errorMessage/regexfor client-side checks. For server-side failures, return acontinueresponse with an errorTextBlockadded to the card.Form data structure change: Slack nests form data as
view.state.values[block_id][action_id].value. Teams flattens it asactivity.value.data[inputId]. The nesting is gone — inputids must be unique across the entire card.Trigger mechanism change: Slack opens modals from
trigger_id(passed in slash command and interaction payloads). Teams opens dialogs fromAction.Submitwithmsteams: { type: 'task/fetch' }or from manifest commands. There is no free-standing "open dialog" API call.Dynamic selects: Slack's
blockSuggestionfires on each keystroke to fetch options server-side. Adaptive CardInput.ChoiceSetwithstyle: "filtered"only filters pre-populated choices client-side. For truly dynamic data, pre-fetch at dialog open time or useData.Query(limited support).Mid-form interactions lost: Slack modals can respond to
blockActionevents mid-form (e.g., showing/hiding fields based on a dropdown). Adaptive Cards do not fire events until submit. Redesign conditional forms as multi-step dialogs.Returning nothing closes with error: If the
dialog.submithandler returnsundefined, Teams shows a generic error. Always return a valid{ status: 200, body: { task: { ... } } }response.
references
https://api.slack.com/surfaces/modals — Slack modal documentation
https://api.slack.com/surfaces/modals/using#pushing — Stacking views with views.push
https://api.slack.com/surfaces/modals/using#closing — notify_on_close and viewClosed
https://api.slack.com/reference/interaction-payloads/views — view_submission payload
https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots — Teams task modules
https://github.com/microsoft/teams.ts — Teams SDK v2
instructions
Use this expert when bridging Slack modal workflows and Teams dialog/task module flows in either direction. It covers the full lifecycle: opening (views.open ↔ dialog.open), submission (viewSubmission ↔ dialog.submit), updating (response_action: update ↔ continue response), stacking (views.push ↔ multi-step redesign), closing (viewClosed ↔ no Teams equivalent), validation (ackWithErrors ↔ client-side + continue), and dynamic selects (blockSuggestion ↔ filtered ChoiceSet). Use when adding cross-platform support in either direction. Pair with ui-block-kit-adaptive-cards-ts.md for converting modal Block Kit to Adaptive Card elements (or vice versa), ../teams/ui.dialogs-task-modules-ts.md for Teams-side dialog patterns, and ../teams/ui.adaptive-cards-ts.md for card construction.
research
Deep Research prompt:
"Write a micro expert for bridging Slack modals and Teams task modules / dialogs bidirectionally. Cover views.open <-> dialog.open, viewSubmission <-> dialog.submit, viewsUpdate <-> continue response, viewClosed <-> no equivalent, blockSuggestion <-> filtered ChoiceSet, blockAction <-> no equivalent, ackWithErrors <-> client-side validation, private_metadata <-> Action.Submit.data, and notify_on_close. Include a comprehensive bidirectional mapping table, a full worked example showing both directions, and pitfalls around stacking, validation, cancel notification, and dynamic selects."
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an API key, Needs Python, Needs Node.js
Security
What our scanner noticed· Medium risk
- Possible data-exfiltration endpoints
- Instructions that use the network
- Downloads piped into a shell
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Microsoft
- Publisher
- Microsoft (Official)
- Source
- microsoft/skills · .github/plugins/microsoft-365-agents-toolkit/skills/teams-app-developer · commit 0696600
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Microsoft.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download teams-app-developer.zip