Generated banner for Teams App Developer
Generated logo for Teams App Developer

Teams App Developer

MicrosoftOfficialPublished by Microsoft

  • Skill
  • Tool integration
  • Software development
  • MIT
  • Medium risk

From the author

Builds, tests, and deploys Microsoft 365 apps and agents for Teams and Copilot. Includes sub-skills for project creation, local testing, cloud deployment, troubleshooting, and Slack-to-Teams migration. USE FOR: Teams agent, bot, tab, message extension, Declarative Agents, Custom Engine Agents, local testing, Agents Playground, Azure resource provision, remote deployment, Slack to Teams migration, cross-platform bot development, Block Kit to Adaptive Cards conversion. DO NOT USE FOR: general web development, non-bot/non-Teams projects.
— the description by Microsoft

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/microsoft/teams-app-developer/download
  2. Unzip 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

  1. Testing Strategy: Recommend Agents Playground first (faster, no M365 needed). Use Teams workflow only if user explicitly requests it.

  2. Environment Variables: NEVER hardcode secrets or make up placeholder values. Always ask users for real values.

  3. Error Handling: Read error messages carefully. Check env/.env.local, .localConfigs, and atk auth list. Common pitfalls:

    • AADSTS7000229 → aadApp/create missing generateServicePrincipal: true in YAML — add it and re-provision

    • Missing TENANT_ID in .localConfigs → SDK uses wrong token authority → 401 from Bot Connector

    • 401 persists after auth fix → devtunnel URL may be blacklisted — create a fresh tunnel

    • See troubleshoot/troubleshoot.md for full diagnostic steps

  4. Long-Running Commands — WAIT for completion:

    • atk new, atk provision, atk deploy can take several minutes

    • Always wait for completion before running the next step (timeout 120000ms+)

  5. Local Service Startup — Hangs terminal (expected):

    • npm run dev, npm start, python app.py, devtunnel host, etc. will hang — the process keeps running indefinitely

    • ALWAYS run as a background process (isBackground=true) — NEVER use isBackground=false for these commands

    • Do 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

  6. Monitor App Logs: Periodically check background terminal output for runtime errors. If the app crashes, read the error, fix the root cause, and restart.

  7. Telemetry Tagging: Before running any atk CLI commands, set the session environment variable so all CLI invocations are tagged as skill-initiated:

    export ATK_CLI_SKILL=true

    Run this once at the start of the session. All subsequent atk commands in the same terminal will inherit it.

ATK CLI Setup

atk --version  # Must be > 1.1.5-beta

If ATK is not found or version is too old:

npm i -g @microsoft/m365agentstoolkit-cli@beta

CLI Global Options

OptionMeaningRecommendation
-iInteractive modeAlways use -i false in automation to avoid hanging
-fProject folderDefault 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.
-hCommand helpUse atk <command> -h for quick syntax checks

Sub-Skills

Sub-SkillWhen to UseReference
create-projectScaffold new project from template, choose template, atk newcreate-project/create-project.md
test-playgroundTest locally with Agents Playground, agentsplayground, quick testingtest-playground/test-playground.md
test-teamsRun on Teams, devtunnel, sideload, Teams testing, test in Copilottest-teams/test-teams.md
provision-deployProvision Azure resources, deploy to cloud, atk provision, atk deployprovision-deploy/provision-deploy.md
troubleshootFix errors, 401, port conflicts, YAML errors, stale botstroubleshoot/troubleshoot.md
slack-to-teamsMigrate Slack bot to Teams, cross-platform bridging, Block Kit to Adaptive Cardsslack-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 IntentWorkflow (read in order)
Build new app from scratchcreate-project → test-playground
Test existing project locallytest-playground (recommended) or test-teams
Deploy to Azureprovision-deploy
Fix broken bottroubleshoot → re-test
Migrate Slack bot to Teamsslack-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

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

  1. Slack views.open(trigger_id, view) maps to Teams dialog.open handler. In Slack, the app calls ctx.client().viewsOpen() with a trigger_id from a slash command or interaction. In Teams, the dialog opens when the user clicks an Action.Submit with { msteams: { type: 'task/fetch' } } in its data, or from a manifest command. The dialog.open handler returns the card form.

  2. Slack app.viewSubmission(callback_id) maps to Teams app.on('dialog.submit', handler). Slack provides form data in view.state.values[block_id][action_id]; Teams provides it in activity.value.data as a flat object keyed by Adaptive Card input ids.

  3. Slack viewsUpdate (updating the current modal) maps to returning a continue response from dialog.submit with a new card. Slack's ctx.ack({ response_action: 'update', view: newView }) becomes returning { status: 200, body: { task: { type: 'continue', value: { title, card } } } }.

  4. 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 in dialog.submit, or redesign as sequential cards in the chat.

  5. 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.

  6. 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 a continue response with the form re-rendered including error TextBlocks, or return a message response with the error text.

  7. Slack private_metadata (arbitrary string stored on the view) maps to embedding hidden state in Action.Submit.data fields. Include any round-trip state (original command args, IDs, step indicators) in the card's submit action data object.

  8. Slack blockSuggestion (typeahead/external data source for selects inside modals) maps to Adaptive Card Input.ChoiceSet with "style": "filtered" for client-side filtering, or Data.Query with 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.

  9. Slack blockAction inside modals (responding to user interactions mid-form without submitting) has no Teams equivalent. Adaptive Card inputs do not fire events until Action.Submit is 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.

  10. Slack modal title, submit, and close labels map to task module title (in the value object) and Adaptive Card Action.Submit button 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 ConceptTeams Dialog EquivalentNotes
views.open(trigger_id, view)dialog.open handler returning continue responseTriggered by Action.Submit with msteams: { type: 'task/fetch' }
viewSubmission(callback_id)dialog.submit handlerForm 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 validationNo native field-level error API
viewClosed(callback_id)(no equivalent)Teams does not notify on cancel
private_metadataAction.Submit.data fieldsEmbed 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 labelsvalue.title + Action.Submit.titleNo 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 minute

Reverse (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.open handler returning continue → views.open(trigger_id, view) -- note: Slack requires a trigger_id from a preceding interaction (slash command, button click, etc.)

  • dialog.submit handler → app.view('callback_id', ...) with view.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.push for stacked modals (Slack supports stacking)

  • Error TextBlock re-render → ctx.ackWithErrors({ block_id: 'error message' }) for inline field-level errors

  • Action.Submit.data hidden fields → private_metadata string on the view

  • Input.ChoiceSet with style: "filtered" → blockSuggestion handler for server-side typeahead

  • Adaptive Card isRequired/errorMessage/regex client-side validation → server-side validation in viewSubmission with ackWithErrors

  • No cancel notification (Teams) → viewClosed(callback_id) with notify_on_close: true (Slack supports cancel callbacks)

pitfalls

  • No modal stacking: Slack's views.push stacks modals. Teams task modules cannot stack. Redesign stacked flows as multi-step forms within a single dialog (route by data.step in the submit handler).

  • No cancel notification: Slack's viewClosed handler fires when a user clicks Cancel (with notify_on_close: true). Teams has no equivalent. Do not rely on cancel callbacks for critical state cleanup.

  • Validation UX is different: Slack's ackWithErrors shows inline red text under specific fields and keeps the modal open. Teams has no server-side field-level error API. Use Adaptive Card isRequired/errorMessage/regex for client-side checks. For server-side failures, return a continue response with an error TextBlock added to the card.

  • Form data structure change: Slack nests form data as view.state.values[block_id][action_id].value. Teams flattens it as activity.value.data[inputId]. The nesting is gone — input ids 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 from Action.Submit with msteams: { type: 'task/fetch' } or from manifest commands. There is no free-standing "open dialog" API call.

  • Dynamic selects: Slack's blockSuggestion fires on each keystroke to fetch options server-side. Adaptive Card Input.ChoiceSet with style: "filtered" only filters pre-populated choices client-side. For truly dynamic data, pre-fetch at dialog open time or use Data.Query (limited support).

  • Mid-form interactions lost: Slack modals can respond to blockAction events 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.submit handler returns undefined, Teams shows a generic error. Always return a valid { status: 200, body: { task: { ... } } } response.

references

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