teams-app-developer
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
Install
npx skills add https://github.com/microsoft/skills/tree/main/.github/plugins/microsoft-365-agents-toolkit/skills/teams-app-developer
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install microsoft-skills@llmmart
git clone https://github.com/microsoft/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole microsoft/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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-provision- Missing
TENANT_IDin.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
Long-Running Commands — WAIT for completion:
atk new,atk provision,atk deploycan take several minutes- Always 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 indefinitely- ALWAYS run as a background process (
isBackground=true) — NEVER useisBackground=falsefor 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
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-beta
If ATK is not found or version is too old:
npm i -g @microsoft/m365agentstoolkit-cli@beta
CLI 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 (skills)
-
create-project
-
create-project.md 8.1 KB
# Create Project Scaffold a new Microsoft 365 agent or Teams app from an ATK template. ## Template Selection Guide | User Wants | Capability | |------------|------------| | Extend M365 Copilot with custom instructions | `declarative-agent` | | Declarative Agent with new API | `declarative-agent-action` | | Declarative Agent with new API (Bearer Token) | `declarative-agent-action-bearer` | | Declarative Agent with new API (OAuth) | `declarative-agent-action-oauth` | | Declarative Agent with existing OpenAPI spec | `declarative-agent-action-from-existing-api` | | Connect MCP Server to Copilot | `declarative-agent-with-action-from-mcp` | | Declarative Agent with Copilot Connector | `declarative-agent-with-graph-connector` | | Declarative Agent for MetaOS | `declarative-agent-meta-os-new-project` | | Declarative Agent from TypeSpec | `declarative-agent-typespec` | | Agent with custom LLM (Azure OpenAI, etc.) | `basic-custom-engine-agent` | | Weather forecast agent | `weather-agent` | | Agent using Azure AI Foundry | `foundry-agent-to-m365` | | Teams chatbot with AI | `teams-agent` | | Teams bot with RAG/knowledge base | `teams-agent-rag-customize` | | Teams Agent with Azure AI Search | `teams-agent-rag-azure-ai-search` | | Teams Agent with Custom API | `teams-agent-rag-custom-api` | | Teams Collaborator Agent | `teams-collaborator-agent` | | Simple Teams echo bot | `bot` | | Teams tab app | `tab` | | Teams message extension | `message-extension` | | Copilot Connector | `copilot-connector` | See [../toolkit/templates.md](../toolkit/templates.md) for the complete template catalog with language support and descriptions. ## Creating Projects Create templates in the current directory with one generic flow: ```bash # 1) Scaffold into a temporary parent folder atk new -c <template-id> -n <project-name> -f /tmp -l <language> -i false # 2) Move generated files from the scaffold subfolder into current directory mv /tmp/<project-name>/. . # 3) Remove the empty scaffold folder rmdir /tmp/<project-name> ``` Common examples: ```bash # Declarative Agent (no -l needed) atk new -c declarative-agent -n my-agent -f /tmp -i false # Declarative Agent with new API atk new -c declarative-agent-action -l typescript -n my-api-agent -f /tmp -i false # Declarative Agent with existing OpenAPI spec atk new -c declarative-agent-action-from-existing-api -n my-agent -a <openapi-spec-url-or-path> -o "GET /repairs" -o "POST /repairs" -f /tmp -i false # Custom Engine Agent atk new -c basic-custom-engine-agent -l typescript -n my-cea -f /tmp -i false # Teams Agent with RAG atk new -c teams-agent-rag-customize -l typescript -n my-rag-agent -f /tmp -i false ``` PowerShell equivalent: ```powershell # 1) Scaffold into temporary folder atk new -c <template-id> -n <project-name> -f $env:TEMP -l <language> -i false # 2) Move files into current directory Move-Item "$env:TEMP\<project-name>\*" . Move-Item "$env:TEMP\<project-name>\.*" . -ErrorAction SilentlyContinue # 3) Remove scaffold folder Remove-Item "$env:TEMP\<project-name>" -Force ``` ## Creating from Samples ```bash atk new sample <sample-id> ``` To place sample files in current directory, scaffold first and then move files from the sample output folder into `.` using the same move pattern as above. | Sample | Sample ID (`atk new sample <sample-id>`) | Tags | | ------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------- | | Langchain Agent with Agent365 SDK in NodeJS | `agent365-langchain-nodejs` | Agent365, TS | | Agent Framework Agent with Agent365 SDK in Python | `agent365-agentframework-python` | Agent365, Python | | OpenAI Agent with Agent365 SDK in Python | `agent365-openai-python` | Agent365, Python | | Claude Agent with Agent365 SDK in NodeJS | `agent365-claude-nodejs` | Agent365, TS | | Tab App with Azure Backend | `hello-world-tab-with-backend` | Tab, TS, Azure Functions, Dev Proxy | | Bot App with SSO Enabled | `bot-sso` | Bot, TS, Adaptive Cards, SSO | | Team Central Dashboard | `team-central-dashboard` | Tab, TS, Azure Functions, SSO | | Copilot connector App | `copilot-connector-app` | Tab, Azure Functions, TS, SSO, Copilot connector | | Teams Conversation Bot using Python | `bot-conversation-python` | Python, Bot, Bot Framework | | Teams Messaging Extensions Search using Python | `msgext-search-python` | Python, Message extension, Bot Framework | | Travel Agent | `travel-agent` | C#, Custom Engine Agent, M365 Copilot Retrieval API, Agents SDK, Agent Framework | | Coffee Agent | `coffee-agent` | TS, Custom Engine Agent, Adaptive Cards, Microsoft Teams SDK | | Data Analyst Agent v2 | `data-analyst-agent-v2` | TS, Custom Engine Agent, Data Visualization, Adaptive Cards, LLM SQL, Microsoft Teams SDK | List all samples with `atk list samples`. ## Notes - `declarative-agent` does NOT require `-l` language flag - `declarative-agent-action-from-existing-api` requires `-a` (OpenAPI spec) and `-o` (operation IDs like `"GET /path"`) - Always use `-i false` for non-interactive scripted creation - `atk new` can take several minutes — wait for completion (timeout 120000ms+) - If template/sample already matches the requirement, do not run dependency install by default; continue only when user asks for next steps ## After Scaffolding Once the project is created: - To test locally → see [../test-playground/test-playground.md](../test-playground/test-playground.md) - To understand project files → see [../toolkit/manifest-and-yaml.md](../toolkit/manifest-and-yaml.md) ## Expert Deep Dives > **Applies to: code-based Teams bots/agents only** (templates: `bot`, `teams-agent*`, `basic-custom-engine-agent`, `weather-agent`, `coffee-agent`, `bot-sso`, `msgext-*`, `tab*`). > > Does **not** apply to declarative agents, API plugins, Copilot connectors, or `declarative-agent-*` / `copilot-connector` templates — those have no source code to scaffold against. For those, follow the in-template instructions and the [Microsoft 365 Copilot extensibility docs](https://learn.microsoft.com/microsoft-365-copilot/extensibility/) directly. For deeper guidance on what `atk new` produces and how to extend it, consult the Teams expert micro-files: | Topic | Expert | |---|---| | Project file layout, `package.json`, `tsconfig.json`, npm scripts, `appPackage/` | [../experts/teams/project.scaffold-files-ts.md](../experts/teams/project.scaffold-files-ts.md) | | `App` constructor, plugins, credentials, runtime initialization | [../experts/teams/runtime.app-init-ts.md](../experts/teams/runtime.app-init-ts.md) | | Teams app manifest schema, scopes, bots/composeExtensions/staticTabs | [../experts/teams/runtime.manifest-ts.md](../experts/teams/runtime.manifest-ts.md) | | Routing handlers (`app.on('message')`, activity types, invokes) | [../experts/teams/runtime.routing-handlers-ts.md](../experts/teams/runtime.routing-handlers-ts.md) |
-
-
docs
-
advanced-features.md 7 KB
# Advanced Features ## Scheduled Messages | Aspect | Slack | Teams | |---|---|---| | Native API | `chat.scheduleMessage()` | **No equivalent** | | Cancel scheduled | `chat.deleteScheduledMessage()` | N/A | | Reminders | `reminders.add()` | **No equivalent** | **Rating:** RED (Slack → Teams), GREEN (Teams → Slack). ### Mitigation Strategies (Slack → Teams) | Strategy | How | Effort | |---|---|---| | **Azure Functions timer + Cosmos DB (Recommended)** | Store scheduled message in Cosmos DB. Azure Functions timer trigger polls and sends via proactive messaging. | 16–24 hrs | | **Azure Queue visibility timeout** | Set visibility timeout to delay message processing. 7-day maximum. | 8–12 hrs | | **Azure Service Bus scheduled messages** | Best for high-volume exact-time delivery. | 12–16 hrs | | **Power Automate** | Offload to Power Automate flows with "Delay until" action. Requires license. | 8–12 hrs | | **In-process timer (dev only)** | `setTimeout` / `node-cron`. Not durable — lost on restart. | 2–4 hrs | ### Reverse Direction (Teams → Slack) Use `chat.scheduleMessage()` and `reminders.add()` directly — native APIs. --- ## Emoji Reactions | Aspect | Slack | Teams | |---|---|---| | Event | `reaction_added` / `reaction_removed` | `messageReaction` | | Reaction types | Unlimited custom emoji | **6 fixed reactions only**: like, heart, laugh, surprised, sad, angry | | Workflow use | Common to use reactions as workflow signals (e.g., `:white_check_mark:` = approved) | Not viable — too few options | **Rating:** RED (Slack → Teams) if reactions are used as workflow signals. ### Mitigation (Slack → Teams) | Strategy | How | Effort | |---|---|---| | **Adaptive Card buttons (Recommended)** | Replace reaction-based workflows with `Action.Submit` buttons on cards (e.g., "Approve" / "Reject"). Better for audit trails. | 4–8 hrs | | **Map to 6 fixed reactions** | Map your most important reactions to like/heart/laugh/surprised/sad/angry. Lossy — only works if you use ≤6 reactions. | 2–4 hrs | ### Reverse Direction (Teams → Slack) Slack supports unlimited custom emoji reactions — direct mapping. --- ## Shortcuts / Message Extensions | Aspect | Slack | Teams | |---|---|---| | Global shortcut | `app.shortcut("callback_id")` | Compose extension with `context: ["compose", "commandBox"]` | | Message shortcut | `app.shortcut("callback_id")` (type: `message_shortcut`) | Action extension with `context: ["message"]` | | Fire-and-forget | Supported (ack + background work) | **Not supported** — must open task module | | Manifest config | Shortcut in app settings | `composeExtensions[].commands[]` | | Message context | `shortcut.message` | `activity.value.messagePayload` | **Rating:** YELLOW — functional equivalents exist but UX differs. ### Key Difference Slack shortcuts can run background actions without showing UI (ack + do work). Teams compose/action extensions always open a task module — there's no fire-and-forget pattern. Use a "minimal dismiss" pattern: return a tiny "Done" card that auto-closes. ### Mitigation (Slack → Teams) | Strategy | How | Effort | |---|---|---| | **Compose extension (Recommended)** | `composeExtensions` with `commandBox` context. Opens task module. | 8–12 hrs | | **Minimal-dismiss pattern** | Task module returns tiny "Done" card for fire-and-forget actions. | 4–8 hrs | | **Bot command replacement** | Replace shortcut with typed command. Simpler but less discoverable. | 2–4 hrs | --- ## Channel Operations | Aspect | Slack | Teams | |---|---|---| | Create channel | `conversations.create()` | Graph `POST /teams/{team-id}/channels` | | Archive channel | `conversations.archive()` | **No equivalent** — Teams can only archive entire Teams | | Set topic | `conversations.setTopic()` | Graph `PATCH /channels/{id}` with `description` | | Invite member | `conversations.invite()` | Graph `POST /channels/{id}/members` (one call per member) | | Remove member | `conversations.kick()` | Graph `DELETE /channels/{id}/members/{membership-id}` (must resolve membership ID first) | | Channel namespace | Flat (channel ID is globally unique) | Team-scoped (need `team-id` + `channel-id`) | | Channel name limits | 80 chars, most characters allowed | 50 chars, no special characters | **Rating:** GREEN for create/topic/invite, YELLOW for remove (membership ID resolution), RED for archive. ### Archive Mitigation (Slack → Teams) | Strategy | How | Effort | |---|---|---| | **Rename with [ARCHIVED] prefix (Recommended)** | Rename channel, update description. Cosmetic but non-destructive. | 4–8 hrs | | **Rename + remove all members** | Stronger enforcement but destructive — members must be re-invited to undo. | 8–12 hrs | | **Team-level archive** | Archive entire Team. Only works if channel is in a dedicated Team. | 2–4 hrs | --- ## Workflows / Automation | Aspect | Slack | Teams | |---|---|---| | Platform | Workflow Builder (free) | Power Automate (licensed for premium connectors) | | Bot integration | `workflow_step_execute` event | Custom connectors or bot-driven orchestration | | Triggers | Channel message, emoji reaction, scheduled, webhook | Same + Approvals connector, Planner, SharePoint | | Migration tool | N/A | **None** — manual rebuild required | **Rating:** YELLOW — functional equivalent exists but different platform, possible licensing. ### Mitigation Strategies | Strategy | How | Effort | |---|---|---| | **Bot-driven orchestration (Recommended)** | Keep workflow logic in the bot. State machine + Adaptive Card buttons + persistent storage. No license dependency. | 16–40 hrs | | **Power Automate rebuild** | Rebuild in Power Automate. Custom steps need Premium license. | 24–80 hrs | | **Hybrid** | Simple flows → Power Automate, complex → bot-driven. | Varies | | **Teams Workflows app** | Simplified UI for basic automations (free). Limited to simple scenarios. | 4–8 hrs | --- ## App Distribution | Aspect | Slack | Teams | |---|---|---| | Directory listing | Slack App Directory (api.slack.com) | Teams App Store via Partner Center | | Review time | Hours to days | 1–2 weeks | | Org-level install | Workspace admin approval | Teams Admin Center tenant-wide deployment | | Dev install | Direct install via OAuth URL | Sideloading (ZIP with manifest + icons) | | Required assets | App icon | 192x192 full-color icon + 32x32 monochrome outline | | Multi-tenant | Per-workspace tokens via `InstallationStore` | `signInAudience: "AzureADMultipleOrgs"` in Azure AD | **Rating:** YELLOW — both have distribution mechanisms but packaging and review differ. ### Sideloading (Dev/Test) Teams sideloading requires: 1. `manifest.json` (schema v1.19+) 2. `color.png` (192x192) 3. `outline.png` (32x32 monochrome) 4. ZIP all three files 5. Upload via Teams client → Apps → Manage your apps → Upload 6. Note: Sideloading may be disabled by admin — check tenant settings ### Reverse Direction (Teams → Slack) Submit to Slack App Directory via api.slack.com. Implement `InstallProvider` for OAuth install flow. Shorter review cycle. -
feature-gaps.md 46.3 KB
# Feature Gap Analysis: Slack ↔ Teams A complete inventory of every feature that does **not** have a direct equivalent on the other platform, organized by severity. Each gap includes mitigations in both directions. ## How to Read This Document - **Slack → Teams** = you have a Slack bot and are adding Teams support - **Teams → Slack** = you have a Teams bot and are adding Slack support - Effort estimates are per-feature implementation hours - Features with direct 1:1 mappings (GREEN) are not listed — see [messaging-and-commands.md](messaging-and-commands.md) and [ui-components.md](ui-components.md) for those --- ## RED Gaps — No Platform Equivalent These features exist on one platform with **no counterpart** on the other. They require redesign, custom infrastructure, or acceptance of reduced functionality. --- ### R1. Ephemeral Messages **Slack has it. Teams does not.** Slack's `chat.postEphemeral()` sends a message visible only to one user in a channel. Teams has no visibility flag — all bot messages are visible to everyone. | Strategy | Direction | How | Effort | |---|---|---|---| | `refresh.userIds` on `Action.Execute` | Slack → Teams | Card shows different content per user. Covers ~80% of cases. Max 60 user IDs per card. | 4–8 hrs | | Route to 1:1 chat | Slack → Teams | Send private content to user's personal bot chat via proactive messaging. Different UX but reliable. | 2–4 hrs | | Build `sendEphemeral()` helper | Slack → Teams | Wrapper that auto-detects context and picks the best strategy. Worth it if many handlers use ephemeral. | 8–12 hrs | | Drop ephemeral behavior | Slack → Teams | Show messages to everyone. Simplest but may expose private data. | 0 hrs | | **Native `chat.postEphemeral()`** | **Teams → Slack** | **Direct API call. No gap in this direction.** | **0 hrs** | --- ### R2. Custom Emoji Reactions **Slack has it. Teams does not.** Slack supports unlimited custom emoji as reactions. Teams supports exactly 6 fixed reactions: like, heart, laugh, surprised, sad, angry. Bots that use reactions as workflow signals (`:white_check_mark:` = approved) cannot map to Teams. | Strategy | Direction | How | Effort | |---|---|---|---| | Adaptive Card buttons | Slack → Teams | Replace reaction workflows with `Action.Submit` buttons (e.g., "Approve" / "Reject"). Better audit trail. | 4–8 hrs | | Map to 6 fixed reactions | Slack → Teams | Map most important reactions to like/heart/laugh/surprised/sad/angry. Lossy — only works with ≤6 reactions. | 2–4 hrs | | **Native emoji reactions** | **Teams → Slack** | **Direct mapping. Slack supports unlimited custom emoji.** | **0 hrs** | --- ### R3. Modal Cancel Notification (`viewClosed`) **Slack has it. Teams does not.** Slack fires `view_closed` when a user dismisses a modal (with `notify_on_close: true`). Teams sends no notification when a dialog is dismissed — the bot never knows the user cancelled. | Strategy | Direction | How | Effort | |---|---|---|---| | Timeout + explicit Cancel button | Slack → Teams | Add a "Cancel" button inside the dialog. Implement 5-min TTL for cleanup of stale locks/state. | 4–8 hrs | | Accept stale state | Slack → Teams | Drop cancel cleanup. Accept that some locks may persist until TTL. | 0 hrs | | **Native `notify_on_close: true`** | **Teams → Slack** | **Set `notify_on_close: true` in `views.open()`. Native support.** | **0 hrs** | --- ### R4. Mid-Form Dynamic Updates **Slack has it. Teams does not.** Slack modals support `dispatch_action: true` on inputs, which fires `block_actions` events while the modal is open. The bot can then call `views.update()` to change the modal dynamically (e.g., show/hide fields based on a dropdown selection). Teams dialogs have no equivalent — Adaptive Card inputs don't fire events until the form is submitted. | Strategy | Direction | How | Effort | |---|---|---|---| | Multi-step dialogs | Slack → Teams | Split dependent fields across dialog steps. Step 1 collects the trigger value; step 2 shows dependent fields. | 8–16 hrs | | `Action.ToggleVisibility` | Slack → Teams | Show/hide elements client-side. Works for simple show/hide but cannot fetch server data. | 2–4 hrs | | Web-based task module | Slack → Teams | Embed a full web form in an iframe with real-time interactivity. Full control but much more effort. | 16–24 hrs | | **Native `block_actions` + `views.update()`** | **Teams → Slack** | **Set `dispatch_action: true` on input elements. Handle `block_actions` and call `views.update()`.** | **2–4 hrs** | --- ### R5. Server-Side Field Validation with Inline Errors **Slack has it. Teams does not.** Slack's `view_submission` handler can return `response_action: "errors"` with a map of `{ block_id: "error message" }` to show inline validation errors without closing the modal. Teams dialogs close on submit — there is no way to keep the dialog open with error messages. | Strategy | Direction | How | Effort | |---|---|---|---| | Re-open dialog with errors | Slack → Teams | On validation failure, return a new dialog card pre-populated with the user's data and error messages in field labels. | 4–8 hrs | | Client-side validation only | Slack → Teams | Use Adaptive Card `isRequired`, `regex`, `maxLength`, `min`/`max`. Covers simple cases but not async checks (e.g., "username taken"). | 1–2 hrs | | **Native `response_action: "errors"`** | **Teams → Slack** | **Return `{ response_action: "errors", errors: { block_id: "msg" } }` from `view_submission` handler.** | **0 hrs** | --- ### R6. Dialog / Modal Stacking **Slack has it. Teams does not.** Slack supports `views.push()` to stack up to 3 modals. The user can navigate back by dismissing the top modal. Teams dialogs do not stack — opening a new dialog replaces the current one. | Strategy | Direction | How | Effort | |---|---|---|---| | Single dialog with step routing | Slack → Teams | One dialog with internal step state. Submit handler checks step number and returns the next step's card. Add a "Back" button that decrements the step. | 8–16 hrs | | Build `StepDialog` helper | Slack → Teams | Reusable class managing step state, forward/back navigation. Worth it if 3+ wizard flows exist. | 16–24 hrs | | Sequential separate dialogs | Slack → Teams | Close current dialog, open next. No back navigation. Degraded UX. | 4–8 hrs | | **Native `views.push()`** | **Teams → Slack** | **Call `views.push()` from within a `view_submission` or `block_actions` handler. Up to 3 levels.** | **0 hrs** | --- ### R7. Scheduled Message API **Slack has it. Teams does not.** Slack provides `chat.scheduleMessage()` and `chat.deleteScheduledMessage()` as first-class APIs. Teams has no server-side scheduling — the bot must build its own. | Strategy | Direction | How | Effort | |---|---|---|---| | Azure Functions timer + Cosmos DB | Slack → Teams | Store message + target time in DB. Timer function polls every minute and sends via proactive messaging. | 16–24 hrs | | Azure Queue visibility timeout | Slack → Teams | Enqueue with `visibilityTimeout` set to the delay. Queue trigger fires at the right time. 7-day max. | 8–12 hrs | | Azure Service Bus scheduled messages | Slack → Teams | `scheduleMessages(msg, scheduledTime)`. Exact-time delivery, native cancellation. Best for high volume. | 12–16 hrs | | Power Automate | Slack → Teams | "Delay until" action in a flow. No code but requires license for custom connectors. | 8–12 hrs | | In-process timer (dev only) | Slack → Teams | `setTimeout` / `node-cron`. Not durable — lost on restart. | 2–4 hrs | | **Native `chat.scheduleMessage()`** | **Teams → Slack** | **Direct API call with `post_at` Unix timestamp. Native cancellation via `deleteScheduledMessage()`.** | **0 hrs** | --- ### R8. Channel Archive **Slack has it. Teams does not.** Slack's `conversations.archive()` archives a channel — it becomes read-only and hidden from the channel list. Teams can only archive an entire Team, not individual channels. | Strategy | Direction | How | Effort | |---|---|---|---| | Rename with `[ARCHIVED]` prefix | Slack → Teams | Rename channel, update description to "Archived on {date}". Non-destructive. Cosmetic only. | 4–8 hrs | | Rename + remove all members | Slack → Teams | Rename + kick everyone. Stronger enforcement but destructive and hard to undo. | 8–12 hrs | | Team-level archive | Slack → Teams | Archive the entire Team via Graph. Only works if the channel has a dedicated Team. | 2–4 hrs | | **Native `conversations.archive()`** | **Teams → Slack** | **Direct API call. Reversible via `conversations.unarchive()`.** | **0 hrs** | --- ### R9. Retroactive Link Unfurling **Slack has it. Teams does not.** Slack unfurls links in existing messages (edited to add a link, or links posted before the bot was installed). Teams only unfurls links in new messages — editing a message to add a link does not trigger unfurling. | Strategy | Direction | How | Effort | |---|---|---|---| | **Accept the limitation (Recommended)** | Slack → Teams | No workaround exists. New message unfurling works fine. | 0 hrs | | Manual preview command | Slack → Teams | Bot command where users paste a URL to get a preview card. Niche use case. | 4–8 hrs | | **Native retroactive unfurling** | **Teams → Slack** | **Slack unfurls retroactively by default. No issue.** | **0 hrs** | --- ### R10. Firewall-Friendly Transport (Socket Mode) **Slack has it. Teams does not.** Slack's Socket Mode uses an outbound WebSocket — no inbound ports needed. The bot can run behind any firewall. Teams requires a public HTTPS endpoint for inbound webhooks. | Strategy | Direction | How | Effort | |---|---|---|---| | Deploy to Azure | Slack → Teams | Host in App Service / Functions / Container Apps. Use Dev Tunnels for local dev. Standard cloud deployment. | 4–8 hrs | | Azure Relay | Slack → Teams | Hybrid connection for strict on-premises firewalls that cannot expose any public endpoint. Adds latency. | 8–16 hrs | | **Native Socket Mode** | **Teams → Slack** | **Set `socketMode: true` with `appToken`. Outbound WebSocket, zero inbound ports.** | **1–2 hrs** | --- ## RED Gap Workarounds Detailed implementation patterns for every RED gap. These are the recommended approaches — pick the one that fits your bot's needs. --- ### R1 Workaround: Ephemeral via `refresh.userIds` The best general-purpose workaround. An `Action.Execute` card with `refresh.userIds` shows personalized content to specific users while showing a default card to everyone else. ```typescript // Teams: per-user card content (replaces chat.postEphemeral) const card = { type: "AdaptiveCard", version: "1.4", refresh: { action: { type: "Action.Execute", verb: "personalView", data: { requestId: "123" }, }, userIds: [actingUserId], // max 60 IDs }, body: [ { type: "TextBlock", text: "A request was submitted." }, // everyone sees this ], }; // When the specified user views the card, Teams invokes the bot: app.on("card.action", async (ctx) => { if (ctx.activity.value?.action?.verb === "personalView") { // Return a personalized card only this user sees return { status: 200, body: { type: "AdaptiveCard", version: "1.4", body: [ { type: "TextBlock", text: "Your request #123 was approved.", weight: "Bolder" }, { type: "TextBlock", text: "Only you can see these details." }, ], }, }; } }); ``` **When this doesn't work:** More than 60 users need per-user views, or the content is plain text (not a card). Fall back to sending a proactive message in the user's 1:1 bot chat. **Reverse (Teams → Slack):** Use `chat.postEphemeral({ channel, user, text })` directly. Native support. --- ### R2 Workaround: Reactions → Adaptive Card Buttons Replace emoji-reaction workflows with explicit card buttons. This actually improves auditability — button clicks are tracked, emoji reactions are not. ```typescript // Before (Slack): reaction-based approval app.event("reaction_added", async ({ event, client }) => { if (event.reaction === "white_check_mark") { await client.chat.postMessage({ channel: event.item.channel, text: `Approved by <@${event.user}>`, thread_ts: event.item.ts, }); } }); // After (Teams): button-based approval const approvalCard = { type: "AdaptiveCard", version: "1.5", body: [{ type: "TextBlock", text: "Request #42 needs approval" }], actions: [ { type: "Action.Submit", title: "Approve", style: "positive", data: { action: "approve", requestId: "42" } }, { type: "Action.Submit", title: "Reject", style: "destructive", data: { action: "reject", requestId: "42" } }, ], }; ``` **When reactions are decorative** (not workflow signals): map to the 6 fixed Teams reactions. Only viable if you use ≤6 distinct reactions. **Reverse (Teams → Slack):** Map `Action.Submit` buttons to emoji reactions via `reactions.add`, or keep as Slack buttons (usually better UX anyway). --- ### R3 Workaround: Cancel Detection via TTL + Explicit Button Since Teams sends no notification when a dialog is dismissed, combine two strategies: ```typescript // 1. Add an explicit Cancel button inside the dialog card const dialogCard = { type: "AdaptiveCard", version: "1.5", body: [/* form fields */], actions: [ { type: "Action.Submit", title: "Submit", data: { action: "submit_form" } }, { type: "Action.Submit", title: "Cancel", data: { action: "cancel_form", lockId: "abc" } }, ], }; // 2. Handle explicit cancel app.on("dialog.submit", async ({ activity, send }) => { const data = activity.value?.data; if (data?.action === "cancel_form") { await releaseLock(data.lockId); return { status: 200, body: { task: { type: "message", value: "Cancelled." } } }; } // ... handle submit ... }); // 3. TTL-based cleanup for users who close via the X button setInterval(async () => { const staleLocks = await getLocksOlderThan(5 * 60_000); // 5 min for (const lock of staleLocks) await releaseLock(lock.id); }, 60_000); ``` **Reverse (Teams → Slack):** Use `notify_on_close: true` in `views.open()` and handle `view_closed` callback. --- ### R4 Workaround: Mid-Form Updates via Multi-Step Dialogs Split dependent fields across dialog steps. Step 1 collects the value that drives the dynamic behavior; step 2 renders the dependent fields. ```typescript app.on("dialog.submit", async ({ activity }) => { const data = activity.value?.data; if (data?.step === 1) { // User selected a category — return step 2 with dependent fields const subcategories = await getSubcategories(data.category); return { status: 200, body: { task: { type: "continue", value: { title: "Step 2 of 2", card: buildStep2Card(data.category, subcategories), }, }, }, }; } if (data?.step === 2) { // Final submission await processForm(data); return { status: 200, body: { task: { type: "message", value: "Done!" } } }; } }); ``` **For simple show/hide** (no server data needed): use `Action.ToggleVisibility` to show/hide card elements client-side. This works for "show advanced options" toggles but cannot populate options from an API. **Reverse (Teams → Slack):** Use `dispatch_action: true` on inputs + `views.update()` in the `block_actions` handler. Native support for real-time form updates. --- ### R5 Workaround: Server Validation via Dialog Re-render On validation failure, return a `continue` response with the same form, pre-populated with the user's values, plus error messages as colored `TextBlock` elements. ```typescript app.on("dialog.submit", async ({ activity }) => { const data = activity.value?.data; const errors: string[] = []; if (!data?.email?.includes("@")) errors.push("Invalid email address"); if ((data?.name?.length ?? 0) < 2) errors.push("Name must be at least 2 characters"); if (errors.length > 0) { return { status: 200, body: { task: { type: "continue", value: { title: "Fix Errors", card: { contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [ // Error banner ...errors.map(e => ({ type: "TextBlock", text: e, color: "Attention", weight: "Bolder", })), // Re-populate form with user's previous values { type: "Input.Text", id: "name", label: "Name", value: data.name ?? "" }, { type: "Input.Text", id: "email", label: "Email", value: data.email ?? "" }, ], actions: [{ type: "Action.Submit", title: "Submit", data: { action: "register" } }], }, }, }, }, }, }; } // Validation passed await processRegistration(data); return { status: 200, body: { task: { type: "message", value: "Registered!" } } }; }); ``` **Combine with client-side validation** for the best UX: add `isRequired`, `regex`, and `errorMessage` to catch obvious errors before the server round-trip. **Reverse (Teams → Slack):** Use `response_action: "errors"` with `{ block_id: "error message" }` natively. --- ### R6 Workaround: Modal Stacking via Step Routing Simulate `views.push` with a single dialog that routes by step number. Include a "Back" button that decrements the step. ```typescript app.on("dialog.submit", async ({ activity }) => { const data = activity.value?.data; const step = data?.step ?? 1; if (data?.action === "back") { return continueDialog(buildStepCard(step - 1, data)); } if (step < 3) { return continueDialog(buildStepCard(step + 1, data)); } // Final step — process all collected data await processWizard(data); return { status: 200, body: { task: { type: "message", value: "Complete!" } } }; }); function continueDialog(card: object) { return { status: 200, body: { task: { type: "continue", value: { title: `Step ${(card as any).step}`, card } } }, }; } function buildStepCard(step: number, previousData: Record<string, unknown>): object { // Each step card embeds ALL previous data in Action.Submit.data // so nothing is lost between steps return { contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [/* step-specific fields */], 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" } }, ], }, step, }; } ``` **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()` natively — up to 3 levels of stacking with built-in "X to go back" behavior. --- ### R7 Workaround: Scheduling via Azure Service Bus The most production-ready approach. Azure Service Bus supports exact-time delivery and native cancellation. ```typescript import { ServiceBusClient } from "@azure/service-bus"; const sbClient = new ServiceBusClient(process.env.SERVICEBUS_CONNECTION!); const sender = sbClient.createSender("scheduled-messages"); // Schedule a message async function scheduleMessage( conversationId: string, text: string, sendAt: Date ): Promise<Long> { const [sequenceNumber] = await sender.scheduleMessages( { body: { conversationId, text } }, sendAt ); return sequenceNumber; // store this for cancellation } // Cancel a scheduled message async function cancelScheduled(sequenceNumber: Long): Promise<void> { await sender.cancelScheduledMessages(sequenceNumber); } // Receiver (runs as a separate process or Azure Function) const receiver = sbClient.createReceiver("scheduled-messages"); receiver.subscribe({ processMessage: async (msg) => { const { conversationId, text } = msg.body; await teamsApp.send(conversationId, text); }, processError: async (err) => console.error(err), }); ``` **For simpler needs:** Azure Queue with `visibilityTimeout` (max 7 days) or Azure Functions timer + Cosmos DB (poll every minute). **Reverse (Teams → Slack):** Use `chat.scheduleMessage({ channel, text, post_at })` natively. --- ### R8 Workaround: Channel Archive via Rename + Description The most widely used workaround. Cosmetic-only — doesn't actually prevent new messages. ```typescript async function archiveChannel( graph: Client, teamId: string, channelId: string ): Promise<void> { const channel = await graph.api(`/teams/${teamId}/channels/${channelId}`).get(); await graph.api(`/teams/${teamId}/channels/${channelId}`).patch({ displayName: `[ARCHIVED] ${channel.displayName}`.substring(0, 50), description: `Archived on ${new Date().toISOString()}. Original: ${channel.description ?? ""}`, }); } ``` **For stronger enforcement:** After renaming, remove all non-owner members. This is destructive (members must be re-invited to undo) but prevents new messages. **Reverse (Teams → Slack):** Use `conversations.archive()` natively. Reversible via `conversations.unarchive()`. --- ### R9 Workaround: Retroactive Unfurling **No workaround exists.** Teams only unfurls links in new messages. Accept this limitation — it affects a small percentage of use cases (links in edited messages or messages sent before the bot was installed). If critical, build a `/preview <url>` bot command that returns a card preview on demand. --- ### R10 Workaround: Firewall Transport **Deploy to a cloud provider.** This is the standard path for any Teams bot. For local development, use Dev Tunnels (built into VS Code) or ngrok. For strict on-premises environments that truly cannot expose any endpoint, Azure Relay provides a hybrid connection where the bot connects outbound to Azure, and Azure proxies inbound Teams traffic through that connection. This adds 10–50ms latency but requires zero inbound firewall rules. **Reverse (Teams → Slack):** Enable Socket Mode with `socketMode: true` and `appToken`. Zero inbound ports, zero tunneling. --- ## YELLOW Gaps — Equivalent Exists but Requires Design Decisions These features have functional equivalents on the other platform, but the mapping is not 1:1 and requires choosing an approach. --- ### Y1. Slash Commands **Slack has native `/command`. Teams does not.** | Strategy | Direction | How | Effort | |---|---|---|---| | Text pattern matching | Slack → Teams | Detect command-like text in `app.on("message")`. Accept `weather` and `/weather`. | 2–4 hrs | | Manifest bot commands | Slack → Teams | Add `commands[]` to manifest for Teams command menu. Not `/` prefix but discoverable. | 1–2 hrs | | Message extension | Slack → Teams | `composeExtensions` for richer command UX with search results or task modules. | 8–12 hrs | | **Native `app.command()`** | **Teams → Slack** | **Register via `app.command("/cmd", handler)`. Add `ack()` call. Configure in Slack app dashboard.** | **2–4 hrs** | --- ### Y2. Thread Broadcast (`reply_broadcast`) **Slack has it as a single call. Teams requires two.** | Strategy | Direction | How | Effort | |---|---|---|---| | Two API calls | Slack → Teams | Call `reply()` (thread) + `send()` (channel) separately. | 1–2 hrs | | `replyWithBroadcast()` wrapper | Slack → Teams | Convenience method that calls both internally. | 2–4 hrs | | **Native `reply_broadcast: true`** | **Teams → Slack** | **Single `say()` call with `reply_broadcast: true`.** | **0 hrs** | --- ### Y3. Thread Discovery **Slack has `conversations.replies()`. Teams uses Graph API.** | Strategy | Direction | How | Effort | |---|---|---|---| | Graph API direct | Slack → Teams | `GET /teams/{teamId}/channels/{channelId}/messages/{messageId}/replies`. Requires `ChannelMessage.Read.All`. | 4–8 hrs | | `getThreadReplies()` helper | Slack → Teams | Wrapper encapsulating Graph client setup, auth, and pagination. | 8–12 hrs | | **Native `conversations.replies()`** | **Teams → Slack** | **Direct API call with thread `ts`.** | **0 hrs** | --- ### Y4/5/6. File Upload **Slack: one call. Teams: 3-step consent flow.** | Strategy | Direction | How | Effort | |---|---|---|---| | `sendFile()` helper | Slack → Teams | Unified wrapper: auto-detects personal/channel, routes to OneDrive/SharePoint, chunks >4 MB. | 24–40 hrs | | Manual FileConsentCard | Slack → Teams | Implement 3-step consent flow directly. Verbose and error-prone. | 16–24 hrs | | **Native `files.uploadV2()`** | **Teams → Slack** | **Single API call. No consent step.** | **1–2 hrs** | --- ### Y7. Link Unfurling Deadline **Slack: 30-minute async. Teams: 5-second sync.** | Strategy | Direction | How | Effort | |---|---|---|---| | Cache-first with prefetch | Slack → Teams | Cache middleware wraps handler. Pre-populate for known URLs. Without this, slow unfurls silently fail. | 12–16 hrs | | Synchronous handler only | Slack → Teams | Direct handler. Only viable for fast data sources (<5 seconds). | 4–8 hrs | | **Native async `chat.unfurl()`** | **Teams → Slack** | **Handle `link_shared` event. Respond within 30 minutes via `chat.unfurl()`.** | **2–4 hrs** | --- ### Y8. Reminders **Slack has `reminders.add()`. Teams does not.** | Strategy | Direction | How | Effort | |---|---|---|---| | Piggyback on scheduler (R7) | Slack → Teams | Reuse scheduled message infrastructure. `setReminder()` stores + sends to 1:1 chat at target time. | 4–8 hrs (if scheduler exists) | | Power Automate + Planner | Slack → Teams | Create Planner tasks with due-date notifications. | 8–12 hrs | | **Native `reminders.add()`** | **Teams → Slack** | **Direct API call. Platform-managed delivery.** | **0 hrs** | --- ### Y9. Dynamic Select Menus (Server-Side Typeahead) **Slack has `external_data_source` + `block_suggestion`. Teams does not.** Slack's `app.options()` handler receives keystrokes and returns filtered results from the server. Teams' `Input.ChoiceSet` is client-side only. | Strategy | Direction | How | Effort | |---|---|---|---| | Pre-populated `Input.ChoiceSet` | Slack → Teams | Load all options at dialog open. Client-side filtering via `style: "filtered"`. Works up to ~500 items. | 2–4 hrs | | Two-step dialog | Slack → Teams | Step 1: text input for search. Step 2: filtered results as `ChoiceSet`. Works for any dataset size. | 8–12 hrs | | Web-based task module | Slack → Teams | Embed a web view with search-as-you-type. Full control. High effort. | 16–24 hrs | | **Native `block_suggestion`** | **Teams → Slack** | **Set `external_data_source: true` on select. Handle `app.options()` for server-side filtering.** | **2–4 hrs** | --- ### Y10. App Home **Slack has `app_home_opened` + `views.publish()`. Teams uses tabs.** | Strategy | Direction | How | Effort | |---|---|---|---| | `tab.fetch` handler | Slack → Teams | Personal tab returns Adaptive Card on every open. Closest to `app_home_opened`. | 4–8 hrs | | Welcome card on install | Slack → Teams | Send card to 1:1 chat on `install.add`. Simple but fires once. | 1–2 hrs | | Static web tab | Slack → Teams | Full web page in iframe. Richer but needs hosting + Teams JS SDK. | 8–16 hrs | | **Native `views.publish()`** | **Teams → Slack** | **Listen for `app_home_opened` event. Call `views.publish()` with Block Kit.** | **2–4 hrs** | --- ### Y11. View Hash (Race Condition Protection) **Slack has `view_hash`. Teams does not.** Slack's `views.update()` accepts a `view_hash` parameter. If the view has changed since the hash was captured, the update is rejected. This prevents race conditions. Teams has no equivalent. | Strategy | Direction | How | Effort | |---|---|---|---| | Manual `_version` field | Slack → Teams | Inject version counter into `Action.Submit.data`. Reject updates where the submitted version doesn't match the stored version. | 2–4 hrs | | Card versioning middleware | Slack → Teams | SDK plugin auto-injecting and checking version counters on every card send/receive. | 4–8 hrs | | **Native `view_hash`** | **Teams → Slack** | **Pass `view_hash` from the previous `views.open()` / `views.update()` response.** | **0 hrs** | --- ### Y12. Global Shortcuts **Slack has `app.shortcut()` (global). Teams uses compose extensions.** | Strategy | Direction | How | Effort | |---|---|---|---| | Compose extension | Slack → Teams | `composeExtensions` with `context: ["compose", "commandBox"]`. Always opens task module. | 8–12 hrs | | Minimal-dismiss pattern | Slack → Teams | Task module returns tiny "Done" card for fire-and-forget actions. | 4–8 hrs | | Bot command | Slack → Teams | Replace shortcut with typed command. Simpler but less discoverable. | 2–4 hrs | | **Native `app.shortcut()`** | **Teams → Slack** | **Register global shortcut callback. Can fire-and-forget (ack + background work).** | **2–4 hrs** | --- ### Y13. Message Shortcuts **Slack has `message_shortcut`. Teams uses action-based message extensions.** | Strategy | Direction | How | Effort | |---|---|---|---| | Action message extension | Slack → Teams | `composeExtensions` command with `context: ["message"]`. Message payload in `activity.value.messagePayload`. | 4–8 hrs | | **Native `message_shortcut`** | **Teams → Slack** | **Register `app.shortcut()` with type `message_shortcut`. Message in `shortcut.message`.** | **2–4 hrs** | --- ### Y14. Confirmation Dialogs on Buttons **Slack has native `confirm` object. Teams does not.** | Strategy | Direction | How | Effort | |---|---|---|---| | `Action.ShowCard` inline | Slack → Teams | Inline expand with "Are you sure?" + Yes/No buttons. Native Adaptive Card. | 2–4 hrs | | Task module confirm | Slack → Teams | Small dialog popup. More prominent, closer to Slack UX. | 4–6 hrs | | `confirmAction()` helper | Slack → Teams | Template function generating confirm cards. Reusable. | 4–8 hrs | | **Native `confirm` object** | **Teams → Slack** | **Add `confirm` object to button element. Platform-rendered popup.** | **0 hrs** | --- ### Y15. Unfurl Domain Wildcards **Slack supports `*.example.com`. Teams requires exact domain listing.** | Strategy | Direction | How | Effort | |---|---|---|---| | Manual enumeration | Slack → Teams | List every subdomain in manifest `domains[]`. Fine for <10. | 1–2 hrs | | Manifest generator script | Slack → Teams | Script reads subdomain list from config and generates manifest array. | 4–8 hrs | | **Native wildcard support** | **Teams → Slack** | **Wildcards work out of the box.** | **0 hrs** | --- ### Y16. All Channel Messages Without @Mention **Slack gets them by default. Teams requires RSC permission.** | Strategy | Direction | How | Effort | |---|---|---|---| | RSC permission | Slack → Teams | Add `ChannelMessage.Read.Group` to manifest `webApplicationInfo.applicationPermissions`. Config only. | 1–2 hrs | | Require @mention | Slack → Teams | Change UX to require @mention. Simplifies permissions but changes behavior. | 0 hrs | | **Default behavior** | **Teams → Slack** | **Slack bots receive all messages in channels they're added to. No config needed.** | **0 hrs** | --- ### Y17. Built-in Retry / Resilience **Slack Bolt has `retryConfig`. Teams SDK has no built-in retry.** | Strategy | Direction | How | Effort | |---|---|---|---| | Build `RetryPlugin` | Slack → Teams | Plugin with exponential backoff, jitter, circuit breaker. | 12–16 hrs | | Manual retry wrapper | Slack → Teams | Hand-roll backoff around outbound calls. Simpler but easy to get wrong. | 4–8 hrs | | **Native Bolt `retryConfig`** | **Teams → Slack** | **Configure in `App` constructor. Built-in exponential backoff.** | **0 hrs** | --- ### Y18. Workflow Builder **Slack has it (free). Teams uses Power Automate (licensed).** | Strategy | Direction | How | Effort | |---|---|---|---| | Bot-driven orchestration | Slack → Teams | State machine + Adaptive Card buttons + persistent storage. No license dependency. | 16–40 hrs | | Power Automate rebuild | Slack → Teams | Rebuild in Power Automate. Custom steps need Premium license. | 24–80 hrs | | Teams Workflows app | Slack → Teams | Simplified UI for basic automations (free). Limited scenarios. | 4–8 hrs | | Hybrid | Slack → Teams | Simple flows → Power Automate, complex → bot-driven. | Varies | | **Native Workflow Builder** | **Teams → Slack** | **Rebuild in Slack Workflow Builder. Free, no license.** | **8–16 hrs** | --- ### Y19. App Distribution **Both platforms have app stores, but packaging and review differ.** | Strategy | Direction | How | Effort | |---|---|---|---| | Org app catalog | Slack → Teams | Publish to organization catalog via Teams Admin Center. Requires admin approval. | 2–4 hrs | | Sideloading | Slack → Teams | ZIP manifest + icons. Upload via Teams client. May be disabled by admin. | 1–2 hrs | | Partner Center (public) | Slack → Teams | Submit to Teams App Store. 1–2 week review. Requires Partner Network account. | 8–16 hrs | | **Slack App Directory** | **Teams → Slack** | **Submit via api.slack.com. Hours-to-days review. Implement `InstallProvider` for OAuth install flow.** | **4–8 hrs** | --- --- ## YELLOW Gap Best Practices Recommended approaches for every YELLOW gap. These are the patterns that produce the best cross-platform UX with the least effort. --- ### Y1. Slash Commands — Best Practice **Use text pattern matching + manifest bot commands together.** Register commands in the Teams manifest for discoverability (users see them in the command menu), AND detect them via text pattern matching as a fallback. Accept both `/weather` and `weather` so users migrating from Slack don't have to retrain muscle memory. ```typescript // Teams: detect both patterns app.message(/^\/?weather$/i, async (ctx) => { const response = await handleWeather(); await ctx.send(response); }); ``` In the Teams manifest: ```json { "commands": [{ "title": "weather", "description": "Check the weather" }] } ``` **Don't:** Create a message extension for every slash command. Reserve extensions for commands that benefit from rich search results or task module UI. --- ### Y2. Thread Broadcast — Best Practice **Write a one-line helper that makes both calls.** Don't over-engineer this. ```typescript async function replyWithBroadcast(ctx: any, text: string): Promise<void> { await ctx.reply(text); await ctx.send(text); } ``` **Don't:** Try to batch these into a single API call — Teams doesn't support it. Two calls is the correct pattern. --- ### Y3. Thread Discovery — Best Practice **Use Graph API directly with the `ctx.appGraph` client.** Don't build a wrapper unless you need pagination across multiple threads. ```typescript const replies = await ctx.appGraph .api(`/teams/${teamId}/channels/${channelId}/messages/${messageId}/replies`) .top(50) .get(); ``` **Watch out for:** `ChannelMessage.Read.All` is an application permission requiring admin consent. If you only need thread replies in the bot's own conversations, you may be able to use delegated permissions instead. --- ### Y4/5/6. File Upload — Best Practice **Build the `sendFile()` helper.** The manual FileConsentCard flow is a 30-line footgun that's easy to get wrong. A helper that auto-detects personal vs. channel context and handles chunking for large files pays for itself after the second use. **Key decisions:** - Personal chat → FileConsentCard flow (requires `supportsFiles: true` in manifest) - Channel → Direct Graph API upload to SharePoint (no consent card) - Files >4 MB → Graph resumable upload session with 320 KB–60 MB chunks **Don't:** Store pending file buffers in memory for long periods. Upload promptly or stream to a temporary blob. --- ### Y7. Link Unfurling — Best Practice **Always use a cache layer.** The 5-second Teams deadline makes this non-optional. Cache aggressively: 1. On first unfurl, fetch and cache the preview data 2. Set a reasonable TTL (5–60 minutes depending on data freshness needs) 3. For known high-traffic URLs, pre-populate the cache on startup ```typescript const cache = new Map<string, { data: any; expires: number }>(); app.on("message.ext.query-link", async ({ activity }) => { const url = activity.value?.url; const cached = cache.get(url); if (cached && cached.expires > Date.now()) { return buildUnfurlResponse(cached.data); } const data = await fetchPreviewData(url); // must complete in <4 seconds cache.set(url, { data, expires: Date.now() + 300_000 }); // 5 min TTL return buildUnfurlResponse(data); }); ``` **Don't:** Make multiple API calls in the unfurl handler. Pre-fetch or batch data sources. --- ### Y8. Reminders — Best Practice **Piggyback on whatever scheduling infrastructure you built for R7.** Don't create a separate system. A reminder is just a scheduled message sent to a 1:1 conversation. ```typescript async function setReminder(userId: string, text: string, when: Date): Promise<void> { const conversationId = await get1to1ConversationId(userId); await scheduleMessage(conversationId, `Reminder: ${text}`, when); } ``` **Don't:** Use Power Automate + Planner for bot reminders — it adds an external dependency and licensing complexity. Keep it in the bot. --- ### Y9. Dynamic Select Menus — Best Practice **Pre-populate with `Input.ChoiceSet` `style: "filtered"` for datasets under 500 items.** This covers the vast majority of cases (user lists, category selects, project dropdowns). ```json { "type": "Input.ChoiceSet", "id": "user_select", "label": "Assign to", "style": "filtered", "choices": [ { "title": "Alice Smith", "value": "alice@company.com" }, { "title": "Bob Jones", "value": "bob@company.com" } ] } ``` **For datasets over 500 items:** Use a two-step dialog. Step 1 is a text input for search. The submit handler queries the server and returns step 2 with filtered results as a `ChoiceSet`. **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. --- ### Y10. App Home — Best Practice **Use `tab.fetch` to return an Adaptive Card.** It fires on every tab open (like `app_home_opened`) and supports `tab.submit` for interactions within the tab. ```typescript app.on("tab.fetch", async (ctx) => { const userData = await getUserDashboard(ctx.activity.from?.aadObjectId ?? ""); return { status: 200, body: { tab: { type: "continue", value: { cards: [{ card: buildDashboardCard(userData) }] }, }, }, }; }); ``` **Don't:** Use a static web tab unless you need rich interactivity beyond what Adaptive Cards can provide (charts, real-time updates, complex navigation). Web tabs require hosting, CORS configuration, and the Teams JS SDK. --- ### Y11. View Hash — Best Practice **Inject a `_version` counter into every card's `Action.Submit.data`.** Increment on every update. Reject submissions where the version doesn't match. ```typescript let cardVersion = 0; function buildCard(data: any): object { cardVersion++; return { type: "AdaptiveCard", version: "1.5", body: [/* ... */], actions: [{ type: "Action.Submit", title: "Update", data: { ...data, _version: cardVersion }, }], }; } app.on("card.action", async (ctx) => { const submitted = ctx.activity.value?.action?.data; if (submitted?._version !== cardVersion) { await ctx.send("This card is outdated. Please use the latest version."); return; } // Process the update... }); ``` **Don't:** Skip version checking for low-traffic bots — race conditions happen even with single users (fast double-clicks, multiple tabs). --- ### Y12. Global Shortcuts — Best Practice **Use compose extensions for actions that open a form.** For fire-and-forget actions (no UI), use the minimal-dismiss pattern: return a tiny "Done" card that auto-closes. ```json { "composeExtensions": [{ "commands": [{ "id": "quickAction", "type": "action", "title": "Quick Action", "context": ["compose", "commandBox"], "fetchTask": true }] }] } ``` **Don't:** Replace every shortcut with a bot command. Commands are less discoverable than compose extensions, which appear in the Teams UI with icons and descriptions. --- ### Y13. Message Shortcuts — Best Practice **Use action-based message extensions with `context: ["message"]`.** This is the closest 1:1 mapping to Slack's message shortcuts. Access the original message via `activity.value.messagePayload` — it contains the message text, sender, and timestamp. **Don't:** Forget to add `fetchTask: true` in the manifest command. Without it, the extension silently does nothing when clicked. --- ### Y14. Confirmation Dialogs — Best Practice **Use `Action.ShowCard` for inline confirmation.** It expands inline without leaving the current context — closest to Slack's native `confirm` popup. ```json { "type": "Action.ShowCard", "title": "Delete", "card": { "type": "AdaptiveCard", "body": [{ "type": "TextBlock", "text": "Are you sure you want to delete this?", "weight": "Bolder" }], "actions": [ { "type": "Action.Submit", "title": "Yes, delete", "style": "destructive", "data": { "action": "confirm_delete", "itemId": "42" } }, { "type": "Action.Submit", "title": "Cancel", "data": { "action": "cancel_delete" } } ] } } ``` **Don't:** Open a full task module dialog for a simple yes/no confirmation. It's too heavy for the interaction. --- ### Y15. Unfurl Domain Wildcards — Best Practice **Enumerate domains in the manifest.** For fewer than 10 subdomains, list them manually. For more, write a build-time script that reads your subdomain list and generates the manifest array. ```json { "composeExtensions": [{ "messageHandlers": [{ "type": "link", "value": { "domains": [ "app.example.com", "docs.example.com", "api.example.com" ] } }] }] } ``` **Don't:** Try to register a single wildcard domain — Teams will reject it silently. --- ### Y16. All Channel Messages — Best Practice **Add the RSC permission to the manifest.** It's config-only, no code change, and matches Slack's default behavior. ```json { "webApplicationInfo": { "id": "{{CLIENT_ID}}", "resource": "api://{{CLIENT_ID}}" }, "authorization": { "permissions": { "resourceSpecific": [ { "name": "ChannelMessage.Read.Group", "type": "Application" } ] } } } ``` Also set `activity.mentions.stripText: true` in the App constructor to remove `<at>bot</at>` text from messages that do include an @mention. **Don't:** Change your UX to require @mention unless your bot genuinely shouldn't listen to all messages. --- ### Y17. Built-in Retry — Best Practice **Build a retry utility with exponential backoff and jitter.** Apply it to all outbound API calls (Graph, proactive messaging, external services). ```typescript async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (err: any) { if (attempt === maxRetries) throw err; const retryAfter = err?.response?.headers?.["retry-after"]; const baseDelay = retryAfter ? parseInt(retryAfter) * 1000 : 1000 * 2 ** attempt; const jitter = Math.random() * 1000; await new Promise(r => setTimeout(r, baseDelay + jitter)); } } throw new Error("Unreachable"); } ``` **For proactive broadcasts** (sending to hundreds of users): use `p-queue` with concurrency control (e.g., 5 concurrent sends) to stay within Teams' rate limits (~1 msg/sec/conversation). **Don't:** Retry without jitter. Without random delay, multiple bot instances retry at the same time and cause a thundering herd. --- ### Y18. Workflow Builder — Best Practice **Keep workflow logic in the bot (bot-driven orchestration).** This avoids Power Automate licensing dependencies and keeps everything in one codebase. Pattern: state machine with Adaptive Card buttons for user decisions, persistent storage for workflow state, and proactive messaging for notifications. **When to use Power Automate instead:** Approval workflows that benefit from the built-in Approvals connector, and simple recurring tasks that business users should manage themselves. **Don't:** Build a hybrid system (some flows in Power Automate, some in the bot) unless you have a clear organizational reason. Two systems means two places to debug. --- ### Y19. App Distribution — Best Practice **Start with sideloading for dev/test, use org catalog for internal deployment, and Partner Center only for public distribution.** Sideloading checklist: 1. `manifest.json` — schema v1.19+, valid `id`, correct `botId` 2. `color.png` — 192x192 full-color icon 3. `outline.png` — 32x32 transparent monochrome outline 4. ZIP all three (no nested folders) 5. Upload via Teams client → Apps → Manage your apps **Don't:** Submit to Partner Center (public store) until the bot is fully stable. The 1–2 week review cycle makes iteration slow. Use org catalog for internal users. --- ## Summary: Gap Asymmetry Most RED gaps are asymmetric — they only apply in one direction. The pattern is clear: | Direction | RED gaps to handle | Why | |---|---|---| | **Slack → Teams** | 10 RED gaps | Teams lacks ephemeral, custom reactions, modal stacking, cancel notifications, mid-form updates, field validation, scheduling, channel archive, retroactive unfurl, Socket Mode | | **Teams → Slack** | 0 RED gaps | Slack has native support for everything Teams offers, plus more | This means **adding Slack to a Teams bot is significantly easier** than adding Teams to a Slack bot. A Teams → Slack migration mostly involves mapping concepts 1:1 (Adaptive Cards → Block Kit, `app.on("message")` → `app.message()`, etc.) with few design decisions. A Slack → Teams migration requires redesigning multiple interaction patterns. ### Effort Estimates by Bot Complexity | Profile | Slack Features Used | Slack → Teams Effort | Teams → Slack Effort | |---|---|---|---| | **A** — Simple | Messages, basic commands, simple cards | 8–16 hrs | 4–8 hrs | | **B** — Moderate | A + ephemeral, threads, files, basic interactivity | 40–80 hrs | 8–16 hrs | | **C** — Complex | B + shortcuts, App Home, unfurling, dynamic selects, modals | 80–160 hrs | 16–32 hrs | | **D** — Full | C + workflows, scheduling, Socket Mode, stacked modals | 160–300 hrs | 32–48 hrs | -
files-and-links.md 4.2 KB
# Files & Links ## File Upload | Aspect | Slack | Teams | |---|---|---| | Upload API | `files.uploadV2()` — single call | FileConsentCard → user consent → Graph API upload (3-step flow) | | Large files | Handled automatically | Graph resumable upload sessions for >4 MB | | Sharing links | `files.sharedPublicURL()` | Graph `createLink()` | | File events | `file_shared` event | Check `activity.attachments` in message handler | | Download | `files.info()` → `url_private` with Bearer token | `attachment.content.downloadUrl` (pre-authenticated, short-lived) | | Manifest config | None | `supportsFiles: true` required | | Context | Works in channels and DMs | FileConsentCard works in personal chat only; channels use direct Graph upload | **Rating:** YELLOW (Slack → Teams), GREEN (Teams → Slack). ### Impact Slack's one-call `files.uploadV2()` becomes a 3-step flow in Teams: send consent card → user approves → upload via Graph API. Missing the `supportsFiles: true` manifest flag causes silent failure. ### Mitigation Strategies (Slack → Teams) | Strategy | How | Effort | |---|---|---| | **`sendFile()` helper (Recommended)** | Unified wrapper: auto-detects personal/channel context, routes to OneDrive or SharePoint, handles >4 MB chunking. The manual flow is error-prone. | 24–40 hrs | | **Manual FileConsentCard** | Implement the 3-step consent flow directly. Works but verbose and easy to get wrong. | 16–24 hrs per upload pattern | ### Reverse Direction (Teams → Slack) Use `files.uploadV2()` directly — much simpler than the Teams consent flow. No consent step needed. ### File Download | Aspect | Slack | Teams | |---|---|---| | URL lifetime | Permanent (with valid token) | Pre-authenticated URL expires quickly | | Auth required | Bearer token in request | URL is pre-authenticated | **Mitigation:** For Teams downloads, use the URL immediately or cache the file. Don't store the download URL for later use. --- ## Link Unfurling / Previews | Aspect | Slack | Teams | |---|---|---| | Event | `link_shared` event (async) | `message.ext.query-link` handler (synchronous) | | Response deadline | 30 minutes (via `chat.unfurl()`) | **5 seconds** | | Domain matching | Wildcards supported (`*.example.com`) | **Exact domain only** — must enumerate every subdomain | | Manifest config | Event subscription in app settings | `composeExtensions[].messageHandlers[].value.domains` | | Retroactive unfurling | Unfurls links in existing messages | **New messages only** | | Response format | Attachment with Block Kit | Adaptive Card via `composeExtension` result | **Rating:** YELLOW for basic unfurling, RED for retroactive unfurling and wildcard domains. ### Impact The 5-second deadline is the critical difference. Slack allows async unfurling up to 30 minutes later. Teams requires a synchronous response within 5 seconds — any slow data source (API call, database query, rendering) will silently fail. ### Mitigation Strategies (Slack → Teams) | Strategy | How | Effort | |---|---|---| | **Cache-first with prefetch (Recommended)** | Cache middleware wraps the handler. Pre-populate cache for known URLs. Without this, slow unfurls silently die. | 12–16 hrs | | **Synchronous handler only** | Direct handler, must return within 5 seconds. Only viable for fast data sources (in-memory, pre-cached). | 4–8 hrs | ### Wildcard Domains (Slack → Teams) | Strategy | How | Effort | |---|---|---| | **Manual enumeration (Recommended)** | List every subdomain in manifest `domains` array. Fine for <10 subdomains. | 1–2 hrs | | **Manifest generator script** | Script reads subdomains from config and generates the manifest array. Worth it for 10+ subdomains. | 4–8 hrs | ### Reverse Direction (Teams → Slack) Use `link_shared` event with `chat.unfurl()`. Slack supports wildcards and async responses — both are easier than the Teams model. ### Retroactive Unfurling | Direction | Behavior | |---|---| | Slack → Teams | **Platform gap.** Teams only unfurls links in new messages. No workaround exists. Consider a bot command where users paste a URL to get a preview card. | | Teams → Slack | Slack unfurls links retroactively by default. No issue. | **Rating:** RED — no mitigation. Accept the limitation. -
identity-and-auth.md 4.3 KB
# Identity & Auth ## User IDs | Aspect | Slack | Teams | |---|---|---| | Format | Prefixed strings: `U...` (user), `C...` (channel), `T...` (team), `B...` (bot) | GUIDs: AAD object IDs, opaque conversation IDs | | User identity | `message.user` → Slack user ID | `activity.from.id` → AAD object ID | | Cross-reference | `users.info({ user })` → email, display name | `userGraph.me()` → email, display name | | Channel identity | `channel_id` (flat namespace) | `conversation.id` (scoped to team) | **Rating:** RED — IDs are completely incompatible. No conversion formula exists. ### Impact Any stored data keyed by Slack user/channel IDs (preferences, history, permissions) cannot be directly used with Teams IDs. A mapping layer is required. ### Mitigation Strategy Use **email** as the common identity attribute: 1. Build a mapping table: `Slack user ID → email → AAD Object ID` 2. Populate from Slack's `users.info()` and Teams' Graph API `users/{id}` 3. Re-key stored data during migration 4. For new dual-platform bots, key data by email from the start Effort: 8–16 hrs depending on data volume. --- ## Authentication & Signing | Aspect | Slack | Teams | |---|---|---| | Request verification | `signingSecret` — HMAC-SHA256 of `v0:{timestamp}:{body}` | Bot Framework JWT — automatic validation by SDK | | Manual verification | Required if using raw HTTP | Required only without SDK (REST-only integration) | | Bot credentials | `SLACK_BOT_TOKEN` (xoxb-...) | `CLIENT_ID` + `CLIENT_SECRET` + `TENANT_ID` | | App-level token | `SLACK_APP_TOKEN` (xapp-..., Socket Mode only) | Not applicable | **Rating:** GREEN — both SDKs handle verification automatically. **Mitigation:** No code changes needed when using SDKs. Both handle signing/verification internally. For REST-only integrations, see the verification patterns below. ### REST-Only Verification **Slack (manual HMAC):** ``` signature = HMAC-SHA256(signingSecret, "v0:{timestamp}:{rawBody}") compare with X-Slack-Signature header reject if timestamp > 5 minutes old ``` **Teams (manual JWT):** ``` fetch OpenID config from https://login.botframework.com/v1/.well-known/openidconfiguration validate JWT from Authorization header verify audience = your bot's CLIENT_ID verify issuer = https://api.botframework.com ``` --- ## OAuth & Tokens | Aspect | Slack | Teams | |---|---|---| | User OAuth | `users:read`, `chat:write`, etc. scopes | Azure AD Graph permissions (delegated) | | Bot token | `xoxb-...` (per-workspace) | Bot Framework token (per-tenant, auto-managed) | | Token storage | `InstallationStore` (per-workspace bot+user tokens) | Not needed — SDK handles token lifecycle | | SSO | Not native — redirect flow | Built-in with `oauth: { defaultConnectionName }` | | Sign-in flow | OAuth redirect to Slack authorize URL | `ctx.signin()` sends OAuth card in chat | | Sign-out | Revoke token via API | `ctx.signout()` | | Multi-tenant | `InstallationStore` with per-workspace tokens | `signInAudience: "AzureADMultipleOrgs"` in Azure AD | **Rating:** YELLOW — both support OAuth but the flows and storage models differ significantly. ### Key Difference Slack requires per-workspace token management via `InstallationStore`. Teams SDK manages tokens automatically — you just configure `clientId`, `clientSecret`, `tenantId`, and `oauth.defaultConnectionName`. ### Mitigation (Slack → Teams) 1. Remove `InstallationStore` (not needed) 2. Register an OAuth connection in Azure Bot resource settings 3. Add `oauth: { defaultConnectionName: "graph" }` to App constructor 4. Guard handlers with `ctx.isSignedIn` check 5. Call `ctx.signin()` when authentication is needed ### Mitigation (Teams → Slack) 1. Implement `InstallationStore` for per-workspace token storage 2. Configure OAuth scopes in Slack app settings 3. Set up `InstallProvider` for the OAuth install flow 4. Store bot and user tokens per workspace 5. Use stored tokens for API calls via `WebClient` ### Slack OAuth Scopes → Teams Graph Permissions | Slack Scope | Teams Graph Permission | Notes | |---|---|---| | `users:read` | `User.Read` (delegated) | | | `users:read.email` | `User.Read` (delegated) | Email included by default | | `chat:write` | Bot sends via SDK (no permission) | | | `channels:read` | `Channel.ReadBasic.All` | | | `channels:history` | `ChannelMessage.Read.All` | | | `files:read` | `Files.Read` | | | `files:write` | `Files.ReadWrite` | | -
infrastructure.md 6.7 KB
# Infrastructure ## Transport | Aspect | Slack | Teams | |---|---|---| | Primary transport | Socket Mode (WebSocket) or HTTP | **HTTPS only** (inbound webhook) | | Firewall-friendly | Socket Mode — outbound WebSocket, no inbound ports | **Requires public HTTPS endpoint** | | Default endpoint | `/slack/events` | `/api/messages` | | Local development | Socket Mode (no tunnel needed) | Dev Tunnels or ngrok required | | Request verification | HMAC-SHA256 (`signingSecret`) | Bot Framework JWT (automatic) | **Rating:** GREEN for HTTP-to-HTTPS, RED for Socket Mode → HTTPS (firewall environments). ### Impact Slack bots using Socket Mode run behind firewalls with zero inbound ports. Teams requires a public HTTPS endpoint — a fundamental architecture change for firewall-restricted environments. ### Mitigation (Slack Socket Mode → Teams) | Strategy | How | Effort | |---|---|---| | **Deploy to Azure (Recommended)** | Host in Azure App Service / Functions / Container Apps. Use Dev Tunnels for local dev. | 4–8 hrs | | **Azure Relay** | Hybrid connection for strict on-premises firewalls. Adds latency. | 8–16 hrs | ### Dual-Bot Transport For bots targeting both platforms simultaneously: | Pattern | How | |---|---| | **Socket Mode + HTTP (Recommended)** | Slack uses WebSocket (no HTTP needed), Teams uses Express on port 3978. No port conflicts. Simplest setup. | | **Shared Express** | Both use HTTP. Slack `ExpressReceiver` at `/slack/events`, Teams adapter at `/api/messages`. Requires careful body-parsing middleware ordering. | --- ## Compute (AWS ↔ Azure) | AWS | Azure | Notes | |---|---|---| | Lambda + API Gateway | Azure Functions | Teams bots need 3-second response; Functions Consumption has 5–10s cold starts | | ECS / Fargate | Container Apps | Best for long-running bots with streaming | | EC2 | App Service | Always-on, predictable latency | ### Cold Start Warning Azure Functions Consumption plan has 5–10 second cold starts that violate the Teams 3-second response timeout. Mitigations: | Strategy | Cost Impact | |---|---| | **App Service with Always On (Recommended)** | Fixed cost but no cold starts | | **Functions Premium with Always Ready** | Higher cost, eliminates cold starts | | **Container Apps (min replicas ≥ 1)** | Moderate cost, no scale-to-zero | --- ## Storage (AWS ↔ Azure) | AWS | Azure | Notes | |---|---|---| | S3 | Blob Storage | Hot/Cool/Archive tiers | | DynamoDB | Cosmos DB | Table API (lowest effort) or Core SQL (richer querying) | | RDS (MySQL) | Azure Database for MySQL | Managed migration service available | | RDS (PostgreSQL) | Azure Database for PostgreSQL | Managed migration service available | | RDS (SQL Server) | Azure SQL | Direct migration path | ### Bot State Storage | Aspect | Slack | Teams | |---|---|---| | SDK storage | No built-in state management | `IStorage` interface with pluggable backends | | Default | Developer manages state | In-memory (lost on restart) | | Production | External DB (Redis, PostgreSQL, etc.) | Cosmos DB, Azure SQL, or custom `IStorage` | **Mitigation:** Implement the Teams `IStorage` interface with Cosmos DB for bot state. Use serverless pricing for development, provisioned RUs for production. --- ## Secrets & Configuration | AWS | Azure | Notes | |---|---|---| | Secrets Manager | Key Vault | Sensitive credentials | | SSM Parameter Store | App Configuration | Non-secret configuration | | IAM roles | Managed Identity | Zero-secret authentication | | Environment variables | App Settings | Runtime configuration | ### Bot Credentials | Credential | Slack | Teams | |---|---|---| | Bot token | `SLACK_BOT_TOKEN` | Managed by SDK (`CLIENT_ID` + `CLIENT_SECRET`) | | Signing/verification | `SLACK_SIGNING_SECRET` | Automatic JWT validation | | Socket Mode | `SLACK_APP_TOKEN` | N/A | | Tenant | N/A | `TENANT_ID` | ### Production Secret Management | Strategy | How | |---|---| | **Key Vault references (Recommended)** | `@Microsoft.KeyVault(SecretUri=...)` in App Settings. Zero-code secret injection. Requires managed identity. | | **Managed identity for bot auth** | `managedIdentityClientId: "system"` in App constructor. Eliminates `CLIENT_SECRET` entirely. | | **`DefaultAzureCredential`** | Chains managed identity → environment → CLI → VS Code. Works everywhere. | --- ## Observability (AWS ↔ Azure) | AWS | Azure | Notes | |---|---|---| | CloudWatch Logs | Application Insights + Log Analytics | KQL query language (different from CloudWatch Insights) | | CloudWatch Metrics | Azure Monitor Metrics | `trackMetric()` | | CloudWatch Alarms | Azure Monitor Alerts | KQL-based alerting | | X-Ray | Application Insights distributed tracing | Operation IDs, `traceparent` headers | ### Bot Health Monitoring Key metrics to track for both platforms: | Metric | Why | |---|---| | Request rate | Volume baseline | | Response time (P50/P95/P99) | Detect slowdowns before they cause timeouts | | Failure rate | Catch errors before users report them | | Active conversations | Usage trends | | AI/external API latency | Dependency health | ### Setup ```typescript // Application Insights — must be first import import appInsights from "applicationinsights"; appInsights.setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING).start(); // Then import everything else import { App } from "@microsoft/teams.apps"; ``` **Pitfall:** Late instrumentation import. `applicationinsights` must run before `http`/`https` are loaded or distributed tracing won't work. --- ## Rate Limiting & Resilience | Aspect | Slack | Teams | |---|---|---| | Rate limit signal | HTTP 429 + `Retry-After` header | HTTP 429 + `Retry-After` header | | Built-in retry | Bolt `retryConfig` option | **No built-in retry** | | Conversation limits | ~1 msg/sec per method per token | ~1 msg/sec per conversation, ~30 msg/min per conversation | | Graph API limits | N/A | Separate throttling (per-app per-tenant) | | Invoke timeout | N/A | 3–10 seconds (varies by invoke type) | **Rating:** GREEN for basic rate limiting, YELLOW for resilience patterns. ### Mitigation (Slack → Teams) Build a `RetryPlugin` with exponential backoff + jitter: | Component | Purpose | |---|---| | **Exponential backoff** | Wait 1s, 2s, 4s, 8s between retries | | **Jitter** | Add random delay to prevent thundering herd | | **Circuit breaker** | Stop retrying after N consecutive failures | | **`p-queue`** | Concurrency control for proactive broadcast (avoid bursting) | Effort: 12–16 hrs for a production-grade retry plugin. ### Reverse Direction (Teams → Slack) Use Bolt's built-in `retryConfig` option: ```typescript const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, // Built-in retry with exponential backoff }); ``` -
interactive-responses.md 4.4 KB
# Interactive Responses ## Ephemeral Messages | Aspect | Slack | Teams | |---|---|---| | User-only messages | `chat.postEphemeral()` or `respond({ response_type: "ephemeral" })` | **No native equivalent** | | Per-user card views | Not available (use ephemeral messages) | `Action.Execute` with `refresh.userIds` | | Default command response | Ephemeral | Visible to everyone | **Rating:** RED (Slack → Teams), GREEN (Teams → Slack). ### Impact Any Slack bot that uses ephemeral messages for user-only feedback — confirmation dialogs, error messages, inline help — has no direct Teams equivalent. Messages are visible to everyone unless workarounds are used. ### Mitigation Strategies (Slack → Teams) | Strategy | How | Effort | |---|---|---| | **`refresh.userIds` (Recommended)** | Adaptive Cards with `Action.Execute` and `refresh.userIds` show different card content per user. Covers ~80% of cases. Limited to 60 user IDs. | 4–8 hrs | | **1:1 chat fallback** | Route ephemeral content to user's personal bot chat via proactive messaging. Different UX (separate conversation) but reliable. | 2–4 hrs | | **`sendEphemeral()` helper** | Wrapper that auto-detects context and picks the best strategy. Worth it if reused across multiple handlers. | 8–12 hrs | | **Drop ephemeral behavior** | Show messages to everyone. Simplest but may expose private data. | 0 hrs | ### Reverse Direction (Teams → Slack) `refresh.userIds` per-user card views map to Slack's native ephemeral messages. Use `chat.postEphemeral()` directly. --- ## Message Updates and Replacements | Aspect | Slack | Teams | |---|---|---| | Replace original | `respond({ replace_original: true })` | Return card from invoke handler, or `ctx.updateActivity(activityId)` | | Delete original | `respond({ delete_original: true })` | `ctx.deleteActivity(activityId)` | | Update by ID | `chat.update({ ts, channel, ... })` | `ctx.updateActivity({ id: activityId, ... })` | | Response URL | `response_url` — valid 30 min, max 5 uses | No equivalent concept | | Activity ID | `message.ts` (timestamp-based) | `activity.id` or `activity.replyToId` | **Rating:** GREEN — direct mapping with different API shapes. ### Key Difference Slack uses `response_url` (a webhook URL that expires after 30 minutes and allows up to 5 responses). Teams has no `response_url` — you update messages by storing and referencing the `activity.id`. **Mitigation:** Store the `activity.id` when sending messages that may need updating. Use `ctx.updateActivity()` with the stored ID. --- ## Button Actions | Aspect | Slack | Teams | |---|---|---| | Handler registration | `app.action("action_id", handler)` | `app.on("card.action", handler)` routing on `data.action` | | Action identifier | `action_id` on button element | `data.action` (or `data.verb`) in `Action.Submit` | | Button value | `action.value` | `activity.value.action.data` | | Acknowledgement | Must `ack()` within 3 seconds | Automatic | | Follow-up response | `respond()` (response_url) | `ctx.send()` or `ctx.updateActivity()` | **Rating:** GREEN — direct mapping with different routing mechanisms. **Mitigation:** In Slack, each button has a unique `action_id` with its own handler. In Teams, all `Action.Submit` buttons route through `card.action`; use a `data.action` field to dispatch: ```typescript // Teams — route by data.action app.on("card.action", async (ctx) => { const action = ctx.activity.value?.action?.data?.action; switch (action) { case "approve": /* ... */ break; case "reject": /* ... */ break; } }); ``` --- ## Confirmation Dialogs | Aspect | Slack | Teams | |---|---|---| | Native support | `confirm` object on button elements | **No native equivalent** | | Behavior | Platform-rendered "Are you sure?" popup | Must be built manually | **Rating:** YELLOW (Slack → Teams), GREEN (Teams → Slack). ### Mitigation Strategies (Slack → Teams) | Strategy | How | Effort | |---|---|---| | **`Action.ShowCard` inline (Recommended)** | Inline expand with "Are you sure?" text and Yes/No buttons. Native Adaptive Card pattern. | 2–4 hrs | | **Task module confirm** | Small dialog popup for confirmation. More prominent, closer to Slack UX. | 4–6 hrs | | **`confirmAction()` helper** | Template function generating confirm cards. Reusable across multiple buttons. | 4–8 hrs | ### Reverse Direction (Teams → Slack) Use the native `confirm` object on button elements. Built-in, no custom code needed. -
messaging-and-commands.md 4.5 KB
# Messaging & Commands ## Message Handling | Aspect | Slack | Teams | |---|---|---| | Handler | `app.message(pattern, handler)` | `app.on("message", handler)` | | Pattern matching | String (substring), RegExp, or catch-all | RegExp or manual `text.match()` | | Reply to channel | `say(text)` | `ctx.send(text)` | | Reply in thread | `say({ text, thread_ts })` | `ctx.reply(text)` | | Get message text | `message.text` | `ctx.activity.text` | | Get sender | `message.user` (Slack ID) | `ctx.activity.from.id` (AAD ID) | **Rating:** GREEN — direct mapping in both directions. **Mitigation:** Extract message handling into a platform-agnostic service layer that receives `(text, userId, platform)` and returns structured data. Each adapter converts to the platform's native format. --- ## Slash Commands | Aspect | Slack | Teams | |---|---|---| | Invocation | `/command args` | No native equivalent | | Handler | `app.command("/cmd", handler)` | `app.on("message")` with text pattern matching | | Acknowledgement | Must `ack()` within 3 seconds | Automatic — no `ack()` | | Default response | Ephemeral (user-only) | Visible to everyone | | Modal trigger | `trigger_id` from command → `views.open()` | `dialog.open` handler or Adaptive Card form | | Registration | Slack app dashboard + `commands` scope | Manifest `commands[]` array (bot commands, not slash) | **Rating:** YELLOW — functional equivalent exists but UX is fundamentally different. ### Impact - Slash commands are a core Slack interaction pattern with no Teams counterpart - Teams bot commands appear in a command menu but don't use `/` prefix - Ephemeral responses don't exist in Teams ### Mitigation Strategies | Strategy | How | Effort | |---|---|---| | **Text commands (Recommended)** | Detect command-like patterns in `app.on("message")`. Accept both `weather` and `/weather`. | 2–4 hrs | | **Manifest bot commands** | Add `commands[]` to manifest for discoverability in Teams command menu. Users type the command name. | 1–2 hrs | | **Message extension** | Use `composeExtensions` for a richer command experience with search results or task modules. | 8–12 hrs | ### Reverse Direction (Teams → Slack) Teams bot commands map directly to Slack slash commands via `app.command()`. Add `ack()` calls (required in Slack, absent in Teams) and configure the command in the Slack app dashboard. --- ## Events / Activities | Slack Event | Teams Activity | Notes | |---|---|---| | `message` | `message` | Direct mapping | | `app_mention` | `message` (in channel) | Teams channels require @mention by default | | `member_joined_channel` | `conversationUpdate` (`membersAdded`) | Different event shape | | `member_left_channel` | `conversationUpdate` (`membersRemoved`) | Different event shape | | `reaction_added` | `messageReaction` | Teams has only 6 fixed reactions | | `app_home_opened` | `install.add` (closest) | No "opened" event in Teams | | `channel_created` | No equivalent | Use Graph API subscription | | `team_join` | `conversationUpdate` (`membersAdded`) | Same event, different context | **Rating:** GREEN for most events, RED for custom emoji reactions. ### @Mention Behavior | Aspect | Slack | Teams | |---|---|---| | Channel messages | Bot receives all messages in joined channels | Bot only receives messages with @mention (default) | | Override | Default behavior | Add `ChannelMessage.Read.Group` RSC permission to manifest | | Mention stripping | Not needed | Set `activity.mentions.stripText: true` in App options | **Mitigation:** To receive all channel messages in Teams without @mention, add RSC permission to the manifest. This is a config-only change (1–2 hrs). --- ## Threading | Aspect | Slack | Teams | |---|---|---| | Reply in thread | `say({ thread_ts: message.ts })` | `ctx.reply(text)` | | Thread broadcast | `say({ thread_ts, reply_broadcast: true })` | Two API calls: `reply()` + `send()` | | Get thread replies | `conversations.replies({ ts })` | Graph API `GET /messages/{id}/replies` | | Thread discovery | Native API | Requires `ChannelMessage.Read.All` Graph permission | **Rating:** GREEN for basic threading, YELLOW for broadcast and discovery. ### Mitigation for Thread Broadcast Slack's `reply_broadcast` posts in both the thread and the channel in one call. Teams requires two separate calls: `reply()` for the thread and `send()` for the channel. Wrap in a helper: ```typescript async function replyWithBroadcast(ctx, text: string): Promise<void> { await ctx.reply(text); // Thread reply await ctx.send(text); // Channel message } ``` Effort: 1–2 hrs. -
middleware-and-handlers.md 4.4 KB
# Middleware & Handler Patterns ## Middleware | Aspect | Slack (Bolt) | Teams SDK v2 | |---|---|---| | Global middleware | `app.use(async ({ next }) => { ... await next(); })` | `app.use(async (ctx) => { ... ctx.next(); })` | | Chaining | Explicit `await next()` — omitting drops the event silently | Explicit `ctx.next()` — omitting stops the pipeline | | Listener middleware | Passed as extra args to `app.message(filter, middleware, handler)` | No equivalent — use guard functions at handler start | | Authorization | Custom middleware checking Slack user/workspace | Bot Framework JWT validation is automatic | **Rating:** GREEN — both have middleware, but Slack's is more granular. ### Key Difference Slack supports **listener middleware** — functions that run only for specific handlers. Teams has no equivalent. Convert listener middleware to guard conditions at the top of each handler: ```typescript // Slack: listener middleware app.message(isAdmin, async ({ say }) => { await say("Admin action"); }); // Teams: guard function app.on("message", async (ctx) => { if (!isAdmin(ctx.activity.from.id)) return; await ctx.send("Admin action"); }); ``` --- ## Acknowledgement (`ack()`) | Aspect | Slack | Teams | |---|---|---| | Required for | Commands, actions, view submissions, shortcuts, options | **Not applicable** — SDK handles automatically | | Deadline | 3 seconds | No manual acknowledgement | | What happens if missed | Slack shows "This app didn't respond" error to user | N/A | | Payload in ack | Commands: optional text/blocks (ephemeral). View submissions: optional `response_action`. | N/A | **Rating:** GREEN — remove `ack()` calls when porting Slack → Teams. ### Impact `ack()` is fundamental to Slack's interaction model. Every interactive handler must acknowledge within 3 seconds or the user sees an error. Teams has no equivalent — the SDK handles response timing automatically. ### Mitigation | Direction | Strategy | |---|---| | Slack → Teams | Remove all `ack()` calls. Move async work that previously happened "after ack" into the main handler body. | | Teams → Slack | Add `await ack()` as the first line of every command, action, view, shortcut, and options handler. Do async work after. | --- ## Handler Registration | Aspect | Slack (Bolt) | Teams SDK v2 | |---|---|---| | Messages | `app.message(pattern, handler)` | `app.message(pattern, handler)` or `app.on("message", handler)` | | Events | `app.event("event_name", handler)` | `app.on("routeName", handler)` | | Actions | `app.action("action_id", handler)` | `app.on("card.action", handler)` — route by `data.action` | | Modals | `app.view("callback_id", handler)` | `app.on("dialog.submit", handler)` | | Shortcuts | `app.shortcut("callback_id", handler)` | `app.on("message.ext.open", handler)` | | Options/typeahead | `app.options("action_id", handler)` | `Input.ChoiceSet` with `style: "filtered"` (client-side) | | Install events | No built-in handler | `app.on("install.add", handler)` | | Lifecycle events | No built-in handler | `app.event("start" | "error" | "signin" | "activity")` | | Order matters | First `app.message()` match wins | First match wins for `app.message()`, last registration wins for `app.on()` | **Rating:** GREEN — different APIs, same concepts. ### Key Mapping ``` Slack Teams ───────────────────────────── ───────────────────────────── app.message(pattern) → app.message(pattern) app.command("/cmd") → app.on("message") + text match app.action("id") → app.on("card.action") app.view("callback_id") → app.on("dialog.submit") app.event("name") → app.on("routeName") app.shortcut("id") → app.on("message.ext.open") app.options("id") → (client-side filtered ChoiceSet) app.use(middleware) → app.use(middleware) app.error(handler) → app.event("error", handler) ``` --- ## Error Handling | Aspect | Slack (Bolt) | Teams SDK v2 | |---|---|---| | Global handler | `app.error(async (error) => { ... })` | `app.event("error", ({ error, log }) => { ... })` | | Unhandled errors | Logged to stderr, process continues | Logged, process continues | | Per-handler errors | try/catch in individual handlers | try/catch in individual handlers | **Rating:** GREEN — equivalent patterns. -
README.md 2.8 KB
# Slack vs Teams: Platform Differences & Bridging Strategies A practical guide for developers adding cross-platform support to an existing bot. Each document covers a category of differences, explains why they matter, and provides concrete mitigation strategies with effort estimates. ## Documents | Document | What It Covers | |---|---| | [**Feature Gaps**](feature-gaps.md) | **Complete inventory of every RED and YELLOW gap with mitigations in both directions** | | [**Workflows**](workflows.md) | **Message-native workflow scenarios: standup, PTO, equipment, account health, break management, incidents** | | [Messaging & Commands](messaging-and-commands.md) | Messages, slash commands, events, threading, @mentions | | [UI Components](ui-components.md) | Block Kit vs Adaptive Cards, modals vs dialogs, App Home vs personal tabs | | [Interactive Responses](interactive-responses.md) | Ephemeral messages, button actions, message updates, confirmation dialogs | | [Identity & Auth](identity-and-auth.md) | User IDs, OAuth, signing/verification, tokens | | [Files & Links](files-and-links.md) | File upload/download, link unfurling/previews | | [Middleware & Handler Patterns](middleware-and-handlers.md) | Middleware chains, ack(), handler registration, error handling | | [Advanced Features](advanced-features.md) | Scheduling, workflows, shortcuts, channel ops, reactions, distribution | | [Infrastructure](infrastructure.md) | Transport, compute, storage, secrets, observability | | [**Eval Harness**](../evals/README.md) | Automated testing for expert routing, completeness, and code patterns | ## Eval Harness The [`evals/`](../evals/) directory contains an automated test harness for the expert system. It validates three dimensions: - **Routing** — 51 test cases across all 7 domains verify queries route to the correct domain, clusters, and expert files - **Completeness** — 9 test cases check experts cover all required concepts for their domain - **Patterns** — 294 TypeScript code blocks across all experts are compiled in-memory to catch syntax errors Pattern evals are fully deterministic (no API key needed). Routing and completeness evals use an LLM judge (OpenAI, Anthropic, or Azure OpenAI). See [`evals/README.md`](../evals/README.md) for setup and usage. ## How to Read These Docs Each difference follows this format: - **What's different** — the concrete behavioral gap - **Impact** — what breaks or degrades if you ignore it - **Mitigation** — one or more strategies ranked by effort and fidelity - **Effort** — rough hours to implement ### Difficulty Ratings | Rating | Meaning | |---|---| | GREEN | Direct mapping exists. Mechanical conversion, minimal design decisions. | | YELLOW | Mapping exists but requires design decisions or trade-offs. | | RED | Platform gap — no equivalent exists. Requires redesign or custom workaround. | -
ui-components.md 6.9 KB
# UI Components ## Block Kit vs Adaptive Cards | Slack Block Kit | Adaptive Card Element | Notes | |---|---|---| | `section` (text) | `TextBlock` | Set `wrap: true`; convert `*bold*` mrkdwn → `**bold**` Markdown | | `section` (fields) | `FactSet` | Each field becomes `{ title, value }` | | `section` (text + accessory) | `ColumnSet` with 2 `Column`s | Col 1 = text, Col 2 = accessory | | `header` | `TextBlock` size `Large`, weight `Bolder` | | | `actions` | `ActionSet` | Max 6 actions in Teams (vs 25 in Slack) | | `divider` | `TextBlock` with `separator: true` | | | `image` | `Image` | `alt_text` (underscore) → `altText` (camelCase) | | `context` | `TextBlock` size `Small`, `isSubtle: true` | | | `input` (plain_text) | `Input.Text` | | | `input` (static_select) | `Input.ChoiceSet` `style: "compact"` | | | `input` (multi_select) | `Input.ChoiceSet` `isMultiSelect: true` | | | `input` (datepicker) | `Input.Date` | | | `input` (timepicker) | `Input.Time` | | | `input` (checkboxes) | `Input.ChoiceSet` `style: "expanded"`, `isMultiSelect: true` | | | `input` (radio_buttons) | `Input.ChoiceSet` `style: "expanded"` | | | `rich_text` | `RichTextBlock` | Schema 1.5+ | | `overflow` menu | **No equivalent** | Redesign as `ActionSet` or `Input.ChoiceSet` dropdown | **Rating:** GREEN for most elements, RED for overflow menus. ### Markdown Differences | Formatting | Slack mrkdwn | Adaptive Card Markdown | |---|---|---| | Bold | `*bold*` | `**bold**` | | Italic | `_italic_` | `_italic_` | | Strikethrough | `~strike~` | `~~strike~~` | | Code | `` `code` `` | `` `code` `` | | Emoji | `:emoji_shortcode:` | Unicode characters only | | User mention | `<@U12345>` | Display name (no mention syntax) | **Impact:** Failing to convert `*bold*` to `**bold**` produces literal asterisks in Teams. Slack emoji shortcodes render as plain text in Adaptive Cards. **Mitigation:** Apply a text transform function when converting between formats: ```typescript // Slack mrkdwn → Adaptive Card Markdown text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "**$1**") .replace(/~([^~]+)~/g, "~~$1~~"); // Adaptive Card Markdown → Slack mrkdwn text.replace(/\*\*([^*]+)\*\*/g, "*$1*") .replace(/~~([^~]+)~~/g, "~$1~"); ``` ### Button / Action Styles | Slack | Adaptive Card | Visual | |---|---|---| | `"primary"` | `"positive"` | Green | | `"danger"` | `"destructive"` | Red | | (default) | `"default"` | Neutral | ### Interaction Model Difference This is the **biggest behavioral shift** between platforms: | Aspect | Slack | Teams | |---|---|---| | Model | **Event-per-interaction** — every select/button fires immediately | **Form-then-submit** — inputs collect, submit sends all at once | | Select behavior | `static_select` fires `block_actions` on selection | `Input.ChoiceSet` does nothing until `Action.Submit` clicked | | Live updates | `dispatch_action: true` for real-time `block_actions` | No equivalent — use `Action.Execute` with refresh for critical cases | | Form data | Per-element: `action.selected_option.value` | Batched: `activity.value` contains all input IDs | **Impact:** Bots that rely on instant-fire selects to update UI dynamically will feel different in Teams. The Teams UX has fewer round trips but less reactivity. **Mitigation:** Accept the batch-submit model for Teams. Group related inputs and submit together. For cases requiring per-interaction updates, use `Action.Execute` with the `refresh` property (schema 1.4+). ### Block / Action Limits | Limit | Slack | Teams | |---|---|---| | Blocks per message | 50 | No formal block limit, but 28 KB payload max | | Blocks per modal/view | 100 | 28 KB payload max | | Actions per block | 25 per `actions` block | 6 per `ActionSet` | | Card payload size | No formal limit | 28 KB after JSON serialization | **Mitigation:** Paginate dense action rows into multiple cards. Consolidate overflow menus into `Input.ChoiceSet` dropdowns. --- ## Modals vs Dialogs (Task Modules) | Aspect | Slack Modal | Teams Dialog (Task Module) | |---|---|---| | Open | `views.open(trigger_id, view)` | Return card from `dialog.open` handler | | Submit handler | `app.view("callback_id")` | `app.on("dialog.submit")` | | Form data location | `view.state.values[block_id][action_id]` | `activity.value.data` (flat object keyed by input `id`) | | Stacking | `views.push()` — up to 3 levels | **Not supported** | | Cancel notification | `notify_on_close: true` → `view_closed` | **Not supported** | | Mid-form updates | `dispatch_action` + `block_actions` + `views.update` | **Not supported** | | Field validation | `ack({ response_action: "errors", errors })` | Client-side only (`isRequired`, `regex`) | | Private metadata | `private_metadata` (3000 char limit) | Hidden fields in `Action.Submit.data` | | View hash (race protection) | `view_hash` parameter in `views.update` | **Not supported** — manual `_version` field needed | **Rating:** YELLOW for basic modal-to-dialog, RED for stacking, cancel notifications, and mid-form updates. ### Mitigation Strategies | Gap | Strategy | Effort | |---|---|---| | **No stacking** | Flatten into single dialog with step routing in submit handler. Include a "Back" button that re-renders the previous step. | 8–16 hrs | | **No cancel notification** | Add explicit "Cancel" button inside the dialog. Implement 5-min timeout-based cleanup for stale locks. | 4–8 hrs | | **No mid-form updates** | Multi-step dialogs for dependent fields. `Action.ToggleVisibility` for simple show/hide. | 8–16 hrs | | **No server-side validation** | Re-open dialog with pre-populated data + error messages in field labels. Use client-side `isRequired`/`regex` where possible. | 4–8 hrs | | **No view hash** | Inject `_version` counter into `Action.Submit.data`. Reject stale updates server-side. | 2–4 hrs | ### Reverse Direction (Teams → Slack) Dialogs map to modals via `views.open()`. Teams' batch-submit model decomposes into Slack's per-element handlers. Client-side validation (`isRequired`, `regex`) becomes server-side validation in `view_submission`. --- ## App Home vs Personal Tab | Aspect | Slack App Home | Teams Personal Tab | |---|---|---| | Render | `views.publish()` with Block Kit | `tab.fetch` handler returns Adaptive Card, or `staticTabs` in manifest for web | | Trigger | `app_home_opened` event | `tab.fetch` fires on every tab open | | Dynamic updates | `views.publish()` any time | `tab.submit` for interactions within tab | | Race protection | `view_hash` parameter | **Not supported** | **Rating:** YELLOW — functional equivalent exists, different event model. ### Mitigation Strategies | Strategy | How | Effort | |---|---|---| | **`tab.fetch` handler (Recommended)** | Return Adaptive Card on every tab open. Closest to `app_home_opened`. | 4–8 hrs | | **Welcome message only** | Send card to 1:1 chat on `install.add`. Simple but fires once. | 1–2 hrs | | **Static web tab** | Full web page in iframe. Richer UI but needs hosting. | 8–16 hrs | -
workflows.md 14.4 KB
# Workflow Scenarios Message-native workflow patterns for Teams bots. These scenarios demonstrate how collaborative business workflows can be initiated, executed, persisted, queried, and displayed entirely within the message surface — no external tools or navigation required. Each scenario follows the same five-element lifecycle: | Element | What It Does | Teams API | |---|---|---| | **Trigger** | How the workflow starts | Bot commands, message extensions, `node-cron`, Graph change notifications | | **State** | Where records live | SharePoint Lists via Graph API (or Dataverse for enterprise) | | **Logic** | How decisions execute | Bot state machine, `Action.Execute` handlers, escalation timers | | **Intelligence** | How AI is layered over state | Azure OpenAI function calling for NL queries over list data | | **Visibility** | How records stay in-channel | Adaptive Cards with `Action.Execute` → in-place refresh | All scenarios are implementable today with a standard Teams bot. No new platform features required. **Example implementation:** [`examples/message-native-workflow/`](../examples/message-native-workflow/) — Daily Standup with all five pillars. **Experts:** The `teams/workflow.*` and `bridge/workflow.composable-platform-ts.md` experts provide implementation guidance for each pillar. --- ## Scenario 1: Daily Standup **Audience:** SMB teams, engineering teams, any recurring check-in cadence. ### User Flow 1. Bot posts a standup prompt card at 9 AM (scheduled trigger) or on `/standup` (command trigger) 2. Each team member fills in yesterday / today / blockers in the card's input fields 3. On submit, `Action.Execute` replaces the prompt with a completed record card in-place 4. Record persisted to SharePoint List with thread link 5. Manager types "show blockers" or "summarize last week" — AI queries the list and renders results ### Five Elements | Element | Implementation | |---|---| | Trigger | `node-cron` scheduled at `STANDUP_CRON` + `/standup` bot command | | State | SharePoint List: Respondent, Date, Yesterday, Today, Blockers, HasBlockers, ThreadActivityId | | Logic | Card form submission → record creation. Edit/save state machine via `Action.Execute` | | Intelligence | `queryStandups(date?, respondent?)`, `queryBlockers(currentOnly?)`, `summarizeStandups(date)` | | Visibility | Prompt card → record card (in-place). Summary card. Blockers card with per-person breakdown | ### Key Cards - **Standup prompt** — `Input.Text` fields for yesterday/today/blockers + Submit button - **Record card** — FactSet showing the response + Edit button - **Summary card** — response count, blocker count, respondent list - **Blockers card** — ColumnSet list of all current blockers by person ### Why It Validates the Vision Exercises structured input (form), durable state (list), in-place updates (Action.Execute), and NL retrieval (AI function calling). This is FHL Option A from the source document. --- ## Scenario 2: Time-Off Requests (PTO) **Audience:** SMB, any team with leave management. ### User Flow 1. Employee types `/pto 2024-03-15 to 2024-03-22` or uses the "New PTO Request" compose extension 2. Bot creates a PTO record card in the thread with request details + Approve/Reject buttons 3. Manager sees the card with `refresh.userIds` showing action buttons only to them 4. Manager clicks Approve — card refreshes in-place to show "Approved by [Manager]" (read-only) 5. Employee receives a proactive notification in the thread 6. Anyone in the channel can type "show PTO for March" — AI queries and renders results ### Five Elements | Element | Implementation | |---|---| | Trigger | Bot command (`/pto START to END`), message extension action (form with date pickers) | | State | SharePoint List: Requester, StartDate, EndDate, HoursRequested, Status, ApprovedBy, ThreadActivityId | | Logic | Single-approver routing. Manager lookup via `GET /users/{id}/manager`. Escalation timer (48h) | | Intelligence | `queryPtoRequests(status?, requester?, month?)` — "Who has PTO next week?", "Show pending requests" | | Visibility | Request card (pending, with Approve/Reject) → Approved card (read-only). PTO list card for queries | ### Approval Routing | Pattern | Behavior | |---|---| | Single | One approver (direct manager), one decision | | Sequential | Manager → Director. Director only sees the card after manager approves | | Parallel-all | HR + Manager must both approve | ### Role-Specific Card Views The `refresh` property on the Adaptive Card targets `refresh.userIds` = the current approver's AAD ID. The approver sees Approve/Reject buttons. Everyone else sees a read-only status card. When the approver acts, the card refreshes for all viewers. --- ## Scenario 3: Equipment / Asset Reservation **Audience:** SMB operations, facilities, shared resource management. ### User Flow 1. Employee types `/book Projector Room-A tomorrow 2pm-4pm` 2. Bot checks for conflicts by querying the list for overlapping reservations 3. If available, bot creates the booking and posts a confirmation card 4. If conflict detected, bot posts a card showing the conflict and suggesting alternatives 5. Late-return alert: if the booking end time passes without a return confirmation, bot sends a reminder 6. Manager types "show all bookings this week" — AI renders a calendar-style summary ### Five Elements | Element | Implementation | |---|---| | Trigger | Bot command (`/book ITEM LOCATION DATE TIME`), message extension search for availability lookup | | State | SharePoint List: Item, Location, BookedBy, StartTime, EndTime, Status (Active/Returned/Overdue), ThreadActivityId | | Logic | Conflict detection via `$filter` on overlapping date ranges. Return confirmation via `Action.Execute`. Overdue timer → proactive reminder | | Intelligence | `queryEquipmentBookings(item?, status?, dateRange?)` — "Is the projector available Friday?", "Show overdue items" | | Visibility | Booking confirmation card. Conflict card with alternatives. Overdue alert card. Weekly summary card | ### Conflict Detection Query ``` fields/Item eq 'Projector' and fields/Location eq 'Room-A' and fields/StartTime lt '2024-03-16T16:00:00Z' and fields/EndTime gt '2024-03-16T14:00:00Z' and fields/Status eq 'Active' ``` If results > 0, there's a conflict. The bot renders the conflicting bookings and suggests the next available slot. --- ## Scenario 4: Account Health Monitoring (CRM) **Audience:** Sales teams, account managers, customer success. ### User Flow 1. Weekly scheduled prompt posts to the sales channel: "Time for account health check-ins" 2. Each account owner fills in: Account name, health status (Green/Yellow/Red), notes, next meeting date 3. Responses aggregate into a durable account health list 4. Stale accounts flagged: if no update in 30 days, bot sends a dormant account alert 5. Before a meeting, manager types "summarize Acme Corp" — AI pulls the last 4 check-ins and renders a trend card ### Five Elements | Element | Implementation | |---|---| | Trigger | Weekly cron schedule. Dormant-account check (daily timer queries for last-update > 30 days) | | State | SharePoint List: AccountName, Owner, HealthStatus (Green/Yellow/Red), Notes, NextMeeting, LastUpdated | | Logic | Staleness detection: daily timer queries `fields/LastUpdated lt '{30-days-ago}'`. Proactive alert to owner | | Intelligence | `queryAccountHealth(account?, status?, owner?)` — "Show all red accounts", "Summarize Acme Corp history" | | Visibility | Check-in prompt card. Account status card (color-coded). Dormant account alert. Trend summary card | ### Trend Analysis The AI function returns the last N check-ins for an account. The LLM summarizes: > *"Acme Corp: 4 check-ins over the last month. Trend: Yellow → Yellow → Red → Red. Key issue: delayed contract renewal (first flagged March 1). Next meeting: March 15."* This is the "intelligence layered over structured state" pattern — the primary differentiation opportunity called out in the source document. --- ## Scenario 5: Frontline Break Management **Audience:** Frontline workers, call centers (e.g., T-Mobile scenario from source document). ### User Flow 1. Agent changes presence to "Away" (auto-detected via Graph presence subscription) 2. Bot removes agent from call queue and starts break timer 3. At 15 minutes, bot sends a reminder card to the agent and their manager 4. At 20 minutes, bot escalates — posts an alert card in the manager channel 5. Agent changes presence to "Available" — bot re-adds to queue, records break duration 6. Manager types "who is on break?" or "average break duration today" — AI queries and responds ### Five Elements | Element | Implementation | |---|---| | Trigger | Graph change notification subscription on `/communications/presences/{userId}` | | State | SharePoint List: EmployeeName, BreakStart, BreakEnd, DurationMinutes, Status (Active/Ended/Escalated) | | Logic | Timer-based escalation (15 min reminder, 20 min escalate). Call queue add/remove via Teams admin APIs. Break record created on "Away", updated on "Available" | | Intelligence | `queryBreakStatus(currentOnly?)` — "Who is on break right now?", "Average break duration this week" | | Visibility | Break started card (in manager channel). Reminder card (to agent). Escalation alert card. Break summary card | ### Why This Is Teams-Native This scenario depends on three capabilities Slack cannot replicate: | Capability | Teams | Slack | |---|---|---| | Presence change subscriptions | Graph `/communications/presences` | Not available | | Shift schedule integration | Shifts API | Not available | | Call queue management | Teams admin APIs + Graph | Not available | ### Technical Requirements - **Graph subscription for presence** requires `Presence.Read.All` application permission and encrypted rich notifications (public/private key pair for notification decryption) - **Presence subscriptions expire in 60 minutes** — aggressive renewal required (55-minute interval) - **Webhook must respond in 3 seconds** — process notifications asynchronously - **In-memory timers don't survive restarts** — use Azure Durable Functions or a Redis-backed job queue for production --- ## Scenario 6: Incident Response **Audience:** IT operations, DevOps, on-call teams. ### User Flow 1. On-call engineer types `/incident P1 Production database connection pool exhausted` 2. Bot creates an incident record, posts a structured incident card, and creates a dedicated incident thread 3. Bot proactively notifies the on-call rotation (looked up from a Shifts schedule or list) 4. Team members post updates in the thread — bot captures tagged updates (`/update Database restarted, monitoring`) 5. Engineer types `/resolve` — bot closes the incident, calculates MTTR, and posts a resolution summary 6. Post-incident: manager types "show P1 incidents this month" — AI generates a summary with MTTR trends ### Five Elements | Element | Implementation | |---|---| | Trigger | `/incident PRIORITY DESCRIPTION` bot command | | State | SharePoint List: IncidentId, Priority (P1-P4), Description, Status (Open/Investigating/Resolved), AssignedTo, CreatedAt, ResolvedAt, MTTR, Updates[] | | Logic | Auto-assign from on-call rotation. Status transitions: Open → Investigating → Resolved. MTTR calculation on resolve. Thread-based update capture | | Intelligence | `queryIncidents(priority?, status?, dateRange?)` — "Show open incidents", "MTTR trend for P1s this quarter" | | Visibility | Incident card (color-coded by priority). Update timeline in thread. Resolution summary card with MTTR | --- ## Composable Platform Pattern All six scenarios follow the same lifecycle. The composable platform approach (see `bridge/workflow.composable-platform-ts.md`) defines workflows as configuration: ```typescript interface WorkflowDefinition { id: string; // "pto", "standup", "equipment" commandPrefix: string; // "/pto", "/standup", "/book" columns: ColumnDefinition[]; // SharePoint List schema statusField: string; // Which column tracks lifecycle routing?: RoutingConfig; // Approval chain config cards: CardTemplates; // Active, completed, list, form queryDescription: string; // AI function calling description filterableColumns: string[]; // Columns exposed to NL queries } ``` A single workflow engine registers handlers from definitions. New workflows require a new `WorkflowDefinition` object, not new handler code. Template workflows (standup, PTO, equipment) serve as reference implementations. ### Scenario Comparison | Scenario | Trigger Types | Approval | State-Driven | NL Queries | Competitive Edge | |---|---|---|---|---|---| | Daily Standup | Scheduled, command | No | No | Blockers, summaries | Structured check-ins as durable records | | PTO Requests | Command, extension | Yes (single/chain) | No | Status, date range, person | Approval routing + NL retrieval | | Equipment Booking | Command, search | No | No | Availability, overdue | Conflict detection + alternatives | | Account Health | Scheduled | No | Timer (staleness) | Trends, status, owner | Trend analysis over time | | Break Management | Presence change | No | Yes (presence) | Current status, averages | Teams-only: presence + Shifts + call queues | | Incident Response | Command | No | No | Priority, MTTR, status | Thread-based update capture + MTTR | --- ## Platform Comparison: Teams vs Slack | Capability | Slack | Teams | Gap | |---|---|---|---| | In-channel workflow creation | Workflow Builder GUI | Power Automate (external) | Teams gap: no in-channel builder | | Structured input forms | `OpenForm` built-in function | Adaptive Card forms (bot) or task modules | Parity | | State persistence | Datastores (50K limit, Slack-hosted) | SharePoint Lists (30M limit, tenant-owned) | Teams advantage | | Card interactivity | Block Kit (new message on action) | Action.Execute (in-place refresh) | Teams advantage | | NL querying over state | Not built-in | AI function calling + structured data | Teams advantage | | Presence/Shifts triggers | Not available | Graph subscriptions | Teams advantage | | Call queue integration | Not available | Teams admin APIs | Teams advantage | | No-code authoring | Workflow Builder | Power Automate | Slack advantage (simpler UX) | | Hosting model | Slack-hosted (Deno) | Self-hosted or Azure | Trade-off | The core thesis: if Teams unifies its existing primitives at the message layer (which a bot can do today), it moves beyond parity — especially for operational and frontline workflows where Slack lacks system-level integration.
-
-
experts
-
bridge
-
app-distribution-packaging-ts.md 14.2 KB
# app-distribution-packaging-ts ## purpose Bridges Slack App Directory distribution and Teams app packaging / Admin Center publishing for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack App Directory → Teams App Store (Partner Center).** Slack apps are listed in the Slack App Directory for public distribution. Teams apps are published to the Microsoft Teams App Store via Partner Center. The review and submission process is completely different — Partner Center requires a Microsoft Partner Network account and compliance with Teams store validation policies. [learn.microsoft.com -- Publish to store](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/publish) 2. **Slack OAuth install flow → Azure Bot registration (no per-workspace tokens).** Slack apps use OAuth to install into each workspace, generating per-workspace `xoxb-` tokens stored in an `InstallationStore`. Teams bots use Azure Bot Framework credentials (`CLIENT_ID`/`CLIENT_SECRET`) that work across all tenants. There are no per-workspace tokens to manage. Delete `InstallationStore` and all OAuth install flow code. [learn.microsoft.com -- Bot registration](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration) 3. **Slack `InstallationStore` → conversation reference storage.** Slack's `InstallationStore` persists tokens per workspace for API calls. Teams doesn't need per-workspace tokens, but you still need to store conversation references for proactive messaging. Replace `InstallationStore` with a conversation reference store keyed by `conversationId`. [learn.microsoft.com -- Proactive messages](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) 4. **Slack org-level install → Teams Admin Center tenant-wide deployment.** Slack Enterprise Grid supports org-level app installation. In Teams, tenant-wide deployment is done via the Teams Admin Center by an IT admin: Manage Apps → Upload/Approve → Deploy to users/groups. No code changes needed — the admin controls distribution. [learn.microsoft.com -- Admin Center](https://learn.microsoft.com/en-us/microsoftteams/manage-apps) 5. **Development install → Teams sideloading.** Slack development apps are installed via the app's manage page or OAuth URL. Teams development apps are sideloaded: upload the app package (ZIP with manifest + icons) directly into Teams. Sideloading must be enabled by the tenant admin. [learn.microsoft.com -- Sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) 6. **Agents Toolkit simplifies packaging, provisioning, and deployment.** Agents Toolkit (VS Code extension or CLI `atk`) automates: Azure resource provisioning, app package generation, sideloading, and publishing. It replaces the manual Azure Portal + zip file workflow. Use `atk package` to generate the app package and `atk publish` to submit. [learn.microsoft.com -- Agents Toolkit](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) 7. **Multi-tenant Slack app → Azure AD multi-tenant app registration.** Slack multi-workspace apps use the App Directory + OAuth per workspace. Teams multi-tenant bots use a single Azure AD app registration with `signInAudience: "AzureADMultipleOrgs"`. Any tenant can install the bot without workspace-specific OAuth. [learn.microsoft.com -- Multi-tenant](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-authentication-basics) 8. **Slack app manifest (`manifest.json`) → Teams app manifest (`manifest.json` in app package).** Both platforms use JSON manifests but with completely different schemas. Slack's manifest includes OAuth scopes, event subscriptions, slash commands. Teams manifest includes `bots`, `composeExtensions`, `staticTabs`, `webApplicationInfo`, `validDomains`. No automatic conversion exists. [learn.microsoft.com -- Manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) 9. **Slack app icons (512x512 + workspace-specific) → Teams icons (color 192x192 + outline 32x32).** Teams requires exactly two icon files in the app package: a full-color icon (192x192 PNG) and an outline/monochrome icon (32x32 PNG with transparent background). The outline icon is used in the Teams activity bar. [learn.microsoft.com -- App icons](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#icons) 10. **Slack app review (hours-days) vs Teams store review (1-2 weeks).** Slack's App Directory review is relatively fast. Teams App Store review via Partner Center is more rigorous and can take 1-2 weeks. Plan for revision cycles — common rejection reasons include missing privacy policy URL, incomplete manifest, and accessibility issues. [learn.microsoft.com -- Store validation](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/prepare/teams-store-validation-guidelines) 11. **Reverse direction (Teams → Slack):** For Teams → Slack, map Teams manifest to Slack app manifest, and Teams Admin Center publishing to Slack App Directory submission. Azure Bot registration credentials map to Slack OAuth install flow with `InstallationStore` for per-workspace tokens. Teams sideloading maps to Slack development install via OAuth URL. The Teams color/outline icon pair maps to Slack's single 512x512 app icon. Azure AD multi-tenant registration maps to Slack App Directory multi-workspace distribution with per-workspace OAuth. ## patterns ### InstallationStore removal + conversation reference storage **Slack (before):** ```typescript import { App, Installation, InstallationQuery } from "@slack/bolt"; // InstallationStore — persist per-workspace tokens const installationStore = { storeInstallation: async (installation: Installation) => { const teamId = installation.team?.id ?? installation.enterprise?.id; await db.put(`installation:${teamId}`, JSON.stringify(installation)); }, fetchInstallation: async (query: InstallationQuery<boolean>) => { const teamId = query.teamId ?? query.enterpriseId; const data = await db.get(`installation:${teamId}`); return JSON.parse(data) as Installation; }, deleteInstallation: async (query: InstallationQuery<boolean>) => { const teamId = query.teamId ?? query.enterpriseId; await db.delete(`installation:${teamId}`); }, }; const app = new App({ signingSecret: process.env.SLACK_SIGNING_SECRET!, clientId: process.env.SLACK_CLIENT_ID!, clientSecret: process.env.SLACK_CLIENT_SECRET!, stateSecret: process.env.SLACK_STATE_SECRET!, installationStore, scopes: ["chat:write", "commands", "channels:history"], }); // Use workspace-specific token for API calls app.message(/hello/i, async ({ say, client }) => { // client automatically uses the workspace's xoxb token await say("Hello!"); }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; // No InstallationStore needed — single credential set works for all tenants const app = new App({ clientId: process.env.CLIENT_ID, // Azure Bot app ID clientSecret: process.env.CLIENT_SECRET, // Azure Bot secret tenantId: process.env.TENANT_ID, // or "common" for multi-tenant logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Store conversation references instead of installations // Needed for proactive messaging (the only thing that replaced InstallationStore's purpose) const conversationRefs = new Map<string, { conversationId: string; serviceUrl: string; tenantId: string; }>(); app.on("install.add", async ({ activity, send }) => { // Persist conversation reference for future proactive messaging const convId = activity.conversation?.id ?? ""; conversationRefs.set(convId, { conversationId: convId, serviceUrl: (activity as any).serviceUrl, tenantId: activity.channelData?.tenant?.id ?? "", }); await send("Bot installed! I'm ready to help."); }); app.on("install.remove", async ({ activity }) => { const convId = activity.conversation?.id ?? ""; conversationRefs.delete(convId); }); app.message(/hello/i, async ({ send }) => { // No workspace token lookup needed — just send await send("Hello!"); }); app.start(3978); ``` ### App Directory → Admin Center deployment **Slack** — submit app to Slack App Directory via api.slack.com dashboard. Users install via the directory. **Teams** — multiple distribution paths: ```shell # Option 1: Sideload for development # Build the app package (manifest.json + icons in a ZIP) atk package --env dev -i false # Upload to Teams: # Teams → Apps → Manage your apps → Upload a custom app # Option 2: Submit to organization's app catalog atk publish --env staging # IT admin approves in Teams Admin Center → Manage Apps # Option 3: Submit to public Teams App Store (Partner Center) # 1. Create Partner Center account # 2. Submit app package for review # 3. Review takes 1-2 weeks # 4. Once approved, appears in Teams App Store # Option 4: Tenant-wide deployment (admin pushes to all users) # Teams Admin Center → Manage Apps → find app → Assign to users/groups # No code changes — purely admin configuration ``` **Teams app package structure:** ``` my-teams-bot.zip ├── manifest.json # Teams-specific manifest (not Slack's) ├── color.png # 192x192 full-color icon └── outline.png # 32x32 monochrome outline icon ``` ### Distribution model mapping table | Slack Distribution | Teams Equivalent | Notes | |---|---|---| | App Directory (public listing) | Teams App Store via Partner Center | Requires partner account; 1-2 week review | | OAuth install flow (per-workspace) | Azure Bot registration (global) | No per-workspace tokens | | `InstallationStore` | Conversation reference store | Only for proactive messaging | | Org-level install (Enterprise Grid) | Teams Admin Center tenant-wide deploy | Admin pushes to users/groups | | Development install (OAuth URL) | Sideloading (upload ZIP) | Admin must enable sideloading | | `manifest.json` (Slack schema) | `manifest.json` (Teams schema) | Completely different schemas | | App icon (512x512) | Color (192x192) + Outline (32x32) | Two icons required | | OAuth scopes (`chat:write`, etc.) | Azure AD permissions + RSC | Different permission model | | Multi-workspace (App Directory) | Multi-tenant (Azure AD) | `signInAudience: "AzureADMultipleOrgs"` | ## pitfalls - **Trying to port the InstallationStore**: Teams does not need per-workspace token storage. Developers who port `InstallationStore` logic create unnecessary complexity. Delete it and use conversation reference storage only for proactive messaging. - **Sideloading disabled by default in many orgs**: IT admins may have disabled sideloading. If the developer can't upload the app package, they need to request sideloading permission from their Teams admin. This is a common blocker during development. - **Partner Center account setup takes time**: Publishing to the Teams App Store requires a Microsoft Partner Network account. Account verification can take days. Start the Partner Center registration early in the migration timeline. - **Icon format rejection**: Teams requires exactly two PNG icons with specific dimensions. The outline icon must have a transparent background. Submitting icons in the wrong format or size causes app package validation failure. - **Multi-tenant vs single-tenant confusion**: Slack apps are inherently multi-workspace when listed in the App Directory. Teams apps must explicitly set multi-tenant in the Azure AD app registration. A single-tenant registration only works in the developer's own organization. - **OAuth scopes → RSC permissions**: Slack OAuth scopes (`channels:history`, `chat:write`) have no direct mapping to Azure AD permissions. Teams uses a combination of Azure AD API permissions and Resource-Specific Consent (RSC) permissions declared in the manifest. This is the most conceptually different part of the migration. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/publish - https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload - https://learn.microsoft.com/en-us/microsoftteams/manage-apps - https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/teams-toolkit-fundamentals - https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema - https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration - https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/prepare/teams-store-validation-guidelines - https://github.com/microsoft/teams.ts - https://api.slack.com/distribution — Slack app distribution ## instructions Use this expert when adding cross-platform support in either direction for app distribution and packaging. It covers: Slack App Directory bridged to Teams App Store (Partner Center), OAuth install flow vs Azure Bot registration, InstallationStore vs conversation reference storage, org-level deployment via Teams Admin Center, sideloading for development, Agents Toolkit for packaging, multi-tenant Azure AD registration, icon requirements, store review timelines, and reverse mapping from Teams manifest/Admin Center back to Slack app manifest and App Directory submission. Pair with `identity-oauth-bridge-ts.md` for the identity/OAuth model change, `../teams/runtime.manifest-ts.md` for Teams manifest creation, and `../teams/runtime.proactive-messaging-ts.md` for conversation reference storage patterns. ## research Deep Research prompt: "Write a micro expert for bridging Slack App Directory distribution and Microsoft Teams app packaging / Admin Center publishing in either direction. Cover: App Directory vs Teams App Store (Partner Center), OAuth install flow vs Azure Bot registration, InstallationStore vs conversation reference storage, org-level install vs Teams Admin Center, sideloading, Agents Toolkit packaging, multi-tenant Azure AD app registration, icon requirements, manifest schema differences, OAuth scope to RSC mapping, store review timeline, and reverse mapping from Teams manifest/publishing back to Slack app manifest and App Directory submission. Include code examples and a mapping table." -
channel-ops-graph-ts.md 15.4 KB
# channel-ops-graph-ts ## purpose Bridges Slack channel operations (conversations.*) and Teams channel management via Microsoft Graph for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack `conversations.create` → Graph `POST /teams/{team-id}/channels`.** Slack creates channels in a flat workspace namespace. Teams channels always belong to a specific team — you must know the `team-id` first. The request body includes `displayName`, `description`, and `membershipType` (standard, private, shared). [learn.microsoft.com -- Create channel](https://learn.microsoft.com/en-us/graph/api/channel-post) 2. **Slack `conversations.archive` → no true archive in Teams.** Teams has no channel archive API. Workarounds: (a) delete the channel (destructive, 30-day soft delete), (b) rename the channel with a `[ARCHIVED]` prefix, (c) remove all members except owners, (d) for the entire team, use `POST /teams/{team-id}/archive`. Individual channel archival is not supported. [learn.microsoft.com -- Archive team](https://learn.microsoft.com/en-us/graph/api/team-archive) 3. **Slack `conversations.invite` → Graph `POST /teams/{team-id}/channels/{channel-id}/members`.** The request body must include the user's Azure AD Object ID (`@odata.type: '#microsoft.graph.aadUserConversationMember'`) and a `roles` array (`['owner']` or `[]` for member). Private channel membership is managed separately from standard channels. [learn.microsoft.com -- Add channel member](https://learn.microsoft.com/en-us/graph/api/channel-post-members) 4. **Slack `conversations.kick` → Graph `DELETE /teams/{team-id}/channels/{channel-id}/members/{membership-id}`.** You must first resolve the `membership-id` by listing channel members (`GET /teams/{team-id}/channels/{channel-id}/members`) and finding the member by their Azure AD Object ID. You cannot delete by user ID directly. [learn.microsoft.com -- Remove member](https://learn.microsoft.com/en-us/graph/api/channel-delete-members) 5. **Slack `conversations.setTopic` → Graph `PATCH /teams/{team-id}/channels/{channel-id}` with `description`.** Slack channels have a separate `topic` field. Teams channels use the `description` field as the closest equivalent. The channel name is updated via the `displayName` field. [learn.microsoft.com -- Update channel](https://learn.microsoft.com/en-us/graph/api/channel-patch) 6. **All channel operations require a `team-id`.** Slack has a flat channel namespace (every channel has a globally-unique `C-ID`). Teams channels are nested under teams. Most operations need both `team-id` and `channel-id`. Resolve team IDs via `GET /me/joinedTeams` or `GET /groups` with Teams filter. [learn.microsoft.com -- List joined teams](https://learn.microsoft.com/en-us/graph/api/user-list-joinedteams) 7. **Channel name restrictions differ from Slack.** Teams channel names cannot contain: `~ # % & * { } / \ : < > ? + | ' "`. Maximum length is 50 characters (Slack allows 80). Channel names must be unique within a team. Validate and sanitize names during migration. [learn.microsoft.com -- Channel limits](https://learn.microsoft.com/en-us/microsoftteams/limits-specifications-teams) 8. **Graph API requires application or delegated permissions.** Channel operations need `Channel.Create`, `ChannelMember.ReadWrite.All`, `Channel.Delete.All` (application permissions) or equivalent delegated permissions. These require Azure AD admin consent. Slack's bot token scopes (`channels:manage`, `channels:write.invites`) have no direct Azure AD equivalent. [learn.microsoft.com -- Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-reference) 9. **Slack `conversations.list` → Graph `GET /teams/{team-id}/channels`.** List all channels in a team. For listing channels across teams, iterate over `GET /me/joinedTeams` first, then list channels per team. There is no single API to list all channels across all teams (unlike Slack's flat listing). [learn.microsoft.com -- List channels](https://learn.microsoft.com/en-us/graph/api/channel-list) 10. **Private channels have separate membership management.** Slack private channels (`is_private: true`) map to Teams private channels (`membershipType: 'private'`). Private channel members are managed via the channel members API, not the team membership. Adding a user to the team does NOT add them to private channels — you must add them to both. [learn.microsoft.com -- Private channels](https://learn.microsoft.com/en-us/microsoftteams/private-channels) 11. **Reverse direction (Teams → Slack):** For Teams → Slack, map Graph API channel operations to Slack's `conversations.*` API methods. `POST /teams/{team-id}/channels` maps to `conversations.create`. `POST /channels/{id}/members` maps to `conversations.invite`. `DELETE /channels/{id}/members/{id}` maps to `conversations.kick`. `PATCH /channels/{id}` with `description` maps to `conversations.setTopic`. Note that Slack has a flat channel namespace (no team-id required) and supports true channel archiving via `conversations.archive`. ## patterns ### Create channel + invite members **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/create-channel", async ({ ack, command, client }) => { await ack(); const [name, ...memberIds] = command.text.split(" "); // Create channel in flat namespace const channel = await client.conversations.create({ name: name.toLowerCase().replace(/\s+/g, "-"), is_private: false, }); // Invite members by Slack user ID if (memberIds.length > 0) { await client.conversations.invite({ channel: channel.channel!.id!, users: memberIds.join(","), // comma-separated U-IDs }); } await client.chat.postMessage({ channel: command.channel_id, text: `Channel <#${channel.channel!.id}> created!`, }); }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import { Client } from "@microsoft/microsoft-graph-client"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Initialize Graph client (use app-only auth in production) function getGraphClient(token: string): Client { return Client.init({ authProvider: (done) => done(null, token), }); } app.message(/^\/?create-channel (.+)$/i, async ({ send, activity }) => { const args = activity.text?.replace(/^\/?create-channel\s+/i, "").split(" ") ?? []; const [rawName, ...memberAadIds] = args; // Sanitize channel name for Teams restrictions const channelName = rawName .replace(/[~#%&*{}\/\\:<>?+|'"]/g, "") .substring(0, 50); // Teams channels require a team-id (no flat namespace) const teamId = activity.channelData?.team?.id; if (!teamId) { await send("This command must be run in a team context."); return; } const graphToken = await getAppOnlyToken(); const graph = getGraphClient(graphToken); // Create channel under the team const channel = await graph.api(`/teams/${teamId}/channels`).post({ displayName: channelName, description: `Created by bot on ${new Date().toISOString()}`, membershipType: "standard", }); // Invite members by Azure AD Object ID (not Slack U-ID) for (const aadId of memberAadIds) { await graph.api(`/teams/${teamId}/channels/${channel.id}/members`).post({ "@odata.type": "#microsoft.graph.aadUserConversationMember", "user@odata.bind": `https://graph.microsoft.com/v1.0/users('${aadId}')`, roles: [], // empty = member, ['owner'] = owner }); } await send(`Channel **${channelName}** created with ${memberAadIds.length} members.`); }); async function getAppOnlyToken(): Promise<string> { // Use @azure/identity ConfidentialClientApplication for production return "..."; } app.start(3978); ``` ### Set topic + archive channel **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/set-topic", async ({ ack, command, client }) => { await ack(); await client.conversations.setTopic({ channel: command.channel_id, topic: command.text, }); await client.chat.postMessage({ channel: command.channel_id, text: `Topic updated to: ${command.text}`, }); }); app.command("/archive-channel", async ({ ack, command, client }) => { await ack(); await client.conversations.archive({ channel: command.channel_id, }); // Channel is now archived — no more messages can be posted }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import { Client } from "@microsoft/microsoft-graph-client"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); function getGraphClient(token: string): Client { return Client.init({ authProvider: (done) => done(null, token) }); } // Set channel description (closest to Slack topic) app.message(/^\/?set-topic (.+)$/i, async ({ send, activity }) => { const topic = activity.text?.replace(/^\/?set-topic\s+/i, "") ?? ""; const teamId = activity.channelData?.team?.id; const channelId = activity.channelData?.channel?.id; if (!teamId || !channelId) { await send("This command must be run in a team channel."); return; } const graph = getGraphClient(await getAppOnlyToken()); await graph.api(`/teams/${teamId}/channels/${channelId}`).patch({ description: topic, }); await send(`Channel description updated to: ${topic}`); }); // Archive channel — no direct equivalent, rename with prefix app.message(/^\/?archive-channel$/i, async ({ send, activity }) => { const teamId = activity.channelData?.team?.id; const channelId = activity.channelData?.channel?.id; if (!teamId || !channelId) { await send("This command must be run in a team channel."); return; } const graph = getGraphClient(await getAppOnlyToken()); // Get current channel name const channel = await graph.api(`/teams/${teamId}/channels/${channelId}`).get(); // Rename with archive prefix (best available workaround) await graph.api(`/teams/${teamId}/channels/${channelId}`).patch({ displayName: `[ARCHIVED] ${channel.displayName}`.substring(0, 50), description: `Archived on ${new Date().toISOString()}. ${channel.description ?? ""}`, }); await send("Channel marked as archived. Note: Teams does not support true channel archival."); }); async function getAppOnlyToken(): Promise<string> { return "..."; } app.start(3978); ``` ### Channel operation mapping table | Slack API | Graph API Equivalent | Notes | |---|---|---| | `conversations.create({ name })` | `POST /teams/{team-id}/channels` | Must specify team-id | | `conversations.archive({ channel })` | *(no equivalent)* | Rename with prefix, or delete | | `conversations.unarchive({ channel })` | *(no equivalent)* | Rename back | | `conversations.invite({ channel, users })` | `POST /teams/{team-id}/channels/{id}/members` | One member per call; needs AAD Object ID | | `conversations.kick({ channel, user })` | `DELETE /channels/{id}/members/{membership-id}` | Must resolve membership-id first | | `conversations.setTopic({ channel, topic })` | `PATCH /channels/{id}` with `description` | Topic → description | | `conversations.rename({ channel, name })` | `PATCH /channels/{id}` with `displayName` | 50 char limit | | `conversations.list()` | `GET /teams/{team-id}/channels` | Per-team, not workspace-wide | | `conversations.info({ channel })` | `GET /teams/{team-id}/channels/{id}` | Needs team-id | | `conversations.members({ channel })` | `GET /teams/{team-id}/channels/{id}/members` | Returns AAD user objects | ## pitfalls - **No flat channel namespace**: Slack's `C-ID` identifies a channel globally. Teams requires both `team-id` and `channel-id` for most operations. Bots must resolve or store the team context from `activity.channelData.team.id`. - **Channel name validation**: Teams rejects names with special characters that Slack allows. Always sanitize channel names before creating. The `#` character — commonly used in Slack — is not allowed in Teams channel names. - **Membership ID resolution for kicks**: You cannot remove a member by Azure AD Object ID alone. First list members, find the matching `conversationMember.id`, then delete by that membership ID. This is a two-API-call operation. - **No true channel archive**: Slack's archive makes a channel read-only while preserving it. Teams has no equivalent. The rename-with-prefix workaround doesn't prevent new messages. True read-only requires deleting the channel (which has a 30-day recovery window). - **Private channel membership is separate**: Adding a user to a team does NOT automatically add them to private channels. You must explicitly add them to each private channel. This differs from Slack where inviting to a private channel only requires the channel invite API. - **Graph API rate limits**: Graph API has its own throttling (separate from Bot Framework). Bulk channel operations (creating many channels, inviting many users) should include retry logic with exponential backoff on HTTP 429 responses. - **Admin consent required**: Application-level Graph permissions (`Channel.Create`, `ChannelMember.ReadWrite.All`) require Azure AD admin consent. This is a deployment-time concern — the bot code may work in dev but fail in production if admin consent hasn't been granted. ## references - https://learn.microsoft.com/en-us/graph/api/channel-post - https://learn.microsoft.com/en-us/graph/api/channel-post-members - https://learn.microsoft.com/en-us/graph/api/channel-delete-members - https://learn.microsoft.com/en-us/graph/api/channel-patch - https://learn.microsoft.com/en-us/graph/api/team-archive - https://learn.microsoft.com/en-us/graph/api/channel-list - https://learn.microsoft.com/en-us/microsoftteams/limits-specifications-teams - https://github.com/microsoft/teams.ts - https://api.slack.com/methods/conversations.create — Slack conversations API ## instructions Use this expert when adding cross-platform support in either direction for channel management operations. It covers: Slack `conversations.*` bridged to Graph API channel endpoints, `conversations.archive` workarounds in Teams, `conversations.invite` bridged to Graph member addition, `conversations.kick` with membership ID resolution, `conversations.setTopic` bridged to channel description update, team-id requirement, channel name restrictions, Graph API permission requirements, and reverse mapping from Graph channel operations back to Slack `conversations.*` methods. Pair with `../teams/graph.usergraph-appgraph-ts.md` for Graph API authentication, `identity-oauth-bridge-ts.md` for user ID mapping (Slack U-ID to AAD Object ID), and `rate-limiting-resilience-ts.md` for Graph API throttling patterns. ## research Deep Research prompt: "Write a micro expert for bridging Slack channel management operations (conversations.create, conversations.archive, conversations.invite, conversations.kick, conversations.setTopic, conversations.list) and Microsoft Teams channel management via the Graph API in either direction. Cover: team-id requirement, channel name restrictions, private channel membership, the lack of channel archive API in Teams, membership ID resolution for removal, Graph API permissions, rate limiting, and reverse mapping from Graph operations back to Slack conversations.* methods. Include TypeScript code examples and a mapping table." -
commands-slash-text-ts.md 18.5 KB
# commands-slash-text-ts ## purpose Bridges Slack slash commands and Teams text commands / message extensions for cross-platform bots targeting Slack, Teams, or both. ## rules 1. Teams bots do **not** have a native slash command system equivalent to Slack's `app.command('/name')`. Slack slash commands must be reimplemented using one of three Teams patterns: text pattern matching, messaging extensions, or manifest command hints. [learn.microsoft.com -- Bots in Teams](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/what-are-bots) 2. The most direct migration path is **text pattern matching** with `app.message(regex)` in the Teams SDK. Map `app.command('/help')` to `app.message(/^\/?help$/i)`. The leading `/?` makes the slash optional so users can type either "help" or "/help". [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Remove all `ack()` calls when migrating to Teams. Teams handlers do not require acknowledgement -- simply process the request and respond. The `ack` concept does not exist in the Teams SDK. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 4. Replace Slack's `respond()` (response_url) and `say()` with the Teams context methods `send()` (new message) and `reply()` (threaded reply). There is no Teams equivalent of Slack's ephemeral response -- all bot messages are visible to participants. [learn.microsoft.com -- Send proactive messages](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) 5. Slack's `trigger_id` for opening modals has no direct Teams equivalent. Instead, send an Adaptive Card with form inputs inline, or use a Task Module (dialog) opened via `dialog.open` handler. Task modules do not require a trigger_id -- they are opened by card actions or link unfurling. [learn.microsoft.com -- Task modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/what-are-task-modules) 6. For command **discoverability**, add entries to the `commands` array in the manifest's `bots` section. These appear as suggestions when users type in the bot's compose box. They are UI hints only -- the bot still receives the text as a regular message. [learn.microsoft.com -- Bot commands](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/create-a-bot-commands-menu) 7. For a richer command UX, use **messaging extensions** (`composeExtensions` in manifest). Search-based extensions let users query and insert results; action-based extensions open a task module form. These replace complex slash commands that opened modals or returned structured data. [learn.microsoft.com -- Message extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions) 8. Slack's `command.text` (the argument string) maps to parsing `activity.text` in Teams. Strip the bot @mention prefix first (set `activity.mentions.stripText: true` in App options), then parse the remaining text for arguments. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Slack's `command.user_id` maps to `activity.from.aadObjectId` (Azure AD Object ID) in Teams. Slack's `command.channel_id` maps to `activity.conversation.id`. These IDs have completely different formats and are not interchangeable. [learn.microsoft.com -- Activity schema](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference) 10. In Teams channels, bots only receive messages when @mentioned (unless configured otherwise via RSC permissions). Slash commands in Slack work without mention. Account for this UX difference by instructing users to @mention the bot or by scoping command bots to personal chat where every message is delivered. [learn.microsoft.com -- Channel conversations](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/channel-and-group-conversations) ## patterns ### Migrating a Slack slash command to Teams text pattern matching **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/status", async ({ ack, command, respond }) => { await ack("Checking status..."); const status = await getSystemStatus(); await respond({ response_type: "in_channel", text: `System status: ${status}`, }); }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import { DevtoolsPlugin } from "@microsoft/teams.dev"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), plugins: [new DevtoolsPlugin()], }); // No ack() needed. Regex makes the leading slash optional. app.message(/^\/?status$/i, async ({ send }) => { const status = await getSystemStatus(); // No ephemeral option -- all messages are visible await send(`System status: ${status}`); }); async function getSystemStatus(): Promise<string> { return "All systems operational"; } app.start(3978); ``` ### Migrating a command that opened a modal to a Teams Adaptive Card form **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/ticket", async ({ ack, command, client }) => { await ack(); await client.views.open({ trigger_id: command.trigger_id, view: { type: "modal", callback_id: "ticket_modal", title: { type: "plain_text", text: "Create Ticket" }, submit: { type: "plain_text", text: "Create" }, blocks: [ { type: "input", block_id: "title_block", label: { type: "plain_text", text: "Title" }, element: { type: "plain_text_input", action_id: "title_input" }, }, ], }, }); }); app.view("ticket_modal", async ({ ack, view, client }) => { const title = view.state.values.title_block.title_input.value!; await ack(); await client.chat.postMessage({ channel: "#tickets", text: `New ticket: ${title}`, }); }); ``` **Teams (after) -- Adaptive Card inline form replaces modal:** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import { DevtoolsPlugin } from "@microsoft/teams.dev"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), plugins: [new DevtoolsPlugin()], }); // User types "ticket" or "/ticket" to get the form card app.message(/^\/?ticket$/i, async ({ send }) => { await send({ attachments: [ { contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: "Create Ticket", weight: "Bolder", size: "Large" }, { type: "Input.Text", id: "ticketTitle", label: "Title", placeholder: "Describe the issue", isRequired: true, errorMessage: "Title is required", }, { type: "Input.ChoiceSet", id: "ticketPriority", label: "Priority", value: "medium", choices: [ { title: "High", value: "high" }, { title: "Medium", value: "medium" }, { title: "Low", value: "low" }, ], }, ], actions: [ { type: "Action.Submit", title: "Create", data: { action: "createTicket" }, }, ], }, }, ], }); }); // Handle the card form submission (replaces app.view handler) app.on("card.action", async ({ activity, send }) => { const data = activity.value?.action?.data ?? activity.value; if (data?.action === "createTicket") { const title = data.ticketTitle; const priority = data.ticketPriority; await send(`Ticket created: ${title} [${priority}]`); return { status: 200 }; } }); app.start(3978); ``` ### Migrating a data-lookup command to a search-based message extension **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // User types: /lookup serverName app.command("/lookup", async ({ ack, command, respond }) => { await ack(); const query = command.text; const results = await searchServers(query); if (results.length === 0) { await respond({ response_type: "ephemeral", text: "No results found." }); return; } await respond({ response_type: "ephemeral", blocks: results.map((r) => ({ type: "section", text: { type: "mrkdwn", text: `*${r.name}*\nStatus: ${r.status} | IP: ${r.ip}` }, })), }); }); ``` **Teams (after) — search-based message extension:** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Search-based message extension replaces /lookup // Triggered from compose box or command bar in Teams app.on("message.ext.query" as any, async ({ activity }) => { const query = activity.value?.queryOptions?.searchText ?? ""; const results = await searchServers(query); return { status: 200, body: { composeExtension: { type: "result", attachmentLayout: "list", attachments: results.map((r) => ({ contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: r.name, weight: "Bolder" }, { type: "TextBlock", text: `Status: ${r.status} | IP: ${r.ip}`, isSubtle: true }, ], }, preview: { contentType: "application/vnd.microsoft.card.thumbnail", content: { title: r.name, text: `${r.status} — ${r.ip}` }, }, })), }, }, }; }); async function searchServers(query: string) { return [{ name: "web-prod-01", status: "healthy", ip: "10.0.1.5" }]; } app.start(3978); ``` **Manifest `composeExtensions` config (required for message extensions):** ```json { "composeExtensions": [ { "botId": "${{BOT_ID}}", "commands": [ { "id": "lookupServer", "type": "query", "title": "Lookup Server", "description": "Search for servers by name", "initialRun": false, "parameters": [ { "name": "searchText", "title": "Server name", "description": "Search for a server", "inputType": "text" } ] } ] } ] } ``` ### Adding manifest commands for discoverability ```json { "bots": [ { "botId": "${{BOT_ID}}", "scopes": ["personal", "team", "groupChat"], "commands": [ { "title": "status", "description": "Check system status" }, { "title": "ticket", "description": "Create a new support ticket" }, { "title": "help", "description": "Show available commands" } ] } ] } ``` **Command mapping reference table:** | Slack Pattern | Teams Equivalent | Notes | |---|---|---| | `app.command('/help', ...)` | `app.message(/^\/?help$/i, ...)` | Text matching; no ack needed | | `ack()` / `ack(text)` | *(remove)* | Teams has no ack concept | | `respond({ response_type: "in_channel" })` | `send(text)` | All Teams messages are visible | | `respond({ response_type: "ephemeral" })` | *(no equivalent)* | Redesign as personal chat or card | | `command.trigger_id` + `views.open()` | Adaptive Card form or `dialog.open` | No trigger_id in Teams | | `command.text` | `activity.text` (after stripping @mention) | Parse arguments from message text | | `command.user_id` (U-ID) | `activity.from.aadObjectId` (AAD GUID) | Different ID format | | `command.channel_id` (C-ID) | `activity.conversation.id` | Different ID format | | `command.response_url` | `send()` / `reply()` | Direct methods, no URL-based responses | | Manifest: Slack app dashboard | Manifest: `bots[].commands[]` | JSON file instead of web UI | ### Best practice: text matching + manifest commands together (Y1) Use **both** text pattern matching and manifest bot commands for the best UX. Manifest commands give discoverability (users see them in the command menu); text matching ensures the bot responds to both `/weather` and `weather` so users migrating from Slack don't retrain muscle memory. ```typescript // Accept both "/weather" and "weather" — regex makes slash optional app.message(/^\/?weather$/i, async ({ send }) => { const weather = await getWeather(); await send(`Current weather: ${weather}`); }); ``` **Manifest (add commands for discoverability):** ```json { "bots": [{ "botId": "${{BOT_ID}}", "scopes": ["personal", "team", "groupChat"], "commands": [ { "title": "weather", "description": "Check the current weather" }, { "title": "status", "description": "Check system status" }, { "title": "help", "description": "Show available commands" } ] }] } ``` **Don't:** Create a message extension for every slash command. Reserve extensions for commands that benefit from rich search results or task module UI. **Reverse (Teams → Slack):** Register commands via `app.command("/name", handler)` with `await ack()`. Configure in the Slack app dashboard. ### Reverse direction (Teams → Slack) For Teams → Slack, map `app.message(regex)` to `app.command('/name')`, add `ack()` calls, and convert Adaptive Card forms to Block Kit modals. Key reverse mappings: - `app.message(/^\/?name$/i, ...)` → `app.command('/name', ...)` with `await ack()` at the top - `send(text)` → `respond({ response_type: 'in_channel', text })` or `say(text)` - `reply(text)` → `say({ text, thread_ts: message.ts })` - Adaptive Card inline form → `views.open(trigger_id, view)` with Block Kit modal - `app.on('card.action', ...)` with `data.action` routing → `app.view('callback_id', ...)` for modal submissions, `app.action('action_id', ...)` for button clicks - Manifest `bots[].commands[]` → Slack App Dashboard slash command configuration - `activity.from.aadObjectId` → `command.user_id` (requires ID mapping table) - `activity.text` (after stripping @mention) → `command.text` (clean argument string) - Message extensions (search-based) → slash commands returning ephemeral blocks, or external data source selects - All visible messages → consider which should be `response_type: 'ephemeral'` for Slack's richer privacy model ## pitfalls - **Expecting slash command UX in Teams**: Teams users do not get the same discoverable `/command` experience. Set expectations that commands are triggered by typing text or using the bot commands menu. - **Forgetting to remove `ack()`**: Leaving `ack()` calls in migrated code causes runtime errors since the Teams context object has no `ack` method. - **Not handling @mention prefix**: In Teams channels, `activity.text` includes the @mention text (e.g., `<at>BotName</at> status`). Set `activity.mentions.stripText: true` in App options or strip manually before matching. - **Relying on ephemeral responses**: Slack commands can respond ephemerally. Teams has no ephemeral messages. Redesign private responses as personal (1:1) chat messages or use Adaptive Cards that only the acting user sees after refresh. - **Ignoring the personal vs channel distinction**: In Slack, slash commands work identically in channels and DMs. In Teams, channel bots require @mention. Consider scoping command-heavy bots to personal chat for a smoother UX. - **Missing manifest commands**: Without `commands` in the manifest, users have no way to discover what the bot supports. Always add command hints for discoverability. - **Complex argument parsing**: Slack's `command.text` arrives as a clean string after the command name. In Teams, you must parse `activity.text` which may include the bot mention, extra whitespace, and varied formatting. - **Missing `composeExtensions` in manifest**: Message extensions (search-based or action-based) require a `composeExtensions` entry in the Teams manifest. Without it, the extension never appears in the compose box or command bar. This is the most common reason message extensions silently fail to load. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/what-are-bots - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/create-a-bot-commands-menu - https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/what-are-task-modules - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/channel-and-group-conversations - https://github.com/microsoft/teams.ts - https://slack.dev/bolt-js/concepts/commands - https://api.slack.com/interactivity/slash-commands ## instructions This expert covers bridging Slack slash commands and Teams text commands / message extensions. Use it when adding cross-platform support in either direction: converting `app.command()` handlers to Teams `app.message()` with regex patterns, or converting Teams text handlers back to Slack slash commands with `ack()` calls. It covers the three Teams alternatives to slash commands (text matching, messaging extensions, manifest commands), response pattern bridging (`respond`/`say` ↔ `send`/`reply`), modal/form bridging (`trigger_id` + `views.open` ↔ Adaptive Card forms / Task Modules), command payload property mapping, ephemeral message handling, and manifest command entries. Pair with `../slack/runtime.slash-commands-ts.md` for Slack command patterns, and `../teams/runtime.routing-handlers-ts.md` for Teams `app.message()` patterns. ## research Deep Research prompt: "Write a micro expert for bridging Slack slash commands and Teams text commands / message extensions bidirectionally. Cover the three Teams alternatives (text pattern matching with app.message regex, messaging extensions, manifest bot commands), side-by-side code examples for bridging in both directions, payload property mapping (command.text <-> activity.text, trigger_id, response_url <-> send/reply), ack() addition/removal, ephemeral response handling, and manifest configuration. Include a mapping table and common pitfalls for both directions." -
cross-platform-advisor-ts.md 36.4 KB
# cross-platform-advisor-ts ## purpose Interactive cross-platform bridging advisor. Detects which platform(s) a bot already targets, determines the bridging direction, analyzes the codebase, and walks the developer through every YELLOW/RED bridging decision — with a "take all defaults" escape hatch on every question. ## rules ### Phase 0: Direction Detection 1. **Detect the existing platform.** Scan the codebase in parallel for platform signatures: | Pattern to search | Platform detected | |---|---| | `@slack/bolt` or `require('slack')` or `app.command(` (Bolt-style) | Slack | | `@microsoft/teams-ai` or `@microsoft/teams.apps` or `teamsBot` or `BotFrameworkAdapter` | Teams | | `Block Kit` or `"type":"section"` or `blocks:` (Slack-style) | Slack | | `AdaptiveCards` or `"type":"AdaptiveCard"` or `CardFactory` | Teams | | `SLACK_BOT_TOKEN` or `SLACK_APP_TOKEN` or `socketMode` | Slack | | `CLIENT_ID` + `CLIENT_SECRET` + `TENANT_ID` (Azure Bot) | Teams | | `ack(` (Slack acknowledgement) | Slack | | `app.on("message"` or `app.message(` (Teams AI style) | Teams | 2. **Determine direction.** Based on what was found: - **Slack only detected** → Direction is **Slack → Teams** (adding Teams support) - **Teams only detected** → Direction is **Teams → Slack** (adding Slack support) - **Both detected** → Dual-platform bot already exists. Ask what they want to do (extend, reconcile, or audit). - **Neither detected** → Ask the developer which platform they're starting from. 3. **Confirm with the developer.** Present the detected direction: ``` header: "Direction" question: "I detected {platform} patterns in your codebase. Which direction are you bridging?" options: - label: "Add Teams to existing Slack bot (Recommended)" description: "Keep Slack, add Teams as a second platform." - label: "Add Slack to existing Teams bot" description: "Keep Teams, add Slack as a second platform." - label: "Audit existing dual-platform bot" description: "Both platforms detected — review coverage and gaps." ``` Adapt the recommended option to match what was detected. If Teams was detected, recommend "Add Slack." ### Phase 1: Codebase Analysis 4. **Scan for platform API usage.** Search the codebase for these patterns to build a feature inventory. Run all searches in parallel: **Slack patterns (relevant when Slack → Teams):** | Pattern to search | What it detects | Maps to | |---|---|---| | `app.command` | Slash commands | G7 | | `app.message` | Message pattern matching | G1 | | `say(` or `respond(` | Simple replies | G2 | | `blocks:` or `Block Kit` or `"type":"section"` | Block Kit UI | G16 | | `views.open` or `views.push` | Modals / stacking | G19, Y24 | | `view_submission` or `viewSubmission` | Modal submission | G20 | | `app.use(` | Middleware | G14 | | `ack(` | Slack acknowledgement | G15 | | `chat.postEphemeral` or `response_type.*ephemeral` | Ephemeral messages | Y1, R1 | | `reply_broadcast` or `broadcast.*true` | Thread broadcast | Y2 | | `conversations.replies` | Thread discovery | Y3 | | `files.upload` or `file_shared` | File upload | Y4/5/6 | | `link_shared` or `chat.unfurl` | Link unfurling | Y7 | | `scheduleMessage` or `chat.schedule` | Scheduled messages | Y8, R7 | | `reminders.add` | Reminders | Y9 | | `conversations.archive` | Channel archive | Y10, R8 | | `conversations.kick` or `conversations.invite` | Channel member mgmt | Y11 | | `app.shortcut` or `global_shortcut` | Global shortcuts | Y13 | | `message_shortcut` | Message shortcuts | Y14 | | `block_suggestion` or `app.options` | Dynamic selects | Y15 | | `app_home_opened` or `views.publish` | App Home | Y16 | | `view_hash` or `hash` (in modal context) | View hash / race cond | Y17 | | `blockAction` (inside modals) | Mid-form updates | R4 | | `ack.*errors` or `response_action.*errors` | Field validation | R5 | | `notify_on_close` or `view_closed` | Cancel notification | R3 | | `workflow_step` or `workflow_step_execute` | Workflow Builder | Y12 | | `reaction_added` or `reaction_removed` | Emoji reactions | R2 | | `SLACK_APP_TOKEN` or `socketMode` or `SocketModeReceiver` | Socket Mode | Y19 | | `retryConfig` or `retry` (in Bolt config) | Built-in retry | Y20 | | `confirm:` or `"confirm"` (on button/action) | Confirmation dialogs | Y21 | | `*.example.com` in manifest or unfurl config | Unfurl wildcards | Y23 | | `conversations.create` or `conversations.setTopic` | Channel ops | Y10/Y11 | **Teams patterns (relevant when Teams → Slack):** | Pattern to search | What it detects | Slack equivalent | |---|---|---| | `app.on("message"` or `activity.text` | Message handling | `app.message` | | `AdaptiveCard` or `CardFactory.adaptiveCard` | Adaptive Cards | Block Kit | | `app.on("dialog"` or `taskModule` | Task module / dialog | `views.open` modal | | `proactiveMessage` or `continueConversation` | Proactive messaging | `chat.postMessage` to channel | | `app.on("messageReaction"` | Reaction events | `reaction_added` | | `refresh.userIds` | Per-user cards | Ephemeral messages | | `MessageExtension` or `composeExtension` | Message extensions | Shortcuts | | `tab.fetch` or `tab.submit` | Personal tabs | App Home | | `Graph` or `graphClient` | Microsoft Graph calls | Slack Web API | | `SSO` or `oauth` (Teams context) | SSO / OAuth | Slack OAuth | | `FileConsentCard` or `supportsFiles` | File consent flow | `files.upload` | | `messageHandlers` (in manifest) | Link unfurling | `link_shared` | | `ChannelMessage.Read.Group` (RSC) | All channel messages | Default in Slack | 5. **Build the feature list.** From scan results, produce a table: `Feature | Found (Y/N) | File:Line | Feature ID`. Only include features where code evidence was found. 6. **Determine the bot profile.** Use the feature list to classify: - **Profile A** — Only GREEN features found (G1–G34) - **Profile B** — GREEN + YELLOW from: Y1, Y2, Y3, Y4/5/6, Y17, Y18, Y21 - **Profile C** — Profile B + any of: Y7, Y8, Y9, Y10, Y11, Y13, Y14, Y15, Y16, Y23, Y24 - **Profile D** — Profile C + any of: Y12, Y19, Y20, Y22, or any RED feature is core Note: For Teams → Slack direction, the profile classification still applies — the feature IDs map to equivalent complexity tiers in the reverse direction. 7. **Present the profile.** Show the developer: - Their detected profile (A/B/C/D) - The bridging direction (Slack → Teams or Teams → Slack) - The feature inventory table - Which phases from the bridging sequence apply (reference `MigrationDecisionMatrix.md` Section 2) - How many YELLOW and RED decisions they need to make ### Phase 2: Decision Walkthrough 8. **Ask one decision at a time.** For each YELLOW/RED feature found in the codebase, present a question using `AskUserQuestion`. Walk through decisions in phase order (matching the bridging phase sequence), not alphabetically. 9. **Decision ordering.** Present decisions in this order (skip any not found in codebase): **Phase 5 — Interactive Responses:** Y1 (Ephemeral), Y21 (Confirmation dialogs), Y17 (View hash) **Phase 7 — Files + Unfurling:** Y4/5/6 (File upload), Y7 (Link unfurling), Y23 (Unfurl wildcards) **Phase 8 — Scheduling + Channel Ops:** Y8 (Scheduled messages), Y9 (Reminders), Y10 (Channel archive), Y11 (Channel member removal) **Phase 9 — Shortcuts + App Home:** Y13 (Global shortcuts), Y14 (Message shortcuts), Y15 (Dynamic selects), Y16 (App Home), Y24 (Multi-step modals) **Phase 10 — Workflows + Distribution:** Y12 (Workflow Builder), Y22 (App Directory) **Phase 11 — Resilience:** Y18 (All channel messages), Y19 (Socket Mode), Y20 (Retry) **Message handling (parallel with Phase 5):** Y2 (reply_broadcast), Y3 (Thread discovery) **RED features (after all YELLOW):** R1 (True ephemeral), R2 (Emoji reactions), R3 (viewClosed), R4 (Mid-form dynamic), R5 (Field validation), R6 (Dialog stacking), R7 (Scheduled API), R8 (Channel archive), R9 (Retroactive unfurl), R10 (Firewall transport) Note: For Teams → Slack direction, adapt the questions to reflect adding Slack equivalents. The same feature IDs apply but the "source" and "target" swap. For example, Y1 becomes "Your bot uses refresh.userIds — Slack supports true ephemeral messages via chat.postEphemeral. Use it directly." 10. **Every question gets an escape hatch.** The final option in every `AskUserQuestion` call MUST be one of: - First question: **"You Decide Everything"** — accept all defaults for ALL decisions (YELLOW + RED), skip remaining questions, jump to Phase 3. - Subsequent questions: **"You Decide Everything Else"** — accept defaults for all REMAINING decisions, skip remaining questions, jump to Phase 3. When the developer picks either escape hatch, record all remaining features as "default" and proceed to Phase 3 immediately. 11. **Question format for YELLOW features.** Each `AskUserQuestion` must include: - `header`: Feature ID (e.g., "Y1 Ephemeral") - `question`: Clear question about which approach they prefer (adapted for bridging direction) - Options from `MigrationDecisionMatrix.md` Section 3, with the **(Recommended)** option listed first - Final option: the escape hatch 12. **Question format for RED features.** Each `AskUserQuestion` must include: - `header`: Feature ID (e.g., "R4 Dynamic") - `question`: What they want to do about the platform gap (adapted for bridging direction) - Options matching the strategies from `MigrationDecisionMatrix.md` Section 4 - Final option: the escape hatch 13. **Record every decision.** Maintain a running decisions table as you go: | Feature | Decision | Option | Notes | |---|---|---|---| | Y1 Ephemeral | `refresh.userIds` | A (Recommended) | — | | Y4/5/6 Files | `sendFile()` helper | B (Recommended) | Default accepted | | ... | ... | ... | ... | ### Phase 3: Bridging Plan Output 14. **Generate the bridging plan.** After all decisions are made (or defaults accepted), produce a single actionable plan with: - **Direction** — Which platform exists, which is being added - **Profile summary** — Profile letter, feature count, phase count - **Decisions summary** — The completed decisions table - **Phase-by-phase implementation order** — For each applicable phase: - Which expert(s) to load: `.experts/bridge/{filename}` - What to implement - Which decision applies (if any) - Go/no-go gate from `MigrationDecisionMatrix.md` Section 2 - **Helpers to build** — List of helper utilities/plugins chosen (e.g., `sendFile()`, `RetryPlugin`), grouped as a "Phase 0" pre-work step - **RED feature workarounds** — For each RED feature, the chosen strategy and implementation approach - **Estimated phase count** — Total phases and which can be parallelized 15. **Always reference, never duplicate.** Point developers to the specific expert files for implementation details. Do NOT reproduce the code patterns from individual experts — just reference them by filename and rule number. ### Phase 4: Per-Project Implementation Order When implementing each bridged project (whether a single sample or a batch), follow this exact sequence. Do NOT skip steps or reorder them. 16. **Step 1 — Write all source files.** Write every file the project needs before running any commands: - `package.json` — dependencies, scripts (`build`, `start`, `dev`) - `tsconfig.json` — TypeScript compiler config - `src/index.ts` — main entry point (and any additional `.ts` files) - `.env.sample` — template with placeholder values for all required env vars - Stub implementations — where an API is not yet wired up, leave a clearly marked `// TODO:` with an explanation of what should go there so the code still compiles. 17. **Step 2 — Install dependencies.** Run `npm install` in the project directory. Verify `node_modules` is created and there are no install errors. 18. **Step 3 — Build and verify.** Run `npm run build`. Must succeed with **zero TypeScript errors**. Fix any issues before proceeding. 19. **Step 4 — Create app manifest.** - **Adding Teams:** Create `appPackage/` directory with `manifest.json` (schema v1.19+), `color.png` (192x192), `outline.png` (32x32). The manifest must be valid and ready to zip for sideloading. - **Adding Slack:** Create or update `manifest.yaml` (Slack app manifest) with bot scopes, event subscriptions, and slash commands. Alternatively, configure via api.slack.com app settings. 20. **Step 5 — Write README.md.** The README is written **last** because it documents the final state of the project. It must contain: - **One-paragraph description** of what the example demonstrates. - **`## Prerequisites`** — Node.js 18+, platform-specific accounts and registrations. - **`## Environment Setup`** — step-by-step instructions for filling out `.env`. - **`## Running Locally`** — full launch sequence with tunneling setup. - **`## Installing the App`** — platform-specific installation instructions (sideloading for Teams, OAuth install for Slack, or both). - **`## What Was Bridged`** — bullet list mapping original platform concept → target platform equivalent. - **`## TODO`** — checklist of remaining items. ## question templates Use these as the basis for each `AskUserQuestion` call. Adapt the question text based on what was found in the codebase (e.g., mention the specific file where the feature was detected) and the bridging direction. ### Y1 — Ephemeral Messages ``` question: "Your bot uses ephemeral messages ({file}:{line}). How should the target platform handle user-only visibility?" header: "Y1 Ephemeral" options: - label: "refresh.userIds (Recommended)" description: "Wrap cards with refresh.userIds for per-user content. Covers ~80% of cases. 4-8 hrs." - label: "Send to 1:1 chat" description: "Route ephemeral content to user's personal bot chat. Different UX but reliable. 2-4 hrs." - label: "Build sendEphemeral() helper" description: "SDK wrapper auto-detecting context. Best if reused across multiple bots. 8-12 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, ephemeral is natively supported via `chat.postEphemeral`. This question may be skipped — just use the native API. ### Y2 — Threaded Replies with reply_broadcast ``` question: "Your bot uses reply_broadcast ({file}:{line}). How should the target platform handle thread + channel posting?" header: "Y2 Broadcast" options: - label: "Two API calls (Recommended)" description: "Call reply() and send() separately. Two lines of code, 1-2 hrs." - label: "Build reply(text, { broadcast }) wrapper" description: "Convenience method that internally sends both calls. 2-4 hrs." - label: "{escape hatch}" ``` ### Y3 — Thread Discovery ``` question: "Your bot reads thread replies ({file}:{line}). How should the target platform fetch thread history?" header: "Y3 Threads" options: - label: "Graph API direct (Recommended)" description: "GET /messages/{id}/replies with ChannelMessage.Read.All permission. 4-8 hrs." - label: "Build getThreadReplies() helper" description: "Wrapper encapsulating Graph client setup and auth. 8-12 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `conversations.replies` directly — native API. ### Y4/5/6 — File Upload ``` question: "Your bot uploads files ({file}:{line}). How should the target platform handle file operations?" header: "Y4-6 Files" options: - label: "Build sendFile() helper (Recommended)" description: "Unified wrapper: auto-detects personal/channel, routes to OneDrive/SharePoint, chunks >4MB. 24-40 hrs. The manual flow is a 30-line footgun." - label: "Manual FileConsentCard flow" description: "Implement the 3-step consent flow yourself. 16-24 hrs per upload pattern." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `files.uploadV2` directly — much simpler than the Teams consent flow. ### Y7 — Link Unfurling ``` question: "Your bot unfurls links ({file}:{line}). How should the target platform handle link previews?" header: "Y7 Unfurl" options: - label: "Cache-first with prefetch (Recommended)" description: "Cache middleware wraps handler. Without this, the 5-second deadline silently kills slow unfurls. 12-16 hrs." - label: "Synchronous handler only" description: "Direct handler, must return within 5 seconds. Only viable for fast data sources. 4-8 hrs." - label: "{escape hatch}" ``` ### Y8 — Scheduled Messages ``` question: "Your bot schedules messages ({file}:{line}). How should the target platform handle deferred delivery?" header: "Y8 Schedule" options: - label: "Functions timer + Cosmos DB (Recommended)" description: "Store in DB, Azure Functions timer polls and sends via proactive messaging. 16-24 hrs." - label: "Full scheduler plugin" description: "Reusable package with scheduleMessage()/cancelScheduledMessage(). 32-48 hrs." - label: "Power Automate delegation" description: "Offload to Power Automate flows. Requires license. 8-12 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `chat.scheduleMessage` directly — native API. ### Y9 — Reminders ``` question: "Your bot sets reminders ({file}:{line}). How should the target platform handle reminder delivery?" header: "Y9 Reminders" options: - label: "Piggyback on scheduler (Recommended)" description: "Reuse Y8 scheduler with setReminder() sending to 1:1 chat. 4-8 hrs if scheduler exists." - label: "Power Automate + Planner" description: "Create Planner tasks with due date notifications. 8-12 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `reminders.add` directly — native API. ### Y10 — Channel Archive ``` question: "Your bot archives channels ({file}:{line}). How should the target platform simulate channel archival?" header: "Y10 Archive" options: - label: "Rename + description (Recommended)" description: "Prefix with [ARCHIVED], update description. Cosmetic but non-destructive. 4-8 hrs." - label: "Rename + remove members" description: "Stronger enforcement but destructive — members must be re-invited to undo. 8-12 hrs." - label: "Team-level archive" description: "Archive entire Team. Only works if channel is in a dedicated Team. 2-4 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `conversations.archive` directly — native API. ### Y11 — Channel Member Removal ``` question: "Your bot removes channel members ({file}:{line}). How should the target platform handle member removal?" header: "Y11 Members" options: - label: "Two-step Graph API (Recommended)" description: "List members to resolve membership-id, then delete. Simple and direct. 4-6 hrs." - label: "Build removeChannelMember() helper" description: "Wrapper that resolves membership ID internally. Cleaner API. 4-8 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `conversations.kick` directly — native API. ### Y12 — Workflow Builder ``` question: "Your bot uses Workflow Builder ({file}:{line}). How should the target platform handle workflow automation?" header: "Y12 Workflows" options: - label: "Bot-driven orchestration (Recommended)" description: "Keep logic in the bot. No license dependency, full control. 16-40 hrs." - label: "Power Automate rebuild" description: "Rebuild in Power Automate. Custom steps need Premium license. 24-80 hrs." - label: "Hybrid approach" description: "Simple flows → Power Automate, complex → bot-driven. Two systems. Varies." - label: "{escape hatch}" ``` ### Y13 — Global Shortcuts ``` question: "Your bot uses global shortcuts ({file}:{line}). How should the target platform expose quick actions?" header: "Y13 Shortcuts" options: - label: "Compose extension (Recommended)" description: "composeExtensions with commandBox context. Always opens task module. 8-12 hrs." - label: "Minimal-dismiss pattern" description: "Task module returns tiny 'Done' card for fire-and-forget actions. 4-8 hrs." - label: "Bot command replacement" description: "Replace shortcut with typed command. Simpler but less discoverable. 2-4 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, map compose extensions to `app.shortcut` with a global shortcut callback. ### Y14 — Message Shortcuts ``` question: "Your bot uses message shortcuts ({file}:{line}). How should the target platform expose message actions?" header: "Y14 MsgAction" options: - label: "Action-based message extension (Recommended)" description: "composeExtensions with message context. Direct mapping. 4-8 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, map action-based message extensions to `app.shortcut` with `message_shortcut` type. ### Y15 — Dynamic Selects ``` question: "Your bot uses dynamic select menus ({file}:{line}). How should the target platform handle server-filtered dropdowns?" header: "Y15 Selects" options: - label: "Pre-populated ChoiceSet (Recommended)" description: "Load all options at dialog open, client-side filtering. Works up to ~500 items. 2-4 hrs." - label: "Two-step dialog" description: "Step 1: text search. Step 2: filtered results as ChoiceSet. Works for any size. 8-12 hrs." - label: "Custom searchable task module" description: "Embed a web view with search-as-you-type UI. Full control. 16-24 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `block_suggestion` with `external_data_source` for native dynamic selects. ### Y16 — App Home ``` question: "Your bot uses App Home ({file}:{line}). How should the target platform present the bot's home experience?" header: "Y16 AppHome" options: - label: "tab.fetch handler (Recommended)" description: "Personal tab fires on every open. Closest to AppHomeOpenedEvent. 4-8 hrs." - label: "install.add welcome only" description: "Send welcome message once on install. Simple but fires only once. 1-2 hrs." - label: "Static tab (web content)" description: "Full web page embedded as personal tab. Richer but needs hosting. 8-16 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, map `tab.fetch` to `app_home_opened` event with `views.publish`. ### Y17 — View Hash ``` question: "Your bot uses view_hash for race conditions ({file}:{line}). How should the target platform protect against stale updates?" header: "Y17 ViewHash" options: - label: "Manual _version field (Recommended)" description: "Inject version counter into Action.Submit.data, reject stale. 2-4 hrs." - label: "Card versioning middleware" description: "SDK plugin auto-injecting and checking versions. 4-8 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, use the native `view_hash` parameter in `views.update` — built-in. ### Y18 — All Channel Messages ``` question: "Your bot receives all channel messages without @mention ({file}:{line}). How should the target platform enable this?" header: "Y18 RSC" options: - label: "RSC permission (Recommended)" description: "Add ChannelMessage.Read.Group to manifest. Config-only, no code change. 1-2 hrs." - label: "Require @mention" description: "Change UX to require @mention. Simplifies permissions. 0 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, Slack receives all channel messages by default when the bot is in the channel. No special config needed. ### Y19 — Socket Mode ``` question: "Your bot uses Socket Mode ({file}:{line}). The target platform requires inbound HTTPS. How do you want to handle transport?" header: "Y19 Transport" options: - label: "Deploy to Azure (Recommended)" description: "Host in Azure for production. Use Dev Tunnels for local dev. 4-8 hrs." - label: "Azure Relay" description: "Hybrid connection for strict on-premises firewalls. Adds latency. 8-16 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, Slack supports Socket Mode for firewall-friendly deployments — a simpler story. ### Y20 — Built-in Retry ``` question: "Your bot uses Bolt's retryConfig ({file}:{line}). How should the target platform handle retry and resilience?" header: "Y20 Retry" options: - label: "Build RetryPlugin (Recommended)" description: "Drop-in plugin with exponential backoff, jitter, circuit breaker. Bad retry causes cascading failures. 12-16 hrs." - label: "Manual retry wrapper" description: "Hand-roll backoff around outbound calls. Simpler but easy to get wrong. 4-8 hrs." - label: "{escape hatch}" ``` ### Y21 — Confirmation Dialogs ``` question: "Your bot uses confirmation dialogs on buttons ({file}:{line}). How should the target platform confirm destructive actions?" header: "Y21 Confirm" options: - label: "Action.ShowCard inline (Recommended)" description: "Inline expand with Yes/No buttons. Native Adaptive Card pattern. 2-4 hrs." - label: "Task module confirm" description: "Small dialog for confirmation. More prominent. 4-6 hrs." - label: "Build confirmAction() helper" description: "Template function generating confirm cards. Reusable. 4-8 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, use the native `confirm` object on button elements — built-in. ### Y22 — App Directory ``` question: "Your bot is listed in an app directory. How should it be distributed on the target platform?" header: "Y22 Distrib" options: - label: "Org app catalog (Recommended)" description: "Publish to organization catalog. Requires Teams admin approval. 2-4 hrs." - label: "Admin sideload" description: "Upload directly via Teams Admin Center. Quick but no catalog listing. 1-2 hrs." - label: "Partner Center (public)" description: "Submit to Teams App Store. 1-2 week review. Requires Partner Network account. 8-16 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, submit to the Slack App Directory via api.slack.com. ### Y23 — Unfurl Domain Wildcards ``` question: "Your bot uses wildcard domain matching for link unfurling ({file}:{line}). How should the target platform list domains?" header: "Y23 Wildcards" options: - label: "Manual enumeration (Recommended)" description: "List every subdomain in manifest. Fine for <10 subdomains. 1-2 hrs." - label: "Manifest generator script" description: "Script reads subdomains from config and generates manifest array. 4-8 hrs." - label: "{escape hatch}" ``` ### Y24 — Multi-Step Modal Stacking ``` question: "Your bot uses views.push for modal stacking ({file}:{line}). How should the target platform handle multi-step forms?" header: "Y24 Stacking" options: - label: "Flatten into single dialog (Recommended)" description: "Single dialog with step routing in submit handler. Manageable for 2-3 steps. 8-16 hrs." - label: "Build StepDialog helper" description: "Reusable class managing step state, back/forward. Worth it if 3+ wizard flows. 16-24 hrs." - label: "Separate sequential dialogs" description: "Close current, open next. No back navigation. Degraded UX. 4-8 hrs." - label: "{escape hatch}" ``` Note: For Teams → Slack, use native `views.push` for stacking — up to 3 levels supported. ### R1 — True Ephemeral Messages ``` question: "Your bot relies on true ephemeral messages — a Teams platform gap. Teams has no visibility:'user' flag. How do you want to handle this?" header: "R1 Ephemeral" options: - label: "Accept & Redesign (Recommended)" description: "refresh.userIds for cards, 1:1 chat for text. Different but functional." - label: "Defer" description: "Drop ephemeral behavior entirely. Show messages to everyone." - label: "{escape hatch}" ``` Note: For Teams → Slack, this is a non-issue — Slack has native ephemeral support. ### R2 — Custom Emoji Reactions ``` question: "Your bot uses emoji reactions as workflow signals — Teams only has 6 fixed reactions. How do you want to handle this?" header: "R2 Reactions" options: - label: "Accept & Redesign (Recommended)" description: "Replace reaction workflows with Action.Submit card buttons. Better for audit trails." - label: "Map to 6 fixed reactions" description: "Map your most important reactions to like/heart/laugh/surprised/sad/angry. Lossy." - label: "{escape hatch}" ``` Note: For Teams → Slack, Slack supports unlimited custom emoji reactions — direct mapping. ### R3 — viewClosed / Cancel Notification ``` question: "Your bot uses viewClosed callbacks — Teams sends no notification on dialog dismiss. How do you want to handle this?" header: "R3 Cancel" options: - label: "Build Custom (Recommended)" description: "Timeout-based cleanup (5-min TTL) + explicit Cancel button inside the dialog." - label: "Defer" description: "Drop cancel cleanup entirely. Accept potential stale locks." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `notify_on_close: true` in `views.open` — native support. ### R4 — Mid-Form Dynamic Updates ``` question: "Your bot uses blockAction inside modals for dynamic form updates — a Teams platform gap. How do you want to handle this?" header: "R4 Dynamic" options: - label: "Accept & Redesign (Recommended)" description: "Multi-step dialogs for dependent fields. Action.ToggleVisibility for simple show/hide." - label: "Build custom web-based task module" description: "Embed a full web form in the task module for complete control. Much more effort." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `block_actions` inside modals with `views.update` — native support. ### R5 — Server-Side Field Validation ``` question: "Your bot uses ackWithErrors for inline field validation — a Teams platform gap. How do you want to handle this?" header: "R5 Validate" options: - label: "Build Custom (Recommended)" description: "Re-open dialog with pre-populated data + error messages in field labels." - label: "Client-side only" description: "Use isRequired/regex/maxLength. Covers simple cases only." - label: "{escape hatch}" ``` Note: For Teams → Slack, use `response_action: errors` in `view_submission` handler — native support. ### R6 — Dialog Stacking ``` question: "Your bot uses views.push for dialog stacking — a Teams platform gap. How do you want to handle this?" header: "R6 Stacking" options: - label: "Accept & Redesign (Recommended)" description: "Single dialog with step routing. Same approach as Y24. Simulate Back with a button." - label: "Build custom web-based task module" description: "Embed a web app with real navigation in the task module. Full control. High effort." - label: "{escape hatch}" ``` Note: For Teams → Slack, use native `views.push` — up to 3 levels. ### R7 — Scheduled Message API ``` question: "Your bot depends on chat.scheduleMessage — a Teams platform gap. Teams has no server-side scheduling. How do you want to handle this?" header: "R7 ScheduleAPI" options: - label: "Build Custom (Recommended)" description: "Self-managed scheduler from Y8 (Cosmos DB + Functions timer). Works, just boilerplate." - label: "Defer" description: "Drop scheduling entirely. Users trigger messages manually." - label: "{escape hatch}" ``` Note: For Teams → Slack, use native `chat.scheduleMessage` — direct mapping. ### R8 — Channel Archive ``` question: "Your bot archives individual channels — Teams can only archive entire Teams. How do you want to handle this?" header: "R8 Archive" options: - label: "Accept & Redesign (Recommended)" description: "Rename with [ARCHIVED] prefix. Good enough for 90% of cases." - label: "Rename + remove all members" description: "Stronger enforcement but destructive. Hard to undo." - label: "{escape hatch}" ``` Note: For Teams → Slack, use native `conversations.archive` — direct mapping. ### R9 — Retroactive Link Unfurling ``` question: "Your bot benefits from retroactive link unfurling — Teams only unfurls links in new messages. How do you want to handle this?" header: "R9 Retroactive" options: - label: "Defer (Recommended)" description: "No workaround exists. Don't waste time. New messages unfurl fine." - label: "Build manual preview command" description: "Bot command where users paste a URL to get a preview card. Niche." - label: "{escape hatch}" ``` ### R10 — Firewall-Friendly Transport ``` question: "Your bot relies on Socket Mode for firewall-friendly transport — Teams requires inbound HTTPS. How do you want to handle this?" header: "R10 Firewall" options: - label: "Accept & Redesign (Recommended)" description: "Deploy to Azure (it's 2026). Dev Tunnels for local dev." - label: "Azure Relay" description: "Hybrid connection for strict on-premises requirements. Adds latency." - label: "{escape hatch}" ``` Note: For Teams → Slack, Slack's Socket Mode provides firewall-friendly transport natively. ## defaults table When the developer picks "You Decide Everything" or "You Decide Everything Else", apply these defaults for all remaining decisions: | Feature | Default Option | Strategy | |---|---|---| | Y1 | A | `refresh.userIds` (Slack→Teams) / `chat.postEphemeral` (Teams→Slack) | | Y2 | A | Two API calls (Slack→Teams) / `reply_broadcast` (Teams→Slack) | | Y3 | A | Graph API direct (Slack→Teams) / `conversations.replies` (Teams→Slack) | | Y4/5/6 | B | `sendFile()` helper (Slack→Teams) / `files.uploadV2` (Teams→Slack) | | Y7 | B | Cache-first with prefetch (Slack→Teams) / `link_shared` + `chat.unfurl` (Teams→Slack) | | Y8 | A | Functions timer + Cosmos DB (Slack→Teams) / `chat.scheduleMessage` (Teams→Slack) | | Y9 | A | Piggyback on Y8 scheduler (Slack→Teams) / `reminders.add` (Teams→Slack) | | Y10 | A | Rename + description (Slack→Teams) / `conversations.archive` (Teams→Slack) | | Y11 | A | Two-step Graph API (Slack→Teams) / `conversations.kick` (Teams→Slack) | | Y12 | B | Bot-driven orchestration | | Y13 | A | Compose extension (Slack→Teams) / `app.shortcut` (Teams→Slack) | | Y14 | A | Action-based message extension (Slack→Teams) / `message_shortcut` (Teams→Slack) | | Y15 | A | Pre-populated ChoiceSet (Slack→Teams) / `block_suggestion` (Teams→Slack) | | Y16 | B | `tab.fetch` handler (Slack→Teams) / `views.publish` (Teams→Slack) | | Y17 | A | Manual `_version` field (Slack→Teams) / `view_hash` (Teams→Slack) | | Y18 | A | RSC permission (Slack→Teams) / Default in Slack (Teams→Slack) | | Y19 | B | Deploy to Azure (Slack→Teams) / Socket Mode (Teams→Slack) | | Y20 | B | `RetryPlugin` (Slack→Teams) / Bolt `retryConfig` (Teams→Slack) | | Y21 | A | `Action.ShowCard` inline (Slack→Teams) / `confirm` object (Teams→Slack) | | Y22 | B | Org app catalog (Slack→Teams) / Slack App Directory (Teams→Slack) | | Y23 | A | Manual enumeration (Slack→Teams) / Wildcard support (Teams→Slack) | | Y24 | A | Flatten into single dialog (Slack→Teams) / `views.push` (Teams→Slack) | | R1 | — | Accept & Redesign (Slack→Teams) / Native (Teams→Slack) | | R2 | — | Accept & Redesign (Slack→Teams) / Native (Teams→Slack) | | R3 | — | Build Custom (Slack→Teams) / `notify_on_close` (Teams→Slack) | | R4 | — | Accept & Redesign (Slack→Teams) / `block_actions` + `views.update` (Teams→Slack) | | R5 | — | Build Custom (Slack→Teams) / `response_action: errors` (Teams→Slack) | | R6 | — | Accept & Redesign (Slack→Teams) / `views.push` (Teams→Slack) | | R7 | — | Build Custom (Slack→Teams) / `chat.scheduleMessage` (Teams→Slack) | | R8 | — | Accept & Redesign (Slack→Teams) / `conversations.archive` (Teams→Slack) | | R9 | — | Defer | | R10 | — | Accept & Redesign (Slack→Teams) / Socket Mode (Teams→Slack) | ## instructions Pair with: - `MigrationDecisionMatrix.md` — source of truth for all decision options, effort estimates, and profile definitions - All 22 bridge experts in `.experts/bridge/` — referenced in the Phase 3 output for implementation details - `SlackToTeamsMigrationAnalysis.md` — cross-reference for feature status (G/Y/R) Do a web search for: - "Microsoft Teams Bot Framework SDK TypeScript latest changes 2026" - "Slack Bolt SDK TypeScript latest changes 2026" ## research Deep Research prompt: "Write an interactive cross-platform bridging advisor for Slack↔Teams bot development. Cover codebase analysis (detecting both Slack and Teams API patterns), direction detection (which platform exists, which to add), bot profile classification (A-D by complexity), and per-feature decision walkthrough for 24 YELLOW and 10 RED platform gaps — with bidirectional defaults for each direction. Include question templates with effort estimates and a defaults table for one-click acceptance." -
cross-platform-architecture-ts.md 8.3 KB
# cross-platform-architecture-ts ## purpose Architecture patterns for hosting both a Slack bot (Bolt.js) and a Teams bot (Bot Framework / Teams SDK) in a single TypeScript server — shared Express instance, separate receiver pipelines, shared business logic layer, and deployment considerations. ## rules 1. **Use a single Express server as the HTTP foundation.** Both Slack (HTTP receiver) and Teams (webhook POST) can share one Express app on one port. Mount Slack routes at `/slack/events` and Teams routes at `/api/messages`. 2. **Keep bot SDKs in separate modules.** Initialize Bolt's `ExpressReceiver` and Teams' `CloudAdapter` independently. Neither should know about the other. Share only the business logic layer. 3. **Extract business logic into a platform-agnostic service layer.** Functions like `processUserMessage(text, userId, context)` should return platform-neutral results (text, structured data). Platform adapters convert to Block Kit or Adaptive Cards. 4. **Use Bolt's `ExpressReceiver` (not the default `HTTPReceiver`) for shared Express.** Create the Express app yourself, pass it to `ExpressReceiver` via the `app` option, and also mount Teams routes on the same instance. 5. **For Socket Mode Slack + HTTP Teams, run both receivers.** Start `SocketModeReceiver` for Slack (WebSocket, no HTTP needed) and Express for Teams webhook. This is simpler than sharing Express — Slack doesn't need an HTTP endpoint at all. 6. **Normalize user identity across platforms.** Map Slack user IDs (`U...`) and Teams AAD object IDs to a common identity. Store mappings in a shared database keyed by email or external ID. 7. **Normalize conversation context.** Create a `ConversationContext` type with `platform: "slack" | "teams"`, `channelId`, `threadId`, `userId`, and `replyFn`. Each platform adapter populates this from its native event. 8. **Handle media differences in the adapter layer.** Slack uses Block Kit (`mrkdwn`, `blocks[]`). Teams uses Adaptive Cards (JSON schema, `AdaptiveCard`). The service layer should return structured data that each adapter renders into the platform's format. 9. **Share environment config but separate credentials.** Use a single `.env` or config file with prefixed keys: `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, `TEAMS_APP_ID`, `TEAMS_APP_PASSWORD`, `TEAMS_TENANT_ID`. 10. **Deploy as a single container or serverless function.** Both bots run in the same Node.js process. Use health checks for both: Slack via Socket Mode ping/pong, Teams via a health probe endpoint. ## patterns ### Shared Express with ExpressReceiver (Slack HTTP) + Teams webhook ```typescript import express from "express"; import { App, ExpressReceiver } from "@slack/bolt"; import { CloudAdapter, ConfigurationServiceClientCredentialFactory, createBotFrameworkAuthenticationFromConfiguration } from "botbuilder"; // 1. Create shared Express app const expressApp = express(); // 2. Initialize Slack with ExpressReceiver const slackReceiver = new ExpressReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET!, app: expressApp, // share the Express instance endpoints: "/slack/events", // Slack events endpoint }); const slackApp = new App({ token: process.env.SLACK_BOT_TOKEN!, receiver: slackReceiver, }); // 3. Initialize Teams on the same Express app const credFactory = new ConfigurationServiceClientCredentialFactory({ MicrosoftAppId: process.env.TEAMS_APP_ID!, MicrosoftAppPassword: process.env.TEAMS_APP_PASSWORD!, MicrosoftAppTenantId: process.env.TEAMS_TENANT_ID!, }); const auth = createBotFrameworkAuthenticationFromConfiguration(null, credFactory); const adapter = new CloudAdapter(auth); expressApp.post("/api/messages", async (req, res) => { await adapter.process(req, res, (context) => teamsBot.run(context)); }); // 4. Health check expressApp.get("/health", (_req, res) => res.json({ slack: "ok", teams: "ok" })); // 5. Start expressApp.listen(3000, () => console.log("Dual bot running on :3000")); ``` ### Socket Mode Slack + HTTP Teams (simpler) ```typescript import { App } from "@slack/bolt"; import express from "express"; // Slack: Socket Mode (no HTTP needed) const slackApp = new App({ token: process.env.SLACK_BOT_TOKEN!, appToken: process.env.SLACK_APP_TOKEN!, socketMode: true, }); // Teams: Express webhook const expressApp = express(); // ... mount Teams adapter on expressApp ... await slackApp.start(); // WebSocket expressApp.listen(3978, () => {}); // HTTP for Teams ``` ### Platform-agnostic service layer ```typescript // service/message-handler.ts — no platform imports export interface BotResponse { text: string; structured?: { title: string; body: string; actions?: { label: string; id: string }[]; }; } export async function handleUserMessage( text: string, userId: string, platform: "slack" | "teams" ): Promise<BotResponse> { // Business logic, AI calls, database queries — platform-agnostic return { text: `You said: ${text}`, structured: { title: "Echo", body: text }, }; } // adapters/slack-adapter.ts import { handleUserMessage } from "../service/message-handler.js"; slackApp.message(/.*/, async ({ message, say }) => { const response = await handleUserMessage( (message as any).text ?? "", (message as any).user ?? "", "slack" ); await say(response.text); // or convert response.structured to Block Kit }); // adapters/teams-adapter.ts import { handleUserMessage } from "../service/message-handler.js"; class TeamsBot extends ActivityHandler { constructor() { super(); this.onMessage(async (context, next) => { const response = await handleUserMessage( context.activity.text ?? "", context.activity.from?.id ?? "", "teams" ); await context.sendActivity(response.text); // or convert to Adaptive Card await next(); }); } } ``` ## pitfalls - **Express body parsing conflicts.** Slack needs raw body parsing for signature verification. Teams needs `express.json()`. Order middleware carefully — apply `express.json()` only to Teams routes, and let `ExpressReceiver` handle Slack routes' body parsing. - **Port conflicts in development.** If Slack's `ExpressReceiver` and your Teams server both try to listen on the same port, one will fail. Share a single `listen()` call, or use Socket Mode for Slack. - **Credential leakage between adapters.** Keep Slack and Teams clients in separate modules. A bug that passes the Slack token to a Teams API call (or vice versa) is hard to debug and a security risk. - **Adaptive Cards and Block Kit are not interchangeable.** Don't try to build a "universal card format" — the data models are fundamentally different. Keep a thin adapter that transforms structured data to each format. - **Tunneling for local development.** You need two tunnel endpoints (one for Slack, one for Teams) or route both through the same tunnel with path-based routing. ngrok or Cloudflare Tunnel work for both. ## references - Bolt.js `ExpressReceiver`: https://slack.dev/bolt-js/concepts/custom-routes - Bot Framework `CloudAdapter`: https://learn.microsoft.com/en-us/javascript/api/botbuilder/cloudadapter - Express 5: https://expressjs.com/en/5x/api.html ## instructions Use this expert when designing a server that hosts both Slack and Teams bots, or when deciding on a deployment architecture for multi-platform bot support. This is the foundational architecture expert for the slack-plus-teams project's core use case. Pair with: `runtime.bolt-foundations-ts.md` (Slack setup), `../teams/runtime.app-init-ts.md` (Teams setup), `identity-oauth-bridge-ts.md` (cross-platform identity), `ui-block-kit-adaptive-cards-ts.md` (UI adapter patterns). ## research Deep Research prompt: "Document architecture patterns for hosting both a Slack Bolt.js bot and a Microsoft Teams Bot Framework bot in a single Node.js/TypeScript server. Cover: shared Express server with route separation, ExpressReceiver for Slack HTTP mode, Socket Mode for Slack + separate HTTP for Teams, CloudAdapter integration on shared Express, platform-agnostic service layer design, Block Kit vs Adaptive Card adapter pattern, identity normalization across platforms, credential separation, environment configuration, health monitoring for both platforms, deployment as single container, and body parsing middleware ordering for signature verification compatibility." -
events-activities-ts.md 22.4 KB
# events-activities-ts ## purpose Bridges Slack event subscriptions and Teams activity handlers for cross-platform bots targeting Slack, Teams, or both. ## rules 1. Slack's `app.event('event_name')` maps to Teams' `app.on('route_name')` pattern. The event names and payload shapes are completely different between the two platforms. Always consult the mapping table below for the correct Teams route. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Slack's `app.message(pattern)` maps directly to Teams' `app.message(pattern)` for pattern-matched messages. For a catch-all, Slack uses `app.message(async ...)` while Teams uses `app.on('message', async ...)`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Slack's `app.event('app_mention')` has no dedicated Teams route. In Teams channels, bots receive messages only when @mentioned, so the standard `app.on('message')` handler already implies a mention context. Check `activity.entities` for mention details or use the `mention` route if available. [learn.microsoft.com -- Mentions in bots](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/channel-and-group-conversations#receive-only-at-mentioned-messages) 4. Slack's `app.event('member_joined_channel')` and `app.event('member_left_channel')` map to Teams' `app.on('conversationUpdate')` with inspection of `activity.membersAdded` or `activity.membersRemoved` arrays. [learn.microsoft.com -- conversationUpdate](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/subscribe-to-conversation-events) 5. Slack's `say()` maps to Teams' `send()` for posting a new message. Slack's threaded replies via `say({ thread_ts })` map to Teams' `reply()` method which uses `replyToId` internally for threaded conversation. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. Slack's `app.event('reaction_added')` and `app.event('reaction_removed')` map to Teams' `app.on('messageReaction')` route. Teams delivers both added and removed reactions in a single route -- inspect `activity.reactionsAdded` and `activity.reactionsRemoved` arrays. [learn.microsoft.com -- Message reactions](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/subscribe-to-conversation-events#message-reaction-events) 7. Slack's ephemeral messages (`respond({ response_type: 'ephemeral' })`) have **no Teams equivalent**. Redesign ephemeral responses as: (a) messages in personal (1:1) chat, (b) Adaptive Cards with user-specific `Action.Execute` refresh, or (c) simply visible messages if privacy is not critical. [learn.microsoft.com -- Conversations](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/conversation-basics) 8. In Teams channels, bots require @mention to receive messages (default behavior). This is fundamentally different from Slack where bots receive all channel messages. To receive all messages without mention, the app must request Resource-Specific Consent (RSC) permission `ChannelMessage.Read.Group`. [learn.microsoft.com -- RSC](https://learn.microsoft.com/en-us/microsoftteams/platform/graph-api/rsc/resource-specific-consent) 9. Slack's `app.event('app_home_opened')` (App Home tab) maps to Teams' static tab or `tab.open` invoke route for personal tabs. There is no direct equivalent -- Teams tabs are web pages rendered in an iframe, not bot-driven views. [learn.microsoft.com -- Personal tabs](https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/what-are-tabs) 10. Teams provides install/uninstall events via `app.on('install.add')` and `app.on('install.remove')` which have no direct Slack equivalent. Use these to send welcome messages and store conversation references for proactive messaging. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 11. **Teams has only 6 reaction types** (`like`, `heart`, `laugh`, `surprised`, `sad`, `angry`) — Slack supports unlimited custom emoji reactions. Bots that use reactions as workflow triggers (e.g., `:white_check_mark:` to mark approved, `:eyes:` to claim a ticket) must be redesigned. Replace reaction-based workflows with `Action.Submit` buttons on Adaptive Cards, which provide explicit, typed actions instead of ambiguous emoji semantics. [learn.microsoft.com -- Message reactions](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/subscribe-to-conversation-events#message-reaction-events) 12. **Threading model differs significantly.** Slack uses `thread_ts` to identify a parent message and `reply_broadcast` to also post a thread reply to the channel. Teams uses `replyToId` in the activity and the `reply()` method. There is **no "also send to channel"** equivalent in Teams — a reply stays in the thread. Thread discovery requires the Graph API: `GET /teams/{team-id}/channels/{channel-id}/messages/{message-id}/replies`. This Graph call requires `ChannelMessage.Read.All` application permission. [learn.microsoft.com -- List replies](https://learn.microsoft.com/en-us/graph/api/chatmessage-list-replies) ## patterns ### Migrating message handlers (say to send, thread_ts to reply) **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Pattern-matched message app.message(/^hello$/i, async ({ message, say }) => { await say(`Hello <@${(message as any).user}>!`); }); // Catch-all message handler app.message(async ({ message, say }) => { if (message.subtype) return; // Reply in thread await say({ text: `You said: ${(message as any).text}`, thread_ts: (message as any).ts, }); }); // App mention event app.event("app_mention", async ({ event, say }) => { await say(`Thanks for mentioning me, <@${event.user}>!`); }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import { DevtoolsPlugin } from "@microsoft/teams.dev"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), plugins: [new DevtoolsPlugin()], }); // Pattern-matched message (same API shape as Slack) app.message(/^hello$/i, async ({ send, activity }) => { await send(`Hello ${activity.from.name}!`); }); // Catch-all message handler app.on("message", async ({ activity, reply }) => { // reply() creates a threaded reply (like say({ thread_ts }) in Slack) await reply(`You said: "${activity.text}"`); }); // No separate app_mention route needed -- in channels, bots only // receive messages when @mentioned, so app.on('message') covers it. // For explicit mention detection: app.on("message", async ({ activity, send }) => { const mentions = activity.entities?.filter( (e: any) => e.type === "mention" && e.mentioned?.id !== activity.recipient?.id ); if (mentions?.length) { await send("I see you mentioned someone!"); } }); app.start(3978); ``` ### Migrating member join/leave and reaction events **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Member joined channel app.event("member_joined_channel", async ({ event, say }) => { await say(`Welcome to the channel, <@${event.user}>!`); }); // Member left channel app.event("member_left_channel", async ({ event, client }) => { await client.chat.postMessage({ channel: event.channel, text: `<@${event.user}> has left the channel.`, }); }); // Reaction added app.event("reaction_added", async ({ event, client }) => { if (event.reaction === "eyes") { await client.chat.postMessage({ channel: event.item.channel, text: `Someone is looking at this! :eyes:`, thread_ts: event.item.ts, }); } }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import { DevtoolsPlugin } from "@microsoft/teams.dev"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), plugins: [new DevtoolsPlugin()], }); // Member joined -- conversationUpdate with membersAdded app.on("conversationUpdate", async ({ activity, send }) => { if (activity.membersAdded?.length) { for (const member of activity.membersAdded) { // Skip the bot itself if (member.id !== activity.recipient?.id) { await send(`Welcome to the channel, ${member.name}!`); } } } // Member left -- conversationUpdate with membersRemoved if (activity.membersRemoved?.length) { for (const member of activity.membersRemoved) { if (member.id !== activity.recipient?.id) { await send(`${member.name} has left the channel.`); } } } }); // Reaction events -- messageReaction route app.on("messageReaction" as any, async ({ activity, send }) => { if (activity.reactionsAdded?.length) { for (const reaction of activity.reactionsAdded) { if (reaction.type === "like") { await send("Someone liked a message!"); } } } }); // Install event (no Slack equivalent) -- good for welcome messages app.on("install.add", async ({ send, activity }) => { await send("Thanks for installing me! Type 'help' to get started."); }); app.start(3978); ``` ### Event mapping reference table | Slack Event / Handler | Teams Route / Handler | Notes | |---|---|---| | `app.message(pattern)` | `app.message(pattern)` | Direct equivalent; Teams uses RegExp | | `app.message(async ...)` (catch-all) | `app.on('message', async ...)` | Named route for catch-all | | `app.event('app_mention')` | `app.on('message')` | Channel messages imply @mention | | `app.event('member_joined_channel')` | `app.on('conversationUpdate')` + `membersAdded` | Check `activity.membersAdded` array | | `app.event('member_left_channel')` | `app.on('conversationUpdate')` + `membersRemoved` | Check `activity.membersRemoved` array | | `app.event('reaction_added')` | `app.on('messageReaction')` + `reactionsAdded` | Inspect `activity.reactionsAdded` | | `app.event('reaction_removed')` | `app.on('messageReaction')` + `reactionsRemoved` | Inspect `activity.reactionsRemoved` | | `app.event('message_changed')` | `app.on('messageUpdate')` | Message edit event | | `app.event('message_deleted')` | `app.on('messageDelete')` | Message deletion event | | `app.event('app_home_opened')` | `app.on('tab.open')` or static tab | Web-based tab, not bot view | | `app.event('team_join')` | `app.on('conversationUpdate')` + `membersAdded` | Same route as channel join | | `say(text)` | `send(text)` | Post new message | | `say({ thread_ts })` | `reply(text)` | Threaded reply | | `say({ thread_ts, reply_broadcast: true })` | `reply(text)` + `send(text)` | No single-call equivalent; must send twice | | `respond({ response_type: 'ephemeral' })` | *(no equivalent)* | Redesign required | | Reaction: any custom emoji (`:white_check_mark:`, `:rocket:`, etc.) | Reaction: 6 fixed types only (`like`, `heart`, `laugh`, `surprised`, `sad`, `angry`) | Custom emoji reactions impossible | | Thread discovery: `conversations.replies(channel, thread_ts)` | Graph API `GET /messages/{id}/replies` | Requires `ChannelMessage.Read.All` permission | | *(no equivalent)* | `app.on('install.add')` | Bot installed event | | *(no equivalent)* | `app.on('install.remove')` | Bot uninstalled event | | *(no equivalent)* | `app.on('typing')` | User typing indicator | ### Reaction workflow workaround: Adaptive Card buttons (R2) Replace Slack's custom emoji reaction workflows with explicit `Action.Submit` buttons on Adaptive Cards — the recommended Teams alternative. ```typescript // Slack (before): reaction-based approval app.event("reaction_added", async ({ event, client }) => { if (event.reaction === "white_check_mark") { await client.chat.postMessage({ channel: event.item.channel, text: `Approved by <@${event.user}>`, thread_ts: event.item.ts, }); } }); // Teams (after): button-based approval app.on("card.action" as any, async ({ activity }) => { const data = activity.value?.action?.data ?? activity.value; if (data?.action === "approve") { return { status: 200, body: { type: "AdaptiveCard", version: "1.5", body: [{ type: "TextBlock", text: `Approved by ${activity.from?.name}`, color: "Good", weight: "Bolder", }], // No actions = card becomes read-only }, }; } }); // Send the approval card (replaces posting a message users react to) function buildApprovalCard(requestId: string): object { return { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: `Request #${requestId} needs approval`, weight: "Bolder" }, ], actions: [ { type: "Action.Submit", title: "Approve", style: "positive", data: { action: "approve", requestId } }, { type: "Action.Submit", title: "Reject", style: "destructive", data: { action: "reject", requestId } }, ], }; } ``` **Why buttons are better:** Buttons provide explicit typed actions with an audit trail. Reactions are ambiguous (`:thumbsup:` vs `:+1:` vs `:white_check_mark:`) and produce no structured data. **Reverse (Teams → Slack):** Slack supports unlimited custom emoji — map directly or keep the button pattern (works on both platforms). ### Thread broadcast helper (Y2) Slack's `reply_broadcast: true` sends a thread reply that also appears in the channel. Teams has no single-call equivalent — use a helper that makes both calls. ```typescript // Teams: replicate reply_broadcast behavior async function replyWithBroadcast( ctx: { reply: (text: string) => Promise<any>; send: (text: string) => Promise<any> }, text: string ): Promise<void> { await ctx.reply(text); // threaded reply await ctx.send(text); // also post to channel } // Usage in a handler app.on("message", async (ctx) => { if (ctx.activity.text?.includes("broadcast")) { await replyWithBroadcast(ctx, "This appears in both the thread and the channel."); } }); ``` **Don't:** Try to batch into a single API call — Teams doesn't support it. Two calls is the correct pattern. **Reverse (Teams → Slack):** Use `say({ text, thread_ts: message.ts, reply_broadcast: true })` natively — single call. ### Thread discovery via Graph API (Y3) Fetching thread replies in Teams requires the Graph API, unlike Slack's simple `conversations.replies()`. ```typescript import { Client } from "@microsoft/microsoft-graph-client"; async function getThreadReplies( graphClient: Client, teamId: string, channelId: string, messageId: string, top: number = 50 ): Promise<any[]> { const response = await graphClient .api(`/teams/${teamId}/channels/${channelId}/messages/${messageId}/replies`) .top(top) .get(); return response.value; } // Usage in a handler (requires ChannelMessage.Read.All application permission) app.on("message", async ({ activity, send }) => { if (activity.text?.match(/^\/?replies/i)) { const replies = await getThreadReplies( graphClient, activity.channelData?.teamsTeamId, activity.channelData?.teamsChannelId, activity.conversation?.id?.split(";")[0] ?? "" ); await send(`Found ${replies.length} replies in this thread.`); } }); ``` **Watch out for:** `ChannelMessage.Read.All` is an application permission requiring admin consent. If you only need replies in the bot's own conversations, delegated permissions may suffice. **Reverse (Teams → Slack):** Use `conversations.replies({ channel, ts: thread_ts })` natively — no special permissions needed. ### RSC permission for all channel messages (Y16) Add RSC permission to the Teams manifest so the bot receives all channel messages without @mention — matching Slack's default behavior. ```json { "webApplicationInfo": { "id": "{{CLIENT_ID}}", "resource": "api://{{CLIENT_ID}}" }, "authorization": { "permissions": { "resourceSpecific": [ { "name": "ChannelMessage.Read.Group", "type": "Application" } ] } } } ``` Also strip @mention text from messages that do include a mention: ```typescript const app = new App({ // ... other options activity: { mentions: { stripText: true } }, }); ``` **Don't:** Change your UX to require @mention unless your bot genuinely shouldn't listen to all messages. **Reverse (Teams → Slack):** Slack bots receive all messages in channels they're added to by default — no config needed. ### Reverse direction (Teams → Slack) For Teams → Slack, reverse the mapping -- Teams routes map back to Slack events: - `app.on('message')` → `app.message(async ...)` catch-all or `app.event('app_mention')` if handling @mentions specifically - `app.message(pattern)` → `app.message(pattern)` (direct equivalent) - `app.on('conversationUpdate')` + `membersAdded` → `app.event('member_joined_channel')` - `app.on('conversationUpdate')` + `membersRemoved` → `app.event('member_left_channel')` - `app.on('messageReaction')` + `reactionsAdded` → `app.event('reaction_added')` -- note Teams has 6 fixed types; Slack supports unlimited custom emoji - `app.on('messageReaction')` + `reactionsRemoved` → `app.event('reaction_removed')` - `app.on('messageUpdate')` → `app.event('message_changed')` - `app.on('messageDelete')` → `app.event('message_deleted')` - `app.on('install.add')` → no direct Slack equivalent (use `app_home_opened` or OAuth completion callback for welcome messages) - `send(text)` → `say(text)` - `reply(text)` → `say({ text, thread_ts: message.ts })` - Add `ack()` calls to Slack event handlers where required - Slack bots receive all channel messages by default (no @mention required) -- adjust UX expectations accordingly ## pitfalls - **Assuming all channel messages are delivered**: In Teams channels, bots only receive messages when @mentioned. This is the biggest behavioral difference from Slack. Design accordingly or use RSC permissions for broader message access. - **Missing ephemeral message redesign**: Code that uses `respond({ response_type: 'ephemeral' })` will not work in Teams. Identify all ephemeral patterns early and plan alternative UX (personal chat, card refresh, or visible messages). - **Not filtering the bot from `membersAdded`**: The `conversationUpdate` event fires when the bot itself is added. Always check `member.id !== activity.recipient?.id` to avoid the bot welcoming itself. - **Thread model differences**: Slack threads use `thread_ts` on individual messages. Teams threaded replies use `reply()` or `replyToId`. The nesting model is similar but the API is different. - **Reaction type mismatch**: Slack reactions use emoji names (e.g., `"eyes"`, `"thumbsup"`). Teams reactions use a limited set of types (`"like"`, `"heart"`, `"laugh"`, `"surprised"`, `"sad"`, `"angry"`). Custom emoji reactions do not exist in Teams. - **Event handler context shape**: Slack event handlers receive `{ event, say, client }`. Teams handlers receive `{ activity, send, reply, stream }`. Do not try to destructure Slack property names from Teams handlers. - **No `client` equivalent for arbitrary API calls**: Slack's `client.chat.postMessage()` for posting to other channels maps to `app.send(conversationId, text)` in Teams. Store conversation IDs at install time for proactive messaging. - **Reaction-based workflows break silently**: A Slack bot using `:white_check_mark:` reactions as approval triggers will not error in Teams — it simply never fires because the custom emoji doesn't exist. Audit all `reaction_added` handlers for custom emoji names before migration. - **No `reply_broadcast` equivalent**: Slack's "also send to channel" flag on threaded replies has no Teams counterpart. If the bot relies on broadcasting thread replies to the main channel, you must send two separate messages: a `reply()` to the thread and a `send()` to the channel. - **Thread discovery requires Graph API with app permissions**: Fetching thread replies in Teams requires calling the Graph API (`/messages/{id}/replies`) with `ChannelMessage.Read.All` application-level permission. This is a significant permission escalation compared to Slack's `conversations.replies` which uses the standard bot token. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/subscribe-to-conversation-events - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/channel-and-group-conversations - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/conversation-basics - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages - https://learn.microsoft.com/en-us/microsoftteams/platform/graph-api/rsc/resource-specific-consent - https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/what-are-tabs - https://github.com/microsoft/teams.ts - https://slack.dev/bolt-js/concepts/events - https://slack.dev/bolt-js/concepts/message-listening ## instructions This expert covers bridging Slack event subscriptions and Teams activity handlers. Use it when adding cross-platform support in either direction: mapping Slack events (app_mention, member_joined_channel, member_left_channel, reaction_added, reaction_removed, message_changed, message_deleted, app_home_opened) to their Teams equivalents (message, conversationUpdate, messageReaction, messageUpdate, messageDelete, tab.open, install.add) or vice versa; converting between `say()`/`send()` and `reply()`/threaded patterns; handling ephemeral message differences; understanding the @mention requirement in Teams channels vs Slack's default all-message delivery; and mapping event payload properties between platforms. The comprehensive mapping table and reverse-direction section provide a quick reference for bridging in both directions. Pair with `../slack/runtime.bolt-foundations-ts.md` for Slack event patterns, and `../teams/runtime.routing-handlers-ts.md` for Teams activity routes. ## research Deep Research prompt: "Write a micro expert for bridging Slack events and Teams activity routes bidirectionally. Cover all major Slack events (app_mention, member_joined_channel, member_left_channel, reaction_added, reaction_removed, message subtypes, app_home_opened) with their Teams equivalents (message, conversationUpdate, messageReaction, messageUpdate, messageDelete, typing, install events) and vice versa. Include side-by-side TypeScript code examples, a comprehensive bidirectional mapping table, payload shape differences, the @mention requirement in channels, ephemeral message handling strategies, and common pitfalls for both directions." -
files-upload-download-ts.md 17.2 KB
# files-upload-download-ts ## purpose Bridges Slack file operations (files.upload, file events) and Teams file consent / OneDrive/SharePoint patterns for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack `files.upload` → Teams FileConsentCard + Graph API upload.** Slack bots upload files directly via `files.upload`. Teams bots cannot directly attach files to messages. Instead: (a) send a FileConsentCard asking the user for upload consent, (b) on consent, upload the file to the user's OneDrive via Graph API, (c) send a file info card with the download link. [learn.microsoft.com -- Send files](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4) 2. **The `supportsFiles: true` manifest flag is required.** Without `"supportsFiles": true` in the bot's manifest entry, Teams will not show file consent cards or allow the bot to handle file-related activities. This flag only works in personal (1:1) scope. [learn.microsoft.com -- Bot manifest](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#bots) 3. **Slack `files.sharedPublicURL` → Graph API `createLink` sharing link.** Slack creates a public URL for a file. In Teams/OneDrive, use the Graph API `POST /drives/{drive-id}/items/{item-id}/createLink` to create a sharing link with the desired permission scope (view, edit, anonymous). [learn.microsoft.com -- Create sharing link](https://learn.microsoft.com/en-us/graph/api/driveitem-createlink) 4. **Slack file events (`file_shared`, `file_created`) → `activity.attachments` in message handler.** When a user sends a file to a Teams bot, the file appears as an attachment on the incoming message activity. Check `activity.attachments` for items with `contentType` of `application/vnd.microsoft.teams.file.download.info`. [learn.microsoft.com -- Receive files](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4#receive-files-in-personal-chat) 5. **Download user-uploaded files via the `downloadUrl` in the attachment.** Each file attachment includes a `content.downloadUrl` with a pre-authenticated URL. Use `fetch()` or `axios` to download the file content. The URL is short-lived — download immediately in the handler. [learn.microsoft.com -- File download](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4) 6. **File upload in channels requires SharePoint, not OneDrive.** In personal chat, files go to the user's OneDrive. In channels, files go to the team's SharePoint document library. The Graph API path changes: `POST /drives/{drive-id}/root:/{folder}/{filename}:/content` where the drive is the channel's SharePoint drive. [learn.microsoft.com -- SharePoint files](https://learn.microsoft.com/en-us/graph/api/driveitem-put-content) 7. **Large files (>4 MB) require Graph resumable upload sessions.** Small files can use simple PUT to Graph API. Files larger than 4 MB must use a resumable upload session: `POST /drives/{drive-id}/items/{parent-id}:/filename:/createUploadSession`, then upload in 320 KB–60 MB chunks. Slack's `files.upload` handled this transparently. [learn.microsoft.com -- Resumable upload](https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession) 8. **FileConsentCard flow is a 3-step protocol.** Step 1: Bot sends a FileConsentCard with filename and size. Step 2: User accepts or declines. Step 3: On accept, Teams sends a `fileConsent/invoke` activity with an `uploadInfo` containing the upload URL. On decline, Teams sends the same invoke with a `declined` action. Handle both cases. [learn.microsoft.com -- File consent](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4#send-files-to-personal-chat) 9. **Slack `files.list` / `files.info` → Graph API drive item queries.** Slack has dedicated file listing APIs. In Teams, files are stored in OneDrive/SharePoint. Use Graph API: `GET /drives/{drive-id}/root/children` to list files, `GET /drives/{drive-id}/items/{item-id}` for file metadata. [learn.microsoft.com -- List items](https://learn.microsoft.com/en-us/graph/api/driveitem-list-children) 10. **File handling only works in personal (1:1) chat scope.** The `supportsFiles` manifest flag and FileConsentCard only work in personal bot conversations. For channel file operations, use Graph API directly without the consent card flow. This is a significant scope limitation compared to Slack where `files.upload` works in any channel. [learn.microsoft.com -- Bot files](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4) 11. **Reverse direction (Teams → Slack):** For Teams → Slack, the reverse is simpler: Slack's `files.uploadV2` is a direct single-call API vs Teams' multi-step consent flow. Map OneDrive/SharePoint file URLs to `files.uploadV2` with a buffer, Graph `createLink` sharing links to `files.sharedPublicURL`, and `activity.attachments` file downloads to Slack `file_shared` event handling. The Slack API handles storage transparently. ## patterns ### Upload a file with FileConsentCard (replaces files.upload) **Slack (before):** ```typescript import { App } from "@slack/bolt"; import fs from "fs"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/export", async ({ ack, command, client }) => { await ack(); const csvData = await generateReport(); // Direct file upload — Slack handles storage await client.files.uploadV2({ channel_id: command.channel_id, filename: "report.csv", file: Buffer.from(csvData), title: "Monthly Report", initial_comment: "Here's your report!", }); }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Store pending uploads keyed by conversation const pendingUploads = new Map<string, Buffer>(); // Step 1: Send FileConsentCard (replaces files.upload) app.message(/^\/?export$/i, async ({ send, activity }) => { const csvData = await generateReport(); const csvBuffer = Buffer.from(csvData); const convId = activity.conversation?.id ?? ""; // Store the file content for later upload pendingUploads.set(convId, csvBuffer); // Send consent card — user must approve the upload await send({ attachments: [{ contentType: "application/vnd.microsoft.teams.card.file.consent", name: "report.csv", content: { description: "Monthly Report — click Accept to save to your OneDrive", sizeInBytes: csvBuffer.length, acceptContext: { filename: "report.csv" }, declineContext: { filename: "report.csv" }, }, }], }); }); // Step 2: Handle consent response app.on("fileConsent" as any, async ({ activity, send }) => { const action = activity.value?.action; const convId = activity.conversation?.id ?? ""; if (action === "accept") { // Step 3: Upload file to the URL provided by Teams const uploadInfo = activity.value?.uploadInfo; const fileContent = pendingUploads.get(convId); if (uploadInfo && fileContent) { // Upload to OneDrive via the pre-signed URL await fetch(uploadInfo.uploadUrl, { method: "PUT", headers: { "Content-Type": "application/octet-stream" }, body: fileContent, }); // Send confirmation with file card await send({ attachments: [{ contentType: "application/vnd.microsoft.teams.card.file.info", name: "report.csv", contentUrl: uploadInfo.contentUrl, content: { uniqueId: uploadInfo.uniqueId, fileType: "csv", }, }], }); } pendingUploads.delete(convId); } else { await send("File upload cancelled."); pendingUploads.delete(convId); } }); async function generateReport(): Promise<string> { return "Name,Status\nServer1,OK\nServer2,Down"; } app.start(3978); ``` ### Receive and process user-uploaded files **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.event("file_shared", async ({ event, client }) => { const fileInfo = await client.files.info({ file: event.file_id }); const file = fileInfo.file!; // Download file content using the private URL + bot token const response = await fetch(file.url_private!, { headers: { Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}` }, }); const content = await response.text(); await client.chat.postMessage({ channel: event.channel_id, text: `Received ${file.name} (${file.size} bytes). Processing...`, }); // Process the file content... }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Files arrive as attachments on regular message activities app.on("message", async ({ activity, send }) => { const fileAttachments = activity.attachments?.filter( (a: any) => a.contentType === "application/vnd.microsoft.teams.file.download.info" ); if (fileAttachments?.length) { for (const attachment of fileAttachments) { const downloadUrl = attachment.content?.downloadUrl; const fileName = attachment.name; if (downloadUrl) { // Download using the pre-authenticated URL (no token needed) const response = await fetch(downloadUrl); const content = await response.text(); await send(`Received ${fileName} (${content.length} chars). Processing...`); // Process the file content... } } } }); app.start(3978); ``` ### Reusable `sendFile()` helper (Y4/5/6 best practice) Build a unified helper that auto-detects personal vs. channel context and handles chunking. This eliminates the 30-line FileConsentCard footgun. ```typescript import { Client } from "@microsoft/microsoft-graph-client"; interface SendFileOptions { filename: string; content: Buffer; description?: string; } async function sendFile( ctx: { send: (msg: any) => Promise<any>; activity: any }, graphClient: Client, options: SendFileOptions ): Promise<void> { const { filename, content, description } = options; const conversationType = ctx.activity.conversation?.conversationType; if (conversationType === "personal") { // Personal chat → FileConsentCard flow await ctx.send({ attachments: [{ contentType: "application/vnd.microsoft.teams.card.file.consent", name: filename, content: { description: description ?? filename, sizeInBytes: content.length, acceptContext: { filename, size: content.length }, declineContext: { filename }, }, }], }); // Store content for the fileConsent handler to pick up pendingUploads.set(ctx.activity.conversation?.id ?? "", { content, filename }); } else { // Channel → Direct Graph API upload to SharePoint const teamId = ctx.activity.channelData?.teamsTeamId; const channelId = ctx.activity.channelData?.teamsChannelId; const driveId = await getChannelDriveId(graphClient, teamId, channelId); if (content.length <= 4 * 1024 * 1024) { // Small file: simple PUT await graphClient .api(`/drives/${driveId}/root:/${filename}:/content`) .put(content); } else { // Large file (>4 MB): resumable upload session const session = await graphClient .api(`/drives/${driveId}/root:/${filename}:/createUploadSession`) .post({ item: { name: filename } }); const chunkSize = 320 * 1024; // 320 KB chunks for (let offset = 0; offset < content.length; offset += chunkSize) { const chunk = content.subarray(offset, offset + chunkSize); const end = Math.min(offset + chunkSize, content.length); await fetch(session.uploadUrl, { method: "PUT", headers: { "Content-Range": `bytes ${offset}-${end - 1}/${content.length}`, "Content-Type": "application/octet-stream", }, body: chunk, }); } } await ctx.send(`File uploaded: ${filename}`); } } async function getChannelDriveId( graphClient: Client, teamId: string, channelId: string ): Promise<string> { const response = await graphClient .api(`/teams/${teamId}/channels/${channelId}/filesFolder`) .get(); return response.parentReference.driveId; } ``` **Key decisions:** - Personal chat → FileConsentCard flow (requires `supportsFiles: true` in manifest) - Channel → Direct Graph API upload to SharePoint (no consent card) - Files >4 MB → Graph resumable upload session with 320 KB chunks **Don't:** Store pending file buffers in memory for long periods. Upload promptly or stream to a temporary blob. **Reverse (Teams → Slack):** Use `files.uploadV2({ channel_id, file: buffer, filename })` — single call, no consent step. ### File operation mapping table | Slack API | Teams Equivalent | Notes | |---|---|---| | `files.uploadV2(channel, file)` | FileConsentCard → Graph PUT | 3-step consent flow; personal chat only | | `files.sharedPublicURL(file)` | Graph `createLink(type, scope)` | Creates OneDrive/SharePoint sharing link | | `files.info(file_id)` | Graph `GET /drives/{id}/items/{id}` | File metadata from OneDrive/SharePoint | | `files.list(channel)` | Graph `GET /drives/{id}/root/children` | List drive items | | `file_shared` event | `activity.attachments` check in message handler | No dedicated event; check attachments on each message | | `file.url_private` + bot token | `attachment.content.downloadUrl` | Pre-authenticated URL; no token needed | | Large file upload | Graph resumable upload session | Required for files > 4 MB | ## pitfalls - **Missing `supportsFiles: true` in manifest**: Without this flag, Teams will not render FileConsentCards and file-related invoke activities will never fire. This is the #1 cause of "file upload doesn't work" during migration. - **FileConsentCard only works in personal (1:1) chat**: Channel bots cannot use the consent card flow. For channel file operations, upload directly via Graph API to the team's SharePoint document library — which requires different Graph API permissions and paths. - **Download URLs are short-lived**: The `downloadUrl` in file attachments is pre-authenticated but expires. Download the file immediately in the message handler. Do not store the URL for later use. - **Large file upload requires chunking**: Files over 4 MB cannot use simple PUT. You must create an upload session and send chunks. Slack's `files.upload` handled this transparently — Teams requires explicit chunking logic. - **Graph API permissions required**: File operations via Graph API require `Files.ReadWrite` (delegated) or `Files.ReadWrite.All` (application) permissions. These must be configured in the Azure AD app registration and consented by an admin for application permissions. - **No file preview in bot messages**: Slack generates inline previews for uploaded images and documents. Teams file info cards show a file icon and name but not an inline preview. For image files, consider embedding the image URL directly in an Adaptive Card `Image` element instead. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4 - https://learn.microsoft.com/en-us/graph/api/driveitem-put-content - https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession - https://learn.microsoft.com/en-us/graph/api/driveitem-createlink - https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#bots - https://github.com/microsoft/teams.ts - https://api.slack.com/methods/files.uploadV2 — Slack files.upload - https://api.slack.com/methods/files.sharedPublicURL — Slack file sharing ## instructions Use this expert when adding cross-platform support in either direction for Slack file operations or Teams file consent / OneDrive/SharePoint patterns. It covers: `files.upload` to FileConsentCard + Graph upload, `files.sharedPublicURL` to Graph sharing links, file event handling via activity attachments, large file resumable uploads, and the personal-chat-only limitation. For Teams → Slack, the reverse is simpler: Slack's `files.uploadV2` is a direct single-call API vs Teams' multi-step consent flow. Pair with `../teams/graph.usergraph-appgraph-ts.md` for Graph API authentication patterns, `../teams/runtime.manifest-ts.md` for the `supportsFiles` manifest flag, and `interactive-responses-ts.md` for the consent card invoke handling pattern. ## research Deep Research prompt: "Write a micro expert for bridging Slack file operations (files.upload, files.sharedPublicURL, file_shared event, file download) and Teams file consent / OneDrive/SharePoint patterns in either direction for cross-platform bots. Cover FileConsentCard 3-step flow, OneDrive/SharePoint Graph API uploads, resumable upload sessions for large files, receiving files via activity attachments, the supportsFiles manifest flag, personal-chat-only limitation, Graph API permission requirements, and reverse-direction notes for Teams → Slack (simpler single-call API). Include TypeScript code examples and a mapping table." -
identity-oauth-bridge-ts.md 19.8 KB
# identity-oauth-bridge-ts ## purpose Bridges Slack and Teams/Azure AD identity systems (user/channel IDs, OAuth, signing) for cross-platform bots targeting Slack, Teams, or both. ## rules 1. Slack uses proprietary ID formats: user IDs start with `U` (e.g., `U01ABCDEF`), channel IDs start with `C` (e.g., `C02GHIJKL`), team/workspace IDs start with `T` (e.g., `T03MNOPQR`), and bot IDs start with `B`. These IDs have no relationship to Teams/Azure AD identifiers and cannot be mapped automatically. [api.slack.com/types](https://api.slack.com/types) 2. Teams identifies users by Azure AD Object ID (a GUID like `00000000-0000-0000-0000-000000000000`), available at `activity.from.aadObjectId`. Conversation IDs are opaque strings like `19:abc123@thread.v2` for channels or `a]concat@...` for personal chats. These formats are fundamentally different from Slack IDs. [learn.microsoft.com -- Activity schema](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference) 3. Slack's request verification via `signingSecret` (HMAC-SHA256 of request body) is replaced by **Bot Framework JWT token validation** in Teams. The Teams SDK handles JWT validation automatically -- no manual signing secret check is needed. [learn.microsoft.com -- Bot authentication](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication) 4. Slack's bot token (`xoxb-...`) used for API calls is replaced by **Azure Bot credentials** (`CLIENT_ID` + `CLIENT_SECRET` + `TENANT_ID`). The Teams SDK uses these to obtain tokens for the Bot Framework service automatically. [learn.microsoft.com -- Register a bot](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration) 5. Slack OAuth scopes (e.g., `chat:write`, `users:read`, `commands`) map to **Azure AD permissions** for the Microsoft Graph API (e.g., `User.Read`, `ChannelMessage.Send`). Slack scopes are configured in the Slack app dashboard; Azure AD permissions are configured in the Azure Portal under App Registration > API Permissions. [learn.microsoft.com -- Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-reference) 6. Slack user tokens (obtained via OAuth `users:read` or user token grant) map to **Teams SSO / OAuth card flow**. In Teams, configure an OAuth connection in the Azure Bot resource, then use `isSignedIn` / `signin()` / `userGraph` in handlers to access the user's delegated token for Graph API calls. [learn.microsoft.com -- Bot SSO](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/bot-sso-overview) 7. To resolve user identity across platforms during migration, use a **shared attribute** like email address. Query Slack's `users.info` API for the user's email, then look up the same email in Azure AD via Microsoft Graph `users?$filter=mail eq '...'`. Build a mapping table of Slack user ID to AAD Object ID. [learn.microsoft.com -- Graph users API](https://learn.microsoft.com/en-us/graph/api/user-list) 8. Any data stored with Slack IDs as keys (user preferences, conversation history, permissions) must be **re-keyed** to Teams/AAD IDs. Plan a data migration step that uses the email-based mapping table to translate stored Slack user IDs to AAD Object IDs. [learn.microsoft.com -- Migration planning](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) 9. Slack workspace-level operations (e.g., listing all users via `users.list`, posting to any channel) require bot scopes. In Teams, equivalent operations use Microsoft Graph with **application permissions** (consented by a tenant admin). Use `appGraph` for service-to-service calls and `userGraph` for delegated user calls. [learn.microsoft.com -- Graph auth overview](https://learn.microsoft.com/en-us/graph/auth/auth-concepts) 10. Teams supports **managed identity** as an alternative to client secret for production deployments on Azure. Set `managedIdentityClientId: 'system'` in App options to use Azure Managed Identity instead of storing secrets in environment variables. [learn.microsoft.com -- Managed identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) ## patterns ### Environment variable mapping between Slack and Teams **Slack `.env`:** ```env # Slack Bot Configuration SLACK_BOT_TOKEN=your-slack-bot-token SLACK_SIGNING_SECRET=your-signing-secret SLACK_APP_TOKEN=your-slack-app-token SLACK_CLIENT_ID=your-slack-client-id SLACK_CLIENT_SECRET=your-slack-client-secret PORT=3000 ``` **Teams `.env`:** ```env # Azure Bot Registration CLIENT_ID=00000000-0000-0000-0000-000000000000 CLIENT_SECRET=your-azure-bot-client-secret TENANT_ID=00000000-0000-0000-0000-000000000000 PORT=3978 ``` **Environment variable mapping table:** | Slack Variable | Teams Variable | Notes | |---|---|---| | `SLACK_BOT_TOKEN` (`xoxb-...`) | `CLIENT_ID` + `CLIENT_SECRET` | Teams SDK manages token acquisition automatically | | `SLACK_SIGNING_SECRET` | *(not needed)* | Bot Framework JWT validation is automatic | | `SLACK_APP_TOKEN` (`xapp-...`) | *(not needed)* | Socket mode is Slack-only; Teams uses HTTPS | | `SLACK_CLIENT_ID` | `CLIENT_ID` | Azure Bot App Registration ID (GUID) | | `SLACK_CLIENT_SECRET` | `CLIENT_SECRET` | Azure Bot App Registration secret | | *(not applicable)* | `TENANT_ID` | Azure AD tenant ID (new for Teams) | | `PORT` (default 3000) | `PORT` (default 3978) | Different conventional defaults | **Identity concept mapping table:** | Slack Concept | Teams/Azure AD Concept | Format | |---|---|---| | User ID (`U01ABCDEF`) | AAD Object ID | GUID (`00000000-...`) | | Channel ID (`C02GHIJKL`) | Conversation ID | `19:abc@thread.v2` | | Team/Workspace ID (`T03MNOPQR`) | Tenant ID | GUID | | Bot ID (`B04STUVWX`) | Bot ID (from App Registration) | GUID | | DM Channel ID (`D05YZABCD`) | Personal conversation ID | Opaque string | | Signing Secret | Bot Framework JWT | Automatic validation | | Bot Token (`xoxb-...`) | Client credentials flow | CLIENT_ID + CLIENT_SECRET | | User Token (`xoxp-...`) | Delegated OAuth token | SSO / OAuth card flow | | OAuth scopes (`chat:write`) | Azure AD permissions (`ChannelMessage.Send`) | Configured in Azure Portal | | Slack App Dashboard | Azure Portal + manifest.json | Config split between portal and file | ### Migrating authentication from Slack OAuth to Teams SSO **Slack (before) -- Using Slack OAuth for user identity:** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/whoami", async ({ ack, command, client }) => { await ack(); // Use the bot token to look up user info const userInfo = await client.users.info({ user: command.user_id }); const email = userInfo.user?.profile?.email ?? "unknown"; const name = userInfo.user?.real_name ?? "unknown"; await client.chat.postMessage({ channel: command.channel_id, text: `You are ${name} (${email})`, }); }); ``` **Teams (after) -- Using Teams SSO and Microsoft Graph:** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import { DevtoolsPlugin } from "@microsoft/teams.dev"; import * as endpoints from "@microsoft/teams.graph-endpoints"; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger("my-bot", { level: "info" }), plugins: [new DevtoolsPlugin()], oauth: { defaultConnectionName: "graph" }, }); app.message(/^\/?whoami$/i, async ({ isSignedIn, signin, userGraph, send }) => { // If user is not signed in, trigger the SSO/OAuth flow if (!isSignedIn) { await signin({ signInButtonText: "Sign In to continue" }); return; } // Use the delegated Graph client to get user profile const me = await userGraph.call(endpoints.me.get); await send(`You are ${me.displayName} (${me.mail})`); }); // Handle successful sign-in app.event("signin", async ({ send, userGraph }) => { const me = await userGraph.call(endpoints.me.get); await send(`Welcome, ${me.displayName}! You are now signed in.`); }); app.start(3978); ``` ### Building a Slack-to-AAD user ID mapping table ```typescript import { WebClient } from "@slack/web-api"; import { Client as GraphClient } from "@microsoft/microsoft-graph-client"; interface UserMapping { slackUserId: string; slackEmail: string; aadObjectId: string | null; aadDisplayName: string | null; } async function buildUserMappingTable( slackClient: WebClient, graphClient: GraphClient ): Promise<UserMapping[]> { const mappings: UserMapping[] = []; // Step 1: Fetch all Slack users const slackUsers = await slackClient.users.list({}); const members = slackUsers.members ?? []; for (const slackUser of members) { if (slackUser.deleted || slackUser.is_bot) continue; const email = slackUser.profile?.email; if (!email) { mappings.push({ slackUserId: slackUser.id!, slackEmail: "", aadObjectId: null, aadDisplayName: null, }); continue; } // Step 2: Look up the same email in Azure AD via Graph try { const result = await graphClient .api("/users") .filter(`mail eq '${email}' or userPrincipalName eq '${email}'`) .select("id,displayName,mail") .get(); const aadUser = result.value?.[0]; mappings.push({ slackUserId: slackUser.id!, slackEmail: email, aadObjectId: aadUser?.id ?? null, aadDisplayName: aadUser?.displayName ?? null, }); } catch { mappings.push({ slackUserId: slackUser.id!, slackEmail: email, aadObjectId: null, aadDisplayName: null, }); } } return mappings; } // Step 3: Use the mapping to re-key stored data async function migrateUserData( mappings: UserMapping[], oldStore: Map<string, unknown>, newStore: Map<string, unknown> ): Promise<void> { for (const mapping of mappings) { if (!mapping.aadObjectId) continue; const data = oldStore.get(mapping.slackUserId); if (data) { newStore.set(mapping.aadObjectId, data); } } } ``` ### Converting Slack OAuth implementation code to Teams OAuth Slack SDKs (especially `java-slack-sdk` and `@slack/bolt`) implement OAuth with explicit services: `InstallationService` for storing tokens, `OAuthStateService` for CSRF, and `OAuthCallbackHandler` for the redirect. Teams replaces ALL of this with declarative config. **Slack Java SDK OAuth (before):** ```java // --- Slack Java SDK OAuth implementation --- // InstallationService — stores bot tokens per workspace public class FileInstallationService implements InstallationService { public void saveInstallerAndBot(Installer installer) { /* persist to DB */ } public Installer findInstaller(String enterpriseId, String teamId) { /* lookup */ } public Bot findBot(String enterpriseId, String teamId) { /* lookup */ } public void deleteBot(Bot bot) { /* remove */ } public void deleteInstaller(Installer installer) { /* remove */ } } // OAuthStateService — generates and validates CSRF state parameter public class FileOAuthStateService implements OAuthStateService { public String issueNewState(Request req) { /* generate random state */ } public boolean isValid(OAuthState state) { /* validate state */ } public void consume(OAuthState state) { /* mark used */ } } // App configuration with OAuth App app = new App(AppConfig.builder() .clientId(System.getenv("SLACK_CLIENT_ID")) .clientSecret(System.getenv("SLACK_CLIENT_SECRET")) .signingSecret(System.getenv("SLACK_SIGNING_SECRET")) .oAuthInstallPath("/slack/install") .oAuthRedirectUriPath("/slack/oauth_redirect") .oAuthCompletionUrl("https://example.com/success") .oAuthCancellationUrl("https://example.com/cancel") .installationService(new FileInstallationService()) .oauthStateService(new FileOAuthStateService()) .build()); ``` **Teams OAuth (after):** ```typescript // --- Teams OAuth — all of the above is replaced by config --- import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger('my-bot', { level: 'info' }), // This single config block replaces: // - InstallationService (token storage is managed by Azure Bot Service) // - OAuthStateService (CSRF handled by Bot Framework) // - OAuthCallbackHandler (redirect handled by Azure Bot Service) // - Token refresh logic (managed by Azure Bot Service) oauth: { defaultConnectionName: 'graph', // configured in Azure Portal // That's it. No custom services needed. }, }); // Instead of Slack's multi-step OAuth flow with custom storage: // 1. Azure Bot Service manages token acquisition, refresh, and storage // 2. CSRF protection is built into the Bot Framework sign-in flow // 3. The OAuth connection is configured in Azure Portal (not code) // 4. Use isSignedIn/signin() in handlers to trigger auth when needed app.message(/^profile$/i, async ({ isSignedIn, signin, userGraph, send }) => { if (!isSignedIn) { await signin(); return; } const me = await userGraph.call(endpoints.me.get); await send(`Signed in as ${me.displayName}`); }); ``` **What gets DELETED during conversion:** | Slack OAuth Component | Teams Equivalent | Action | |---|---|---| | `InstallationService` + DB storage | Azure Bot Service token cache | Delete entirely | | `OAuthStateService` + CSRF tokens | Bot Framework built-in CSRF | Delete entirely | | `OAuthCallbackHandler` + redirect routes | Azure Bot Service callbacks | Delete entirely | | Token refresh / expiry logic | Azure Bot Service auto-refresh | Delete entirely | | `/slack/install` route | Teams app install flow | Delete entirely | | `/slack/oauth_redirect` route | Azure Bot Service | Delete entirely | | Multi-workspace token lookup | Managed identity / tenant config | Delete entirely | | Slack OAuth scopes in code | Azure Portal API Permissions | Configure in portal | ### Reverse direction (Teams → Slack) For Teams → Slack, the same mapping table applies in reverse. AAD Object IDs need mapping to Slack user IDs via email lookup. Key reverse mappings: - `activity.from.aadObjectId` (GUID) → Slack User ID (`U...`) via email-based lookup: query Graph `users/{aadObjectId}` for email, then `users.lookupByEmail` in Slack - `activity.conversation.id` (`19:abc@thread.v2`) → Slack Channel ID (`C...`) via channel name mapping or a stored lookup table - `CLIENT_ID` + `CLIENT_SECRET` + `TENANT_ID` → `SLACK_BOT_TOKEN` (`xoxb-...`) + `SLACK_SIGNING_SECRET` - Azure AD permissions (`ChannelMessage.Send`, `User.Read`) → Slack OAuth scopes (`chat:write`, `users:read`) - Teams SSO / OAuth card flow → Slack OAuth with `InstallationService` and `OAuthStateService` (Slack requires explicit token storage and refresh logic that Azure Bot Service handles automatically) - Bot Framework JWT validation (automatic) → Slack signing secret HMAC-SHA256 verification (must add `signingSecret` to Bolt config) - Azure Managed Identity → no Slack equivalent; use environment variables or secret manager for Slack tokens - The email-based user mapping table built for Slack → Teams works identically in reverse ## pitfalls - **Assuming Slack IDs can be reused**: Slack IDs (`U...`, `C...`, `T...`) are completely incompatible with Teams/AAD IDs. Any code that stores or references Slack IDs must be updated to use AAD Object IDs and conversation IDs. - **Manual signing secret validation**: Developers sometimes port Slack's HMAC verification middleware to Teams. This is unnecessary -- the Bot Framework validates JWT tokens automatically. Remove all signing secret verification code. - **Expecting ephemeral identity context**: Slack's `user_id` is always present in command and action payloads. In Teams, `activity.from.aadObjectId` may be `undefined` in some contexts (e.g., webhook-originated activities). Always null-check. - **OAuth scope confusion**: Slack scopes like `chat:write` do not map 1:1 to Azure AD permissions. Audit each Slack scope used and find the equivalent Graph permission. Some Slack capabilities require multiple Graph permissions or a different API approach entirely. - **Storing tokens insecurely**: Slack bot tokens are long-lived strings. Azure Bot credentials use short-lived JWT tokens managed by the SDK. Never try to cache or store Bot Framework tokens manually. - **Skipping the user mapping step**: Without building a Slack-to-AAD mapping table, any user-specific data (preferences, history, permissions) stored under Slack IDs becomes inaccessible. Plan this migration step early. - **Tenant ID confusion**: Slack workspaces have a single team ID. Azure AD tenants can contain multiple Teams organizations. Ensure `TENANT_ID` is set correctly -- use the specific tenant ID for single-tenant apps or `common` for multi-tenant. - **Forgetting to configure OAuth connection**: Teams SSO requires an OAuth connection configured in the Azure Bot resource (Settings > OAuth Connection Settings). Without it, `signin()` calls fail silently. - **Porting OAuth implementation code instead of deleting it**: Slack's `InstallationService`, `OAuthStateService`, and `OAuthCallbackHandler` have NO Teams equivalent. Azure Bot Service handles token storage, CSRF, and callbacks automatically. Attempting to port these services wastes effort and introduces bugs. Delete them entirely and use the `oauth: { defaultConnectionName }` config. - **Custom token refresh logic**: Slack apps often implement manual token refresh with `oauth.v2.access`. Azure Bot Service refreshes tokens automatically. Delete all refresh code. ## references - https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication - https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/bot-sso-overview - https://learn.microsoft.com/en-us/graph/permissions-reference - https://learn.microsoft.com/en-us/graph/api/user-list - https://learn.microsoft.com/en-us/graph/auth/auth-concepts - https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview - https://api.slack.com/types - https://api.slack.com/methods/users.info - https://github.com/microsoft/teams.ts ## instructions This expert covers bridging Slack and Teams/Azure AD identity and authentication systems. Use it when adding cross-platform support in either direction: understanding the differences between Slack IDs (U/C/T/B prefixed) and Teams IDs (AAD Object IDs, conversation IDs); bridging signing/verification (Slack signing secret ↔ Bot Framework JWT); mapping environment variables (SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET ↔ CLIENT_ID, CLIENT_SECRET, TENANT_ID); converting between Slack OAuth and Teams SSO with Microsoft Graph; building a bidirectional user mapping table using email as the shared attribute; bridging Slack OAuth scopes and Azure AD permissions; and configuring authentication for either platform. Pair with `../teams/auth.oauth-sso-ts.md` for Teams OAuth/SSO flow, and `../teams/graph.usergraph-appgraph-ts.md` for Graph API user lookup during identity mapping. ## research Deep Research prompt: "Write a micro expert for bridging Slack and Teams/Azure AD identity systems bidirectionally. Cover Slack ID formats (U/C/T/B IDs) vs Teams IDs (AAD Object IDs, conversation IDs), signing/verification bridging (signing secret <-> Bot Framework JWT), environment variable mapping in both directions, Slack OAuth <-> Teams SSO flow, Slack scopes <-> Azure AD Graph permissions, building a bidirectional user mapping table via email lookup, data re-keying strategies, and managed identity for production. Include mapping tables and TypeScript examples." -
index.md 13 KB
# bridge-router ## purpose Route cross-platform bridging tasks to the minimal set of micro-expert files. Each expert covers bridging between Slack and Teams (or AWS and Azure) in either direction. Read only the clusters that match the user's request. ## task clusters ### Block Kit <-> Adaptive Cards When: converting Block Kit JSON to Adaptive Card JSON or vice versa, mapping Slack blocks to card elements, mapping Adaptive Card elements to Block Kit blocks Read: - `ui-block-kit-adaptive-cards-ts.md` Cross-domain deps: `../slack/ui.block-kit-ts.md` (Slack Block Kit patterns), `../teams/ui.adaptive-cards-ts.md` (Teams Adaptive Card patterns) ### Commands: Slash <-> Text When: bridging slash commands between Slack and Teams, command registration differences, porting commands in either direction Read: - `commands-slash-text-ts.md` Cross-domain deps: `../slack/runtime.slash-commands-ts.md` (Slack command patterns), `../teams/runtime.routing-handlers-ts.md` (Teams app.message() patterns) ### Events <-> Activities When: mapping Slack events to Teams activity handlers or vice versa, event model differences Read: - `events-activities-ts.md` Cross-domain deps: `../slack/runtime.bolt-foundations-ts.md` (Slack event patterns), `../teams/runtime.routing-handlers-ts.md` (Teams activity routes) ### Identity & OAuth Bridge When: bridging Slack OAuth/identity and Azure AD/Entra ID, user mapping, SSO, OAuth implementation code (InstallationService, OAuthStateService, token refresh) Read: - `identity-oauth-bridge-ts.md` Cross-domain deps: `../teams/auth.oauth-sso-ts.md` (Teams OAuth/SSO flow), `../teams/graph.usergraph-appgraph-ts.md` (Graph API for user lookup) ### Middleware <-> Handlers When: converting Slack Bolt middleware chains to Teams handler patterns or vice versa, porting global/listener middleware, removing or adding ack() Read: - `middleware-handlers-ts.md` Cross-domain deps: `../slack/runtime.bolt-foundations-ts.md` (Slack middleware patterns), `../teams/runtime.routing-handlers-ts.md` (Teams handler patterns) ### Modals <-> Dialogs When: bridging Slack modals (views.open, viewSubmission, viewsUpdate, viewClosed, blockSuggestion in modals) and Teams task module / dialog flows Read: - `ui-modals-dialogs-ts.md` Cross-domain deps: `../teams/ui.dialogs-task-modules-ts.md` (Teams dialog patterns), `ui-block-kit-adaptive-cards-ts.md` (converting modal UI between Block Kit and Adaptive Cards) ### App Home <-> Personal Tab When: bridging Slack App Home tab (AppHomeOpenedEvent, views.publish) and Teams personal tab or bot welcome card Read: - `ui-app-home-personal-tab-ts.md` Cross-domain deps: `events-activities-ts.md` (event mapping), `../teams/ui.adaptive-cards-ts.md` (card construction), `../teams/runtime.proactive-messaging-ts.md` (background updates) ### Legacy Attachments <-> Cards When: bridging pre-Block Kit legacy Slack attachments (callback_id, color, actions, attachmentAction) and Adaptive Cards Read: - `ui-legacy-attachments-cards-ts.md` Cross-domain deps: `../teams/ui.adaptive-cards-ts.md` (Teams card patterns) ### Transport: Socket Mode <-> HTTPS When: bridging Slack Socket Mode, RTM, or HTTP Events API and Teams Bot Framework HTTPS transport Read: - `transport-socketmode-https-ts.md` Cross-domain deps: `../teams/runtime.app-init-ts.md` (Teams app startup), `../teams/dev.debug-test-ts.md` (ngrok/Dev Tunnels setup) ### Infrastructure: Compute When: bridging Lambda and Azure Functions, compute migration, serverless porting in either direction Read: - `infra-compute-ts.md` - `infra-secrets-config-ts.md` (App Settings / env vars needed for compute config) ### Infrastructure: Storage When: bridging S3 and Blob Storage, DynamoDB and Cosmos DB, storage migration in either direction Read: - `infra-storage-ts.md` Cross-domain deps: `../teams/state.storage-patterns-ts.md` (IStorage interface for bot state on Cosmos DB) ### Infrastructure: Secrets & Config When: bridging AWS Secrets Manager and Azure Key Vault, SSM and App Configuration Read: - `infra-secrets-config-ts.md` Cross-domain deps: `../security/secrets-ts.md` (secrets management best practices) ### Infrastructure: Observability When: bridging CloudWatch and Application Insights, X-Ray and Azure Monitor, logging migration Read: - `infra-observability-ts.md` Cross-domain deps: `../teams/dev.debug-test-ts.md` (Teams SDK logging with ConsoleLogger) ### Interactive Responses When: bridging respond({ replace_original }), respond({ delete_original }), chat.update, chat.postEphemeral, deferred responses, response_url patterns between Slack and Teams Read: - `interactive-responses-ts.md` Cross-domain deps: `../teams/ui.adaptive-cards-ts.md` (card construction), `../teams/runtime.proactive-messaging-ts.md` (deferred update infrastructure) ### Files: Upload & Download When: bridging files.upload, files.sharedPublicURL, file events, file download/upload patterns between platforms Read: - `files-upload-download-ts.md` Cross-domain deps: `../teams/graph.usergraph-appgraph-ts.md` (Graph API auth), `../teams/runtime.manifest-ts.md` (supportsFiles flag) ### Link Unfurl <-> Preview When: bridging link_shared event and chat.unfurl() (Slack) with link preview cards (Teams) Read: - `link-unfurl-preview-ts.md` Cross-domain deps: `../teams/ui.message-extensions-ts.md` (message extension patterns), `../teams/runtime.manifest-ts.md` (messageHandlers domain config) ### Shortcuts <-> Extensions When: bridging Slack global shortcuts and message shortcuts with Teams message extensions or compose extensions Read: - `shortcuts-extensions-ts.md` Cross-domain deps: `../teams/ui.message-extensions-ts.md` (message extension patterns), `../teams/ui.dialogs-task-modules-ts.md` (task module details) ### Scheduling & Deferred Send When: bridging chat.scheduleMessage, chat.deleteScheduledMessage, reminders.add, timer-based patterns between platforms Read: - `scheduling-deferred-send-ts.md` Cross-domain deps: `../teams/runtime.proactive-messaging-ts.md` (proactive send infrastructure), `../teams/state.storage-patterns-ts.md` (persisting scheduled items) ### Channel Ops <-> Graph When: bridging conversations.create, conversations.archive, conversations.invite, conversations.kick, conversations.setTopic via Graph API Read: - `channel-ops-graph-ts.md` Cross-domain deps: `../teams/graph.usergraph-appgraph-ts.md` (Graph API auth), `identity-oauth-bridge-ts.md` (user ID mapping) ### Workflows <-> Automation When: bridging Slack Workflow Builder workflows, custom workflow steps (workflow_step_execute), and Power Automate flows Read: - `workflows-automation-ts.md` Cross-domain deps: `../teams/ui.adaptive-cards-ts.md` (card construction for bot-driven workflows), `../teams/runtime.proactive-messaging-ts.md` (flow-triggered bot messages) ### Composable Workflow Platform When: composable workflow architecture, reusable workflow engine, WorkflowDefinition, template workflows, five-element lifecycle, workflow platform design, workflow operating layer Read: - `workflow.composable-platform-ts.md` Cross-domain deps: `../teams/workflow.sharepoint-lists-ts.md` (state), `../teams/workflow.message-native-records-ts.md` (visibility), `../teams/workflow.triggers-compose-ts.md` (triggers), `../teams/ai.conversational-query-ts.md` (intelligence), `../teams/workflow.approvals-inline-ts.md` (routing) ### App Distribution & Packaging When: bridging Slack App Directory listing, OAuth install flow, InstallationStore, org-level installs and Teams sideloading, app packaging, Teams Admin Center Read: - `app-distribution-packaging-ts.md` Cross-domain deps: `identity-oauth-bridge-ts.md` (identity model bridge), `../teams/runtime.manifest-ts.md` (Teams manifest creation) ### Rate Limiting & Resilience When: bridging rate limiting patterns, retry logic, throttling handling, proactive broadcast resilience, circuit breaker between platforms Read: - `rate-limiting-resilience-ts.md` Cross-domain deps: `../teams/runtime.proactive-messaging-ts.md` (proactive send infrastructure), `../teams/graph.usergraph-appgraph-ts.md` (Graph API throttling) ### Cross-Platform Advisor When: starting a cross-platform bridging project, assessing scope, making bridging decisions, "help me add Teams", "help me add Slack", "help me migrate", "what do I need to do to bridge" Read: - `cross-platform-advisor-ts.md` Note: This expert orchestrates the full bridging workflow — it detects direction, scans the codebase, classifies the bot profile, walks through decisions, then routes to the individual experts above for implementation. ### Cross-Platform Architecture When: hosting both bots in a single server, shared Express, dual bot, single process, platform-agnostic service layer, deployment architecture Read: - `cross-platform-architecture-ts.md` Cross-domain deps: `../slack/runtime.bolt-foundations-ts.md` (Slack setup), `../teams/runtime.app-init-ts.md` (Teams setup) ### Python Cross-Platform When: Python dual-platform, Python unified server, `slack_bolt` + `microsoft_teams`, FastAPI shared server, Python Slack + Teams, Tier 2, Python adaptation Read: - `python-cross-platform.md` Cross-domain deps: `../slack/bolt-python.md` (Slack Python SDK), `../teams/teams-python.md` (Teams Python SDK), `cross-platform-architecture-ts.md` (architecture patterns to adapt) ### REST-Only Integration When: Java, C#, Go, Ruby, no SDK, raw HTTP, Bot Framework REST API, Slack Events API, Slack Web API, manual JWT validation, manual signature verification, language without native SDK Read: - `rest-only-integration-ts.md` Cross-domain deps: `cross-platform-architecture-ts.md` (if mixing REST with TS SDK) ### Composite: Full Slack <-> Teams Bridge When: complete end-to-end cross-platform bridging between Slack and Teams bots Read: - `ui-block-kit-adaptive-cards-ts.md` - `commands-slash-text-ts.md` - `events-activities-ts.md` - `identity-oauth-bridge-ts.md` - `middleware-handlers-ts.md` - `transport-socketmode-https-ts.md` - `ui-modals-dialogs-ts.md` - `ui-app-home-personal-tab-ts.md` - `ui-legacy-attachments-cards-ts.md` - `interactive-responses-ts.md` - `files-upload-download-ts.md` - `link-unfurl-preview-ts.md` - `shortcuts-extensions-ts.md` - `scheduling-deferred-send-ts.md` - `channel-ops-graph-ts.md` - `workflows-automation-ts.md` - `app-distribution-packaging-ts.md` - `rate-limiting-resilience-ts.md` Cross-domain deps: `../teams/project.scaffold-files-ts.md` (scaffold the new Teams project), `../teams/runtime.app-init-ts.md` (initialize the Teams app), `../teams/runtime.manifest-ts.md` (create the Teams manifest) ### Composite: Full AWS <-> Azure Bridge When: complete end-to-end infrastructure bridging between AWS and Azure Read: - `infra-compute-ts.md` - `infra-storage-ts.md` - `infra-secrets-config-ts.md` - `infra-observability-ts.md` Cross-domain deps: `../security/secrets-ts.md` (secrets hygiene for Azure) ## combining rule If a request involves both Slack↔Teams app bridging **and** AWS↔Azure infra bridging, read files from **both** composite clusters. ## file inventory `app-distribution-packaging-ts.md` | `channel-ops-graph-ts.md` | `workflow.composable-platform-ts.md` | `commands-slash-text-ts.md` | `cross-platform-advisor-ts.md` | `cross-platform-architecture-ts.md` | `events-activities-ts.md` | `files-upload-download-ts.md` | `identity-oauth-bridge-ts.md` | `infra-compute-ts.md` | `infra-observability-ts.md` | `infra-secrets-config-ts.md` | `infra-storage-ts.md` | `interactive-responses-ts.md` | `link-unfurl-preview-ts.md` | `middleware-handlers-ts.md` | `python-cross-platform.md` | `rate-limiting-resilience-ts.md` | `rest-only-integration-ts.md` | `scheduling-deferred-send-ts.md` | `shortcuts-extensions-ts.md` | `transport-socketmode-https-ts.md` | `ui-app-home-personal-tab-ts.md` | `ui-block-kit-adaptive-cards-ts.md` | `ui-legacy-attachments-cards-ts.md` | `ui-modals-dialogs-ts.md` | `workflows-automation-ts.md` <!-- Updated 2026-02-27: Reframed from migrate-router to bridge-router — bidirectional cross-platform bridging between Slack↔Teams and AWS↔Azure. Renamed all files to platform-neutral names. --> <!-- Updated 2026-02-27: Added cross-platform-architecture-ts (dual-bot hosting) and rest-only-integration-ts (SDK-less HTTP patterns for Java/C#/Go). --> <!-- Updated 2026-02-28: Added RED gap workarounds and YELLOW gap best practices to expert files: interactive-responses-ts (R1 refresh.userIds, Y11 _version), events-activities-ts (R2 reaction→button, Y16 RSC manifest), ui-modals-dialogs-ts (R3 cancel TTL, R4/R6 step routing, R5 validation re-render), scheduling-deferred-send-ts (R7 Service Bus), link-unfurl-preview-ts (Y7 cache middleware), commands-slash-text-ts (Y1 text+manifest), rate-limiting-resilience-ts (Y17 retry+p-queue). --> <!-- Updated 2026-03-05: Added workflow.composable-platform-ts for composable workflow operating layer architecture --> <!-- Updated 2026-02-28: Added remaining missing patterns: events-activities-ts (Y2 replyWithBroadcast helper, Y3 Graph thread replies), files-upload-download-ts (Y4/5/6 sendFile helper), ui-modals-dialogs-ts (Y9 dynamic selects), ui-block-kit-adaptive-cards-ts (Y14 Action.ShowCard confirmation), link-unfurl-preview-ts (Y15 manifest domain generator), transport-socketmode-https-ts (R10 Azure Relay for on-prem). --> -
infra-compute-ts.md 14.6 KB
# infra-compute-ts ## purpose Bridges AWS and Azure compute infrastructure for cross-platform bot hosting. Covers Lambda/ECS/EC2 to Azure App Service/Functions/Container Apps (and the reverse). The common direction is AWS → Azure, but the service mappings apply bidirectionally. > **Note:** AWS → Azure is the most common direction for this expert. For Azure → AWS, reverse the mappings: App Service → EC2/ECS, Azure Functions → Lambda + API Gateway, Container Apps → ECS/Fargate. ## rules 1. Map AWS compute services to Azure equivalents using this decision matrix: Lambda + API Gateway maps to Azure Functions (Consumption or Premium), ECS/Fargate maps to Azure Container Apps, EC2 maps to Azure App Service (or Azure VMs for lift-and-shift). Choose based on existing architecture and workload characteristics. [learn.microsoft.com -- Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview) 2. Teams bots require an HTTPS endpoint at `/api/messages` that accepts POST requests from the Bot Framework. Azure App Service and Container Apps provide this natively; Azure Functions requires an HTTP-triggered function bound to that route. [learn.microsoft.com -- Bot messaging endpoint](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-basics) 3. Teams expects bot responses within 3 seconds for synchronous invoke activities (card actions, dialogs, message extensions). Azure Functions Consumption plan cold starts (5-10 seconds for Node.js) will violate this. Use the Premium plan (pre-warmed instances) or App Service (always-on) for production Teams bots. [learn.microsoft.com -- Functions Premium](https://learn.microsoft.com/en-us/azure/azure-functions/functions-premium-plan) 4. Enable "Always On" for Azure App Service deployments to prevent the app from unloading after idle periods. Without it, the first request after idle triggers a cold start that can cause Teams timeouts. Set this in Configuration > General Settings or via CLI. [learn.microsoft.com -- App Service Always On](https://learn.microsoft.com/en-us/azure/app-service/configure-common) 5. Use Node.js 20 LTS or later as the runtime stack. Set this explicitly in App Service (Configuration > General Settings > Stack: Node, Version: 20-lts) or in the Azure Functions `host.json` and app settings. The Teams AI Library v2 requires Node 20+. [learn.microsoft.com -- Node.js on App Service](https://learn.microsoft.com/en-us/azure/app-service/configure-language-nodejs) 6. Migrate environment variables from AWS Lambda environment / SSM to Azure App Settings. App Settings are injected as `process.env` variables at runtime, equivalent to Lambda environment variables. Use deployment slots for staging/production separation. [learn.microsoft.com -- App Settings](https://learn.microsoft.com/en-us/azure/app-service/configure-common#configure-app-settings) 7. Configure health check endpoints for all Azure compute targets. App Service supports built-in health checks (Configuration > Health check path: `/api/health`). Container Apps use liveness and readiness probes. This replaces Lambda/ECS health monitoring. [learn.microsoft.com -- Health checks](https://learn.microsoft.com/en-us/azure/app-service/monitor-instances-health-check) 8. For streaming and WebSocket scenarios (e.g., AI streaming responses via `stream.emit()`), use App Service or Container Apps with WebSocket support enabled. Azure Functions Consumption plan does not support WebSockets. Enable WebSockets in App Service under Configuration > General Settings. [learn.microsoft.com -- WebSockets](https://learn.microsoft.com/en-us/azure/app-service/configure-common#configure-general-settings) 9. Use deployment slots in App Service for zero-downtime deployments, replacing blue/green patterns built with Lambda aliases/versions or ECS rolling updates. Swap staging to production after validation. [learn.microsoft.com -- Deployment slots](https://learn.microsoft.com/en-us/azure/app-service/deploy-staging-slots) 10. For complex multi-container deployments (previously ECS task definitions with sidecars), use Azure Container Apps with multiple containers per revision, or Azure Kubernetes Service for full orchestration control. Container Apps supports scale-to-zero similar to Fargate Spot. [learn.microsoft.com -- Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/overview) 11. **Azure Functions Premium "Always Ready" instances eliminate cold starts.** Set `WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUT` to cap horizontal scaling, and configure `alwaysReady` in the Premium plan to keep N instances warm. This is the serverless equivalent of ECS minimum task count. Required for Teams bots that must respond to invoke activities within 3 seconds. [learn.microsoft.com -- Functions Premium Always Ready](https://learn.microsoft.com/en-us/azure/azure-functions/functions-premium-plan#always-ready-instances) 12. **Container Apps with Dapr sidecars replaces ECS multi-container patterns.** ECS task definitions with multiple containers (app + sidecar) map to Container Apps revisions with Dapr enabled. Dapr provides service-to-service invocation, state management, pub/sub, and secrets — replacing custom service mesh code. Service discovery uses Dapr app IDs instead of ECS service discovery or Cloud Map. [learn.microsoft.com -- Dapr on Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/dapr-overview) ## patterns ### AWS Lambda to Azure App Service deployment ```shell # Create resource group and App Service plan az group create --name my-bot-rg --location eastus az appservice plan create \ --name my-bot-plan \ --resource-group my-bot-rg \ --sku B1 \ --is-linux # Create the web app with Node.js 20 az webapp create \ --name my-teams-bot \ --resource-group my-bot-rg \ --plan my-bot-plan \ --runtime "NODE:20-lts" # Enable Always On and WebSockets az webapp config set \ --name my-teams-bot \ --resource-group my-bot-rg \ --always-on true \ --web-sockets-enabled true # Set application settings (replaces Lambda env vars) az webapp config appsettings set \ --name my-teams-bot \ --resource-group my-bot-rg \ --settings \ CLIENT_ID="your-client-id" \ CLIENT_SECRET="your-client-secret" \ TENANT_ID="your-tenant-id" \ OPENAI_API_KEY="your-openai-key" \ PORT="8080" \ NODE_ENV="production" # Deploy from zip (build locally first: npm run build && zip -r dist.zip .) az webapp deployment source config-zip \ --name my-teams-bot \ --resource-group my-bot-rg \ --src ./dist.zip # Configure health check az webapp config set \ --name my-teams-bot \ --resource-group my-bot-rg \ --generic-configurations '{"healthCheckPath": "/api/health"}' ``` ### Azure Functions HTTP trigger for Teams bot endpoint ```typescript // src/functions/messages.ts import { app as azFunc, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; // Initialize the Teams app once (reused across invocations) const teamsApp = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger("my-bot", { level: "info" }), }); teamsApp.on("message", async ({ send, activity }) => { await send(`You said: "${activity.text}"`); }); // Azure Functions HTTP trigger bound to /api/messages azFunc.http("messages", { methods: ["POST"], authLevel: "anonymous", route: "api/messages", handler: async (req: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => { try { const body = await req.json(); // Forward the request to the Teams app for processing // In practice, use the adapter pattern from @microsoft/teams.apps // to bridge Azure Functions HTTP to the Teams app's Express handler return { status: 200, jsonBody: { status: "ok" } }; } catch (error) { context.error("Error processing message:", error); return { status: 500, jsonBody: { error: "Internal server error" } }; } }, }); ``` ### Container Apps deployment for ECS/Fargate migration ```shell # Create Container Apps environment (replaces ECS cluster) az containerapp env create \ --name my-bot-env \ --resource-group my-bot-rg \ --location eastus # Deploy container (replaces ECS task definition + service) az containerapp create \ --name my-teams-bot \ --resource-group my-bot-rg \ --environment my-bot-env \ --image myregistry.azurecr.io/my-teams-bot:latest \ --target-port 3978 \ --ingress external \ --min-replicas 1 \ --max-replicas 10 \ --cpu 0.5 \ --memory 1.0Gi \ --env-vars \ CLIENT_ID="your-client-id" \ CLIENT_SECRET=secretref:client-secret \ TENANT_ID="your-tenant-id" \ NODE_ENV="production" # Configure scaling rule based on HTTP concurrent requests az containerapp update \ --name my-teams-bot \ --resource-group my-bot-rg \ --scale-rule-name http-rule \ --scale-rule-type http \ --scale-rule-http-concurrency 50 ``` ### Container Apps with Dapr sidecar (ECS multi-container migration) ```shell # Create a Dapr-enabled Container App (replaces ECS task with sidecar containers) az containerapp create \ --name my-teams-bot \ --resource-group my-bot-rg \ --environment my-bot-env \ --image myregistry.azurecr.io/my-teams-bot:latest \ --target-port 3978 \ --ingress external \ --min-replicas 1 \ --max-replicas 10 \ --enable-dapr \ --dapr-app-id my-teams-bot \ --dapr-app-port 3978 \ --dapr-app-protocol http ``` **TypeScript: invoking another service via Dapr (replaces ECS service discovery):** ```typescript import { DaprClient, HttpMethod } from "@dapr/dapr"; const dapr = new DaprClient(); // Invoke another Container App by its Dapr app ID // Replaces: http://service-name.local:3000/api/data (ECS service discovery) async function callDataService(query: string) { const response = await dapr.invoker.invoke( "data-service", // Dapr app ID (replaces ECS service name) `api/search?q=${query}`, // method/path HttpMethod.GET ); return response; } ``` ## pitfalls - **Azure Functions Consumption cold starts**: Node.js cold starts on the Consumption plan can take 5-10 seconds. Teams invoke activities (card actions, dialogs) time out at 3 seconds. Either use the Premium plan with at least one pre-warmed instance, or use App Service with Always On enabled. - **Forgetting Always On**: App Service without Always On unloads the app after ~20 minutes idle. The next incoming Teams message triggers a full restart, causing timeout errors. Always enable Always On for bot workloads. - **Port mismatch**: Azure App Service expects the app to listen on `process.env.PORT` (defaults to `8080`), not the Teams default of `3978`. Set `PORT` in App Settings or update `app.start(process.env.PORT || 8080)` for App Service deployments. - **Missing /api/messages route**: The Azure Bot registration messaging endpoint must point to `https://your-app.azurewebsites.net/api/messages`. If the Teams app listens on a different path, update the Bot registration accordingly. - **Lambda-style single-invocation patterns**: AWS Lambda processes one request per invocation. Azure App Service and Container Apps are long-running processes. Remove any Lambda-specific initialization/teardown patterns (handler export patterns, context.callbackWaitsForEmptyEventLoop) and use the standard `app.start()` pattern. - **Deployment slot swap without warming**: Swapping a cold staging slot to production causes the same cold-start problem. Use slot warm-up rules or send traffic to staging before swapping. - **Container Apps scale-to-zero**: If min-replicas is 0, the first request after scale-down has a cold start. Set `--min-replicas 1` for production Teams bots to ensure instant responses. - **Functions Premium Always Ready is not free-tier**: Always Ready instances incur charges even when idle. Budget for at least 1 always-ready instance per production function app. Without it, the Premium plan still has occasional cold starts during scale-out events. - **Dapr sidecar port conflict**: Dapr's default HTTP port is 3500 and gRPC is 50001. Ensure your app does not bind to these ports. The `--dapr-app-port` flag tells Dapr which port YOUR app listens on — this must match your Express/Teams `app.start()` port. ## references - [Azure App Service overview](https://learn.microsoft.com/en-us/azure/app-service/overview) - [Azure Functions Node.js developer guide](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node) - [Azure Container Apps overview](https://learn.microsoft.com/en-us/azure/container-apps/overview) - [Azure Functions Premium plan](https://learn.microsoft.com/en-us/azure/azure-functions/functions-premium-plan) - [Deploy a bot to Azure](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-deploy-az-cli) - [Azure App Service deployment slots](https://learn.microsoft.com/en-us/azure/app-service/deploy-staging-slots) - [Configure Node.js apps for App Service](https://learn.microsoft.com/en-us/azure/app-service/configure-language-nodejs) - [AWS to Azure services comparison](https://learn.microsoft.com/en-us/azure/architecture/aws-professional/services) ## instructions This expert bridges compute infrastructure between AWS and Azure for cross-platform bot hosting. Use it when adding cross-platform support in either direction and you need to: - Map compute services between clouds (Lambda ↔ Azure Functions, ECS ↔ Container Apps, EC2 ↔ App Service) - Configure Azure App Service for a Teams bot with proper Always On, WebSocket, and Node.js runtime settings - Set up Azure Functions as a Teams bot endpoint while avoiding cold-start pitfalls - Deploy containerized bots to Azure Container Apps as a replacement for ECS/Fargate - Bridge environment variables and deployment configurations between AWS and Azure - Configure health checks, scaling rules, and deployment slots for production bot hosting For Azure → AWS (less common): reverse the mappings. App Service maps to EC2 or Elastic Beanstalk, Azure Functions maps to Lambda + API Gateway, Container Apps maps to ECS/Fargate. Pair with `infra-secrets-config-ts.md` for App Settings and environment variable configuration, and `../teams/dev.debug-test-ts.md` for local development setup. ## research Deep Research prompt: "Write a micro expert for bridging bot compute between AWS and Azure. Provide a bidirectional decision matrix mapping AWS Lambda+API Gateway ↔ Azure Functions, ECS/Fargate ↔ Container Apps, and EC2 ↔ App Service. Include Node/TS hosting patterns, ingress/routing, env var configuration, scaling differences, cold start mitigation for Teams 3-second response requirements, and bot endpoint considerations. Include deployment CLI examples for both directions." -
infra-observability-ts.md 14 KB
# infra-observability-ts ## purpose Bridges AWS and Azure observability for cross-platform bot monitoring. Covers CloudWatch to Azure Monitor/Application Insights/Log Analytics (and the reverse). The common direction is AWS → Azure, but the service mappings apply bidirectionally. > **Note:** AWS → Azure is the most common direction for this expert. For Azure → AWS, reverse the mappings: Application Insights → CloudWatch + X-Ray, Log Analytics (KQL) → CloudWatch Logs Insights, Azure Monitor Alerts → CloudWatch Alarms + SNS. ## rules 1. Map CloudWatch Logs to Application Insights and Log Analytics. Application Insights provides structured telemetry (requests, dependencies, exceptions, traces) while Log Analytics is the query engine (KQL) for exploring that data. Both replace CloudWatch Logs Insights. [learn.microsoft.com -- Application Insights overview](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) 2. Instrument Node.js Teams bots with the `applicationinsights` npm package. Call `setup()` and `start()` before any other imports to enable automatic dependency tracking, request correlation, and exception capture. This replaces AWS X-Ray SDK instrumentation. [learn.microsoft.com -- Node.js Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/nodejs) 3. Map CloudWatch Metrics to Azure Monitor Metrics. Custom metrics sent via `trackMetric()` in Application Insights appear in Azure Monitor Metrics Explorer, replacing CloudWatch custom metrics and `putMetricData` calls. [learn.microsoft.com -- Custom metrics](https://learn.microsoft.com/en-us/azure/azure-monitor/app/api-custom-events-metrics) 4. Map CloudWatch Alarms to Azure Monitor Alerts. Create alert rules on Application Insights metrics (response time, failure rate, exception count) or log-based alerts using KQL queries. This replaces CloudWatch Alarm + SNS notification patterns. [learn.microsoft.com -- Azure Monitor Alerts](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-overview) 5. Map AWS X-Ray distributed tracing to Application Insights distributed tracing. Application Insights automatically correlates requests across services using operation IDs. The `applicationinsights` SDK propagates trace context headers (`traceparent`) automatically. [learn.microsoft.com -- Distributed tracing](https://learn.microsoft.com/en-us/azure/azure-monitor/app/distributed-trace-data) 6. Integrate with the Teams SDK `ConsoleLogger` by creating a custom logger implementation that forwards to Application Insights. Use `trackTrace()` for log messages, `trackException()` for errors, and `trackEvent()` for business events (bot installs, card actions). [learn.microsoft.com -- Application Insights API](https://learn.microsoft.com/en-us/azure/azure-monitor/app/api-custom-events-metrics) 7. Set the Application Insights connection string via the `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable in App Settings. Do not hardcode connection strings. App Service and Functions have built-in Application Insights integration that can be enabled without code changes for basic telemetry. [learn.microsoft.com -- Connection strings](https://learn.microsoft.com/en-us/azure/azure-monitor/app/sdk-connection-string) 8. Use KQL queries in Log Analytics to diagnose bot issues, replacing CloudWatch Logs Insights queries. Query `requests`, `dependencies`, `exceptions`, and `traces` tables. Pin frequently used queries to Azure dashboards for team visibility. [learn.microsoft.com -- KQL overview](https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/) 9. Configure sampling to control telemetry volume and cost. Application Insights supports adaptive sampling (automatic) and fixed-rate sampling. For production bots with high message volume, set a sampling percentage to avoid excessive costs while retaining representative data. [learn.microsoft.com -- Sampling](https://learn.microsoft.com/en-us/azure/azure-monitor/app/sampling-classic-api) 10. Build Azure dashboards for bot health monitoring, replacing CloudWatch Dashboards. Include panels for request rate, response time (P50/P95/P99), failure rate, active conversations, and AI model latency. Use Application Insights workbooks for detailed investigation views. [learn.microsoft.com -- Dashboards](https://learn.microsoft.com/en-us/azure/azure-monitor/app/overview-dashboard) ## patterns ### Application Insights setup for a Teams bot ```typescript // src/instrumentation.ts — MUST be imported before all other modules import * as appInsights from "applicationinsights"; appInsights .setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) .setAutoCollectRequests(true) .setAutoCollectPerformance(true) .setAutoCollectExceptions(true) .setAutoCollectDependencies(true) .setAutoCollectConsole(true, true) // capture console.log and console.error .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C) .setSendLiveMetrics(true) .start(); export const telemetryClient = appInsights.defaultClient; ``` ```typescript // src/index.ts import { telemetryClient } from "./instrumentation.js"; // import first! import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Track custom events for bot lifecycle app.on("install.add", async ({ send, activity }) => { telemetryClient.trackEvent({ name: "BotInstalled", properties: { conversationType: activity.conversation.conversationType ?? "personal", tenantId: activity.conversation.tenantId ?? "unknown", }, }); await send("Hello! I am now installed."); }); // Track AI prompt latency as a custom metric app.on("message", async ({ send, activity }) => { const start = Date.now(); // ... process with AI prompt ... const duration = Date.now() - start; telemetryClient.trackMetric({ name: "AIPromptLatency", value: duration, properties: { conversationId: activity.conversation.id }, }); }); // Track unhandled errors app.event("error", ({ error }) => { telemetryClient.trackException({ exception: error as Error }); }); app.start(process.env.PORT || 3978); ``` ### KQL queries for bot diagnostics ```text // Request latency for the /api/messages endpoint (replaces CloudWatch Logs Insights) requests | where name == "POST /api/messages" | where timestamp > ago(24h) | summarize avg(duration), percentile(duration, 50), percentile(duration, 95), percentile(duration, 99), count() by bin(timestamp, 5m) | render timechart // Failed requests with exception details requests | where success == false | where timestamp > ago(1h) | join kind=inner ( exceptions | where timestamp > ago(1h) ) on operation_Id | project timestamp, name, resultCode, duration, exceptionType = type, exceptionMessage = outerMessage | order by timestamp desc | take 50 // Bot install/uninstall events over time customEvents | where name in ("BotInstalled", "BotUninstalled") | where timestamp > ago(7d) | summarize count() by name, bin(timestamp, 1d) | render columnchart // AI prompt latency distribution customMetrics | where name == "AIPromptLatency" | where timestamp > ago(24h) | summarize avg(value), percentile(value, 95), max(value) by bin(timestamp, 15m) | render timechart // Dependency call failures (external APIs, databases) dependencies | where success == false | where timestamp > ago(6h) | summarize failureCount = count() by target, name, resultCode | order by failureCount desc ``` ### Custom logger that bridges ConsoleLogger to Application Insights ```typescript // src/logger.ts import * as appInsights from "applicationinsights"; import { ILogger } from "@microsoft/teams.common"; export class AppInsightsLogger implements ILogger { private client: appInsights.TelemetryClient; private name: string; constructor(name: string, client?: appInsights.TelemetryClient) { this.name = name; this.client = client ?? appInsights.defaultClient; } error(message: string, ...args: unknown[]): void { const formatted = this.format(message, args); console.error(`[${this.name}] ${formatted}`); this.client.trackTrace({ message: formatted, severity: appInsights.Contracts.SeverityLevel.Error, properties: { component: this.name }, }); } warn(message: string, ...args: unknown[]): void { const formatted = this.format(message, args); console.warn(`[${this.name}] ${formatted}`); this.client.trackTrace({ message: formatted, severity: appInsights.Contracts.SeverityLevel.Warning, properties: { component: this.name }, }); } info(message: string, ...args: unknown[]): void { const formatted = this.format(message, args); console.info(`[${this.name}] ${formatted}`); this.client.trackTrace({ message: formatted, severity: appInsights.Contracts.SeverityLevel.Information, properties: { component: this.name }, }); } debug(message: string, ...args: unknown[]): void { const formatted = this.format(message, args); console.debug(`[${this.name}] ${formatted}`); this.client.trackTrace({ message: formatted, severity: appInsights.Contracts.SeverityLevel.Verbose, properties: { component: this.name }, }); } log(message: string, ...args: unknown[]): void { this.info(message, ...args); } child(name: string): ILogger { return new AppInsightsLogger(`${this.name}/${name}`, this.client); } private format(message: string, args: unknown[]): string { return args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message; } } // Usage in src/index.ts: // import { AppInsightsLogger } from "./logger.js"; // const app = new App({ // logger: new AppInsightsLogger("my-bot"), // ... // }); ``` ## pitfalls - **Late instrumentation import**: The `applicationinsights` setup must run before importing any other modules (especially `http`/`https`). If imported after, automatic dependency tracking and request correlation will not work. Always import the instrumentation module first in your entry point. - **Missing connection string**: If `APPLICATIONINSIGHTS_CONNECTION_STRING` is not set, the SDK initializes silently in no-op mode. Telemetry is lost without any error. Always verify the connection string is configured in App Settings. - **CloudWatch Logs Insights queries not portable**: CloudWatch Logs Insights query syntax is completely different from KQL. All existing dashboard queries must be manually rewritten in KQL. The table structures also differ (e.g., `@timestamp` becomes `timestamp`, `@message` becomes `message`). - **Cost surprise from high-volume bots**: Application Insights charges per GB of ingested telemetry. A high-traffic bot logging every message can generate significant costs. Configure sampling early and exclude verbose trace levels in production. - **Console.log not structured**: Raw `console.log` statements captured by Application Insights appear as unstructured trace messages. Use `trackEvent()`, `trackMetric()`, and `trackTrace()` with properties for queryable, structured telemetry. - **X-Ray annotations not migrated**: AWS X-Ray annotations and metadata have no automatic migration path to Application Insights custom properties. Manually map important annotations to `trackTrace()` or `trackEvent()` property bags. - **Forgetting to flush on shutdown**: Application Insights batches telemetry before sending. If the process exits abruptly (e.g., container restart), buffered telemetry is lost. Call `telemetryClient.flush()` in a graceful shutdown handler. ## references - [Application Insights overview](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) - [Application Insights for Node.js](https://learn.microsoft.com/en-us/azure/azure-monitor/app/nodejs) - [Application Insights API reference](https://learn.microsoft.com/en-us/azure/azure-monitor/app/api-custom-events-metrics) - [KQL quick reference](https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/kql-quick-reference) - [Azure Monitor Alerts](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-overview) - [Application Insights sampling](https://learn.microsoft.com/en-us/azure/azure-monitor/app/sampling-classic-api) - [Distributed tracing in Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/distributed-trace-data) - [AWS to Azure services comparison -- Management and monitoring](https://learn.microsoft.com/en-us/azure/architecture/aws-professional/services#management-and-monitoring) ## instructions This expert bridges observability between AWS and Azure for cross-platform bot monitoring. Use it when adding cross-platform support in either direction and you need to: - Map monitoring services between clouds (CloudWatch ↔ Azure Monitor, X-Ray ↔ Application Insights, CloudWatch Alarms ↔ Azure Alerts) - Instrument a Node.js Teams bot with the `applicationinsights` npm package - Write KQL queries for bot diagnostics (latency, errors, usage patterns) - Build Azure dashboards for bot health monitoring - Bridge the Teams SDK `ConsoleLogger` to Application Insights telemetry For Azure → AWS (less common): reverse the mappings. Application Insights maps to CloudWatch + X-Ray, KQL maps to CloudWatch Logs Insights, Azure Alerts map to CloudWatch Alarms + SNS. Pair with `../teams/dev.debug-test-ts.md` for Teams SDK ConsoleLogger integration, and `infra-compute-ts.md` for Application Insights instrumentation on the target compute platform. ## research Deep Research prompt: "Write a micro expert for bridging observability between AWS CloudWatch and Azure Monitor/Application Insights for cross-platform bots. Cover structured logging with the applicationinsights npm package, distributed tracing (X-Ray ↔ Application Insights), KQL ↔ CloudWatch Logs Insights query mapping, custom metrics, alert rules, dashboard setup, and cost management with sampling bidirectionally. Include instrumentation code examples and diagnostic queries." -
infra-secrets-config-ts.md 13.2 KB
# infra-secrets-config-ts ## purpose Bridges AWS and Azure secrets/configuration management for cross-platform bot deployments. Covers Secrets Manager/SSM to Key Vault/App Configuration (and the reverse). The common direction is AWS → Azure, but the service mappings apply bidirectionally. > **Note:** AWS → Azure is the most common direction for this expert. For Azure → AWS, reverse the mappings: Key Vault → Secrets Manager, App Configuration → SSM Parameter Store, managed identity → IAM roles. ## rules 1. Map AWS Secrets Manager to Azure Key Vault for storing sensitive credentials (CLIENT_SECRET, OPENAI_API_KEY, database passwords). Key Vault provides versioning, soft-delete, access policies, and audit logging, similar to Secrets Manager. Use `@azure/keyvault-secrets` for programmatic access. [learn.microsoft.com -- Key Vault overview](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) 2. Map AWS SSM Parameter Store to Azure App Configuration for non-secret configuration values (feature flags, endpoint URLs, tuning parameters). App Configuration supports key-value pairs, labels for environments, and feature management. Use `@azure/app-configuration` for programmatic access. [learn.microsoft.com -- App Configuration overview](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview) 3. Use managed identity (system-assigned or user-assigned) to access Key Vault from Azure compute, eliminating the need for Key Vault credentials in code. This replaces IAM role-based access patterns used with AWS Secrets Manager. Configure with `@azure/identity` DefaultAzureCredential. [learn.microsoft.com -- Managed identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) 4. For App Service deployments, use Key Vault references in App Settings instead of direct secret values. The syntax `@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/MySecret/)` resolves secrets at runtime without application code changes. This is the simplest migration path from `.env` files. [learn.microsoft.com -- Key Vault references](https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references) 5. Migrate all Teams bot environment variables from `.env` files to Azure App Settings for production. Required variables: `CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`. Common additions: `OPENAI_API_KEY` or `AZURE_OPENAI_*`, `APPLICATIONINSIGHTS_CONNECTION_STRING`, `PORT`. Keep `.env` for local development only. [learn.microsoft.com -- App Settings](https://learn.microsoft.com/en-us/azure/app-service/configure-common#configure-app-settings) 6. Never commit secrets to source control. Add `.env` to `.gitignore`. Use `.env.example` or `.env.template` with placeholder values to document required variables. This applies equally to AWS and Azure workflows. [OWASP -- Secrets in source code](https://owasp.org/www-community/vulnerabilities/Use_of_hard-coded_password) 7. Configure secret rotation for `CLIENT_SECRET` using Key Vault rotation policies or Azure AD app credential rotation. AWS Secrets Manager automatic rotation maps to Key Vault auto-rotation with Event Grid notifications. Plan for multi-credential overlap during rotation windows. [learn.microsoft.com -- Key Vault rotation](https://learn.microsoft.com/en-us/azure/key-vault/secrets/tutorial-rotation) 8. Use the Teams SDK `managedIdentityClientId` option for zero-secret bot authentication in production. Set to `"system"` for system-assigned managed identity or the client ID string for user-assigned identity. This eliminates the need for `CLIENT_SECRET` in production entirely. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Apply least-privilege access policies to Key Vault. Grant only "Get" and "List" secret permissions to the bot's managed identity. Do not grant "Set", "Delete", or management permissions to runtime identities. Use separate access policies for deployment pipelines vs. runtime. [learn.microsoft.com -- Key Vault access policy](https://learn.microsoft.com/en-us/azure/key-vault/general/assign-access-policy) 10. For local development, use `DefaultAzureCredential` from `@azure/identity` which chains multiple credential sources: environment variables, managed identity, Azure CLI login, and VS Code credentials. This provides a unified auth pattern that works locally and in production without code changes. [learn.microsoft.com -- DefaultAzureCredential](https://learn.microsoft.com/en-us/azure/developer/javascript/sdk/authentication/credential-chains#use-defaultazurecredential-for-flexibility) ## patterns ### Accessing Key Vault secrets from a Teams bot ```typescript // src/config.ts import { DefaultAzureCredential } from "@azure/identity"; import { SecretClient } from "@azure/keyvault-secrets"; interface BotConfig { clientId: string; clientSecret: string; tenantId: string; openaiApiKey: string; } export async function loadConfig(): Promise<BotConfig> { const vaultUrl = process.env.KEY_VAULT_URL; // In production: uses managed identity automatically // Locally: uses Azure CLI credentials or env vars if (vaultUrl) { const credential = new DefaultAzureCredential(); const client = new SecretClient(vaultUrl, credential); const [clientId, clientSecret, tenantId, openaiKey] = await Promise.all([ client.getSecret("bot-client-id"), client.getSecret("bot-client-secret"), client.getSecret("bot-tenant-id"), client.getSecret("openai-api-key"), ]); return { clientId: clientId.value!, clientSecret: clientSecret.value!, tenantId: tenantId.value!, openaiApiKey: openaiKey.value!, }; } // Fallback to environment variables for local development return { clientId: process.env.CLIENT_ID ?? "", clientSecret: process.env.CLIENT_SECRET ?? "", tenantId: process.env.TENANT_ID ?? "", openaiApiKey: process.env.OPENAI_API_KEY ?? "", }; } // src/index.ts import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import { loadConfig } from "./config.js"; const config = await loadConfig(); const app = new App({ clientId: config.clientId, clientSecret: config.clientSecret, tenantId: config.tenantId, logger: new ConsoleLogger("my-bot", { level: "info" }), }); app.on("message", async ({ send }) => { await send("Bot is running with Key Vault secrets!"); }); app.start(process.env.PORT || 3978); ``` ### App Service Key Vault references (zero-code secret injection) ```shell # Create Key Vault az keyvault create \ --name my-bot-vault \ --resource-group my-bot-rg \ --location eastus # Store secrets in Key Vault az keyvault secret set --vault-name my-bot-vault --name "BotClientId" --value "your-client-id" az keyvault secret set --vault-name my-bot-vault --name "BotClientSecret" --value "your-client-secret" az keyvault secret set --vault-name my-bot-vault --name "BotTenantId" --value "your-tenant-id" az keyvault secret set --vault-name my-bot-vault --name "OpenAiApiKey" --value "your-openai-key" # Enable system-assigned managed identity on App Service az webapp identity assign \ --name my-teams-bot \ --resource-group my-bot-rg # Grant the managed identity access to Key Vault secrets PRINCIPAL_ID=$(az webapp identity show --name my-teams-bot --resource-group my-bot-rg --query principalId -o tsv) az keyvault set-policy \ --name my-bot-vault \ --object-id "$PRINCIPAL_ID" \ --secret-permissions get list # Set App Settings with Key Vault references (no secrets in App Settings!) az webapp config appsettings set \ --name my-teams-bot \ --resource-group my-bot-rg \ --settings \ CLIENT_ID="@Microsoft.KeyVault(SecretUri=https://my-bot-vault.vault.azure.net/secrets/BotClientId/)" \ CLIENT_SECRET="@Microsoft.KeyVault(SecretUri=https://my-bot-vault.vault.azure.net/secrets/BotClientSecret/)" \ TENANT_ID="@Microsoft.KeyVault(SecretUri=https://my-bot-vault.vault.azure.net/secrets/BotTenantId/)" \ OPENAI_API_KEY="@Microsoft.KeyVault(SecretUri=https://my-bot-vault.vault.azure.net/secrets/OpenAiApiKey/)" ``` ### Managed identity bot configuration (zero-secret production) ```typescript // src/index.ts — Production: no CLIENT_SECRET needed at all import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ clientId: process.env.CLIENT_ID, tenantId: process.env.TENANT_ID, // Use managed identity instead of CLIENT_SECRET // "system" for system-assigned, or a specific client ID for user-assigned managedIdentityClientId: process.env.MANAGED_IDENTITY_CLIENT_ID ?? "system", logger: new ConsoleLogger("my-bot", { level: "info" }), }); app.on("message", async ({ send }) => { await send("Running with managed identity - no secrets in config!"); }); app.start(process.env.PORT || 3978); ``` ## pitfalls - **Key Vault references showing raw `@Microsoft.KeyVault(...)` string**: If the App Service cannot resolve Key Vault references, the raw reference string is used as the value instead of the secret. This happens when managed identity lacks "Get" permission on the vault or when the secret URI is malformed. Check the App Service "Configuration" blade for a green checkmark next to each reference. - **DefaultAzureCredential slow locally**: `DefaultAzureCredential` tries multiple credential sources in sequence. If early sources timeout (e.g., managed identity endpoint on a dev machine), it can take 10+ seconds. For local development, use `AzureCliCredential` directly or set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` environment variables. - **Forgetting to restart after App Settings change**: Azure App Service caches environment variables at startup. After updating App Settings or Key Vault references, restart the App Service to pick up the new values. - **SSM Parameter Store hierarchical paths not mapped**: AWS SSM supports hierarchical parameter paths (`/myapp/prod/db-password`). Azure App Configuration uses flat key-value pairs with optional labels. Flatten the hierarchy or use labels (`key=db-password, label=prod`) during migration. - **Secret rotation breaking the bot**: When rotating `CLIENT_SECRET` in Azure AD, both the old and new credentials must be valid simultaneously during the transition. Add the new credential first, update Key Vault, then remove the old credential after confirming the bot works. - **Mixing .env and App Settings**: In production, App Settings override `.env` values. If both are present with different values, the App Settings value wins. Remove `.env` from deployment packages to avoid confusion. - **Key Vault soft-delete blocking recreation**: Key Vault has soft-delete enabled by default. If you delete and recreate a vault with the same name, the operation fails. Purge the soft-deleted vault first or use a different name. ## references - [Azure Key Vault overview](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) - [Azure App Configuration overview](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview) - [Key Vault references for App Service](https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references) - [Managed identities overview](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) - [@azure/identity -- DefaultAzureCredential](https://learn.microsoft.com/en-us/azure/developer/javascript/sdk/authentication/credential-chains) - [@azure/keyvault-secrets npm](https://www.npmjs.com/package/@azure/keyvault-secrets) - [Key Vault secret rotation tutorial](https://learn.microsoft.com/en-us/azure/key-vault/secrets/tutorial-rotation) - [AWS to Azure services comparison -- Security](https://learn.microsoft.com/en-us/azure/architecture/aws-professional/services#security-identity-and-access) ## instructions This expert bridges secrets and configuration management between AWS and Azure for cross-platform bot hosting. Use it when adding cross-platform support in either direction and you need to: - Map secrets services between clouds (Secrets Manager ↔ Key Vault, SSM ↔ App Configuration) - Set up Key Vault references in App Service App Settings for zero-code secret injection - Configure managed identity for passwordless access to Key Vault and other Azure services - Bridge `.env` files to production-ready App Settings on either cloud - Implement the `managedIdentityClientId` option in the Teams SDK for zero-secret bot authentication - Plan secret rotation for CLIENT_SECRET and other credentials For Azure → AWS (less common): reverse the mappings. Key Vault maps to Secrets Manager, App Configuration maps to SSM Parameter Store, managed identity maps to IAM roles. Pair with `../security/secrets-ts.md` for general secrets management best practices, and `../teams/runtime.app-init-ts.md` for the Teams bot credentials that need to be stored. ## research Deep Research prompt: "Write a micro expert for bridging secrets/config between AWS and Azure for cross-platform bots. Cover Secrets Manager ↔ Key Vault mapping, SSM ↔ App Configuration, Key Vault references in App Service, managed identity ↔ IAM roles, @azure/keyvault-secrets and @azure/identity SDK usage, .env to App Settings migration, and secret rotation patterns bidirectionally. Include code examples and CLI commands." -
infra-storage-ts.md 14.3 KB
# infra-storage-ts ## purpose Bridges AWS and Azure data storage for cross-platform bot state and application data. Covers S3/DynamoDB/RDS to Azure Blob Storage/Cosmos DB/Azure SQL (and the reverse). The common direction is AWS → Azure, but the service mappings apply bidirectionally. > **Note:** AWS → Azure is the most common direction for this expert. For Azure → AWS, reverse the mappings: Blob Storage → S3, Cosmos DB → DynamoDB, Azure SQL → RDS. ## rules 1. Map AWS S3 to Azure Blob Storage for file and object storage. Both provide tiered storage (Hot/Cool/Archive maps to S3 Standard/IA/Glacier), versioning, and lifecycle policies. Use `@azure/storage-blob` for programmatic access. Container names in Blob Storage are equivalent to S3 buckets. [learn.microsoft.com -- Blob Storage overview](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-overview) 2. Map AWS DynamoDB to Azure Cosmos DB for NoSQL key-value and document storage. Cosmos DB offers multiple APIs: Core SQL (recommended for new development), Table API (closest DynamoDB migration path), and MongoDB API. Choose based on query complexity and migration effort. [learn.microsoft.com -- Cosmos DB overview](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction) 3. Map AWS RDS (MySQL/PostgreSQL/SQL Server) to the equivalent Azure managed database: RDS MySQL maps to Azure Database for MySQL, RDS PostgreSQL maps to Azure Database for PostgreSQL, RDS SQL Server maps to Azure SQL Database. Schema and data can be migrated with Azure Database Migration Service. [learn.microsoft.com -- Azure SQL overview](https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-paas-overview) 4. Implement the Teams SDK `IStorage` interface for bot state management with Cosmos DB. The `IStorage` interface requires `get(key)`, `set(key, value)`, and `delete(key)` methods. This replaces any custom DynamoDB state store used by a Slack Bolt bot. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. For simple bot state (conversation history, user preferences), use Cosmos DB Core SQL API with a single container partitioned by the state key. This provides single-digit millisecond reads, automatic indexing, and serverless pricing for low-traffic bots. [learn.microsoft.com -- Cosmos DB serverless](https://learn.microsoft.com/en-us/azure/cosmos-db/serverless) 6. Use managed identity or connection strings stored in Key Vault for database access. Never hardcode connection strings in source code. For Cosmos DB, use `@azure/cosmos` with `DefaultAzureCredential` for managed identity access, or store the connection string in Key Vault. [learn.microsoft.com -- Cosmos DB RBAC](https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac) 7. For DynamoDB to Cosmos DB Table API migration, use the Azure Cosmos DB Data Migration Tool or custom scripts. Table API preserves the key-value access pattern (PartitionKey + RowKey), making it the lowest-effort migration path. However, Core SQL API offers richer querying capabilities for future needs. [learn.microsoft.com -- Cosmos DB Table API](https://learn.microsoft.com/en-us/azure/cosmos-db/table/introduction) 8. Configure Cosmos DB request units (RUs) appropriately. DynamoDB uses read/write capacity units (RCUs/WCUs); Cosmos DB uses RUs. A simple bot state read costs approximately 1 RU. Start with serverless mode (pay-per-request) for development and low traffic, switch to provisioned throughput for predictable workloads. [learn.microsoft.com -- Request units](https://learn.microsoft.com/en-us/azure/cosmos-db/request-units) 9. Plan data migration strategy: for S3 to Blob Storage, use AzCopy or Azure Data Factory for bulk migration. For DynamoDB to Cosmos DB, export to JSON from DynamoDB and import with the Cosmos DB Data Migration Tool. For RDS, use Azure Database Migration Service for online migration with minimal downtime. [learn.microsoft.com -- AzCopy](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-v10) 10. Implement retry logic and handle throttling for Cosmos DB operations. Unlike DynamoDB which returns `ProvisionedThroughputExceededException`, Cosmos DB returns HTTP 429 with a `x-ms-retry-after-ms` header. The `@azure/cosmos` SDK has built-in retry logic, but configure `maxRetryCount` and `retryAfterInMs` for your workload. [learn.microsoft.com -- Cosmos DB best practices](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/best-practice-dotnet) ## patterns ### IStorage implementation with Cosmos DB for bot state ```typescript // src/storage/cosmos-storage.ts import { CosmosClient, Container, Database } from "@azure/cosmos"; import { IStorage } from "@microsoft/teams.common"; export class CosmosDbStorage<T = unknown> implements IStorage<string, T> { private container: Container; private initialized = false; constructor( private cosmosClient: CosmosClient, private databaseId: string, private containerId: string, ) { this.container = this.cosmosClient .database(this.databaseId) .container(this.containerId); } async initialize(): Promise<void> { if (this.initialized) return; // Create database and container if they don't exist const { database } = await this.cosmosClient.databases.createIfNotExists({ id: this.databaseId, }); await database.containers.createIfNotExists({ id: this.containerId, partitionKey: { paths: ["/id"] }, }); this.container = this.cosmosClient .database(this.databaseId) .container(this.containerId); this.initialized = true; } async get(key: string): Promise<T | undefined> { await this.initialize(); try { const { resource } = await this.container.item(key, key).read<T & { id: string }>(); if (!resource) return undefined; // Strip Cosmos DB metadata before returning const { id, _rid, _self, _etag, _attachments, _ts, ...data } = resource as Record<string, unknown>; return data as T; } catch (error: unknown) { if ((error as { code: number }).code === 404) return undefined; throw error; } } async set(key: string, value: T): Promise<void> { await this.initialize(); await this.container.items.upsert({ id: key, ...value as object }); } async delete(key: string): Promise<void> { await this.initialize(); try { await this.container.item(key, key).delete(); } catch (error: unknown) { if ((error as { code: number }).code !== 404) throw error; } } } ``` ```typescript // src/index.ts — Using CosmosDbStorage with the Teams app import { App } from "@microsoft/teams.apps"; import { CosmosClient } from "@azure/cosmos"; import { CosmosDbStorage } from "./storage/cosmos-storage.js"; const cosmosClient = new CosmosClient(process.env.COSMOS_CONNECTION_STRING!); const storage = new CosmosDbStorage(cosmosClient, "teams-bot", "state"); const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, storage, // Cosmos DB backs all bot state }); app.on("message", async ({ send, activity }) => { // State is now persisted to Cosmos DB via the IStorage interface await send(`Echo: ${activity.text}`); }); app.start(process.env.PORT || 3978); ``` ### S3 to Azure Blob Storage migration and access ```typescript // src/storage/blob-client.ts import { BlobServiceClient, ContainerClient } from "@azure/storage-blob"; import { DefaultAzureCredential } from "@azure/identity"; // Using managed identity (production) const blobServiceClient = new BlobServiceClient( `https://${process.env.STORAGE_ACCOUNT_NAME}.blob.core.windows.net`, new DefaultAzureCredential(), ); // Or using connection string (development) // const blobServiceClient = BlobServiceClient.fromConnectionString( // process.env.AZURE_STORAGE_CONNECTION_STRING!, // ); export async function uploadFile( containerName: string, blobName: string, content: Buffer, ): Promise<string> { const containerClient = blobServiceClient.getContainerClient(containerName); await containerClient.createIfNotExists(); const blockBlobClient = containerClient.getBlockBlobClient(blobName); await blockBlobClient.upload(content, content.length); return blockBlobClient.url; } export async function downloadFile( containerName: string, blobName: string, ): Promise<Buffer> { const containerClient = blobServiceClient.getContainerClient(containerName); const blobClient = containerClient.getBlobClient(blobName); const response = await blobClient.download(); const chunks: Buffer[] = []; for await (const chunk of response.readableStreamBody!) { chunks.push(Buffer.from(chunk)); } return Buffer.concat(chunks); } // Migration command: bulk copy from S3 to Blob Storage // azcopy copy "https://s3.amazonaws.com/my-bucket" \ // "https://mystorageaccount.blob.core.windows.net/my-container?SAS_TOKEN" \ // --recursive ``` ### Cosmos DB with managed identity (replacing DynamoDB IAM role access) ```typescript // src/storage/cosmos-managed.ts import { CosmosClient } from "@azure/cosmos"; import { DefaultAzureCredential } from "@azure/identity"; // Managed identity access — no connection string needed // Requires Cosmos DB RBAC role assignment: // az cosmosdb sql role assignment create \ // --account-name my-cosmos-db \ // --resource-group my-bot-rg \ // --scope "/" \ // --principal-id <managed-identity-principal-id> \ // --role-definition-id 00000000-0000-0000-0000-000000000002 # Built-in Data Contributor const credential = new DefaultAzureCredential(); const cosmosClient = new CosmosClient({ endpoint: process.env.COSMOS_ENDPOINT!, // https://my-cosmos-db.documents.azure.com:443/ aadCredentials: credential, }); // Usage is identical to connection-string-based access const database = cosmosClient.database("teams-bot"); const container = database.container("state"); // Read an item const { resource } = await container.item("user-123", "user-123").read(); // Upsert an item await container.items.upsert({ id: "user-123", messages: [], preferences: { theme: "dark" }, }); ``` ## pitfalls - **DynamoDB to Cosmos DB partition key mismatch**: DynamoDB uses a composite key (partition key + sort key). Cosmos DB Core SQL API uses a single partition key with a separate `id` field. Plan the key mapping carefully. If using Table API, PartitionKey + RowKey maps more directly. - **Cosmos DB RU starvation**: Unlike DynamoDB auto-scaling which adjusts capacity based on traffic, Cosmos DB provisioned throughput has a fixed RU limit. Exceeding it causes 429 errors. Start with serverless mode or configure auto-scale (400-4000 RU/s) to handle traffic bursts. - **Connection string in source code**: Cosmos DB connection strings contain the master key with full read/write access. Never hardcode them. Use managed identity with RBAC for production, or store connection strings in Key Vault. - **Forgetting to create the database/container**: Unlike DynamoDB which creates tables on demand (with `CreateTable`), Cosmos DB requires explicit database and container creation. Use `createIfNotExists()` in the storage implementation or create resources via infrastructure-as-code. - **Blob Storage access tier costs**: S3 to Blob Storage migration may change cost profiles. Blobs default to Hot tier; if the data is rarely accessed (like archived conversation logs), set to Cool or Archive tier to reduce costs. - **Cosmos DB item size limit**: Cosmos DB items are limited to 2 MB. DynamoDB items are limited to 400 KB. While the Cosmos limit is higher, storing large conversation histories in a single item can approach this limit. Consider splitting long histories across multiple items. - **Missing index policy tuning**: Cosmos DB indexes all properties by default (unlike DynamoDB where you must explicitly create secondary indexes). This is convenient but increases RU cost for writes. Exclude large text fields from indexing if they are never queried. - **AzCopy SAS token expiration**: When using AzCopy for S3 to Blob migration, SAS tokens have expiration times. For large migrations that take hours, set a sufficiently long expiration or use managed identity with AzCopy. ## references - [Azure Blob Storage overview](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-overview) - [Azure Cosmos DB overview](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction) - [Azure SQL Database overview](https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-paas-overview) - [Cosmos DB Table API](https://learn.microsoft.com/en-us/azure/cosmos-db/table/introduction) - [Cosmos DB RBAC with managed identity](https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac) - [@azure/cosmos npm](https://www.npmjs.com/package/@azure/cosmos) - [@azure/storage-blob npm](https://www.npmjs.com/package/@azure/storage-blob) - [AzCopy tool](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-v10) - [AWS to Azure services comparison -- Storage](https://learn.microsoft.com/en-us/azure/architecture/aws-professional/services#storage) ## instructions This expert bridges data storage between AWS and Azure for cross-platform bot hosting. Use it when adding cross-platform support in either direction and you need to: - Map storage services between clouds (S3 ↔ Blob Storage, DynamoDB ↔ Cosmos DB, RDS ↔ Azure SQL) - Implement the Teams SDK `IStorage` interface backed by Cosmos DB for persistent bot state - Choose between Cosmos DB Core SQL API and Table API for DynamoDB migration - Set up managed identity access for Cosmos DB and Blob Storage (replacing IAM roles) - Plan bulk data migration with AzCopy, Data Migration Tool, or Azure Data Factory For Azure → AWS (less common): reverse the mappings. Blob Storage maps to S3, Cosmos DB maps to DynamoDB, Azure SQL maps to RDS. Pair with `../teams/state.storage-patterns-ts.md` for implementing the Teams SDK IStorage interface with Cosmos DB, and `infra-secrets-config-ts.md` for securing connection strings. ## research Deep Research prompt: "Write a micro expert for bridging bot storage between AWS and Azure. Map S3 ↔ Azure Blob Storage, DynamoDB ↔ Cosmos DB (Core SQL vs Table API), and RDS ↔ Azure SQL/PostgreSQL bidirectionally. Include implementing the Teams SDK IStorage interface with Cosmos DB, managed identity access patterns, data migration strategies with AzCopy and Data Migration Tool, partition key mapping, and Node.js client code examples." -
interactive-responses-ts.md 19.6 KB
# interactive-responses-ts ## purpose Bridges Slack interactive response patterns (respond, replace_original, ephemeral) and Teams card/message update patterns for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack `respond({ replace_original: true })` → Teams invoke response with card.** In Slack, `respond()` with `replace_original` replaces the message that triggered the interaction. In Teams, return a new Adaptive Card from the `card.action` handler's return value — the Bot Framework replaces the card inline. The handler must return `{ status: 200, body: { ... } }` with the replacement card. [learn.microsoft.com -- Universal Actions](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview) 2. **Slack `respond({ delete_original: true })` → Teams `deleteActivity(activityId)`.** Slack's delete-original flag removes the message. In Teams, call `deleteActivity(activityId)` on the turn context. You must store the original activity ID (from the `send()` return value or `activity.replyToId`) to delete it later. [learn.microsoft.com -- Delete activity](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-delete-activity) 3. **Slack `chat.update(channel, ts, ...)` → Teams `updateActivity(activityId, activity)`.** Both platforms support editing a bot's own message after sending. The key difference: Slack identifies messages by `channel + ts`, Teams uses `activityId` (returned from `send()`). Store the activity ID at send time. [learn.microsoft.com -- Update activity](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-update-activity) 4. **Slack `chat.postEphemeral()` has NO Teams equivalent.** Ephemeral messages visible only to one user do not exist in Teams. Redesign strategies: (a) send a message in the user's 1:1 bot chat, (b) use `Action.Execute` with `refresh.userIds` to show per-user card content, (c) simply send a visible message if privacy is not critical, (d) use a task module/dialog for private interaction. [learn.microsoft.com -- Conversations](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/conversation-basics) 5. **Deferred response pattern: send "processing..." card, update later.** Slack's `response_url` allows 5 follow-up messages within 30 minutes. Teams has no `response_url` concept. Instead: (a) return a "Processing..." card from the invoke handler immediately, (b) store the conversation reference and activity ID, (c) use proactive messaging to update the card when processing completes. No expiry limit on updates. [learn.microsoft.com -- Proactive messages](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) 6. **Slack `response_url` (30-min, 5 follow-ups) → `send()` / `updateActivity()` with no expiry.** Slack's response_url is a webhook with time and count limits. Teams' `send()` and `updateActivity()` work indefinitely as long as you have a valid conversation reference. This is actually more flexible — but requires you to store conversation references yourself. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. **`Action.Execute` with `refresh.userIds` enables per-user card views.** Slack broadcasts the same message to everyone; only the interacting user sees ephemeral responses. Teams' `Action.Execute` with `refresh` can show different card content to different users — up to 60 user IDs per card. When specified users view the card, Teams automatically invokes the bot to get their personalized version. [learn.microsoft.com -- User-specific views](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/user-specific-views) 8. **Store activity IDs at send time.** Every `send()` in Teams returns an activity ID (or resource response). Store this ID if you need to update or delete the message later. Slack uses `channel + ts`; Teams uses a single opaque `activityId` string. Failing to store the ID means you cannot update the message. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. **Slack `respond({ response_type: 'in_channel' })` → `send()`.** Slack's `in_channel` response type makes an ephemeral-by-default response visible to everyone. In Teams, all bot messages are visible by default — simply call `send()`. There is no visibility toggle. [learn.microsoft.com -- Bot messages](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/conversation-messages) 10. **Card action handler must return within 3 seconds.** Teams invoke activities (including `Action.Execute` and `Action.Submit`) require a synchronous response within ~3 seconds. If processing takes longer, return a "processing" card immediately and update asynchronously via proactive messaging. Slack's `response_url` had a 30-minute window; Teams' invoke has a 3-second window. [learn.microsoft.com -- Invoke activities](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/conversation-messages) 11. **Reverse direction (Teams → Slack):** For Teams → Slack, map `updateActivity` to `respond({ replace_original: true })`, and card refresh (`Action.Execute` with `refresh.userIds`) to ephemeral messages via `chat.postEphemeral`. `deleteActivity` maps to `chat.delete(channel, ts)`. The 3-second invoke deadline has no Slack equivalent -- Slack's `response_url` gives 30 minutes, which is more lenient. ## patterns ### Card replacement flow (replace_original → invoke response) **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Send initial message with a button app.command("/approve", async ({ ack, respond }) => { await ack(); await respond({ response_type: "in_channel", blocks: [ { type: "section", text: { type: "mrkdwn", text: "Request #123 needs approval" }, accessory: { type: "button", text: { type: "plain_text", text: "Approve" }, action_id: "approve_request", value: "123", }, }, ], }); }); // Replace the original message when button is clicked app.action("approve_request", async ({ ack, respond, body }) => { await ack(); await respond({ replace_original: true, blocks: [ { type: "section", text: { type: "mrkdwn", text: `Request #123 — *Approved* by <@${body.user.id}>`, }, }, ], }); }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Send initial approval card app.message(/^\/?approve$/i, async ({ send }) => { const response = await send({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: "Request #123 needs approval", weight: "Bolder" }, ], actions: [{ type: "Action.Execute", title: "Approve", verb: "approveRequest", data: { requestId: "123" }, }], }, }], }); // Store response.id if you need to update/delete later via proactive messaging }); // Handle Action.Execute — return replacement card (replaces replace_original) app.on("card.action" as any, async ({ activity }) => { const data = activity.value?.action?.data ?? activity.value; if (data?.verb === "approveRequest") { const approver = activity.from?.name ?? "Someone"; // Returning a card from the handler replaces the original card inline return { status: 200, body: { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: `Request #${data.requestId} — **Approved** by ${approver}`, wrap: true, }, ], // No actions = card becomes read-only after approval }, }; } }); app.start(3978); ``` ### Deferred response with processing indicator **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.action("run_report", async ({ ack, respond }) => { await ack(); // Immediate feedback await respond({ replace_original: true, text: "Generating report..." }); // Long-running task — uses response_url (valid for 30 min, 5 follow-ups) const report = await generateReport(); // takes 15 seconds await respond({ replace_original: true, blocks: [ { type: "section", text: { type: "mrkdwn", text: `Report ready: ${report.url}` }, }, ], }); }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Store conversation references for proactive updates const conversationRefs = new Map<string, any>(); app.on("card.action" as any, async ({ activity, send }) => { const data = activity.value?.action?.data ?? activity.value; if (data?.verb === "runReport") { // Store conversation reference for later proactive update const convRef = { conversationId: activity.conversation?.id, serviceUrl: (activity as any).serviceUrl, }; // Return "processing" card immediately (must respond within 3 seconds) const processingCard = { status: 200, body: { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: "Generating report...", isSubtle: true }, { type: "TextBlock", text: "This may take a moment. The card will update when ready.", wrap: true, size: "Small", }, ], }, }; // Kick off async work — update the card when done // No 30-minute expiry like Slack's response_url setImmediate(async () => { try { const report = await generateReport(); // Proactive message to update the card await send({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: "Report Ready", weight: "Bolder" }, { type: "TextBlock", text: `[Download Report](${report.url})`, wrap: true }, ], }, }], }); } catch (err) { await send("Report generation failed. Please try again."); } }); return processingCard; } }); async function generateReport() { // Simulate long-running work await new Promise((r) => setTimeout(r, 15000)); return { url: "https://example.com/report.pdf" }; } app.start(3978); ``` ### Ephemeral workaround: `refresh.userIds` (R1) Use `Action.Execute` with `refresh.userIds` to show personalized card content to specific users — the closest Teams equivalent to Slack's `chat.postEphemeral()`. ```typescript // Send a card where only the acting user sees personalized content async function sendWithEphemeralView( send: (msg: any) => Promise<any>, actingUserId: string, publicText: string, privateData: Record<string, unknown> ): Promise<void> { await send({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.4", refresh: { action: { type: "Action.Execute", verb: "personalView", data: privateData, }, userIds: [actingUserId], // max 60 IDs }, body: [ { type: "TextBlock", text: publicText }, // everyone sees this ], }, }], }); } // When the specified user views the card, Teams invokes the bot: app.on("card.action" as any, async ({ activity }) => { const data = activity.value?.action?.data ?? activity.value; if (data?.verb === "personalView") { return { status: 200, body: { type: "AdaptiveCard", version: "1.4", body: [ { type: "TextBlock", text: "This content is only visible to you.", weight: "Bolder" }, { type: "FactSet", facts: [ { title: "Request ID", value: data.requestId }, { title: "Status", value: "Pending your review" }, ]}, ], }, }; } }); ``` **Key constraints:** Max 60 user IDs per card. Requires `Action.Execute` (not `Action.Submit`). Manifest version must be ≥1.12. **Reverse (Teams → Slack):** Map card refresh to `chat.postEphemeral(channel, user, { blocks })`. ### Card version checking (Y11) Inject a `_version` counter into `Action.Submit.data` to prevent race conditions — the Teams equivalent of Slack's `view_hash` parameter. ```typescript // Track version per card instance const cardVersions = new Map<string, number>(); function buildVersionedCard(cardId: string, data: any): object { const version = (cardVersions.get(cardId) ?? 0) + 1; cardVersions.set(cardId, version); return { type: "AdaptiveCard", version: "1.5", body: [/* card content */], actions: [{ type: "Action.Submit", title: "Update", data: { ...data, _cardId: cardId, _version: version }, }], }; } app.on("card.action" as any, async ({ activity, send }) => { const submitted = activity.value?.action?.data ?? activity.value; const currentVersion = cardVersions.get(submitted?._cardId); if (submitted?._version !== currentVersion) { await send("This card is outdated. Please use the latest version."); return { status: 200 }; } // Process the update safely... }); ``` **Don't:** Skip version checking even for low-traffic bots — fast double-clicks and multiple tabs cause race conditions. **Reverse (Teams → Slack):** Use `view_hash` from `views.open()` / `views.update()` responses natively. ### Response pattern mapping table | Slack Pattern | Teams Equivalent | Notes | |---|---|---| | `respond({ replace_original: true, blocks })` | Return card from `card.action` handler | Inline card replacement | | `respond({ delete_original: true })` | `deleteActivity(activityId)` | Must store activity ID | | `respond({ response_type: 'in_channel' })` | `send(text)` | All Teams messages are visible | | `respond({ response_type: 'ephemeral' })` | *(no equivalent)* | Redesign: 1:1 chat, Action.Execute refresh, or visible | | `chat.update(channel, ts, ...)` | `updateActivity(activityId, activity)` | Store activity ID from send() | | `chat.delete(channel, ts)` | `deleteActivity(activityId)` | Store activity ID from send() | | `chat.postEphemeral(channel, user, ...)` | *(no equivalent)* | Use Action.Execute `refresh.userIds` for per-user views | | `response_url` (30-min, 5 follow-ups) | `send()` / `updateActivity()` | No expiry, no count limit | | Button click → `ack()` + `respond()` | `card.action` handler → return card | No ack needed | ## pitfalls - **Forgetting to store activity IDs**: Unlike Slack where `channel + ts` identifies any message, Teams requires the `activityId` returned from `send()`. If you don't store it, you cannot update or delete the message later. This is the #1 migration failure for interactive patterns. - **3-second invoke timeout**: Slack's `ack()` gave you 3 seconds to acknowledge, then `response_url` gave 30 minutes for follow-up. Teams invoke handlers must return the full response (including replacement card) within ~3 seconds. Anything longer requires the deferred pattern (return processing card, update proactively). - **No ephemeral messages — silent behavioral change**: Code using `chat.postEphemeral()` will not error during migration — it simply has no equivalent. The migrated bot must explicitly choose an alternative strategy. Audit all `postEphemeral` calls before migration. - **`Action.Execute` vs `Action.Submit`**: `Action.Submit` sends data to the bot but does NOT support automatic card refresh or per-user views. `Action.Execute` (Universal Actions) supports both. Always use `Action.Execute` for interactive cards that need replacement or per-user content. Requires manifest version 1.12+. - **`refresh.userIds` limit of 60**: The per-user card refresh feature (`Action.Execute` with `refresh.userIds`) supports a maximum of 60 user IDs per card. For broader audiences, send the base card to everyone and only personalize for the acting user. - **Card replacement only works for invoke responses**: You can only replace a card inline by returning a new card from the invoke handler. If the interaction is not an invoke (e.g., a proactive message), you must use `updateActivity()` instead. - **`deleteActivity` may not work in all contexts**: Deleting activities works in 1:1 and group chats but may be restricted in channels depending on permissions. Test deletion behavior in your target conversation types. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/user-specific-views - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages - https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-update-activity - https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-delete-activity - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/conversation-messages - https://github.com/microsoft/teams.ts - https://api.slack.com/interactivity/handling — Slack interactive responses - https://api.slack.com/methods/chat.update — Slack chat.update - https://api.slack.com/methods/chat.postEphemeral — Slack chat.postEphemeral ## instructions Use this expert when adding cross-platform support in either direction for Slack interactive response patterns or Teams card/message update patterns. It covers: `respond({ replace_original })` to invoke card replacement, `respond({ delete_original })` to `deleteActivity()`, `chat.update()` to `updateActivity()`, `chat.postEphemeral()` redesign strategies, deferred response patterns (processing card + proactive update), `response_url` elimination, and `Action.Execute` with `refresh.userIds` for per-user card views. For Teams → Slack, map `updateActivity` to `respond({ replace_original })`, and card refresh to ephemeral messages. Pair with `../teams/ui.adaptive-cards-ts.md` for card construction patterns, `../teams/runtime.proactive-messaging-ts.md` for deferred update infrastructure, and `events-activities-ts.md` for the underlying event/activity mapping. ## research Deep Research prompt: "Write a micro expert for bridging Slack interactive response patterns (respond, replace_original, ephemeral) and Teams card/message update patterns in either direction for cross-platform bots. Cover: respond({ replace_original }) to invoke card replacement and vice versa, respond({ delete_original }) to deleteActivity, chat.update to updateActivity, chat.postEphemeral redesign strategies, response_url expiry semantics, deferred response patterns with processing indicators, Action.Execute with refresh.userIds for per-user views, reverse-direction mapping from Teams to Slack, and the 3-second invoke timeout constraint. Include side-by-side TypeScript code examples and a mapping table." -
link-unfurl-preview-ts.md 17.1 KB
# link-unfurl-preview-ts ## purpose Bridges Slack link unfurling (link_shared, chat.unfurl) and Teams link preview (messageHandlers) for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack `app.event('link_shared')` + `chat.unfurl()` → Teams `message.ext.query-link` handler.** Slack fires a `link_shared` event and the bot calls `chat.unfurl()` asynchronously. Teams uses a compose extension handler that must return the unfurl card synchronously. The handler name in the Teams SDK is `message.ext.query-link` (or the equivalent `composeExtension/queryLink` activity). [learn.microsoft.com -- Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling) 2. **Manifest `composeExtensions[].messageHandlers` with domain list is required.** Unlike Slack where you register unfurl domains in the app dashboard, Teams requires them in the manifest JSON under `composeExtensions[0].messageHandlers[0].value.domains`. Only URLs matching these domains trigger unfurling. [learn.microsoft.com -- Manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#composeextensionsmessagehandlers) 3. **Teams has a 5-second synchronous response deadline.** Slack's `link_shared` event allows async unfurling — the bot receives the event, processes it, then calls `chat.unfurl()` within 30 minutes. Teams' `query-link` is an invoke that must return the preview card within ~5 seconds. If data fetching takes longer, return a minimal card and cannot update later. [learn.microsoft.com -- Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling) 4. **The bot must be installed in the conversation for unfurling to work.** Slack link unfurling works in any channel where the app is installed (workspace-level). Teams link unfurling only works in conversations where the bot is explicitly installed. Users may need to @mention the bot or add it to the team/chat first. [learn.microsoft.com -- Link unfurling prerequisites](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling#prerequisites) 5. **No retroactive unfurling of already-posted links.** Slack can unfurl links in messages already posted (if the app is added later). Teams only unfurls links at the time they are composed/sent. Links in existing messages are never retroactively unfurled. [learn.microsoft.com -- Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling) 6. **Slack unfurl supports multiple links per message; Teams handles one at a time.** Slack's `link_shared` event includes an array of `links` from the message. Teams invokes the `query-link` handler once per URL. If a message contains multiple matching URLs, the handler is called multiple times. [learn.microsoft.com -- Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling) 7. **Return an Adaptive Card (not Hero/Thumbnail) for rich previews.** Slack unfurls return attachment objects with `title`, `text`, `thumb_url`, `color`. Teams link unfurling should return Adaptive Cards for the richest preview. The response format wraps the card in a `composeExtension` result with `type: "result"`. [learn.microsoft.com -- Cards in extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling#response) 8. **Domain matching is exact — no wildcards for subdomains.** Slack unfurl domain matching supports wildcards. Teams manifest `messageHandlers.value.domains` requires exact domain entries. To match `foo.example.com` and `bar.example.com`, list both explicitly. [learn.microsoft.com -- Manifest domains](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) 9. **Slack's unfurl `is_bot_token_only` flag → not applicable.** Slack distinguishes between user-token and bot-token unfurling. Teams link unfurling always runs as the bot identity. There is no user-token mode. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. **Cache unfurl results where possible.** Since the 5-second deadline is strict, cache API responses for frequently unfurled URLs. Slack's async model made caching less critical. In Teams, a cache miss that takes >5 seconds means the unfurl silently fails with no preview shown. [learn.microsoft.com -- Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling) 11. **Reverse direction (Teams → Slack):** For Teams → Slack, map `messageHandlers` domain config to `link_shared` event subscription (configured in the Slack app dashboard under Unfurl Domains), and preview card responses to `chat.unfurl` calls. The key advantage in reverse is that Slack's async model (`chat.unfurl` within 30 minutes) is more forgiving than Teams' 5-second synchronous deadline. Adaptive Card preview content maps to Slack unfurl attachment objects with `title`, `text`, `thumb_url`, and `color`. ## patterns ### link_shared → query-link handler migration **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Async unfurl — no time pressure app.event("link_shared", async ({ event, client }) => { const unfurls: Record<string, any> = {}; for (const link of event.links) { if (link.domain === "myapp.example.com") { const match = link.url.match(/\/issues\/(\d+)/); if (match) { const issue = await fetchIssue(match[1]); // can take 10+ seconds unfurls[link.url] = { title: `Issue #${issue.id}: ${issue.title}`, text: issue.description, color: issue.status === "open" ? "#36a64f" : "#e01e5a", thumb_url: issue.assignee?.avatarUrl, footer: `Status: ${issue.status}`, }; } } } if (Object.keys(unfurls).length > 0) { await client.chat.unfurl({ ts: event.message_ts, channel: event.channel, unfurls, }); } }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Simple in-memory cache to meet 5-second deadline const issueCache = new Map<string, { data: any; expiry: number }>(); // Synchronous unfurl — must respond within 5 seconds app.on("message.ext.query-link" as any, async ({ activity }) => { const url: string = activity.value?.url ?? ""; const match = url.match(/\/issues\/(\d+)/); if (!match) { return { status: 200, body: {} }; // No preview for unrecognized URLs } const issueId = match[1]; let issue: any; // Check cache first (critical for meeting 5-second deadline) const cached = issueCache.get(issueId); if (cached && cached.expiry > Date.now()) { issue = cached.data; } else { issue = await fetchIssue(issueId); issueCache.set(issueId, { data: issue, expiry: Date.now() + 5 * 60_000 }); } const statusColor = issue.status === "open" ? "good" : "attention"; return { status: 200, body: { composeExtension: { type: "result", attachmentLayout: "list", attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: `Issue #${issue.id}: ${issue.title}`, weight: "Bolder", size: "Medium", }, { type: "TextBlock", text: issue.description, wrap: true, maxLines: 3, }, { type: "ColumnSet", columns: [ { type: "Column", width: "auto", items: [{ type: "TextBlock", text: `Status: **${issue.status}**`, color: statusColor, }], }, { type: "Column", width: "stretch", items: [{ type: "TextBlock", text: issue.assignee?.name ? `Assigned: ${issue.assignee.name}` : "Unassigned", isSubtle: true, horizontalAlignment: "Right", }], }, ], }, ], actions: [{ type: "Action.OpenUrl", title: "View Issue", url, }], }, preview: { contentType: "application/vnd.microsoft.card.thumbnail", content: { title: `Issue #${issue.id}: ${issue.title}`, text: `Status: ${issue.status}`, }, }, }], }, }, }; }); async function fetchIssue(id: string) { return { id, title: "Login broken on Safari", description: "Users report...", status: "open", assignee: { name: "Alice", avatarUrl: "" } }; } app.start(3978); ``` ### Manifest domain configuration **Slack** — domains are configured in the Slack app dashboard under "Event Subscriptions > Unfurl Domains". **Teams** — domains must be in the manifest JSON: ```json { "composeExtensions": [ { "botId": "${{BOT_ID}}", "messageHandlers": [ { "type": "link", "value": { "domains": [ "myapp.example.com", "issues.example.com" ] } } ], "commands": [] } ] } ``` ### Unfurl mapping table | Slack Pattern | Teams Equivalent | Notes | |---|---|---| | `app.event('link_shared')` | `app.on('message.ext.query-link')` | Invoke-based, not event-based | | `chat.unfurl(ts, channel, unfurls)` | Return card from handler | Synchronous response | | Unfurl domains in app dashboard | Manifest `messageHandlers.value.domains` | JSON config, not web UI | | Async unfurl (up to 30 min) | Synchronous (5-second deadline) | Must respond immediately | | Multiple links in one event | One invoke per URL | Handler called N times | | Wildcard domain matching | Exact domain matching only | List all subdomains explicitly | | `is_bot_token_only` flag | *(not applicable)* | Always bot identity | | Attachment unfurl format | Adaptive Card in composeExtension result | Richer card format | ### Cache middleware best practice (Y7) The 5-second Teams deadline makes caching non-optional. Always use a cache layer for unfurl handlers. ```typescript // Reusable cache-first unfurl wrapper const unfurlCache = new Map<string, { data: any; expires: number }>(); function withUnfurlCache<T>( fetchFn: (url: string) => Promise<T>, ttlMs: number = 300_000 // 5 min default ) { return async (url: string): Promise<T> => { const cached = unfurlCache.get(url); if (cached && cached.expires > Date.now()) { return cached.data as T; } const data = await fetchFn(url); // must complete in <4 seconds unfurlCache.set(url, { data, expires: Date.now() + ttlMs }); return data; }; } // Usage const cachedFetchIssue = withUnfurlCache( async (url: string) => { const id = url.match(/\/issues\/(\d+)/)?.[1]; return id ? await fetchIssue(id) : null; }, 5 * 60_000 // 5 min TTL ); app.on("message.ext.query-link" as any, async ({ activity }) => { const url: string = activity.value?.url ?? ""; const issue = await cachedFetchIssue(url); if (!issue) return { status: 200, body: {} }; return buildUnfurlResponse(issue); }); ``` **Best practices:** - Set TTL based on data freshness needs (5–60 minutes) - Pre-populate cache for known high-traffic URLs on startup - Never make multiple API calls inside the unfurl handler — pre-fetch or batch - For production, replace the `Map` with Redis or a shared cache **Don't:** Skip caching even for "fast" data sources. Network latency + cold starts can push you past 5 seconds. **Reverse (Teams → Slack):** Slack's 30-minute async model makes caching less critical, but still recommended for performance. ## pitfalls - **Missing `messageHandlers` in manifest**: Without the `messageHandlers` array in `composeExtensions`, link unfurling never triggers. The bot receives no activity for matching URLs. This is the #1 deployment issue for link unfurling. - **5-second deadline with no fallback**: If data fetching exceeds 5 seconds, the unfurl silently fails — no error card, no retry. Users see a plain URL with no preview. Implement aggressive caching and fast-path responses. - **Bot must be installed in the conversation**: Unlike Slack where workspace-level app installation enables unfurling everywhere, Teams requires the bot to be installed in each team/chat where unfurling should work. Users may not understand why links aren't unfurling in some conversations. - **No retroactive unfurling**: Existing messages with matching URLs are never unfurled when the bot is installed later. Only new messages trigger the handler. Slack supports unfurling existing messages. - **Exact domain matching**: `*.example.com` is not supported. If your app has URLs across `app.example.com`, `api.example.com`, and `docs.example.com`, all three must be listed separately in the manifest. For apps with many subdomains, use a build-time manifest generator script (see Y15 pattern below). - **Adaptive Card size limit**: Link preview cards are subject to the standard 28 KB Adaptive Card size limit. Keep previews concise — unfurl cards with embedded images or long descriptions may be silently truncated. ### Domain wildcard workaround: manifest generator (Y15) Teams requires exact domain listing — no wildcards. For apps with many subdomains, automate manifest generation at build time. ```typescript // scripts/generate-manifest-domains.ts import fs from "fs"; // Source of truth: your subdomain list (from config, DNS, or API) const BASE_DOMAIN = "example.com"; const SUBDOMAINS = ["app", "docs", "api", "staging", "portal", "admin"]; function generateManifestDomains(): string[] { return SUBDOMAINS.map(sub => `${sub}.${BASE_DOMAIN}`); } // Read the template manifest const manifest = JSON.parse(fs.readFileSync("manifest.template.json", "utf8")); // Inject domains into composeExtensions messageHandlers manifest.composeExtensions[0].messageHandlers[0].value.domains = generateManifestDomains(); // Also inject into validDomains (required for link unfurling) manifest.validDomains = [ ...new Set([...(manifest.validDomains ?? []), ...generateManifestDomains()]), ]; fs.writeFileSync("manifest.json", JSON.stringify(manifest, null, 2)); console.log(`Generated manifest with ${SUBDOMAINS.length} domains.`); ``` Add to your build pipeline: `ts-node scripts/generate-manifest-domains.ts` before packaging. **Don't:** Try to register a single wildcard domain — Teams silently rejects it with no error message. **Reverse (Teams → Slack):** Slack supports `*.example.com` wildcards natively in the app dashboard. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling - https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#composeextensionsmessagehandlers - https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions - https://github.com/microsoft/teams.ts - https://api.slack.com/reference/messaging/link-unfurling — Slack link unfurling - https://api.slack.com/methods/chat.unfurl — Slack chat.unfurl ## instructions Use this expert when adding cross-platform support in either direction for Slack link unfurling or Teams link preview. It covers: `link_shared` event to `message.ext.query-link` handler, `chat.unfurl()` to synchronous card response, manifest `messageHandlers` domain configuration, the 5-second response deadline, installation requirement, and the lack of retroactive unfurling. For Teams → Slack, map `messageHandlers` domain config to `link_shared` event subscription, and preview card responses to `chat.unfurl` calls. Pair with `../teams/ui.message-extensions-ts.md` for general message extension patterns, `../teams/runtime.manifest-ts.md` for manifest configuration, and `ui-block-kit-adaptive-cards-ts.md` for converting between Slack attachment unfurl format and Adaptive Cards. ## research Deep Research prompt: "Write a micro expert for bridging Slack link unfurling (link_shared event + chat.unfurl) and Teams link preview (compose extension query-link handler, messageHandlers) in either direction for cross-platform bots. Cover: manifest messageHandlers domain configuration, the 5-second synchronous response deadline vs Slack's async model, bot installation requirement, no retroactive unfurling, exact domain matching, Adaptive Card response format, caching strategies, per-URL invocation, and reverse-direction mapping from Teams messageHandlers to Slack link_shared subscriptions and chat.unfurl calls. Include TypeScript code examples and a mapping table." -
middleware-handlers-ts.md 15.4 KB
# middleware-handlers-ts ## purpose Bridges Slack Bolt middleware chains and Teams SDK handler patterns for cross-platform bots targeting Slack, Teams, or both. ## rules 1. Slack Bolt uses an explicit middleware chain: `app.use((args) => { ... await next(); })` for global middleware, and per-listener middleware as extra arguments to `app.message()`, `app.action()`, etc. Teams SDK v2 uses `app.on()` route handlers that execute in registration order with no explicit `next()` call. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Slack's `next()` function must be called to pass control to the next middleware. In Teams, all matching handlers for a route execute — there is no `next()` to call. To short-circuit (prevent later handlers), return early or use a guard pattern. 3. Slack global middleware (`app.use()`) runs on EVERY request before any listener. In Teams, register a `app.on('message', ...)` handler FIRST (before other message handlers) to achieve the same effect. Handler registration order determines execution order. 4. Slack listener middleware (per-handler) like `app.message(authMiddleware, actualHandler)` has no direct Teams equivalent. Refactor as: (a) a shared guard function called at the top of each handler, (b) a wrapper/decorator function that wraps handlers, or (c) a first-registered catch-all handler that sets context. 5. Slack's `ack()` (acknowledge within 3 seconds) has NO equivalent in Teams. Teams does not require acknowledgement — the Bot Framework handles the HTTP response automatically. Remove all `ack()` calls and restructure code that splits work into "before ack" and "after ack" phases. 6. Slack's `say()` (post to the conversation where the event occurred) maps directly to Teams' `send()`. Both send a message to the current conversation. Slack's `respond()` (respond to the original webhook URL) maps to `send()` for new messages or `ctx.updateActivity()` for updating the original message. The webhook URL pattern does not exist in Teams. 7. Slack's `context` object (custom properties attached via middleware) → Teams uses the activity object and handler arguments directly. For shared state across handlers, use `app.state` or closure-scoped variables. 8. Slack error middleware (`app.error(async (error) => { ... })`) → Teams error handling via try/catch in individual handlers or a global `app.on('error', ...)` handler. The error shape differs significantly: Slack provides a destructured object `{ error, context, body }` where `context` contains bot/team metadata and `body` contains the full event payload, while Teams provides the raw `Error` object plus the activity context via handler arguments. For Teams → Slack: wrap the raw Error with context/body metadata to match Slack's shape. 9. The Java Slack SDK's formal middleware chain (`Middleware` interface with `apply(req, resp, chain)` → `chain.next(req, resp)`) is structurally identical to Express middleware. When converting Java middleware, first understand the intent, then rewrite as a Teams guard function or wrapper. 10. Slack's authorization middleware (built-in, validates tokens per workspace in multi-tenant apps) is replaced by Bot Framework JWT validation (automatic) and Azure AD authentication. Remove custom authorization middleware entirely. ## patterns ### Slack global middleware → Teams first-registered handler **Slack (before):** ```typescript import { App, NextFn } from '@slack/bolt'; const app = new App({ token: '...', signingSecret: '...' }); // Global middleware: runs on every request app.use(async ({ next, logger, body }) => { logger.info(`Request type: ${body.type}`); const start = Date.now(); await next(); logger.info(`Completed in ${Date.now() - start}ms`); }); // Global auth middleware app.use(async ({ next, context, client }) => { const authResult = await client.auth.test(); context.botUserId = authResult.user_id; await next(); }); app.message(/hello/i, async ({ say }) => { await say('Hi there!'); }); ``` **Teams (after):** ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import pino from 'pino'; const log = pino({ name: 'my-bot' }); const app = new App({ logger: new ConsoleLogger('my-bot', { level: 'info' }), }); // No global middleware API — use first-registered handlers instead. // Logging is built into the Teams SDK via ConsoleLogger. // Auth is handled automatically by Bot Framework JWT validation. // For cross-cutting concerns, use a wrapper function: function withLogging<T extends (...args: any[]) => Promise<void>>( handler: T, ): T { return (async (...args: any[]) => { const start = Date.now(); try { await handler(...args); } finally { log.info(`Handler completed in ${Date.now() - start}ms`); } }) as T; } app.message( /hello/i, withLogging(async ({ send }) => { await send('Hi there!'); }), ); ``` ### Slack listener middleware (per-handler auth) → Teams guard function **Slack (before):** ```typescript // Listener middleware: only this handler requires admin check async function requireAdmin({ message, client, next }: any) { const userInfo = await client.users.info({ user: message.user }); if (userInfo.user?.is_admin) { await next(); // allow handler to proceed } // Not calling next() short-circuits the chain } app.message(/^!admin/, requireAdmin, async ({ message, say }) => { await say(`Admin command received from <@${message.user}>`); }); ``` **Teams (after):** ```typescript // Guard function: replaces listener middleware async function isAdmin(aadObjectId: string): Promise<boolean> { // Check admin status via Graph API or custom logic const adminIds = new Set([process.env.ADMIN_AAD_ID]); return adminIds.has(aadObjectId); } app.message(/^!admin/, async ({ activity, send }) => { // Guard at the top of the handler (replaces middleware chain) if (!(await isAdmin(activity.from.aadObjectId ?? ''))) { await send('You must be an admin to use this command.'); return; // Early return replaces "not calling next()" } await send(`Admin command received from ${activity.from.name}`); }); ``` ### Java SDK middleware chain → Teams handler wrapper **Java (before):** ```java // Formal middleware interface public class LoggingMiddleware implements Middleware { @Override public Response apply(Request req, Response resp, MiddlewareChain chain) throws Exception { long start = System.currentTimeMillis(); logger.info("Processing: {}", req.getRequestType()); Response result = chain.next(req); logger.info("Completed in {}ms", System.currentTimeMillis() - start); return result; } } public class RateLimitMiddleware implements Middleware { private final RateLimiter limiter; @Override public Response apply(Request req, Response resp, MiddlewareChain chain) throws Exception { if (!limiter.tryAcquire(req.getContext().getTeamId())) { return Response.builder().statusCode(429).body("Rate limited").build(); } return chain.next(req); } } // Registration app.use(new LoggingMiddleware()); app.use(new RateLimitMiddleware(limiter)); ``` **Teams TypeScript (after):** ```typescript // Middleware becomes wrapper functions (no formal chain) import pino from 'pino'; const log = pino({ name: 'my-bot' }); // Rate limiter as a guard utility class RateLimiter { private counts = new Map<string, { count: number; resetAt: number }>(); tryAcquire(key: string, limit = 10, windowMs = 60_000): boolean { const now = Date.now(); const entry = this.counts.get(key); if (!entry || now > entry.resetAt) { this.counts.set(key, { count: 1, resetAt: now + windowMs }); return true; } if (entry.count >= limit) return false; entry.count++; return true; } } const limiter = new RateLimiter(); // Handler wrapper that combines logging + rate limiting type MessageHandler = (ctx: any) => Promise<void>; function withMiddleware(handler: MessageHandler): MessageHandler { return async (ctx) => { const start = Date.now(); const tenantId = ctx.activity.channelData?.tenant?.id ?? 'unknown'; // Rate limiting (replaces RateLimitMiddleware) if (!limiter.tryAcquire(tenantId)) { await ctx.send('Rate limited. Please try again later.'); return; } // Logging (replaces LoggingMiddleware) log.info({ type: ctx.activity.type }, 'Processing'); try { await handler(ctx); } finally { log.info(`Completed in ${Date.now() - start}ms`); } }; } // Apply to handlers app.message(/^!deploy/, withMiddleware(async ({ send }) => { await send('Deploying...'); })); ``` ### Removing ack() and restructuring pre/post-ack logic **Slack (before):** ```typescript app.command('/deploy', async ({ ack, respond, command }) => { // Must ack within 3 seconds await ack('Starting deployment...'); // Slow work happens AFTER ack (Slack already got the 200 OK) const result = await runDeployment(command.text); await respond(`Deployment ${result.status}: ${result.url}`); }); ``` **Teams (after):** ```typescript app.message(/^\/deploy\s*(.*)/i, async ({ send, activity }) => { // No ack() needed — Teams handles the HTTP response // Send an immediate response (replaces ack with message) await send('Starting deployment...'); // Slow work — just do it inline, no pre/post-ack split needed const target = activity.text?.match(/^\/deploy\s*(.*)/i)?.[1] ?? ''; const result = await runDeployment(target); await send(`Deployment ${result.status}: ${result.url}`); }); ``` ### say() → send() and error handling differences **Slack (before):** ```typescript // say() posts to the conversation where the event occurred app.message(/help/i, async ({ say, message }) => { await say(`Hey <@${message.user}>, here's what I can do...`); }); // Global error handler — receives { error, context, body } app.error(async ({ error, context, body }) => { console.error(`Error in team ${context.teamId}:`, error.message); console.error('Event body:', body.type); // context has botUserId, teamId, etc. set by middleware // body has the full Slack event payload }); ``` **Teams (after):** ```typescript // send() is the Teams equivalent of say() — posts to the current conversation app.message(/help/i, async ({ send, activity }) => { await send(`Hey ${activity.from.name}, here's what I can do...`); }); // Global error handler — receives the raw Error + activity context app.on('error', async ({ error, activity }) => { // Teams provides the raw Error object, not { error, context, body } console.error(`Error in tenant ${activity?.conversation?.tenantId}:`, (error as Error).message); console.error('Activity type:', activity?.type); // No context bag — use activity properties directly // No body — the activity IS the event payload }); ``` ### Reverse direction (Teams → Slack) For Teams → Slack, convert handler wrappers/guards back to formal middleware chains with `next()`. Add `ack()` calls where required. Key reverse mappings: - Wrapper/decorator functions → `app.use(async ({ next, ... }) => { ... await next(); })` for global middleware - Guard functions at top of handler → listener middleware: `app.message(guardMiddleware, actualHandler)` - Early `return` for short-circuit → omit `await next()` to stop the chain - `send()` for interim status → `ack('status message')` for immediate acknowledgement within 3 seconds - `ctx.updateActivity()` → `respond({ replace_original: true, ... })` - `app.on('error', ...)` → `app.error(async ({ error, context, body }) => { ... })` - Handler registration order → explicit `app.use()` registration order for middleware chain - Closure-scoped state / `app.state` → `context` object properties set by middleware (e.g., `context.botUserId`) - Inline sequential work → split into pre-`ack()` (fast) and post-`ack()` (slow) phases where needed - Bot Framework JWT validation (automatic, remove) → add `signingSecret` to Bolt config for request verification ## pitfalls - **Looking for `next()`**: Teams has no middleware chain with `next()`. Every registered handler for a matching route runs. Stop thinking in chains and think in "ordered handler list." - **Porting `ack()` as an empty response**: `ack()` is a Slack-specific 3-second HTTP response requirement. Teams has no equivalent. Remove it entirely — don't replace it with an empty `send()`. - **Porting `respond()` URL-based replies**: Slack's `respond()` uses a `response_url` webhook. Teams has no response URL concept. Replace with `send()` for new messages or `ctx.updateActivity()` for updating existing messages. - **Middleware that sets `context` properties**: Slack middleware often attaches custom data to `context` (e.g., `context.botUserId`). In Teams, use the handler's arguments directly (`activity.recipient.id` for bot ID) or closure-scoped state. There is no mutable `context` bag. - **Authorization middleware being ported**: Slack's built-in `authorize` function (multi-tenant token lookup) and custom auth middleware should NOT be ported. Bot Framework JWT validation is automatic. Remove all token verification middleware. - **Pre/post `ack()` split logic**: Slack apps commonly split handlers into "before ack" (fast, returns 200) and "after ack" (slow, async work). In Teams, this split is unnecessary — just do the work sequentially. Send an interim status message if the user needs feedback while waiting. - **Java `MiddlewareChain.next()` return value**: Java middleware can inspect the Response returned by `chain.next()` and modify it. Teams handlers don't return responses to a chain — they call `send()` directly. Post-processing middleware must become wrapper functions. ## references - https://slack.dev/bolt-js/concepts/global-middleware -- Slack Bolt global middleware - https://slack.dev/bolt-js/concepts/listener-middleware -- Slack listener middleware - https://api.slack.com/interactivity/handling#acknowledgment_response -- Slack ack() requirement - https://github.com/microsoft/teams.ts -- Teams SDK v2 handler patterns - https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication -- Bot Framework auth (replaces Slack signing secret) ## instructions Use this expert when bridging Slack middleware patterns and Teams handler patterns in either direction. The key conceptual shift is: Slack uses a formal middleware chain with `next()` and `ack()`, while Teams uses ordered route handlers with no chain, no acknowledgement requirement, and automatic authentication. For Slack → Teams: replace global middleware with first-registered handlers or wrappers, replace listener middleware with guards, remove `ack()`, remove authorization middleware. For Teams → Slack: convert wrappers/guards back to formal middleware chains with `next()`, add `ack()` calls, add signing secret verification. Pair with `events-activities-ts.md` for the event/route mapping and `../teams/runtime.routing-handlers-ts.md` for Teams handler registration patterns. ## research Deep Research prompt: "Write a micro expert for bridging Slack Bolt middleware and Teams SDK v2 handler patterns bidirectionally. Cover: global middleware (app.use with next()) <-> first-registered handlers, listener middleware <-> guard functions, ack() addition/removal strategy, respond() <-> send()/updateActivity(), Java Middleware interface <-> TypeScript wrapper functions, authorization middleware bridging, context property migration, error handling middleware, and pre/post-ack logic restructuring. Include 4 worked examples covering both directions." -
python-cross-platform.md 9.3 KB
# python-cross-platform ## purpose Unified Python server architecture for dual-platform Slack + Teams bots — combining `slack_bolt` and `microsoft_teams` in a single Python codebase. ## rules 1. Use **FastAPI** as the shared web framework. The Teams Python SDK uses FastAPI internally, and Slack Bolt has an `AsyncSlackRequestHandler` adapter for FastAPI. Both SDKs can share one FastAPI app and one process. [slack_bolt.adapter.fastapi, microsoft_teams.apps] 2. Mount the Slack handler at `/slack/events` and let the Teams SDK handle `/api/messages` (its default). Both endpoints run in the same FastAPI process, each routing to its own SDK. [FastAPI route mounting] 3. Use `AsyncApp` (not sync `App`) for Slack Bolt when combining with Teams, since the Teams SDK is async-only. Mixing sync Slack Bolt with async Teams SDK in one process causes event loop conflicts. [slack_bolt.async_app] 4. Build a **shared service layer** between platforms. Platform handlers call the same business logic — the Slack handler converts Slack payloads to service calls, and the Teams handler converts Teams activities to the same service calls. This mirrors the TS cross-platform architecture pattern. [experts/bridge/cross-platform-architecture-ts.md] 5. For AI features, use a single model client shared between platforms. Both `slack_bolt` handlers and `microsoft_teams` handlers can call the same OpenAI/Azure OpenAI client. Do not duplicate model initialization per platform. [shared service pattern] 6. Handle platform-specific UI by converting between Block Kit (Slack) and Adaptive Cards (Teams) at the adapter layer. The service layer returns platform-agnostic data; each platform adapter formats it for its UI framework. [experts/bridge/block-kit-to-adaptive-cards-ts.md concepts] 7. Use a single `.env` file for both platforms' credentials: `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, `SLACK_APP_TOKEN` (if Socket Mode) for Slack; `CLIENT_ID`, `CLIENT_SECRET` for Teams. Load with `python-dotenv`. [environment config] 8. For local development, use Slack Socket Mode (no public URL needed) alongside the Teams SDK's HTTP endpoint. The Slack `SocketModeHandler` runs in a background thread while FastAPI serves Teams on a port. [slack_bolt.adapter.socket_mode] 9. Store user identity mappings between platforms. A Slack user ID (`U...`) and a Teams user AAD object ID are different identifiers for the same person. Build a mapping table keyed by email or employee ID. [experts/bridge/identity-linking-ts.md concepts] 10. Deploy as a single container or process. Both SDKs share the Python runtime, dependencies, and service layer. Use `uvicorn` to run the FastAPI app, with Slack Socket Mode starting as a background task if needed. [deployment pattern] ## patterns ### Unified FastAPI server with both SDKs ```python import asyncio import os from contextlib import asynccontextmanager from fastapi import FastAPI, Request from slack_bolt.async_app import AsyncApp from slack_bolt.adapter.fastapi import AsyncSlackRequestHandler from microsoft_teams.apps import App as TeamsApp, ActivityContext from microsoft_teams.api import MessageActivity # --- Shared service layer --- async def handle_greeting(user_name: str) -> str: return f"Hello, {user_name}! How can I help?" async def handle_status_request() -> dict: return {"api": "healthy", "db": "healthy", "queue": "degraded"} # --- Slack setup --- slack_app = AsyncApp( token=os.environ["SLACK_BOT_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"], ) @slack_app.message("hello") async def slack_hello(message, say): result = await handle_greeting(f"<@{message['user']}>") await say(result) @slack_app.command("/status") async def slack_status(ack, respond): await ack("Checking...") status = await handle_status_request() await respond( f"API: {status['api']} | DB: {status['db']} | Queue: {status['queue']}" ) slack_handler = AsyncSlackRequestHandler(slack_app) # --- Teams setup --- teams_app = TeamsApp( client_id=os.environ.get("CLIENT_ID"), client_secret=os.environ.get("CLIENT_SECRET"), ) @teams_app.on_message_pattern(r"^hello") async def teams_hello(ctx: ActivityContext[MessageActivity]): user_name = ctx.activity.from_property.name or "there" result = await handle_greeting(user_name) await ctx.send(result) @teams_app.on_message_pattern(r"^status$") async def teams_status(ctx: ActivityContext[MessageActivity]): status = await handle_status_request() await ctx.send( f"API: {status['api']} | DB: {status['db']} | Queue: {status['queue']}" ) # --- FastAPI combines both --- @asynccontextmanager async def lifespan(app: FastAPI): # Start Teams SDK in background asyncio.create_task(teams_app.start(port=None)) yield fastapi_app = FastAPI(lifespan=lifespan) @fastapi_app.post("/slack/events") async def slack_events(req: Request): return await slack_handler.handle(req) # Teams registers its own /api/messages route via HttpPlugin # Mount Teams routes into the shared FastAPI app fastapi_app.mount("/", teams_app.http.app) if __name__ == "__main__": import uvicorn uvicorn.run(fastapi_app, host="0.0.0.0", port=3000) ``` ### Platform adapter pattern for UI conversion ```python from dataclasses import dataclass from typing import Any @dataclass class StatusCard: """Platform-agnostic data structure""" title: str fields: dict[str, str] action_label: str def to_slack_blocks(card: StatusCard) -> list[dict[str, Any]]: """Convert to Slack Block Kit""" fields = [ {"type": "mrkdwn", "text": f"*{k}:* {v}"} for k, v in card.fields.items() ] return [ {"type": "section", "text": {"type": "mrkdwn", "text": f"*{card.title}*"}}, {"type": "section", "fields": fields}, { "type": "actions", "elements": [ { "type": "button", "text": {"type": "plain_text", "text": card.action_label}, "action_id": "refresh_status", } ], }, ] def to_adaptive_card(card: StatusCard) -> dict[str, Any]: """Convert to Teams Adaptive Card""" facts = [{"title": k, "value": v} for k, v in card.fields.items()] return { "type": "AdaptiveCard", "version": "1.4", "body": [ {"type": "TextBlock", "text": card.title, "weight": "Bolder"}, {"type": "FactSet", "facts": facts}, ], "actions": [ {"type": "Action.Submit", "title": card.action_label} ], } ``` ## pitfalls - **Event loop conflicts**: Mixing sync Slack `App` with async Teams SDK causes `RuntimeError: This event loop is already running`. Always use `AsyncApp` for Slack when combining with Teams. - **Port collision**: Both SDKs default to different ports (Slack: 3000, Teams: 3978). When combining, use one port for the shared FastAPI app and configure both SDKs to use it. - **Double handling**: If both Slack and Teams are mounted on the same FastAPI app, ensure routes don't overlap. Slack uses `/slack/events`, Teams uses `/api/messages` — keep them separate. - **Python version**: The Teams Python SDK requires **Python 3.12+**. Slack Bolt supports 3.9+. The combined project must use 3.12+ to satisfy both. - **Credential isolation**: Never mix Slack tokens with Teams credentials. Use clear env var prefixes (`SLACK_*` for Slack, `CLIENT_*` / `AZURE_*` for Teams) to avoid accidental cross-contamination. - **No Python-specific TS experts**: All architecture and bridging experts (`cross-platform-architecture-ts.md`, `block-kit-to-adaptive-cards-ts.md`, etc.) contain TypeScript code. Use them for design patterns but translate all code to Python. ## references - https://slack.dev/bolt-python/concepts - https://slack.dev/bolt-python/concepts/adapters - teams.py source: packages/apps/src/microsoft_teams/apps/ - experts/bridge/cross-platform-architecture-ts.md (patterns to adapt) - experts/bridge/block-kit-to-adaptive-cards-ts.md (UI conversion concepts) ## instructions This expert covers the unified Python server architecture for Tier 2 dual-platform bots. Use it when building a Python bot that serves both Slack and Teams from a single codebase. It covers the FastAPI integration pattern, shared service layer, platform adapters, and deployment model. Pair with: `slack/bolt-python.md` for Slack-side Python SDK details. `teams/teams-python.md` for Teams-side Python SDK details. `bridge/cross-platform-architecture-ts.md` for architectural patterns (translate to Python). `bridge/block-kit-to-adaptive-cards-ts.md` for UI conversion concepts (translate to Python). `bridge/identity-linking-ts.md` for user mapping concepts. ## research Deep Research prompt: "Write a micro expert on building a unified Python server that combines Slack Bolt (slack_bolt AsyncApp) and Microsoft Teams SDK (microsoft_teams) in a single FastAPI application. Cover FastAPI route mounting (/slack/events for Slack, /api/messages for Teams), shared service layer pattern, platform adapter pattern for Block Kit vs Adaptive Cards, environment configuration for both platforms, Socket Mode for local dev alongside HTTP for Teams, async-only requirement, Python 3.12+ version constraint, deployment as single container, and identity mapping between Slack user IDs and Teams AAD object IDs. Source from slack_bolt adapter.fastapi, microsoft_teams.apps HttpPlugin, and cross-platform architecture patterns." -
rate-limiting-resilience-ts.md 17.6 KB
# rate-limiting-resilience-ts ## purpose Bridges Slack and Teams rate limiting patterns, retry logic, and resilience strategies for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack 429 + `Retry-After` header → same pattern for Bot Framework and Graph API.** Both platforms return HTTP 429 with a `Retry-After` header (seconds) when throttled. The retry pattern is identical: wait the specified duration, then retry. The difference is in the rate limits themselves. [learn.microsoft.com -- Graph throttling](https://learn.microsoft.com/en-us/graph/throttling) 2. **Slack Bolt retry config → manual retry with exponential backoff + jitter.** Slack Bolt has built-in retry (`retryConfig: { retries: 3 }`). The Teams SDK does not have built-in retry. Implement exponential backoff with jitter: `delay = min(baseDelay * 2^attempt + random(0, jitter), maxDelay)`. [learn.microsoft.com -- Retry guidance](https://learn.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific) 3. **Teams Bot Framework rate limits: ~1 msg/sec per conversation, ~30 msg/min per conversation.** These are soft limits that vary by channel type (1:1 vs group vs channel). Exceeding them results in 429 responses. Slack's rate limits are per-method (e.g., `chat.postMessage` at ~1/sec per token). Teams limits are per-conversation, not per-method. [learn.microsoft.com -- Bot rate limiting](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/rate-limit) 4. **Graph API has separate throttling from Bot Framework.** Graph API rate limits are per-app and per-tenant, varying by API. Common limits: 10,000 requests/10 minutes per app, with lower limits for specific APIs (e.g., channel messages). Graph 429s include `Retry-After` headers. These are independent of Bot Framework message rate limits. [learn.microsoft.com -- Graph throttling](https://learn.microsoft.com/en-us/graph/throttling) 5. **Proactive broadcast to many conversations needs a send queue.** Sending the same message to 500 users at once will hit rate limits. Implement a queue with concurrency control: process N messages concurrently, respect per-conversation limits, and handle 429s with retry. Use `p-limit`, `p-queue`, or a custom queue. [npmjs.com/p-queue](https://www.npmjs.com/package/p-queue) 6. **Circuit breaker pattern (`opossum`) protects against cascading failures.** When an external service (your database, a third-party API) is down, the bot should fail fast instead of timing out on every request. Use `opossum` to wrap external calls: after N failures, the circuit opens and rejects immediately for a cooldown period. [npmjs.com/opossum](https://www.npmjs.com/package/opossum) 7. **Slack `slack_api_error` with `response.headers['retry-after']` → same extraction pattern for Teams.** The error handling pattern is similar: catch HTTP errors, check for 429 status, extract `Retry-After`, and schedule retry. The API client libraries differ but the logic is identical. [learn.microsoft.com -- Graph error handling](https://learn.microsoft.com/en-us/graph/errors) 8. **Bot Framework Connector API has a separate 30-second timeout.** Beyond rate limits, the Bot Framework Connector API has a response timeout. If the bot doesn't respond to an invoke within ~3-10 seconds (depending on activity type), the Connector may retry or time out. This is separate from rate limiting but can compound issues under load. [learn.microsoft.com -- Bot Framework](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-overview) 9. **Graph API batch requests reduce API call volume.** Instead of N individual Graph API calls, batch up to 20 requests in a single `POST /$batch` call. This counts as fewer requests against rate limits and reduces network overhead. Useful for bulk channel operations, user lookups, or file operations. [learn.microsoft.com -- JSON batching](https://learn.microsoft.com/en-us/graph/json-batching) 10. **Log and monitor throttling events.** Unlike Slack where Bolt logs retries automatically, Teams throttling must be explicitly logged. Track: 429 count, average retry delay, circuit breaker state, queue depth. Use Application Insights custom metrics or console logging. Throttling spikes indicate you're approaching platform limits. [learn.microsoft.com -- App Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/nodejs) 11. **Reverse direction (Teams → Slack):** For Teams → Slack, Slack Bolt provides built-in `retryConfig`. Map custom Teams retry plugins to Bolt's retry configuration. Slack rate limits are per-method-per-token (not per-conversation like Teams). The `p-queue` and circuit breaker patterns apply equally in both directions. For Graph API batch requests, there is no Slack equivalent — individual API calls are needed but Bolt's built-in retry handles 429s automatically. ## patterns ### Exponential backoff wrapper **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, // Built-in retry handling retryConfig: { retries: 3, factor: 2, // exponential backoff }, }); // Bolt automatically retries on 429 app.message(/hello/i, async ({ say }) => { await say("Hello!"); // auto-retried on rate limit }); ``` **Teams (after) — manual retry wrapper:** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const logger = new ConsoleLogger("my-bot", { level: "info" }); const app = new App({ logger, }); // Exponential backoff with jitter — replaces Bolt's retryConfig async function withRetry<T>( fn: () => Promise<T>, options: { maxRetries?: number; baseDelayMs?: number; maxDelayMs?: number; jitterMs?: number; } = {} ): Promise<T> { const { maxRetries = 3, baseDelayMs = 1000, maxDelayMs = 30_000, jitterMs = 500, } = options; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (err: any) { const status = err?.statusCode ?? err?.response?.status ?? err?.code; const isRetryable = status === 429 || status === 503 || status === 502; if (!isRetryable || attempt === maxRetries) { throw err; } // Use Retry-After header if available, otherwise exponential backoff const retryAfterSec = err?.response?.headers?.["retry-after"]; let delay: number; if (retryAfterSec) { delay = parseInt(retryAfterSec, 10) * 1000; } else { delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs); } // Add jitter to prevent thundering herd delay += Math.random() * jitterMs; logger.warn( `Rate limited (attempt ${attempt + 1}/${maxRetries}). ` + `Retrying in ${Math.round(delay)}ms...` ); await new Promise((resolve) => setTimeout(resolve, delay)); } } throw new Error("withRetry: unreachable"); } // Usage: wrap any API call that might be rate limited app.message(/hello/i, async ({ send }) => { await withRetry(() => send("Hello!")); }); app.start(3978); ``` ### Rate-limited proactive broadcast **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Broadcast to all channels — Bolt retries handle 429s app.command("/broadcast", async ({ ack, command, client }) => { await ack(); const channels = await client.conversations.list({ types: "public_channel" }); for (const channel of channels.channels ?? []) { try { await client.chat.postMessage({ channel: channel.id!, text: command.text, }); } catch (err: any) { if (err.data?.error === "ratelimited") { const retryAfter = parseInt(err.data.response_metadata?.retry_after ?? "1", 10); await new Promise((r) => setTimeout(r, retryAfter * 1000)); await client.chat.postMessage({ channel: channel.id!, text: command.text }); } } } }); ``` **Teams (after) — queued broadcast with concurrency control:** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import PQueue from "p-queue"; const logger = new ConsoleLogger("my-bot", { level: "info" }); const app = new App({ logger }); // Store conversation references at install time const conversationRefs = new Map<string, { conversationId: string; serviceUrl: string; }>(); app.on("install.add", async ({ activity }) => { const convId = activity.conversation?.id ?? ""; conversationRefs.set(convId, { conversationId: convId, serviceUrl: (activity as any).serviceUrl, }); }); // Rate-limited broadcast queue // Concurrency: 5 simultaneous sends, 200ms between each const sendQueue = new PQueue({ concurrency: 5, interval: 200, intervalCap: 1, // 1 task per interval per concurrency slot }); app.message(/^\/?broadcast (.+)$/i, async ({ send, activity }) => { const text = activity.text?.replace(/^\/?broadcast\s+/i, "") ?? ""; const targets = Array.from(conversationRefs.values()); await send(`Broadcasting to ${targets.length} conversations...`); let sent = 0; let failed = 0; const promises = targets.map((ref) => sendQueue.add(async () => { try { await withRetry(() => app.send(ref.conversationId, text)); sent++; } catch (err) { failed++; logger.error(`Failed to send to ${ref.conversationId}:`, err); } }) ); await Promise.all(promises); await send(`Broadcast complete: ${sent} sent, ${failed} failed.`); }); // withRetry from previous pattern async function withRetry<T>(fn: () => Promise<T>): Promise<T> { for (let attempt = 0; attempt < 3; attempt++) { try { return await fn(); } catch (err: any) { const status = err?.statusCode ?? err?.response?.status; if (status !== 429 || attempt === 2) throw err; const retryAfter = parseInt(err?.response?.headers?.["retry-after"] ?? "2", 10); await new Promise((r) => setTimeout(r, retryAfter * 1000 + Math.random() * 500)); } } throw new Error("unreachable"); } app.start(3978); ``` ### Circuit breaker for downstream services ```typescript import CircuitBreaker from "opossum"; // Wrap an external API call with a circuit breaker const fetchUserData = new CircuitBreaker( async (userId: string) => { const response = await fetch(`https://api.internal.com/users/${userId}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }, { timeout: 5000, // If the function takes longer than 5s, trigger a failure errorThresholdPercentage: 50, // Open circuit when 50% of requests fail resetTimeout: 30_000, // After 30s, try again (half-open) volumeThreshold: 5, // Minimum 5 requests before evaluating threshold } ); // Circuit events for monitoring fetchUserData.on("open", () => logger.warn("Circuit OPEN — failing fast")); fetchUserData.on("halfOpen", () => logger.info("Circuit HALF-OPEN — testing")); fetchUserData.on("close", () => logger.info("Circuit CLOSED — normal operation")); // Usage in a handler app.message(/^\/?user (.+)$/i, async ({ send, activity }) => { const userId = activity.text?.match(/user\s+(\S+)/)?.[1] ?? ""; try { const user = await fetchUserData.fire(userId); await send(`User: ${user.name} (${user.email})`); } catch (err: any) { if (err.message === "Breaker is open") { await send("The user service is temporarily unavailable. Please try again later."); } else { await send(`Error fetching user: ${err.message}`); } } }); ``` ### Best practice: retry utility + p-queue broadcast (Y17) **Always build a retry utility with exponential backoff and jitter.** Apply it to all outbound API calls. For proactive broadcasts, combine with `p-queue` concurrency control. ```typescript // Production retry utility — apply to all outbound calls async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (err: any) { if (attempt === maxRetries) throw err; const retryAfter = err?.response?.headers?.["retry-after"]; const baseDelay = retryAfter ? parseInt(retryAfter) * 1000 : 1000 * 2 ** attempt; const jitter = Math.random() * 1000; await new Promise(r => setTimeout(r, baseDelay + jitter)); } } throw new Error("Unreachable"); } // Proactive broadcast with concurrency control import PQueue from "p-queue"; const broadcastQueue = new PQueue({ concurrency: 5, interval: 200, intervalCap: 1 }); async function broadcastToAll( conversationIds: string[], message: string ): Promise<{ sent: number; failed: number }> { let sent = 0, failed = 0; const promises = conversationIds.map(convId => broadcastQueue.add(async () => { try { await withRetry(() => app.send(convId, message)); sent++; } catch { failed++; } }) ); await Promise.all(promises); return { sent, failed }; } ``` **Key rules:** - **Always add jitter.** Without it, multiple bot instances retry simultaneously (thundering herd). - **Set a max queue depth.** Unbounded queues accumulate thousands of items in memory. - **Treat 503 the same as 429.** Both are retryable with backoff. **Don't:** Retry without jitter, or use Bolt's `retryConfig` and assume it covers Graph API calls (it only covers Slack API calls). **Reverse (Teams → Slack):** Configure Bolt's built-in `retryConfig: { retries: 3, factor: 2 }` for Slack API calls. The `p-queue` pattern applies equally for Slack broadcasts. ### Rate limit comparison table | Aspect | Slack | Teams Bot Framework | Teams Graph API | |---|---|---|---| | Rate limit scope | Per-method per-token | Per-conversation | Per-app per-tenant | | Message send limit | ~1/sec per token | ~1/sec per conversation | N/A (use Bot Framework) | | Throttle response | HTTP 429 + `Retry-After` | HTTP 429 + `Retry-After` | HTTP 429 + `Retry-After` | | Built-in retry (SDK) | Bolt `retryConfig` | None (manual) | None (manual) | | Batch API | N/A | N/A | `POST /$batch` (up to 20) | | Burst limit | ~30/min per token | ~30/min per conversation | Varies by API | ## pitfalls - **No built-in retry in Teams SDK**: Slack Bolt's `retryConfig` automatically retries rate-limited requests. The Teams SDK has no equivalent. You must implement retry logic yourself or use a library wrapper. - **Per-conversation vs per-token limits**: Slack rate limits are per-method-per-token (global). Teams Bot Framework limits are per-conversation. Sending to 100 different conversations simultaneously is fine; sending 100 messages to the same conversation will be throttled. - **Graph API and Bot Framework throttling are independent**: A bot can be rate-limited on Graph API calls (user lookups, channel operations) while Bot Framework message sends are fine, or vice versa. Implement retry logic for both independently. - **Thundering herd on retry**: Without jitter, all rate-limited requests retry at exactly the same time, causing another burst. Always add random jitter to retry delays. - **Queue depth unbounded**: Using `p-queue` without a size limit can accumulate thousands of pending messages in memory. Set a maximum queue size and reject new items when full (with a user-facing error). - **Circuit breaker not covering all dependencies**: The circuit breaker should wrap every external dependency (database, third-party API, Graph API) — not just one. A bot with an unprotected dependency can still cascade-fail. - **Forgetting to handle 503 Service Unavailable**: In addition to 429, Bot Framework may return 503 during outages. Treat 503 the same as 429 (retryable with backoff). ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/rate-limit - https://learn.microsoft.com/en-us/graph/throttling - https://learn.microsoft.com/en-us/graph/json-batching - https://learn.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific - https://learn.microsoft.com/en-us/azure/azure-monitor/app/nodejs - https://www.npmjs.com/package/p-queue - https://www.npmjs.com/package/opossum - https://github.com/microsoft/teams.ts - https://api.slack.com/docs/rate-limits — Slack rate limits ## instructions Use this expert when adding cross-platform support in either direction for rate limiting and resilience. It covers: Slack Bolt `retryConfig` bridged to Teams manual exponential backoff + jitter, Teams Bot Framework per-conversation rate limits, Graph API per-app throttling, proactive broadcast with send queue concurrency control, circuit breaker pattern with `opossum`, Graph API batch requests, monitoring throttling events, and reverse mapping from custom Teams retry logic back to Bolt's built-in retry configuration. Pair with `../teams/runtime.proactive-messaging-ts.md` for proactive send infrastructure, `../teams/graph.usergraph-appgraph-ts.md` for Graph API patterns, and `scheduling-deferred-send-ts.md` for rate-limited scheduled sends. ## research Deep Research prompt: "Write a micro expert for bridging Slack and Teams rate limiting patterns, retry logic, and resilience strategies in either direction. Cover: Bolt retryConfig vs manual exponential backoff + jitter, Teams Bot Framework per-conversation rate limits (1 msg/sec, 30 msg/min), Graph API per-app throttling, proactive broadcast send queues with concurrency control, circuit breaker pattern with opossum, Graph API $batch for reducing call volume, 429/503 retry handling, monitoring, and reverse mapping from Teams retry patterns back to Slack Bolt's built-in retry configuration. Include TypeScript code examples and a comparison table." -
rest-only-integration-ts.md 8.4 KB
# rest-only-integration-ts ## purpose Raw HTTP integration patterns for Teams and Slack without native SDKs — Bot Framework REST API for Teams, Slack Events API + Web API for Slack. For Java, C#, Go, or any language that lacks an official Bolt or Teams SDK. ## rules 1. **Use the Bot Framework REST API for Teams when no SDK is available.** The REST API is language-agnostic. Authenticate via Azure AD OAuth2 client credentials, then POST activities to the Bot Connector service URL. 2. **Use the Slack Events API + Web API for Slack when no Bolt SDK is available.** Receive events via HTTP POST webhooks (with signature verification), respond via `chat.postMessage` and other Web API methods. 3. **Verify Slack request signatures manually.** Compute `HMAC-SHA256` of `v0:{timestamp}:{request_body}` using your signing secret. Compare against the `X-Slack-Signature` header. Reject if timestamp is older than 5 minutes. 4. **Verify Teams JWT tokens manually.** Validate the `Authorization: Bearer <token>` header against Azure AD's OpenID configuration. Check `iss`, `aud` (your app ID), and token expiration. Use your platform's JWT library. 5. **Acquire Bot Framework tokens via Azure AD.** POST to `https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token` with `client_id`, `client_secret`, and `scope=https://api.botframework.com/.default`. 6. **Send Teams messages via the Bot Connector API.** POST to `{serviceUrl}/v3/conversations/{conversationId}/activities` with the activity JSON and a `Bearer` token. The `serviceUrl` comes from the inbound activity. 7. **Send Slack messages via the Web API.** POST to `https://slack.com/api/chat.postMessage` with `Authorization: Bearer xoxb-...` and a JSON body containing `channel`, `text`, and optionally `blocks`. 8. **Acknowledge Slack events within 3 seconds.** Return HTTP 200 immediately, then process async. For interactions (actions, commands, shortcuts), return a JSON body or empty 200 to acknowledge. 9. **Handle the Slack URL verification challenge.** When Slack sends `{ type: "url_verification", challenge: "..." }`, respond with `{ challenge: "..." }` and HTTP 200. This only happens once during setup. 10. **Return HTTP 200/201 for Teams webhook POSTs.** The Bot Framework expects a 200 response. For invoke activities, return a JSON body with `{ status: 200, body: ... }`. 11. **Store the `serviceUrl` from Teams activities.** Each inbound activity includes a `serviceUrl` that may change. Use it for subsequent API calls to that conversation. Cache per conversation. 12. **Use `response_url` for Slack interaction responses.** Actions, commands, and shortcuts include a `response_url`. POST to it within 30 minutes with `{ text, response_type }` for follow-up messages without needing the Web API. ## patterns ### Slack signature verification (pseudocode, any language) ``` function verifySlackSignature(signingSecret, timestamp, body, signature): if abs(now() - timestamp) > 300: // 5 minutes return false basestring = "v0:" + timestamp + ":" + body computed = "v0=" + hmac_sha256(signingSecret, basestring) return timingSafeCompare(computed, signature) // HTTP handler: timestamp = request.headers["X-Slack-Request-Timestamp"] signature = request.headers["X-Slack-Signature"] if not verifySlackSignature(SECRET, timestamp, rawBody, signature): return 401 ``` ### Teams token acquisition (HTTP, any language) ``` POST https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token Content-Type: application/x-www-form-urlencoded client_id={appId} &client_secret={appPassword} &scope=https://api.botframework.com/.default &grant_type=client_credentials Response: { "access_token": "eyJ...", "expires_in": 3600 } ``` ### Send a Teams message (HTTP, any language) ``` POST {serviceUrl}/v3/conversations/{conversationId}/activities Authorization: Bearer {access_token} Content-Type: application/json { "type": "message", "text": "Hello from a REST client!", "from": { "id": "{botAppId}", "name": "My Bot" } } ``` ### Send a Slack message (HTTP, any language) ``` POST https://slack.com/api/chat.postMessage Authorization: Bearer xoxb-your-token Content-Type: application/json { "channel": "C123ABC", "text": "Hello from a REST client!", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Hello from a REST client!" } } ] } ``` ### Slack event webhook handler (pseudocode) ``` function handleSlackEvent(request): verifySignature(request) body = parseJSON(request.body) if body.type == "url_verification": return { challenge: body.challenge } if body.type == "event_callback": event = body.event // Process event async... return 200 // acknowledge immediately if body.type == "interactive": // action, shortcut, or view_submission return 200 // ack, then use response_url for follow-up ``` ### Teams JWT validation (pseudocode) ``` function validateTeamsJWT(authHeader, appId): token = authHeader.replace("Bearer ", "") // Fetch keys from https://login.botframework.com/v1/.well-known/openidconfiguration claims = jwt_verify(token, publicKeys) assert claims.aud == appId assert claims.iss starts with "https://api.botframework.com" assert claims.exp > now() return claims ``` ## pitfalls - **Slack signature uses raw body, not parsed JSON.** You must verify against the exact bytes received, not a re-serialized JSON string. Many frameworks parse the body before your handler — use middleware to capture the raw body. - **Teams `serviceUrl` varies by region.** Don't hardcode it. The URL may be `https://smba.trafficmanager.net/...` or `https://emea.ng.msg.teams.microsoft.com/...` depending on the tenant's region. - **Bot Framework tokens expire after 1 hour.** Cache the token and refresh before expiry. Don't acquire a new token for every outbound message — this adds latency and hits rate limits. - **Slack's `response_url` expires after 30 minutes.** If you need to update a message later, use `chat.update` with the message `ts` instead. - **Teams proactive messaging requires a conversation reference.** You can't just POST to a user ID — you need the `conversationId` and `serviceUrl` from a previous inbound activity. Store these on first contact. - **No Adaptive Card support via REST without the schema.** You must construct the full Adaptive Card JSON yourself. Use the Adaptive Card Designer (https://adaptivecards.io/designer/) to prototype, then embed the JSON in your API calls. - **Slack interactive payload is form-encoded, not JSON.** Actions, shortcuts, and view submissions arrive as `application/x-www-form-urlencoded` with a `payload` field containing JSON. Parse the `payload` field, not the raw body. ## references - Bot Framework REST API: https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference - Slack Events API: https://api.slack.com/events-api - Slack Web API: https://api.slack.com/web - Slack request verification: https://api.slack.com/authentication/verifying-requests-from-slack - Azure AD token endpoint: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow - Adaptive Card Designer: https://adaptivecards.io/designer/ ## instructions Use this expert when integrating with Teams or Slack from a language that lacks a native SDK (Java for Teams, C# for Slack, Go, Ruby, etc.), or when building a lightweight integration that doesn't warrant a full SDK dependency. The patterns use pseudocode that's translatable to any language. Pair with: `cross-platform-architecture-ts.md` (if also using TS for one platform), `../teams/runtime.app-init-ts.md` (for TS Teams SDK comparison), `../slack/runtime.bolt-foundations-ts.md` (for TS Slack SDK comparison). ## research Deep Research prompt: "Document raw HTTP integration patterns for Microsoft Teams Bot Framework and Slack without native SDKs. Cover: Bot Framework REST Connector API (POST activities, GET conversations), Azure AD client credentials OAuth2 token acquisition, JWT validation for inbound webhooks, Slack Events API webhook setup (URL verification challenge, event_callback processing), Slack Web API methods (chat.postMessage, chat.update, views.open), Slack request signature verification (HMAC-SHA256, timing-safe comparison, timestamp validation), response_url for interaction follow-ups, Adaptive Card JSON construction for REST, Block Kit JSON construction for REST, serviceUrl caching for Teams, and rate limiting considerations." -
scheduling-deferred-send-ts.md 17.8 KB
# scheduling-deferred-send-ts ## purpose Bridges Slack scheduling (chat.scheduleMessage, reminders) and Teams deferred delivery patterns for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack `chat.scheduleMessage` has NO Teams equivalent.** Teams has no built-in scheduled message API. Replace with: store the message + target time in persistent storage, then use a timer mechanism to send proactively at the scheduled time via `app.send(conversationId, message)`. [learn.microsoft.com -- Proactive messages](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) 2. **Slack `chat.deleteScheduledMessage` → delete from your own storage/queue.** Since scheduled messages are self-managed in Teams, cancellation is simply removing the pending item from your storage (database row, queue message, cron job). No platform API call needed. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. **Slack `reminders.add` → persistent storage + background poll + proactive send.** Slack reminders are platform-managed with DM delivery. In Teams, the bot must: (a) store the reminder with target user/time, (b) poll or use a timer to detect due reminders, (c) send a proactive message to the user's 1:1 chat. [learn.microsoft.com -- Proactive messages](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) 4. **In-process timers (`node-cron`, `setTimeout`) are for development only.** `node-cron` or `setTimeout` work for local dev and single-instance deployments. They are NOT durable — a process restart loses all scheduled items. Never use in-process timers for production scheduled messages. [npmjs.com/node-cron](https://www.npmjs.com/package/node-cron) 5. **Azure Functions timer trigger provides durable serverless scheduling.** Create a timer-triggered function that polls your database for due messages and sends them proactively. The CRON expression configures frequency (e.g., `"0 */1 * * * *"` for every minute). Azure manages the timer lifecycle across restarts. [learn.microsoft.com -- Timer trigger](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer) 6. **Azure Queue Storage with visibility timeout enables exact-time scheduling.** Enqueue a message with `visibilityTimeout` set to the delay duration. The message becomes visible at the target time, triggering a queue-triggered function that sends the proactive message. Maximum visibility timeout is 7 days. [learn.microsoft.com -- Queue trigger](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-trigger) 7. **Azure Service Bus scheduled messages support exact-time delivery.** `ServiceBusSender.scheduleMessages(message, scheduledEnqueueTimeUtc)` enqueues with a future delivery time. No polling needed — Service Bus delivers at the exact time. Supports cancellation via `cancelScheduledMessage(sequenceNumber)`. Best for high-volume scheduled sends. [learn.microsoft.com -- Service Bus scheduling](https://learn.microsoft.com/en-us/azure/service-bus-messaging/message-sequencing#scheduled-messages) 8. **Power Automate "Recurrence" trigger is a no-code alternative.** For simple recurring messages (daily standup reminder, weekly digest), a Power Automate flow with a Recurrence trigger can send messages via the bot's webhook or Graph API without code. Good for business users managing their own schedules. [learn.microsoft.com -- Power Automate Recurrence](https://learn.microsoft.com/en-us/power-automate/triggers-introduction#recurrence-trigger) 9. **Store conversation references at install time for proactive messaging.** All scheduled/reminder sends require a valid conversation reference (including `serviceUrl`). Capture and persist the reference in the `install.add` handler. Without it, the bot cannot send proactive messages at scheduled time. [learn.microsoft.com -- Conversation reference](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages#get-the-conversation-reference) 10. **Rate limiting applies to bulk scheduled sends.** Teams limits bots to ~1 message/second per conversation and ~30 messages/minute per conversation. If many scheduled messages are due at the same time (e.g., "send daily digest to 500 users at 9 AM"), implement a send queue with concurrency control and staggered delivery. [learn.microsoft.com -- Rate limits](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/rate-limit) 11. **Reverse direction (Teams → Slack):** For Teams → Slack, this is simpler — Slack has native `chat.scheduleMessage` and `reminders.add` APIs. Timer-based infrastructure (Azure Functions, Queue Storage, Service Bus) can be replaced with direct Slack API calls. Map proactive send patterns to `chat.scheduleMessage` with a `post_at` Unix timestamp. Map Power Automate recurrence flows to Slack Workflow Builder scheduled triggers or `reminders.add` for user-facing reminders. ## patterns ### node-cron + proactive messaging (development / single-instance) **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Schedule a message for 30 minutes from now app.command("/remind", async ({ ack, command, client }) => { await ack(); const postAt = Math.floor(Date.now() / 1000) + 30 * 60; const result = await client.chat.scheduleMessage({ channel: command.channel_id, text: command.text, post_at: postAt, }); await client.chat.postMessage({ channel: command.channel_id, text: `Reminder set! ID: ${result.scheduled_message_id}`, }); }); // Cancel a scheduled message app.command("/cancel-remind", async ({ ack, command, client }) => { await ack(); await client.chat.deleteScheduledMessage({ channel: command.channel_id, scheduled_message_id: command.text.trim(), }); await client.chat.postMessage({ channel: command.channel_id, text: "Reminder cancelled.", }); }); ``` **Teams (after) — node-cron for dev:** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import cron from "node-cron"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // In-memory store (replace with database in production) const scheduledMessages = new Map<string, { conversationId: string; text: string; sendAt: Date; cronTask?: cron.ScheduledTask; }>(); // Store conversation references at install time const conversationRefs = new Map<string, any>(); app.on("install.add", async ({ activity }) => { const convId = activity.conversation?.id ?? ""; conversationRefs.set(convId, { conversationId: convId, serviceUrl: (activity as any).serviceUrl, }); }); // Schedule a reminder app.message(/^\/?remind (.+)$/i, async ({ send, activity }) => { const text = activity.text?.replace(/^\/?remind\s+/i, "") ?? ""; const convId = activity.conversation?.id ?? ""; const id = `rem_${Date.now()}`; const sendAt = new Date(Date.now() + 30 * 60_000); // 30 min from now scheduledMessages.set(id, { conversationId: convId, text, sendAt }); // Schedule with node-cron (NOT durable — dev only) const task = cron.schedule( cronFromDate(sendAt), async () => { await app.send(convId, text); scheduledMessages.delete(id); task.stop(); }, { scheduled: true } ); scheduledMessages.get(id)!.cronTask = task; await send(`Reminder set for ${sendAt.toISOString()}. ID: ${id}`); }); // Cancel a reminder app.message(/^\/?cancel-remind (\S+)$/i, async ({ send, activity }) => { const id = activity.text?.match(/cancel-remind\s+(\S+)/i)?.[1] ?? ""; const item = scheduledMessages.get(id); if (item) { item.cronTask?.stop(); scheduledMessages.delete(id); await send("Reminder cancelled."); } else { await send("Reminder not found."); } }); function cronFromDate(date: Date): string { return `${date.getMinutes()} ${date.getHours()} ${date.getDate()} ${date.getMonth() + 1} *`; } app.start(3978); ``` ### Azure Functions timer + Cosmos DB (production) **Timer-triggered function (polls for due messages):** ```typescript // src/functions/sendScheduledMessages.ts import { app as azFunc, InvocationContext, Timer } from "@azure/functions"; import { CosmosClient } from "@azure/cosmos"; const cosmos = new CosmosClient(process.env.COSMOS_CONNECTION!); const container = cosmos.database("botdb").container("scheduled-messages"); // Runs every minute — checks for due scheduled messages azFunc.timer("sendScheduledMessages", { schedule: "0 */1 * * * *", // every minute handler: async (timer: Timer, context: InvocationContext) => { const now = new Date().toISOString(); // Query for messages due now or overdue const { resources: dueMessages } = await container.items .query({ query: "SELECT * FROM c WHERE c.sendAt <= @now AND c.status = 'pending'", parameters: [{ name: "@now", value: now }], }) .fetchAll(); for (const msg of dueMessages) { try { // Send proactive message via Teams bot // In practice, import your Teams app instance and call app.send() await sendProactiveMessage(msg.conversationId, msg.text, msg.serviceUrl); // Mark as sent await container.item(msg.id, msg.conversationId).replace({ ...msg, status: "sent", sentAt: new Date().toISOString(), }); } catch (err) { context.error(`Failed to send scheduled message ${msg.id}:`, err); // Mark as failed for retry await container.item(msg.id, msg.conversationId).replace({ ...msg, status: "failed", error: String(err), }); } } context.log(`Processed ${dueMessages.length} scheduled messages.`); }, }); async function sendProactiveMessage(conversationId: string, text: string, serviceUrl: string) { // Use Bot Framework REST API or your Teams app instance // POST to {serviceUrl}/v3/conversations/{conversationId}/activities const response = await fetch(`${serviceUrl}/v3/conversations/${conversationId}/activities`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getBotToken()}`, }, body: JSON.stringify({ type: "message", text, }), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); } async function getBotToken(): Promise<string> { // Obtain token via client credentials flow return "..."; } ``` **Scheduling endpoint (called from the bot handler):** ```typescript // In your bot handler — schedule a message app.message(/^\/?remind (.+)$/i, async ({ send, activity }) => { const text = activity.text?.replace(/^\/?remind\s+/i, "") ?? ""; const convId = activity.conversation?.id ?? ""; const sendAt = new Date(Date.now() + 30 * 60_000); // Persist to Cosmos DB — timer function will pick it up await container.items.create({ id: `rem_${Date.now()}`, conversationId: convId, serviceUrl: (activity as any).serviceUrl, text, sendAt: sendAt.toISOString(), status: "pending", createdBy: activity.from?.aadObjectId, }); await send(`Reminder set for ${sendAt.toISOString()}.`); }); ``` ### Azure Service Bus scheduled messages (R7 — production, exact-time) The most production-ready approach for exact-time delivery with native cancellation support. ```typescript import { ServiceBusClient } from "@azure/service-bus"; const sbClient = new ServiceBusClient(process.env.SERVICEBUS_CONNECTION!); const sender = sbClient.createSender("scheduled-messages"); // Schedule a message for exact-time delivery async function scheduleMessage( conversationId: string, text: string, sendAt: Date ): Promise<Long> { const [sequenceNumber] = await sender.scheduleMessages( { body: { conversationId, text } }, sendAt ); return sequenceNumber; // store this for cancellation } // Cancel a scheduled message async function cancelScheduled(sequenceNumber: Long): Promise<void> { await sender.cancelScheduledMessages(sequenceNumber); } // Receiver (runs as a separate process or Azure Function) const receiver = sbClient.createReceiver("scheduled-messages"); receiver.subscribe({ processMessage: async (msg) => { const { conversationId, text } = msg.body; await app.send(conversationId, text); }, processError: async (err) => console.error(err), }); ``` **Bot handler integration:** ```typescript app.message(/^\/?schedule (.+) at (.+)$/i, async ({ send, activity }) => { const match = activity.text?.match(/schedule (.+) at (.+)/i); const text = match?.[1] ?? ""; const sendAt = new Date(match?.[2] ?? ""); const convId = activity.conversation?.id ?? ""; const seqNum = await scheduleMessage(convId, text, sendAt); // Store seqNum in database for cancellation await send(`Scheduled for ${sendAt.toISOString()}. Cancel ID: ${seqNum}`); }); ``` **When to use Service Bus vs other approaches:** - **Service Bus:** High-volume, exact-time delivery, native cancellation. Best overall. - **Queue Storage:** Simple delays under 7 days. Cheaper. No native cancellation. - **Cosmos DB + Timer:** Unlimited delay. Minute-level precision. Most flexible. **Reverse (Teams → Slack):** Use `chat.scheduleMessage({ channel, text, post_at })` natively. ### Scheduling approach comparison | Approach | Durability | Precision | Max Delay | Cancellation | Best For | |---|---|---|---|---|---| | `setTimeout` / `node-cron` | None (lost on restart) | ~1 sec | Unlimited | In-memory | Dev only | | Azure Functions timer | Durable | ~1 min (poll interval) | Unlimited | Delete DB row | General production | | Queue Storage visibility timeout | Durable | ~seconds | 7 days | Delete queue message | Short delays, simple | | Service Bus scheduled messages | Durable | ~seconds | Unlimited | `cancelScheduledMessage()` | High-volume, exact-time | | Power Automate Recurrence | Durable | ~1 min | Unlimited | Disable flow | No-code recurring | ## pitfalls - **In-process timers are not durable**: `setTimeout` and `node-cron` lose all scheduled items on process restart, deployment, or scaling event. Never use for production. This is the #1 migration failure — developers assume their timer survives restarts like Slack's `scheduleMessage`. - **Missing conversation reference at send time**: Proactive messaging requires a valid `serviceUrl` and `conversationId` stored at install time. If the bot hasn't stored these, it cannot send scheduled messages. Always persist conversation references in the `install.add` handler. - **Rate limiting on bulk sends**: Sending 500 scheduled messages at 9:00 AM will hit the ~1 msg/sec/conversation limit. Implement a staggered send queue with delays between messages. Service Bus or Queue Storage with staggered visibility timeouts helps distribute load. - **Timer function CRON precision**: Azure Functions timer triggers run at CRON intervals (e.g., every minute), not at exact timestamps. A message scheduled for 9:00:30 may not send until 9:01:00. For higher precision, use Queue Storage visibility timeout or Service Bus scheduled messages. - **Queue Storage 7-day visibility timeout limit**: Messages with visibility timeout > 7 days silently default to 7 days. For long-horizon scheduling (weeks/months), use a database + timer function approach instead. - **Power Automate requires premium license for custom connectors**: Sending via the bot's API requires a custom connector or HTTP action in Power Automate, which may need a premium license depending on the organization's plan. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages - https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer - https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-trigger - https://learn.microsoft.com/en-us/azure/service-bus-messaging/message-sequencing#scheduled-messages - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/rate-limit - https://learn.microsoft.com/en-us/power-automate/triggers-introduction - https://github.com/microsoft/teams.ts - https://api.slack.com/methods/chat.scheduleMessage — Slack scheduled messages - https://api.slack.com/methods/reminders.add — Slack reminders ## instructions Use this expert when adding cross-platform support in either direction for scheduled messages, reminders, and deferred delivery. It covers: Slack `chat.scheduleMessage` bridged to Teams timer + proactive send, `reminders.add` bridged to persistent storage patterns, in-process timers (dev), Azure Functions timer triggers (production), Queue Storage visibility timeout, Service Bus scheduled messages, Power Automate Recurrence, rate limiting for bulk sends, conversation reference storage requirements, and reverse mapping from Teams deferred patterns back to Slack native scheduling APIs. Pair with `../teams/runtime.proactive-messaging-ts.md` for proactive messaging infrastructure, `../teams/state.storage-patterns-ts.md` for persisting scheduled items, and `slack-interactive-responses-to-teams-ts.md` for deferred response patterns. ## research Deep Research prompt: "Write a micro expert for bridging Slack scheduled messages (chat.scheduleMessage, chat.deleteScheduledMessage) and reminders (reminders.add) with Microsoft Teams deferred delivery patterns in either direction. Cover: proactive messaging with stored conversation references, in-process timers (node-cron/setTimeout) for dev, Azure Functions timer trigger for production, Queue Storage visibility timeout, Service Bus scheduled messages, Power Automate Recurrence, rate limiting for bulk sends, cancellation patterns, and reverse mapping from Teams deferred infrastructure back to Slack native scheduling APIs. Include TypeScript code examples and a comparison table." -
shortcuts-extensions-ts.md 17.5 KB
# shortcuts-extensions-ts ## purpose Bridges Slack shortcuts (global and message) and Teams message extensions / compose extensions for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack global shortcuts → Teams action-based compose extensions with `context: ['compose', 'commandBox']`.** Slack global shortcuts appear in the lightning bolt menu and don't reference a specific message. In Teams, the equivalent is a compose extension with `fetchTask: true` and action context targeting the compose box and command bar. [learn.microsoft.com -- Action-based extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command) 2. **Slack message shortcuts → Teams action-based extensions with `context: ['message']`.** Slack message shortcuts appear in the message context menu (⋮ → More actions). In Teams, action-based extensions with `context: ['message']` appear in the message overflow menu (... → More actions). The target message content is available in the invoke payload. [learn.microsoft.com -- Message context](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command#choose-action-command-invoke-locations) 3. **Slack `trigger_id` + `views.open()` → Teams `fetchTask: true` + task module.** Slack shortcuts use the `trigger_id` to open a modal. Teams action-based extensions use `fetchTask: true` in the manifest, which causes Teams to invoke the bot's `message.ext.open` handler to fetch the task module (dialog) content. No trigger_id needed. [learn.microsoft.com -- Task module from extension](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/create-task-module) 4. **Slack `shortcut.message` (target message in message shortcuts) → Teams `activity.value.messagePayload`.** When a message shortcut is invoked in Slack, the message object is in `shortcut.message`. In Teams, the message that was acted upon is in `activity.value.messagePayload` with `id`, `body.content`, `from`, `createdDateTime`, and `attachments`. [learn.microsoft.com -- Message payload](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command#payload-activity-properties-when-invoked-from-a-message) 5. **Manifest `composeExtensions[].commands[]` with `type: "action"` is required.** Unlike Slack where shortcuts are configured in the app dashboard, Teams requires each action command to be declared in the manifest JSON with its title, description, parameters, and context array. Without this, the extension never appears. [learn.microsoft.com -- Manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#composeextensionscommands) 6. **Slack `app.shortcut('callback_id')` → Teams handler routing via `activity.value.commandId`.** Slack routes shortcuts by `callback_id`. Teams invokes the same `message.ext.open` handler for all action commands — differentiate by checking `activity.value.commandId` against the command `id` in the manifest. [learn.microsoft.com -- Handle action](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/respond-to-task-module-submit) 7. **Task module response replaces Slack modal view return.** Slack's `views.open()` returns a view object with blocks. Teams' `message.ext.open` handler returns a task module response containing either an Adaptive Card or an iframe URL. The Adaptive Card path is closest to Slack's modal behavior. [learn.microsoft.com -- Task modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/what-are-task-modules) 8. **Slack `view_submission` → Teams `message.ext.submit`.** When the user submits the task module form, Teams invokes the `message.ext.submit` handler (or `composeExtension/submitAction` activity). The form data is in `activity.value.data`. The handler can return a card to insert into the compose box, send a message, or show another task module. [learn.microsoft.com -- Handle submit](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/respond-to-task-module-submit) 9. **No "fire and forget" shortcuts in Teams.** Slack global shortcuts can trigger background actions without showing a modal (just `ack()` + do work). Teams action-based extensions always show a task module if `fetchTask: true`. To mimic fire-and-forget, return a minimal confirmation card from the task module and process in the background. [learn.microsoft.com -- Action commands](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command) 10. **Teams action extensions can insert cards into the compose box.** Slack shortcuts post messages via `say()` or `respond()`. Teams action extensions can return a card that gets inserted into the user's compose box for them to review and send. This is a UX improvement — the user controls when the message is posted. Return `{ composeExtension: { type: 'result', attachments: [...] } }` from the submit handler. [learn.microsoft.com -- Respond to submit](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/respond-to-task-module-submit#respond-with-an-adaptive-card-message-sent-from-a-bot) 11. **Reverse direction (Teams → Slack):** For Teams → Slack, map compose extensions to `app.shortcut` with `global_shortcut` or `message_shortcut` type. Action-based extensions with `context: ['compose', 'commandBox']` map to Slack global shortcuts; extensions with `context: ['message']` map to Slack message shortcuts. Task module forms become Slack modals opened via `views.open()` with a `trigger_id`. The `message.ext.submit` handler maps to a Slack `view_submission` handler. ## patterns ### Message shortcut → action-based message extension **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Message shortcut — appears in message context menu app.shortcut("create_ticket_from_message", async ({ ack, shortcut, client }) => { await ack(); const message = (shortcut as any).message; await client.views.open({ trigger_id: shortcut.trigger_id, view: { type: "modal", callback_id: "ticket_from_message", title: { type: "plain_text", text: "Create Ticket" }, submit: { type: "plain_text", text: "Create" }, blocks: [ { type: "input", block_id: "title_block", label: { type: "plain_text", text: "Ticket Title" }, element: { type: "plain_text_input", action_id: "title_input", initial_value: message.text?.substring(0, 100) ?? "", }, }, { type: "input", block_id: "priority_block", label: { type: "plain_text", text: "Priority" }, element: { type: "static_select", action_id: "priority_select", options: [ { text: { type: "plain_text", text: "High" }, value: "high" }, { text: { type: "plain_text", text: "Medium" }, value: "medium" }, { text: { type: "plain_text", text: "Low" }, value: "low" }, ], }, }, ], private_metadata: JSON.stringify({ channel: message.channel, messageTs: message.ts, }), }, }); }); app.view("ticket_from_message", async ({ ack, view, client }) => { await ack(); const title = view.state.values.title_block.title_input.value!; const priority = view.state.values.priority_block.priority_select.selected_option?.value; const meta = JSON.parse(view.private_metadata); await client.chat.postMessage({ channel: meta.channel, text: `Ticket created: *${title}* [${priority}]`, thread_ts: meta.messageTs, }); }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // message.ext.open — returns task module (replaces views.open with trigger_id) app.on("message.ext.open" as any, async ({ activity }) => { const commandId = activity.value?.commandId; if (commandId === "createTicketFromMessage") { // Target message content (replaces shortcut.message) const messagePayload = activity.value?.messagePayload; const messageText = messagePayload?.body?.content ?? ""; return { status: 200, body: { task: { type: "continue", value: { title: "Create Ticket", card: { contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [ { type: "Input.Text", id: "ticketTitle", label: "Ticket Title", value: messageText.substring(0, 100), isRequired: true, }, { type: "Input.ChoiceSet", id: "priority", label: "Priority", value: "medium", choices: [ { title: "High", value: "high" }, { title: "Medium", value: "medium" }, { title: "Low", value: "low" }, ], }, ], actions: [{ type: "Action.Submit", title: "Create", }], }, }, }, }, }, }; } }); // message.ext.submit — handle form submission (replaces app.view handler) app.on("message.ext.submit" as any, async ({ activity, send }) => { const data = activity.value?.data; if (data) { const title = data.ticketTitle; const priority = data.priority; // Send confirmation to the conversation await send(`Ticket created: **${title}** [${priority}]`); } return { status: 200, body: {} }; }); app.start(3978); ``` **Manifest for the message action extension:** ```json { "composeExtensions": [ { "botId": "${{BOT_ID}}", "commands": [ { "id": "createTicketFromMessage", "type": "action", "title": "Create Ticket", "description": "Create a ticket from this message", "context": ["message"], "fetchTask": true } ] } ] } ``` ### Global shortcut → compose extension **Slack (before):** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Global shortcut — appears in the ⚡ menu app.shortcut("quick_note", async ({ ack, shortcut, client }) => { await ack(); await client.views.open({ trigger_id: shortcut.trigger_id, view: { type: "modal", callback_id: "quick_note_modal", title: { type: "plain_text", text: "Quick Note" }, submit: { type: "plain_text", text: "Save" }, blocks: [ { type: "input", block_id: "note_block", label: { type: "plain_text", text: "Note" }, element: { type: "plain_text_input", action_id: "note_input", multiline: true, }, }, ], }, }); }); app.view("quick_note_modal", async ({ ack, view }) => { const note = view.state.values.note_block.note_input.value!; await ack(); await saveNote(note); }); ``` **Teams (after):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); app.on("message.ext.open" as any, async ({ activity }) => { if (activity.value?.commandId === "quickNote") { return { status: 200, body: { task: { type: "continue", value: { title: "Quick Note", card: { contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [{ type: "Input.Text", id: "noteText", label: "Note", isMultiline: true, isRequired: true, }], actions: [{ type: "Action.Submit", title: "Save" }], }, }, }, }, }, }; } }); app.on("message.ext.submit" as any, async ({ activity }) => { const note = activity.value?.data?.noteText; if (note) { await saveNote(note); } // Return empty to close the task module return { status: 200, body: {} }; }); async function saveNote(note: string) { /* persist note */ } app.start(3978); ``` **Manifest for compose/commandBox action:** ```json { "composeExtensions": [ { "botId": "${{BOT_ID}}", "commands": [ { "id": "quickNote", "type": "action", "title": "Quick Note", "description": "Save a quick note", "context": ["compose", "commandBox"], "fetchTask": true } ] } ] } ``` ### Shortcut mapping table | Slack Pattern | Teams Equivalent | Notes | |---|---|---| | `app.shortcut('callback_id')` (global) | `message.ext.open` + `commandId` check | Compose extension action | | `app.shortcut('callback_id')` (message) | `message.ext.open` + `commandId` check | `context: ['message']` in manifest | | `shortcut.trigger_id` + `views.open()` | `fetchTask: true` → return task module | No trigger_id needed | | `shortcut.message` | `activity.value.messagePayload` | Target message content | | `callback_id` routing | `activity.value.commandId` routing | Different field name | | `view_submission` handler | `message.ext.submit` handler | Form data in `activity.value.data` | | `ack()` + background work | Return minimal card + async work | No fire-and-forget | | `say()` / `respond()` after shortcut | `send()` or return compose card | Can insert into compose box | ## pitfalls - **Missing `composeExtensions` commands in manifest**: Each shortcut must have a corresponding command entry in the manifest with `type: "action"`. Without it, the action never appears in Teams' UI. - **Forgetting `fetchTask: true`**: Without this flag, Teams won't invoke the `message.ext.open` handler. Instead, it expects parameters defined in the manifest and skips the task module entirely. - **`context` array determines placement**: Omitting the `context` array or using wrong values means the action appears in unexpected places or not at all. Use `['message']` for message shortcuts, `['compose', 'commandBox']` for global shortcuts. - **`messagePayload` HTML content**: The target message body in `activity.value.messagePayload.body.content` may be HTML-formatted (not plain text). Parse or strip HTML before using as form default values. - **No background-only shortcuts**: Slack allows shortcuts that just `ack()` and do work silently. Teams action extensions always present a task module. Wrap background actions in a minimal "Processing..." → "Done" card flow. - **Submit handler must return within 3 seconds**: Like all invoke activities, the `message.ext.submit` handler must respond quickly. Long-running operations should return immediately and process asynchronously. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command - https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/create-task-module - https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/respond-to-task-module-submit - https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#composeextensionscommands - https://github.com/microsoft/teams.ts - https://api.slack.com/interactivity/shortcuts — Slack shortcuts - https://api.slack.com/reference/interaction-payloads/shortcuts — Slack shortcut payloads ## instructions Use this expert when adding cross-platform support in either direction for shortcuts and message/compose extensions. It covers: Slack global shortcuts bridged to Teams compose extensions, Slack message shortcuts bridged to Teams action-based extensions with `context: ['message']`, `trigger_id` vs `fetchTask: true`, target message access via `messagePayload`, task module form flows bridged to Slack modals, and reverse mapping from Teams extensions to Slack shortcuts. Pair with `../teams/ui.message-extensions-ts.md` for general message extension patterns, `../teams/ui.dialogs-task-modules-ts.md` for task module details, and `ui-modals-dialogs-ts.md` for modal-to-dialog conversion. ## research Deep Research prompt: "Write a micro expert for bridging Slack shortcuts (global shortcuts and message shortcuts) and Microsoft Teams action-based message extensions in either direction. Cover: manifest composeExtensions command config with context arrays, fetchTask: true for task module invocation, trigger_id elimination, message payload access for message shortcuts, view_submission to message.ext.submit, the lack of fire-and-forget shortcuts in Teams, compose box card insertion, and reverse mapping from Teams compose/action extensions back to Slack shortcuts. Include TypeScript code examples and a mapping table." -
transport-socketmode-https-ts.md 14.3 KB
# transport-socketmode-https-ts ## purpose Bridges Slack transport (Socket Mode, HTTP Events API) and Teams Bot Framework HTTPS transport for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack has 3 transport modes; Teams has 1.** Slack supports HTTP webhooks (Events API), Socket Mode (WebSocket for firewalled environments), and RTM (legacy WebSocket). Teams uses exclusively HTTPS via the Azure Bot Framework Service channel. All three Slack transports collapse into one Teams model. [learn.microsoft.com -- Bot Framework](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-overview) 2. **Slack Socket Mode (`@slack/socket-mode`) has NO Teams equivalent.** Socket Mode exists because Slack apps behind firewalls can't receive inbound HTTP. In Teams, the bot MUST expose a public HTTPS endpoint. Use Azure App Service, ngrok (dev), or Azure Dev Tunnels for connectivity. For strict on-premises environments that truly cannot expose any endpoint, Azure Relay provides a hybrid connection where the bot connects outbound to Azure, and Azure proxies inbound Teams traffic through that connection. This adds 10–50ms latency but requires zero inbound firewall rules. [learn.microsoft.com -- Azure Relay](https://learn.microsoft.com/en-us/azure/azure-relay/relay-what-is-it) 3. **Slack's `xapp-` token for Socket Mode → not needed.** Socket Mode uses a special app-level token. Teams uses `CLIENT_ID`/`CLIENT_SECRET`/`TENANT_ID` for all communication. Remove all `SLACK_APP_TOKEN` references. 4. **The WebSocket connection lifecycle disappears.** Slack Socket Mode manages a persistent WebSocket: connect, reconnect on failure, handle `disconnect` events, manage `envelope_id` acknowledgements. In Teams, the Bot Framework sends HTTP POST requests to your endpoint — no connection management needed. 5. **Slack Socket Mode envelope acknowledgement → not needed.** In Socket Mode, each event arrives in an envelope with an `envelope_id` that must be acknowledged within 3 seconds. In Teams, the HTTP response itself IS the acknowledgement — the Bot Framework sends a POST, your server returns 200. 6. **Slack RTM API is fully deprecated — do not port.** If the source project uses RTM (`rtm.start`, `rtm.connect`), it's already legacy. Convert directly to Teams HTTPS handlers without attempting to map RTM patterns. 7. **Teams' deployment model requires a public HTTPS endpoint.** Unlike Socket Mode (outbound-only), Teams bots receive inbound HTTPS from the Bot Framework Service. This means: (a) you need a domain/IP, (b) you need TLS, (c) you need the endpoint registered in the Azure Bot resource. 8. **Slack's retry mechanism (`x-slack-retry-num` header, `x-slack-retry-reason`)** is replaced by Bot Framework delivery guarantees. Teams does not retry failed deliveries in the same way — if your endpoint is down, activities may be lost. Ensure high availability. 9. **Java SDK's `SocketModeClient` classes (`SocketModeApp`, `SocketModeClient`, `JavaxWebSocketClient`, `TyrusWebSocketClient`)** are entirely eliminated. Delete all Socket Mode client code, connection management, reconnection logic, and WebSocket libraries. 10. **Slack's event subscription URL verification challenge (`url_verification` event)** has no Teams equivalent. Teams verifies your endpoint via the Bot Framework registration in Azure Portal, not via an HTTP challenge. Remove all challenge-response code. 11. **Transport is inherently asymmetric.** Slack supports both Socket Mode (outbound WebSocket) and HTTP (inbound webhooks), while Teams requires HTTPS exclusively. For Teams → Slack, adding Socket Mode is optional but useful for firewall-restricted environments. A cross-platform bot typically uses HTTP/HTTPS for both platforms, with Socket Mode as an optional Slack-only enhancement. 12. **Add a health check endpoint for production hosting.** Azure App Service, Container Apps, and Kubernetes all use HTTP health probes to determine if the app is alive. Expose `GET /api/health` returning 200 with a JSON body. Configure the probe path in your hosting platform so failed health checks trigger automatic restarts instead of silent failures. [learn.microsoft.com -- Health checks](https://learn.microsoft.com/en-us/azure/app-service/monitor-instances-health-check) ## patterns ### Slack Socket Mode → Teams HTTPS endpoint **Slack Socket Mode (before):** ```typescript // --- Slack with Socket Mode --- import { App } from '@slack/bolt'; import { SocketModeReceiver } from '@slack/bolt'; // Socket Mode: outbound WebSocket, no public endpoint needed const receiver = new SocketModeReceiver({ appToken: process.env.SLACK_APP_TOKEN!, // xapp-... token // Manages WebSocket connection lifecycle internally: // - Connects to wss://wss-primary.slack.com // - Handles reconnection on disconnect // - Acknowledges each envelope_id within 3 seconds }); const app = new App({ token: process.env.SLACK_BOT_TOKEN!, receiver, // Uses Socket Mode instead of HTTP }); app.message(/hello/i, async ({ say }) => { await say('Hello via Socket Mode!'); }); await app.start(); console.log('Connected via WebSocket (no public URL needed)'); ``` **Teams (after):** ```typescript // --- Teams with HTTPS endpoint --- import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; // Teams: inbound HTTPS, public endpoint required const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger('my-bot', { level: 'info' }), plugins: [new DevtoolsPlugin()], // No socket mode, no WebSocket, no app-level token // Bot Framework sends HTTPS POST to your /api/messages endpoint }); app.message(/hello/i, async ({ send }) => { await send('Hello via HTTPS!'); }); app.start(3978); // Requires public HTTPS endpoint: // - Dev: ngrok http 3978 or Azure Dev Tunnels // - Prod: Azure App Service with custom domain + TLS ``` ### Java SDK Socket Mode classes → DELETE **Java (before):** ```java // --- Java Socket Mode setup (DELETE ALL OF THIS) --- import com.slack.api.bolt.App; import com.slack.api.bolt.socket_mode.SocketModeApp; // WebSocket client selection import com.slack.api.socket_mode.SocketModeClient; import javax.websocket.WebSocketContainer; App app = new App(AppConfig.builder() .singleTeamBotToken(System.getenv("SLACK_BOT_TOKEN")) .build()); app.event(MessageEvent.class, (req, ctx) -> { ctx.say("Hello!"); return ctx.ack(); }); // Socket Mode wrapper — manages WebSocket connection lifecycle SocketModeApp socketModeApp = new SocketModeApp( System.getenv("SLACK_APP_TOKEN"), // xapp-... token app // wraps the Bolt app ); socketModeApp.start(); // connects via WebSocket // Internally manages: // - WebSocket connection to Slack // - Automatic reconnection // - Envelope ID acknowledgement // - Multiple client backends (Tyrus, Java-WebSocket) ``` **Teams TypeScript (after):** ```typescript // --- Teams: everything above is replaced by this --- import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger('my-bot', { level: 'info' }), }); app.on('message', async ({ send }) => { await send('Hello!'); }); app.start(3978); // No SocketModeApp, no WebSocket client, no app-level token // Delete: SocketModeApp, SocketModeClient, javax.websocket imports, // Tyrus/Java-WebSocket dependencies, xapp token config ``` ### Transport comparison table | Aspect | Slack HTTP (Events API) | Slack Socket Mode | Teams Bot Framework | |---|---|---|---| | Direction | Inbound HTTP POST | Outbound WebSocket | Inbound HTTPS POST | | Public endpoint | Required | Not required | Required | | TLS | Required | N/A (outbound) | Required | | Authentication | Signing secret HMAC | App-level token | Bot Framework JWT (auto) | | Event delivery | HTTP POST per event | WebSocket frames | HTTPS POST per activity | | Acknowledgement | Return HTTP 200 in 3s | Send envelope_id ack | Return HTTP 200 | | Retry on failure | Yes (`x-slack-retry-*`) | Reconnect WebSocket | Limited retries | | Connection mgmt | Stateless | Client manages WS | Stateless | | Firewall-friendly | No (needs inbound) | Yes (outbound only) | No (needs inbound) | | Dev tunneling | ngrok / localtunnel | Not needed | ngrok / Dev Tunnels | ### Health check endpoint pattern ```typescript import express from 'express'; const webApp = express(); // Health check for Azure App Service / Container Apps probes webApp.get('/api/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), }); }); // The Teams app uses the same Express server (or integrate with app.start()) webApp.listen(process.env.PORT || 3978, () => { console.log(`Bot running on port ${process.env.PORT || 3978}`); }); ``` ### Production deployment stages | Stage | Hosting | Endpoint | Notes | |---|---|---|---| | **Local dev** | `localhost:3978` + Dev Tunnel | `https://<tunnel-id>.devtunnels.ms/api/messages` | Free; tunnels expire after idle timeout | | **Staging** | Azure App Service (B1) | `https://my-bot-staging.azurewebsites.net/api/messages` | Use deployment slots; Always On enabled | | **Production** | Azure App Service (S1+) or Container Apps | `https://my-bot.azurewebsites.net/api/messages` | Custom domain + managed TLS; health check configured | ### Environment variable cleanup | Slack Variable | Action | Why | |---|---|---| | `SLACK_APP_TOKEN` (`xapp-...`) | Delete | Socket Mode only | | `SLACK_BOT_TOKEN` (`xoxb-...`) | Replace with `CLIENT_ID`+`CLIENT_SECRET` | Different auth model | | `SLACK_SIGNING_SECRET` | Delete | Bot Framework JWT is auto | | `SLACK_CLIENT_ID` | Replace with `CLIENT_ID` | Azure Bot app ID | | `SLACK_CLIENT_SECRET` | Replace with `CLIENT_SECRET` | Azure Bot secret | | *(add new)* | `TENANT_ID` | Azure AD tenant | ## pitfalls - **Trying to use WebSockets with Teams**: Teams bots use HTTPS, not WebSocket. The Bot Framework Service sends activities as HTTP POST requests. Do not attempt to create a WebSocket server for Teams. - **Forgetting the public endpoint requirement**: Socket Mode works behind firewalls with no public URL. Teams bots MUST have a public HTTPS endpoint. In development, use `ngrok http 3978` or Azure Dev Tunnels. In production, use Azure App Service. - **Porting reconnection logic**: Socket Mode clients implement complex reconnection (backoff, failover). Delete all reconnection code — HTTPS is stateless, there's nothing to reconnect. - **Porting envelope acknowledgement**: Socket Mode requires acknowledging each event's `envelope_id`. Teams has no envelope concept — the HTTP 200 response IS the acknowledgement. Remove all envelope handling. - **Slack's URL verification challenge**: Slack's Events API sends a `url_verification` challenge to verify your endpoint. Teams doesn't do this — endpoint verification happens during Azure Bot registration. Delete challenge handlers. - **RTM API patterns**: If the source uses RTM (`rtm.connect`, `rtm.start`), these are completely obsolete even in Slack. Do not attempt to map RTM patterns — convert directly to Teams HTTPS handlers. - **Missing TLS in production**: Teams requires HTTPS. Azure App Service provides TLS automatically. If self-hosting, you must configure TLS certificates. - **Assuming event delivery retries**: Slack retries failed HTTP deliveries (with `x-slack-retry-num`). Bot Framework has limited retry guarantees. Design for idempotency but don't depend on retries. - **Dev tunnels expire**: Azure Dev Tunnels and ngrok free-tier URLs expire after idle timeouts or session restarts. The Bot Framework registration must be updated with the new URL each time. Use a persistent tunnel ID or switch to Azure-hosted staging for stable endpoints. - **No health check = blind restarts**: Without a health check endpoint, Azure App Service cannot distinguish between a crashed app and a slow response. The platform may restart a healthy but busy instance, or leave a crashed instance running. Always configure `/api/health` and set the health check path in the hosting platform. ## references - https://api.slack.com/apis/connections/socket -- Slack Socket Mode documentation - https://api.slack.com/apis/connections/events-api -- Slack Events API (HTTP) - https://api.slack.com/rtm -- Slack RTM API (deprecated) - https://learn.microsoft.com/en-us/azure/bot-service/bot-service-overview -- Bot Framework architecture - https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication -- Bot Framework authentication - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/debug/locally-with-an-ide -- Local dev with tunneling - https://github.com/microsoft/teams.ts -- Teams SDK v2 ## instructions Use this expert when adding cross-platform support in either direction for Slack transport (Socket Mode, HTTP Events API) or Teams Bot Framework HTTPS transport. The core message: **all three Slack transports collapse into one Teams model (inbound HTTPS)**. Transport is inherently asymmetric -- Slack supports both Socket Mode and HTTP, while Teams requires HTTPS. For Teams → Slack, adding Socket Mode is optional but useful for firewall-restricted environments. Focus on: (1) understanding transport differences between platforms, (2) envelope acknowledgement vs HTTP response patterns, (3) setting up the HTTPS endpoint with proper TLS, (4) configuring Azure Bot registration. Pair with `events-activities-ts.md` for event/activity mapping once the transport layer is resolved, and `../teams/runtime.app-init-ts.md` for Teams app initialization. ## research Deep Research prompt: "Write a micro expert for bridging Slack transport (Socket Mode WebSocket, HTTP Events API) and Teams Bot Framework HTTPS transport in either direction for cross-platform bots. Cover: why all three Slack transports collapse into one Teams model, transport asymmetry (Socket Mode is Slack-only), Socket Mode as optional enhancement for firewall-restricted environments, public HTTPS endpoint requirement, Bot Framework JWT authentication, deployment options (Azure App Service, ngrok, Dev Tunnels), environment variable cleanup, and transport comparison table." -
ui-app-home-personal-tab-ts.md 14.5 KB
# ui-app-home-personal-tab-ts ## purpose Bridges Slack App Home and Teams personal tab / bot welcome card for cross-platform bots targeting Slack, Teams, or both. ## rules 1. Slack's App Home is a dedicated per-user tab in the Slack app sidebar, rendered by the bot via `views.publish`. Teams has no direct equivalent of a bot-rendered home tab. The closest alternatives are: (a) a personal bot conversation with a welcome Adaptive Card, (b) a static personal tab (web page in iframe), or (c) a bot-powered tab using `tab.fetch`/`tab.submit` handlers. 2. Slack `app.event(AppHomeOpenedEvent)` fires when a user navigates to the bot's Home tab. In Teams, the equivalent trigger for a personal bot conversation is `app.on('install.add')` (first install) or `app.on('conversationUpdate')` with `membersAdded` (bot added to 1:1 chat). There is no "user opened the chat" event — the bot sends its home card proactively at install time. 3. Slack `views.publish(user_id, view)` publishes a view to a specific user's Home tab. In Teams, send an Adaptive Card to the user's 1:1 conversation using `send()` in the install handler or via proactive messaging. The card serves as the "home" experience. 4. Slack's Home tab Block Kit JSON maps to an Adaptive Card. Convert using the block-kit-to-adaptive-cards mapping table. The card replaces the full home view — use `Container` and `ColumnSet` for layout density. 5. Slack Home tab dynamic updates (re-calling `views.publish` with new content) map to sending a new card or updating the existing card via `updateActivity` in Teams. Store the original activity ID to update it later. 6. Slack's `view.hash` for race condition protection (only update if the hash matches) has no Teams equivalent. Teams card updates via `updateActivity` always overwrite. If concurrent updates are a concern, implement application-level versioning in the card's `Action.Submit.data`. 7. For a richer home experience equivalent to Slack's App Home, consider a **static tab** — a web page declared in the Teams manifest (`staticTabs` array) that loads in an iframe. This supports full HTML/JS and is closer to Slack's App Home flexibility, but requires hosting a web page. 8. Teams SDK v2 supports `tab.fetch` and `tab.submit` handlers for Adaptive Card-based tabs (no iframe needed). The bot returns an Adaptive Card in response to `tab.fetch`, and handles form submissions via `tab.submit`. This is the closest behavioral match to Slack's `views.publish` pattern. 9. When migrating App Home with action buttons, remember that Slack Home tab actions fire `blockAction` events. In Teams, Adaptive Card buttons in 1:1 chat fire `adaptiveCards.actionSubmit` handlers. The routing mechanism changes but the concept is the same. 10. Slack App Home can show different content per user based on `event.user`. In Teams 1:1 chat, the bot always talks to one user, so personalization is inherent. For tab-based approaches, use `tab.fetch` which receives user context in the activity. 11. **Reverse direction (Teams → Slack):** For Teams → Slack, map `tab.fetch` to `app_home_opened` event with `views.publish` for dynamic content. The Adaptive Card tab content maps to Block Kit views. `tab.submit` actions map to `view_submission` or `block_actions` events. The `install.add` welcome card maps to a `views.publish` call triggered by `app_home_opened`. ## patterns ### Option A: Welcome card on install (simplest) **Slack (before):** ```kotlin app.event(AppHomeOpenedEvent::class.java) { e, ctx -> val res = ctx.client().viewsPublish { it.userId(e.event.user) .viewAsString(homeViewJson) .hash(e.event.view?.hash) } ctx.ack() } ``` **Teams (after):** ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; const app = new App({ logger: new ConsoleLogger('home-bot'), }); // Send a "home" card when the bot is installed (replaces AppHomeOpenedEvent) app.on('install.add', async ({ send }) => { await send({ type: 'message', attachments: [{ contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: 'Welcome to the App!', size: 'Large', weight: 'Bolder', }, { type: 'TextBlock', text: `Last updated: ${new Date().toISOString()}`, isSubtle: true, wrap: true, }, ], actions: [ { type: 'Action.Submit', title: 'Action A', data: { verb: 'actionA' }, }, { type: 'Action.Submit', title: 'Action B', data: { verb: 'actionB' }, }, ], }, }], }); }); // Handle button clicks on the home card app.on('adaptiveCards.actionSubmit' as any, async ({ activity, send }) => { const verb = activity.value?.verb; if (verb === 'actionA') { await send('You clicked Action A!'); } else if (verb === 'actionB') { await send('You clicked Action B!'); } }); app.start(3978); ``` ### Option B: Adaptive Card tab (closest to App Home) ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; const app = new App({ logger: new ConsoleLogger('tab-bot'), }); // tab.fetch replaces AppHomeOpenedEvent — fires when user opens the tab app.on('tab.fetch' as any, async ({ activity }) => { const userId = activity.from?.id; return { status: 200, body: { tab: { type: 'continue', value: { cards: [{ card: { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: 'Home', size: 'Large', weight: 'Bolder', }, { type: 'TextBlock', text: `Hello, user ${userId}! Updated: ${new Date().toISOString()}`, wrap: true, }, { type: 'ActionSet', actions: [ { type: 'Action.Submit', title: 'Refresh', data: { verb: 'refresh' }, }, ], }, ], }, }, }], }, }, }, }; }); // tab.submit handles actions within the tab app.on('tab.submit' as any, async ({ activity }) => { const verb = activity.value?.data?.verb; if (verb === 'refresh') { // Return updated tab content return { status: 200, body: { tab: { type: 'continue', value: { cards: [{ card: { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [{ type: 'TextBlock', text: `Refreshed at ${new Date().toISOString()}`, }], }, }, }], }, }, }, }; } return { status: 200, body: {} }; }); app.start(3978); ``` ### Option C: Static tab with hosted web page (most flexible) **Manifest `staticTabs` entry:** ```json { "staticTabs": [ { "entityId": "homeTab", "name": "Home", "contentUrl": "https://your-app.azurewebsites.net/tab/home", "scopes": ["personal"] } ], "validDomains": [ "your-app.azurewebsites.net" ] } ``` **Express route serving the tab page:** ```typescript import express from 'express'; import path from 'path'; const webApp = express(); // Serve static assets webApp.use('/tab/assets', express.static(path.join(__dirname, 'public'))); // Tab page route — returns HTML that initializes the Teams JS SDK webApp.get('/tab/home', (req, res) => { res.send(`<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Home</title> <script src="https://res.cdn.office.net/teams-js/2.24.0/js/MicrosoftTeams.min.js"></script> <style> body { font-family: Segoe UI, sans-serif; margin: 20px; } .card { background: #f5f5f5; border-radius: 8px; padding: 16px; margin: 8px 0; } </style> </head> <body> <div id="app">Loading...</div> <script> // Teams JS SDK initialization is REQUIRED for tabs microsoftTeams.app.initialize().then(() => { return microsoftTeams.app.getContext(); }).then((context) => { const userId = context.user?.id; const userName = context.user?.displayName ?? 'User'; document.getElementById('app').innerHTML = '<h1>Welcome, ' + userName + '</h1>' + '<div class="card"><h3>Quick Actions</h3>' + '<button onclick="doAction(\\'refresh\\')">Refresh Data</button> ' + '<button onclick="doAction(\\'settings\\')">Settings</button></div>'; }); function doAction(verb) { // Use Teams JS SDK to communicate or fetch data fetch('/tab/api/action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ verb, timestamp: Date.now() }), }).then(r => r.json()).then(data => { console.log('Action result:', data); }); } </script> </body> </html>`); }); // API endpoint for tab actions webApp.post('/tab/api/action', express.json(), (req, res) => { const { verb } = req.body; res.json({ status: 'ok', verb, processedAt: new Date().toISOString() }); }); webApp.listen(3000, () => console.log('Tab server on :3000')); ``` ### Bridging decision table | Slack App Home Feature | Option A: 1:1 Welcome Card | Option B: Adaptive Card Tab | Option C: Static Tab (iframe) | |---|---|---|---| | Trigger on open | `install.add` (once) | `tab.fetch` (every open) | Page load | | Dynamic content | Proactive message update | Return new card on each fetch | Full web app | | User actions | `actionSubmit` handlers | `tab.submit` handlers | Web forms/JS | | Complexity | Low | Medium | High | | Manifest changes | None | `staticTabs` with `contentBotId` | `staticTabs` with `contentUrl` | | Best for | Simple welcome/info | Dashboard-like home tabs | Rich interactive UIs | ## pitfalls - **No "opened" event in 1:1 chat**: Slack fires `AppHomeOpenedEvent` every time the user navigates to the Home tab. Teams has no equivalent for 1:1 bot chat. The bot is notified when installed, not when the user opens the chat. Use `tab.fetch` (Option B) if you need an on-open trigger. - **views.publish is proactive**: Slack's `views.publish` can be called anytime to update the Home tab for any user. In Teams, updating a 1:1 message requires a stored conversation reference and the original activity ID. Set up proactive messaging infrastructure if you need background updates. - **Race condition protection gone**: Slack's `view.hash` prevents concurrent updates from clobbering each other. Teams has no equivalent. If multiple processes might update the same card, implement optimistic locking in your application layer. - **Block Kit → Adaptive Card**: The home view's Block Kit JSON must be converted to an Adaptive Card. The Home tab often uses `actions` blocks with buttons — these become `Action.Submit` buttons in the Adaptive Card. See `ui-block-kit-adaptive-cards-ts.md` for the full mapping. - **Manifest required for tabs**: Options B and C require a `staticTabs` entry in the Teams manifest. Option A (1:1 chat) does not require manifest changes beyond the base bot registration. - **Tab card size limits**: Adaptive Card tabs are subject to the same 28 KB card size limit. If the Slack Home tab rendered long lists, paginate or load data on demand. - **Static tab requires a hosted web page**: Option C (static tab) requires deploying and hosting a web page accessible via HTTPS. This is a separate hosting concern from the bot itself. Use the same Azure App Service or add a route to your existing Express server. - **`validDomains` must include the tab host**: If the `contentUrl` domain is not listed in the manifest's `validDomains` array, Teams will refuse to load the tab with a blank iframe. This is the most common static tab deployment failure. - **Teams JS SDK initialization is mandatory**: Every tab page must call `microsoftTeams.app.initialize()` before accessing any Teams context. Without it, the tab loads but `getContext()` returns nothing and deep links fail. The SDK script must be loaded from the official CDN or npm package. ## references - https://api.slack.com/surfaces/app-home — Slack App Home documentation - https://api.slack.com/events/app_home_opened — AppHomeOpenedEvent reference - https://api.slack.com/methods/views.publish — views.publish API - https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/what-are-tabs — Teams tabs overview - https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/create-personal-tab — Personal tabs - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages — Proactive messaging - https://github.com/microsoft/teams.ts — Teams SDK v2 ## instructions Use this expert when adding cross-platform support in either direction for Slack App Home or Teams personal tab / bot welcome card. It covers three bridging paths: (A) a welcome Adaptive Card in 1:1 bot chat (simplest), (B) an Adaptive Card-based tab using `tab.fetch`/`tab.submit` (closest to App Home behavior), and (C) a static web tab in an iframe (most flexible). For Teams → Slack, map `tab.fetch` to `app_home_opened` event with `views.publish` for dynamic content. The decision table helps choose the right approach based on requirements. Pair with `ui-block-kit-adaptive-cards-ts.md` for converting between Block Kit and Adaptive Cards, `../teams/ui.adaptive-cards-ts.md` for card construction, and `../teams/runtime.proactive-messaging-ts.md` for background card updates. ## research Deep Research prompt: "Write a micro expert on bridging Slack App Home (AppHomeOpenedEvent, views.publish, dynamic home tab with Block Kit) and Microsoft Teams personal tab / bot welcome card in either direction. Cover three approaches: 1:1 bot welcome card, Adaptive Card-based tabs (tab.fetch/tab.submit), and static tabs (iframe). Include reverse-direction notes for Teams → Slack mapping, a decision matrix, side-by-side code examples, and pitfalls around proactive messaging, race conditions, and manifest configuration." -
ui-block-kit-adaptive-cards-ts.md 21.5 KB
# ui-block-kit-adaptive-cards-ts ## purpose Bridges Slack Block Kit and Teams Adaptive Card UI structures for cross-platform bots targeting Slack, Teams, or both. ## rules 1. Target Adaptive Cards schema version `1.5` for Teams desktop/mobile compatibility (learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format). 2. Every Slack `action_id` must become a key inside the Adaptive Card `Action.Submit.data` object so the bot can route by `data.action` (adaptivecards.io/explorer/Action.Submit.html). 3. Slack `block_id` has no direct equivalent -- encode it in `Action.Submit.data.blockId` if you need round-trip tracing. 4. Slack mrkdwn uses `*bold*` and `_italic_`; Adaptive Cards use standard Markdown (`**bold**`, `_italic_`) inside `TextBlock.text` with `"style": "default"` (adaptivecards.io/explorer/TextBlock.html). 5. Slack `image_url` fields map to `Image.url`; always set `Image.altText` (required for accessibility in Teams). 6. Slack modals (`views.open` / `views.push`) map to Teams task modules invoked via `task/fetch` and rendered with an Adaptive Card; submission maps to `task/submit` (learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots). 7. Slack `view_submission` payload fields map: `view.state.values[block_id][action_id].value` becomes the flat `data` object returned by `task/submit`, keyed by each input's `id`. 8. Slack `static_select` maps to `Input.ChoiceSet` with `"style": "compact"`. Slack `multi_static_select` maps to `Input.ChoiceSet` with `"isMultiSelect": true` (adaptivecards.io/explorer/Input.ChoiceSet.html). 9. Slack `overflow` menu has no Adaptive Card equivalent -- redesign as `ActionSet` with multiple `Action.Submit` buttons or a single `Input.ChoiceSet` dropdown. 10. Teams Adaptive Cards support `ColumnSet`/`Column` and `FactSet` which have no Block Kit equivalent -- use them to improve layout density during migration. ## strategy The core principle: **map the blocks mechanically, then redesign the layout and interaction model to be native Teams rather than ported Slack.** A 1:1 block-to-element swap produces a functional card that looks like Slack awkwardly wearing a Teams suit. Follow these four phases in order. ### Phase 1: Map for correctness Get every block producing the correct output using the mapping table below. This is mechanical work: - `header` → `TextBlock` Large/Bolder - `section` fields → `FactSet` - `button` → `Action.Submit` with `data.verb` - Convert `*bold*` mrkdwn to `**bold**` standard Markdown - Replace `:emoji_shortcodes:` with Unicode equivalents (Teams does not render Slack shortcodes) - Replace `<@U12345>` mentions with display names (Slack mention syntax does not work in Adaptive Cards) - Swap button styles: `"primary"` → `"positive"`, `"danger"` → `"destructive"` - Add explicit submit buttons wherever Slack had instant-fire selects ### Phase 2: Upgrade the layout Once correct, leverage Adaptive Card strengths that have no Block Kit equivalent: - Replace flat block lists with `ColumnSet`/`Container` for denser, structured layouts - Use semantic container styles (`"attention"` = red, `"good"` = green, `"warning"` = yellow) instead of faking status with emoji - Add client-side validation (`isRequired`, `errorMessage`, `regex`, `min`/`max`) instead of relying entirely on server-side checks - Use `Input.ChoiceSet` with `"style": "filtered"` for typeahead search to replace `external_select` server-side handlers - Use `FactSet` for clean key/value pairs instead of manual mrkdwn formatting in `section.fields` ### Phase 3: Rethink the interaction model This is the biggest behavioral shift. Slack's model is **event-per-interaction** -- every select and button fires immediately. Teams' model is **form-then-submit**. - Group related inputs together and submit them as a batch with a single `Action.Submit` - Accept fewer round trips -- the UX feels different, so lean into it rather than fighting it - Use `Action.Execute` with the card `refresh` property if you genuinely need per-interaction updates or per-user card views - Slack ephemeral messages for per-user content → Universal Actions (`Action.Execute`) for per-user card states from the same message ### Phase 4: Handle what doesn't convert Have an explicit plan for each gap: - `overflow` menu → redesign as `Input.ChoiceSet` dropdown or an `ActionSet` with multiple buttons - Stacked modals (`views.push`) → flatten into multi-step cards or sequential task modules (Teams task modules do not stack) - `dispatch_action` live updates → accept the batch-submit model, or use `Action.Execute` refresh for critical cases - `private_metadata` → embed hidden state in `Action.Submit.data` fields or use bot conversation state - `view_submission` with field-level errors keeping the modal open → no equivalent in Teams; validate client-side with `isRequired`/`regex`, or close the task module and send an error message - Action count overflow (Slack allows 25 per block, Teams allows 6 per `ActionSet`) → paginate into multiple cards or consolidate into dropdowns ## patterns ### mapping-table | Slack Block Kit | Adaptive Card Element | Notes | |---------------------------|-------------------------------|----------------------------------------------------| | `section` (text) | `TextBlock` | Set `wrap: true`; convert mrkdwn to standard MD | | `section` (text+accessory)| `ColumnSet` with 2 `Column`s | Col 1 = TextBlock, Col 2 = accessory element | | `section` (fields) | `FactSet` | Each field becomes a `Fact { title, value }` | | `actions` | `ActionSet` | Contains `Action.Submit` / `Action.OpenUrl` | | `divider` | `TextBlock` with `separator` | `{ "type": "TextBlock", "text": " ", "separator": true }` | | `header` | `TextBlock` size `Large` | `{ "type": "TextBlock", "size": "Large", "weight": "Bolder" }` | | `image` | `Image` | Set `url`, `altText`, optional `size` | | `context` | `TextBlock` size `Small` | `{ "type": "TextBlock", "size": "Small", "isSubtle": true }` | | `input` (plain_text) | `Input.Text` | `id` = action_id, `label` maps to `Input.Text.label` | | `input` (static_select) | `Input.ChoiceSet` | `style: "compact"` for dropdown | | `input` (multi_select) | `Input.ChoiceSet` multiSelect | `"isMultiSelect": true` | | `input` (datepicker) | `Input.Date` | Format: `YYYY-MM-DD` | | `input` (timepicker) | `Input.Time` | Format: `HH:mm` | | `input` (checkboxes) | `Input.ChoiceSet` expanded | `"style": "expanded", "isMultiSelect": true` | | `input` (radio_buttons) | `Input.ChoiceSet` expanded | `"style": "expanded", "isMultiSelect": false` | | `rich_text` | `TextBlock` + `RichTextBlock` | RichTextBlock available in schema 1.5+ | ### actions-mapping | Slack Element | Adaptive Card Action | Key Differences | |----------------------|----------------------------|----------------------------------------------------| | `button` | `Action.Submit` | `value` moves into `data`; `style: "danger"` maps to `style: "destructive"` | | `button` (url) | `Action.OpenUrl` | `url` field is identical | | `overflow` | *No equivalent* | Redesign as `ActionSet` or `Input.ChoiceSet` | | `static_select` | `Input.ChoiceSet` + Submit | Slack fires on select; Teams needs explicit submit | | `external_select` | `Input.ChoiceSet` + `Action.Submit` with `data.query` | Implement typeahead via `Input.ChoiceSet` with `"style": "filtered"` (schema 1.5) | | `multi_static_select`| `Input.ChoiceSet` multi | Teams returns comma-separated string of values | ### reverse-direction (Teams → Slack) For Teams → Slack, reverse the mapping table. Adaptive Card elements map back to Block Kit blocks: - `TextBlock` Large/Bolder → `header` - `FactSet` → `section` with `fields` - `Action.Submit` with `data.verb` → `button` with `value` - Convert `**bold**` standard Markdown to `*bold*` mrkdwn - Replace Unicode emoji with `:emoji_shortcodes:` where Slack supports them - Swap button styles: `"positive"` → `"primary"`, `"destructive"` → `"danger"` - `ColumnSet`/`Container` layouts → flatten to linear `section` blocks (Block Kit has no grid) - `Input.ChoiceSet` with `style: "filtered"` → `external_select` with server-side options handler - `Input.ChoiceSet` + `Action.Submit` → `static_select` in `actions` block (fires immediately on select) - `Action.Execute` per-user refresh → ephemeral messages for per-user content - Client-side validation (`isRequired`, `regex`) → server-side validation in `view_submission` handler - Semantic container styles (`"attention"`, `"good"`) → emoji-based status indicators or colored attachment sidebars Key behavioral shift (Teams → Slack): The Adaptive Card **form-then-submit** model must be decomposed into Slack's **event-per-interaction** model. Each input that previously submitted as part of a batch may need its own `block_actions` handler if the Slack UX expects instant-fire behavior. ### worked-example-1: button workflow Slack Block Kit message with approve/reject buttons converted to Adaptive Card. ```typescript // --- Slack Block Kit (original) --- import type { KnownBlock } from "@slack/types"; const slackBlocks: KnownBlock[] = [ { type: "section", block_id: "request_info", text: { type: "mrkdwn", text: "*Expense Report #1042*\nAmount: $350.00" }, }, { type: "actions", block_id: "approval_actions", elements: [ { type: "button", action_id: "approve_expense", text: { type: "plain_text", text: "Approve" }, style: "primary", value: "1042", }, { type: "button", action_id: "reject_expense", text: { type: "plain_text", text: "Reject" }, style: "danger", value: "1042", }, ], }, ]; // --- Adaptive Card (converted) --- interface AdaptiveCard { type: "AdaptiveCard"; $schema: string; version: string; body: Record<string, unknown>[]; actions?: Record<string, unknown>[]; } const adaptiveCard: AdaptiveCard = { type: "AdaptiveCard", $schema: "http://adaptivecards.io/schemas/adaptive-card.json", version: "1.5", body: [ { type: "TextBlock", text: "**Expense Report #1042**\nAmount: $350.00", wrap: true, }, ], actions: [ { type: "Action.Submit", title: "Approve", style: "positive", data: { action: "approve_expense", blockId: "approval_actions", value: "1042", }, }, { type: "Action.Submit", title: "Reject", style: "destructive", data: { action: "reject_expense", blockId: "approval_actions", value: "1042", }, }, ], }; ``` Handler comparison: ```typescript // --- Slack handler (Bolt) --- // app.action("approve_expense", async ({ action, ack, respond }) => { // await ack(); // const expenseId = action.value; // "1042" // await respond({ text: `Expense ${expenseId} approved.` }); // }); // --- Teams handler (Teams AI SDK) --- import { App, TurnState } from "@microsoft/teams-ai"; import { CardFactory } from "botbuilder"; export function registerExpenseHandlers(app: App<TurnState>): void { app.adaptiveCards.actionSubmit("approve_expense", async (ctx, _state, data) => { const expenseId = (data as Record<string, string>).value; // "1042" const reply = CardFactory.adaptiveCard({ type: "AdaptiveCard", version: "1.5", body: [{ type: "TextBlock", text: `Expense ${expenseId} approved.` }], }); await ctx.updateActivity({ type: "message", id: ctx.activity.replyToId, attachments: [reply], }); return undefined; }); } ``` ### worked-example-2: modal form Slack modal with text input and select converted to Teams task module with Adaptive Card form. ```typescript // --- Slack modal (original, opened via views.open) --- import type { View } from "@slack/types"; const slackModal: View = { type: "modal", callback_id: "create_ticket", title: { type: "plain_text", text: "Create Ticket" }, submit: { type: "plain_text", text: "Submit" }, blocks: [ { type: "input", block_id: "title_block", label: { type: "plain_text", text: "Title" }, element: { type: "plain_text_input", action_id: "ticket_title", placeholder: { type: "plain_text", text: "Enter title..." }, }, }, { type: "input", block_id: "priority_block", label: { type: "plain_text", text: "Priority" }, element: { type: "static_select", action_id: "ticket_priority", options: [ { text: { type: "plain_text", text: "High" }, value: "high" }, { text: { type: "plain_text", text: "Medium" }, value: "medium" }, { text: { type: "plain_text", text: "Low" }, value: "low" }, ], }, }, ], }; // --- Adaptive Card for Teams task module (converted) --- const taskModuleCard = { type: "AdaptiveCard" as const, $schema: "http://adaptivecards.io/schemas/adaptive-card.json", version: "1.5", body: [ { type: "TextBlock", text: "Create Ticket", size: "Large", weight: "Bolder", }, { type: "Input.Text", id: "ticket_title", label: "Title", placeholder: "Enter title...", isRequired: true, }, { type: "Input.ChoiceSet", id: "ticket_priority", label: "Priority", style: "compact", isRequired: true, choices: [ { title: "High", value: "high" }, { title: "Medium", value: "medium" }, { title: "Low", value: "low" }, ], }, ], actions: [ { type: "Action.Submit", title: "Submit", data: { action: "create_ticket" }, }, ], }; ``` Task module invocation and submission handler: ```typescript import { TeamsActivityHandler, TurnContext, TaskModuleResponse, CardFactory, } from "botbuilder"; class TicketBot extends TeamsActivityHandler { // Replaces Slack's views.open -- triggered by messaging extension or Action.Submit async handleTeamsTaskModuleFetch( context: TurnContext ): Promise<TaskModuleResponse> { return { task: { type: "continue", value: { title: "Create Ticket", width: "medium", height: "medium", card: CardFactory.adaptiveCard(taskModuleCard), }, }, }; } // Replaces Slack's view_submission handler async handleTeamsTaskModuleSubmit( context: TurnContext ): Promise<TaskModuleResponse | void> { const formData = context.activity.value?.data as { action: string; ticket_title: string; ticket_priority: string; }; // Slack: view.state.values.title_block.ticket_title.value const title = formData.ticket_title; // Slack: view.state.values.priority_block.ticket_priority.selected_option.value const priority = formData.ticket_priority; await context.sendActivity(`Ticket created: "${title}" [${priority}]`); // Return void to close the task module (like no response_action in Slack) return undefined; } } ``` ### Confirmation dialog pattern (Y14) Use `Action.ShowCard` for inline confirmation — the Teams equivalent of Slack's native `confirm` object on buttons. ```typescript // Slack: button with confirm dialog const slackButton = { type: "button", text: { type: "plain_text", text: "Delete" }, style: "danger", action_id: "delete_item", value: "42", confirm: { title: { type: "plain_text", text: "Are you sure?" }, text: { type: "mrkdwn", text: "This action cannot be undone." }, confirm: { type: "plain_text", text: "Yes, delete" }, deny: { type: "plain_text", text: "Cancel" }, }, }; // Teams: Action.ShowCard inline confirmation const teamsConfirmAction = { type: "Action.ShowCard", title: "Delete", card: { type: "AdaptiveCard", body: [ { type: "TextBlock", text: "Are you sure? This action cannot be undone.", weight: "Bolder", color: "Attention", }, ], actions: [ { type: "Action.Submit", title: "Yes, delete", style: "destructive", data: { action: "confirm_delete", itemId: "42" }, }, { type: "Action.Submit", title: "Cancel", data: { action: "cancel_delete" }, }, ], }, }; ``` **Why `Action.ShowCard`:** Expands inline without leaving the current context — closest to Slack's native `confirm` popup. No task module overhead. **Don't:** Open a full task module dialog for a simple yes/no confirmation. It's too heavy for the interaction. **Reverse (Teams → Slack):** Add a `confirm` object directly to the button element. Platform-rendered popup with zero effort. ## pitfalls - **mrkdwn vs Markdown**: Slack uses `*bold*` and `~strike~`; Adaptive Cards expect `**bold**` and `~~strike~~`. Failing to convert produces literal asterisks in Teams. - **Instant-fire selects**: Slack `static_select` inside an `actions` block fires `block_actions` immediately on selection. Adaptive Card `Input.ChoiceSet` does nothing until an `Action.Submit` is clicked -- you must add an explicit submit button. - **Button style names differ**: Slack `"primary"` = green, `"danger"` = red. Adaptive Cards use `"positive"` and `"destructive"`. Using Slack names silently falls back to default styling. - **Action count limit**: Teams Adaptive Cards support a maximum of 6 actions per `ActionSet`. Slack allows up to 25 elements in an `actions` block. Redesign dense action rows into paginated cards or dropdowns. - **`overflow` menu**: No Adaptive Card equivalent exists. Replace with an `Input.ChoiceSet` dropdown or multiple `Action.Submit` buttons. - **`multi_static_select` return format**: Slack returns `selected_options` as an array of objects. `Input.ChoiceSet` with `isMultiSelect` returns a single comma-separated string (e.g., `"a,b,c"`). Split server-side. - **No `dispatch_action` equivalent**: Slack inputs can set `dispatch_action: true` to fire events on every keystroke. Adaptive Cards only submit on explicit `Action.Submit`. - **Image sizing**: Slack `image` uses `alt_text` (underscore); Adaptive Card `Image` uses `altText` (camelCase). Slack fills width by default; set Adaptive Card `"size": "stretch"` to match. - **`private_metadata`**: Slack modals carry `private_metadata` for state. In Teams task modules, embed hidden state inside `Action.Submit.data` fields or use bot conversation state. - **Schema version**: Using features above 1.5 (e.g., `Action.Execute` for Universal Actions) requires verifying Teams client support. Stick to 1.5 for broadest compatibility. - **Card replacement**: Slack `respond({ replace_original: true })` replaces the message. In Teams, use `context.updateActivity()` with the original activity ID, or return an `adaptiveCard/action` invoke response. ## references - https://api.slack.com/reference/block-kit/blocks -- Slack Block Kit block type reference - https://api.slack.com/reference/block-kit/block-elements -- Slack interactive element reference - https://api.slack.com/surfaces/modals -- Slack modal (views.open) documentation - https://adaptivecards.io/explorer/ -- Adaptive Cards schema explorer (all element types) - https://adaptivecards.io/explorer/Action.Submit.html -- Action.Submit schema and data field - https://adaptivecards.io/explorer/Input.ChoiceSet.html -- Input.ChoiceSet (select/multi-select) - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference -- Teams Adaptive Card support - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots -- Task modules from bots - https://learn.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model -- Universal Actions ## instructions This expert covers bridging Slack Block Kit and Teams Adaptive Card UI structures in TypeScript. Use it when adding cross-platform support in either direction: (1) bridging a Slack Bolt app to also target Teams, (2) bridging a Teams bot to also target Slack, (3) converting Block Kit JSON payloads to Adaptive Card JSON or vice versa, (4) redesigning modal workflows into task modules or task modules into modals, or (5) mapping interactive action handlers between platforms. Start with the strategy section to understand the four-phase approach (map for correctness → upgrade layout → rethink interactions → handle gaps), consult the mapping table and reverse-direction section for specific element types, and adapt the worked examples to your use case. Pair with `../slack/ui.block-kit-ts.md` for Slack Block Kit patterns, and `../teams/ui.adaptive-cards-ts.md` for Teams Adaptive Card patterns and constraints. ## research Deep Research prompt: "Write a micro expert for bridging Slack Block Kit and Teams Adaptive Cards bidirectionally. Include: mapping table (Block Kit blocks <-> card elements) in both directions, interactive actions mapping (action_id <-> data.action), selects/inputs mapping, modal/task-module workflow redesign in both directions, unsupported features and redesign recommendations for each platform, and 2 worked examples (a button workflow and a modal form)." -
ui-legacy-attachments-cards-ts.md 11.2 KB
# ui-legacy-attachments-cards-ts ## purpose Bridges pre-Block Kit Slack legacy attachments and Teams Adaptive Cards for cross-platform bots targeting Slack, Teams, or both. ## rules 1. Slack legacy attachments (`message.attachments[]`) predate Block Kit and use a flat JSON structure with `text`, `fallback`, `color`, `callback_id`, and `actions[]`. These map to a single Adaptive Card with `TextBlock` body elements and `Action.Submit` actions. 2. Slack `app.attachmentAction(callback_id)` handles button clicks on legacy attachments. In Teams, this maps to `app.on('adaptiveCards.actionSubmit')` or `app.adaptiveCards.actionSubmit(verb, handler)` where `verb` is embedded in `Action.Submit.data`. 3. Slack legacy attachment `color` (hex string like `"#3AA3E3"` or named like `"good"`, `"warning"`, `"danger"`) maps to Adaptive Card `Container` with `style` property: `"good"` → `"good"`, `"warning"` → `"warning"`, `"danger"` → `"attention"`. For custom hex colors, wrap the card content in a `Container` with `"style": "emphasis"` (no arbitrary hex colors in Adaptive Cards). 4. Slack legacy attachment `fallback` (plain-text fallback for notifications) maps to the `fallback` property on the Adaptive Card's `content` object (e.g., `{ ..., "fallback": "Fallback text for notifications" }`). Always provide this for accessibility. 5. Slack legacy attachment `actions[]` with `type: "button"` map to Adaptive Card `Action.Submit` buttons. The button `name` and `value` become keys in `Action.Submit.data`. The `callback_id` becomes the `verb` routing key. 6. Slack legacy `confirm` objects (confirmation dialogs on buttons) have no direct Adaptive Card equivalent. Redesign as: (a) an `Action.ShowCard` that reveals a confirmation sub-card with Confirm/Cancel buttons, or (b) a two-step flow where the first click sends a confirmation card and the second click executes the action. 7. Slack `attachment_type: "default"` has no Adaptive Card equivalent — it was a Slack internal marker. Remove it during migration. 8. Slack legacy attachment `actions[]` with `type: "select"` (dropdown menus) map to Adaptive Card `Input.ChoiceSet` with `style: "compact"`. Remember that Adaptive Card selects require an explicit `Action.Submit` button — they do not fire on selection like Slack. 9. Slack `respond({ replace_original: true })` (replacing the original message after an attachment action) maps to Teams `updateActivity()` with the original activity ID and a new Adaptive Card attachment. 10. Messages mixing legacy attachments AND Block Kit blocks should be bridged to a single Adaptive Card. The attachment text becomes header/body `TextBlock` elements and the Block Kit portion follows the standard block-kit-to-adaptive-cards mapping. 11. **Reverse direction (Teams → Slack):** While not recommended (Block Kit is preferred), Adaptive Cards can be mapped to legacy attachment format if targeting very old Slack integrations. Map `TextBlock` to `attachments[].text`, `Container` style to `color`, and `Action.Submit` to `actions[].type: "button"`. Prefer converting to Block Kit instead of legacy attachments for new Slack integrations. ## patterns ### Legacy attachment with buttons → Adaptive Card **Slack (before):** ```kotlin // --- Slack legacy attachment JSON --- val message = """ { "text": "Would you like to play a game?", "attachments": [ { "text": "Choose a game to play", "fallback": "You are unable to choose a game", "callback_id": "wopr_game", "color": "#3AA3E3", "attachment_type": "default", "actions": [ { "name": "game", "text": "Chess", "type": "button", "value": "chess" }, { "name": "game", "text": "Falken's Maze", "type": "button", "value": "maze" }, { "name": "game", "text": "Thermonuclear War", "style": "danger", "type": "button", "value": "war", "confirm": { "title": "Are you sure?", "text": "Wouldn't you prefer a good game of chess?", "ok_text": "Yes", "dismiss_text": "No" } } ] } ] } """ app.attachmentAction("wopr_game") { req, ctx -> ctx.respond(secondMessage) ctx.ack() } ``` **Teams (after):** ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; const app = new App({ logger: new ConsoleLogger('game-bot'), }); // The game selection card (replaces legacy attachment) const gameCard = { type: 'AdaptiveCard' as const, version: '1.5', fallback: 'You are unable to choose a game', body: [ { type: 'TextBlock', text: 'Would you like to play a game?', size: 'Medium', weight: 'Bolder', }, { type: 'TextBlock', text: 'Choose a game to play', wrap: true, }, ], actions: [ { type: 'Action.Submit', title: 'Chess', data: { verb: 'wopr_game', game: 'chess' }, }, { type: 'Action.Submit', title: "Falken's Maze", data: { verb: 'wopr_game', game: 'maze' }, }, { // Dangerous action — use Action.ShowCard for confirmation type: 'Action.ShowCard', title: 'Thermonuclear War', card: { type: 'AdaptiveCard', body: [ { type: 'TextBlock', text: "Are you sure? Wouldn't you prefer a good game of chess?", wrap: true, color: 'Attention', }, ], actions: [ { type: 'Action.Submit', title: 'Yes', style: 'destructive', data: { verb: 'wopr_game', game: 'war' }, }, // "No" simply collapses the ShowCard — no action needed ], }, }, ], }; // Send the game card when the user says "play" app.on('message', async ({ activity, send }) => { if (activity.text?.match(/play/i)) { await send({ type: 'message', attachments: [{ contentType: 'application/vnd.microsoft.card.adaptive', content: gameCard, }], }); } }); // Handle game selection (replaces app.attachmentAction("wopr_game")) // TODO: Replace with app.adaptiveCards.actionSubmit if using teams-ai SDK app.on('adaptiveCards.actionSubmit' as any, async ({ activity, send }) => { const data = activity.value; if (data?.verb === 'wopr_game') { const game = data.game; await send(`You chose: ${game}. Let's play!`); // TODO: Send the follow-up card (replaces secondMessage / replace_original) } }); app.start(3978); ``` ### Mapping reference table | Slack Legacy Attachment | Adaptive Card Equivalent | Notes | |---|---|---| | `attachments[].text` | `TextBlock` in `body` | Convert mrkdwn to standard Markdown | | `attachments[].fallback` | Card-level `fallback` property | For notifications and accessibility | | `attachments[].color` (`"good"`) | `Container` with `style: "good"` | Green styling | | `attachments[].color` (`"warning"`) | `Container` with `style: "warning"` | Yellow styling | | `attachments[].color` (`"danger"`) | `Container` with `style: "attention"` | Red styling | | `attachments[].color` (`"#hex"`) | `Container` with `style: "emphasis"` | No arbitrary hex; use closest semantic style | | `attachments[].callback_id` | `Action.Submit.data.verb` | Routing key for action handlers | | `actions[].type: "button"` | `Action.Submit` | `name`/`value` → `data` keys | | `actions[].style: "danger"` | `Action.Submit` with `style: "destructive"` | | | `actions[].confirm` | `Action.ShowCard` with confirm sub-card | Or two-step confirmation flow | | `actions[].type: "select"` | `Input.ChoiceSet` + `Action.Submit` | Requires explicit submit button | | `attachment_type: "default"` | *(remove)* | No equivalent needed | | `app.attachmentAction(id)` | `app.adaptiveCards.actionSubmit(verb)` | Or `app.on('adaptiveCards.actionSubmit')` | | `respond({ replace_original })` | `updateActivity(activityId, card)` | Must store original activity ID | ## pitfalls - **No arbitrary colors**: Slack attachments support any hex color via the `color` field. Adaptive Cards only support semantic styles (`"good"`, `"warning"`, `"attention"`, `"emphasis"`, `"accent"`, `"default"`). Map to the closest semantic meaning rather than exact color matching. - **Confirmation dialogs require redesign**: Slack's `confirm` object is a built-in dialog. Adaptive Cards have no equivalent. `Action.ShowCard` is the closest — it reveals an inline sub-card. For a modal confirmation, use a task module flow instead. - **Select fires differently**: Slack legacy selects fire immediately on selection. Adaptive Card `Input.ChoiceSet` requires a separate `Action.Submit` click. This changes the UX — inform users of the change. - **Mixed attachments + blocks**: Some Slack messages combine legacy attachments with Block Kit blocks. Merge both into a single Adaptive Card. The attachment text becomes `TextBlock`s at the top, followed by the converted Block Kit elements. - **`replace_original` requires activity ID**: Slack's `respond({ replace_original: true })` works with just the `response_url`. In Teams, you need the original activity ID to call `updateActivity()`. Store the activity ID when you send the card (returned from `send()`). - **`callback_id` routing**: Slack routes attachment actions by `callback_id`. Teams routes by the `verb` (or custom key) in `Action.Submit.data`. Ensure every button includes a routing key in its `data` object. ## references - https://api.slack.com/reference/messaging/attachments — Slack legacy attachments (deprecated but supported) - https://api.slack.com/legacy/interactive-messages — Legacy interactive messages (attachment actions) - https://adaptivecards.io/explorer/Action.ShowCard.html — Action.ShowCard (inline reveal) - https://adaptivecards.io/explorer/Container.html — Container with style property - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference — Teams card reference - https://github.com/microsoft/teams.ts — Teams SDK v2 ## instructions Use this expert when adding cross-platform support in either direction for Slack legacy attachments or Teams Adaptive Cards. It covers converting attachment JSON to Adaptive Card JSON and vice versa, mapping `attachmentAction` handlers to `actionSubmit` handlers, redesigning confirmation dialogs, handling message replacement, and dealing with mixed attachment + Block Kit messages. For Teams → Slack, Adaptive Cards can be mapped to legacy attachment format if targeting very old Slack integrations, though Block Kit is preferred. Pair with `ui-block-kit-adaptive-cards-ts.md` if the message also contains Block Kit blocks, and `../teams/ui.adaptive-cards-ts.md` for Adaptive Card construction patterns. ## research Deep Research prompt: "Write a micro expert on bridging Slack legacy message attachments (pre-Block Kit) and Teams Adaptive Cards in either direction for cross-platform bots. Cover: attachment text/color/fallback/callback_id/actions mapping, button and select action conversion, confirm dialog redesign with Action.ShowCard, attachmentAction handler bridging to adaptiveCards.actionSubmit, replace_original to updateActivity, mixed attachments + Block Kit messages, color mapping limitations, and reverse-direction notes for Teams → Slack legacy attachment mapping. Include a worked example converting between formats." -
ui-modals-dialogs-ts.md 21.4 KB
# 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 `id`s. 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 `TextBlock`s, 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):** ```kotlin // 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):** ```typescript 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. ```typescript // 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. ```typescript // 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. ```typescript 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 `id`s 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 - 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." -
workflow.composable-platform-ts.md 13.7 KB
# workflow.composable-platform-ts ## purpose Architectural guide for building a composable, reusable workflow operating layer inside Teams — the five-element framework (trigger, state, logic, intelligence, visibility) as a platform pattern, not a point solution. ## rules 1. **Every workflow follows the same five-element lifecycle.** (1) Trigger — how it starts, (2) State — where records live, (3) Logic — how decisions and automation execute, (4) Intelligence — how AI is layered over state, (5) Visibility — how records remain embedded in channels. Design every workflow as an instantiation of this lifecycle. 2. **Define workflows as configuration, not code.** A workflow definition specifies: trigger type + parameters, list schema (columns and types), routing rules (approval chain, auto-assign), query functions (NL schemas), and card templates (active/completed/error). The runtime consumes these definitions generically. 3. **Use a `WorkflowDefinition` interface as the core abstraction.** This interface describes the workflow's schema, triggers, routing, and card templates. The runtime registers handlers dynamically from definitions. New workflows require a new definition object, not new handler code. 4. **Template workflows are reference implementations.** Provide polished, out-of-the-box definitions for common scenarios: time-off requests, equipment booking, daily standup, account health. These serve as both usable workflows and examples for customization. 5. **The runtime is a generic workflow engine.** A single set of handlers (message, `card.action`, proactive, webhooks) dispatch to the correct workflow based on the verb/command prefix in the message or action data. The engine creates records, processes actions, and renders cards for any registered workflow. 6. **SharePoint Lists are the default state backend.** Each workflow definition maps to a SharePoint list. The engine creates lists on first use, following the schema in the definition. For enterprise needs, swap to Dataverse without changing the workflow definition. 7. **Card templates are parameterized, not hardcoded.** Define card templates as functions that take a record and return an Adaptive Card. The workflow definition includes templates for: `activeCard`, `completedCard`, `listCard`, and `formCard`. The engine calls the right template based on record state. 8. **Query functions are auto-generated from the schema.** Given a workflow definition's column schema, generate AI function-calling schemas automatically: each filterable column becomes a parameter. This eliminates writing per-workflow query functions manually. 9. **Extensibility points for ecosystem partners.** The composable platform should expose: (a) custom trigger types (plugin new event sources), (b) custom logic steps (plugin business rules), (c) custom card templates (brand and layout), (d) custom state backends (plugin storage). Each point has a defined interface. 10. **Cross-workflow queries are first-class.** The engine registers a `queryAnyWorkflow` function that searches across all registered workflow lists. Users ask "what's overdue?" and get results from PTO, equipment, and standup workflows combined. 11. **Power Automate integration is optional, not required.** The composable platform can execute logic in-bot (state machine) or delegate to Power Automate flows. Workflow definitions specify `executionMode: "bot" | "powerAutomate" | "hybrid"`. Bot mode is the default for SMB; Power Automate mode for enterprise. ## patterns ### WorkflowDefinition interface ```typescript interface WorkflowDefinition { id: string; // Unique workflow identifier name: string; // Display name description: string; // Used in command suggestions and AI descriptions commandPrefix: string; // e.g., "/pto", "/book", "/standup" // Schema columns: ColumnDefinition[]; // Maps to SharePoint List columns statusField: string; // Which column tracks lifecycle state statusValues: { active: string[]; // e.g., ["Pending", "InProgress"] completed: string[]; // e.g., ["Approved", "Rejected", "Done"] }; // Triggers triggers: TriggerConfig[]; // Routing routing?: { type: "none" | "single" | "sequential" | "parallel-any" | "parallel-all"; approverSource: "fixed" | "manager" | "field"; // Where to find the approver approverField?: string; // Column name if approverSource is "field" escalationTimeoutMs?: number; }; // Cards cards: { active: (record: any) => object; completed: (record: any) => object; list: (records: any[]) => object; form?: () => object; // For message extension action trigger }; // AI queryDescription: string; // Describes when AI should call the query function filterableColumns: string[]; // Columns exposed as AI function parameters } interface ColumnDefinition { name: string; type: "text" | "number" | "dateTime" | "choice" | "personOrGroup" | "boolean"; choices?: string[]; // For choice columns required?: boolean; } interface TriggerConfig { type: "command" | "messageExtension" | "scheduled" | "stateChange"; config: Record<string, any>; // Trigger-specific configuration } ``` ### Register a workflow from a definition ```typescript function registerWorkflow(app: any, engine: WorkflowEngine, definition: WorkflowDefinition) { // Command trigger const commandTrigger = definition.triggers.find((t) => t.type === "command"); if (commandTrigger) { const regex = new RegExp(`^\\${definition.commandPrefix}\\s*(.*)$`, "i"); app.message(regex, async (ctx: any) => { await engine.handleCommand(ctx, definition); }); } // Scheduled trigger const scheduledTrigger = definition.triggers.find((t) => t.type === "scheduled"); if (scheduledTrigger) { cron.schedule(scheduledTrigger.config.cron, async () => { await engine.handleScheduled(definition); }); } // Register AI query function engine.registerQueryFunction(definition); } ``` ### Generic workflow engine ```typescript class WorkflowEngine { private definitions = new Map<string, WorkflowDefinition>(); private graphClient: Client; private siteId: string; private lists = new Map<string, string>(); // workflowId -> listId async handleCommand(ctx: any, def: WorkflowDefinition) { const params = parseCommandParams(ctx.activity.text!, def); const record = await this.createRecord(def, { ...params, requesterId: ctx.activity.from?.aadObjectId, requesterName: ctx.activity.from?.name, conversationId: ctx.activity.conversation?.id, serviceUrl: ctx.activity.serviceUrl, }); const card = def.cards.active(record); const response = await ctx.send({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: card, }], }); // Store activity ID for future updates await this.updateRecordField(def, record.id, "CardActivityId", response.id); // Start escalation timer if routing is configured if (def.routing?.escalationTimeoutMs) { this.startEscalation(def, record); } } async handleAction(ctx: any, verb: string, data: any) { const def = this.definitions.get(data.workflowId); if (!def) return; const record = await this.getRecord(def, data.recordId); if (verb === "approve" || verb === "reject") { return this.processApproval(ctx, def, record, verb, data.comment); } if (verb.startsWith("refresh")) { const card = record.status === "completed" ? def.cards.completed(record) : def.cards.active(record); return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.card.adaptive", value: card, }, }; } } registerQueryFunction(def: WorkflowDefinition) { // Auto-generate AI function schema from definition const parameters: Record<string, any> = {}; for (const col of def.filterableColumns) { const colDef = def.columns.find((c) => c.name === col); if (!colDef) continue; switch (colDef.type) { case "choice": parameters[col] = { type: "string", enum: colDef.choices }; break; case "dateTime": parameters[col] = { type: "string", description: `Filter by ${col} (ISO date)` }; break; case "personOrGroup": parameters[col] = { type: "string", description: `Filter by ${col} name` }; break; default: parameters[col] = { type: "string" }; } } return { name: `query_${def.id}`, description: def.queryDescription, parameters: { type: "object", properties: parameters }, }; } private async createRecord(def: WorkflowDefinition, fields: Record<string, any>) { const listId = await this.ensureList(def); const item = await this.graphClient .api(`/sites/${this.siteId}/lists/${listId}/items`) .post({ fields }); return { id: item.id, ...item.fields }; } private async ensureList(def: WorkflowDefinition): Promise<string> { if (this.lists.has(def.id)) return this.lists.get(def.id)!; // Check if list exists, create if not try { const existing = await this.graphClient .api(`/sites/${this.siteId}/lists`) .filter(`displayName eq '${def.name}'`) .get(); if (existing.value.length > 0) { this.lists.set(def.id, existing.value[0].id); return existing.value[0].id; } } catch { /* List doesn't exist */ } const list = await this.graphClient .api(`/sites/${this.siteId}/lists`) .post({ displayName: def.name, list: { template: "genericList" }, columns: def.columns.map(colDefToGraphColumn), }); this.lists.set(def.id, list.id); return list.id; } } ``` ### Template workflow: Time-Off Request ```typescript const ptoWorkflow: WorkflowDefinition = { id: "pto", name: "PTO Requests", description: "Time-off and vacation request workflow", commandPrefix: "/pto", columns: [ { name: "Requester", type: "personOrGroup", required: true }, { name: "StartDate", type: "dateTime", required: true }, { name: "EndDate", type: "dateTime", required: true }, { name: "HoursRequested", type: "number" }, { name: "Status", type: "choice", choices: ["Pending", "Approved", "Rejected"] }, { name: "ApprovedBy", type: "personOrGroup" }, { name: "Reason", type: "text" }, ], statusField: "Status", statusValues: { active: ["Pending"], completed: ["Approved", "Rejected"], }, triggers: [ { type: "command", config: { pattern: "/pto START to END" } }, { type: "messageExtension", config: { commandId: "createPto" } }, ], routing: { type: "single", approverSource: "manager", escalationTimeoutMs: 48 * 60 * 60 * 1000, // 48 hours }, cards: { active: buildPtoActiveCard, completed: buildPtoCompletedCard, list: buildPtoListCard, }, queryDescription: "Query PTO/time-off requests. Use when user asks about PTO, vacation, leave, days off.", filterableColumns: ["Status", "Requester", "StartDate"], }; // Register registerWorkflow(app, engine, ptoWorkflow); ``` ## pitfalls - **Over-abstraction kills velocity.** The composable platform should start with 2-3 template workflows and extract common patterns. Don't build the full generic engine before validating with real workflows. - **Schema migrations are hard.** Once a SharePoint List is created, adding required columns or changing types is disruptive. Version your schemas and handle missing columns gracefully. - **Generic engines produce generic cards.** Template card functions should be polished, not auto-generated. The best workflow UX comes from purpose-built card layouts, not generic field renderers. - **Power Automate hybrid mode adds complexity.** Supporting both bot-native and Power Automate execution means two code paths, two monitoring surfaces, and two failure modes. Default to bot-native for the FHL; add Power Automate later. - **Ecosystem extensibility requires stable interfaces.** Don't expose extension points until the core patterns stabilize through 3+ real workflow implementations. ## references - https://learn.microsoft.com/en-us/graph/api/resources/list - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview - https://learn.microsoft.com/en-us/power-automate/getting-started ## instructions Use this expert when designing the overall composable workflow architecture. Covers the five-element lifecycle framework, WorkflowDefinition interface, generic engine patterns, template workflows, auto-generated AI query schemas, and extensibility points. Pair with `../teams/workflow.sharepoint-lists-ts.md` for state persistence, `../teams/workflow.message-native-records-ts.md` for card-as-record patterns, `../teams/workflow.triggers-compose-ts.md` for trigger unification, `../teams/ai.conversational-query-ts.md` for NL retrieval, and `../teams/workflow.approvals-inline-ts.md` for approval routing. ## research Deep Research prompt: "Write a micro expert on designing a composable workflow platform inside Microsoft Teams (TypeScript). Cover: five-element lifecycle framework (trigger, state, logic, intelligence, visibility), WorkflowDefinition configuration interface, generic workflow engine that dispatches from definitions, template/reference workflows (PTO, equipment, standup), auto-generated AI function schemas from column definitions, SharePoint Lists as pluggable state backend, Power Automate hybrid execution mode, and ecosystem extensibility points. Include complete patterns for the definition interface, engine registration, and one template workflow." -
workflows-automation-ts.md 14.3 KB
# workflows-automation-ts ## purpose Bridges Slack Workflow Builder and Teams Power Automate / bot-driven orchestration for cross-platform bots targeting Slack, Teams, or both. ## rules 1. **Slack Workflow Builder → Power Automate flows (manual rebuild required).** There is no automated migration tool. Slack workflows are drag-and-drop automations with triggers and steps. Power Automate flows serve the same purpose but with a completely different builder, trigger system, and step library. Each workflow must be manually recreated. [learn.microsoft.com -- Power Automate](https://learn.microsoft.com/en-us/power-automate/getting-started) 2. **Slack workflow triggers → Power Automate triggers.** Slack triggers include: webhook, shortcut, new channel message, emoji reaction, user joins channel. Power Automate equivalents: HTTP request (webhook), Teams message trigger, approval trigger, Recurrence (scheduled), and 400+ connectors. Map each trigger individually. [learn.microsoft.com -- Triggers](https://learn.microsoft.com/en-us/power-automate/triggers-introduction) 3. **Slack custom steps (`workflow_step_execute`) → Power Automate custom connectors.** Slack bots can register custom workflow steps that appear in the Workflow Builder. In Power Automate, the equivalent is a custom connector wrapping your bot's REST API. The connector defines actions, inputs, and outputs that appear in the flow designer. [learn.microsoft.com -- Custom connectors](https://learn.microsoft.com/en-us/connectors/custom-connectors/) 4. **Slack approval workflows → Power Automate Approvals connector (built-in).** Slack workflows that collect approvals via emoji reactions or form submissions map to Power Automate's native Approvals connector. It provides: approval request creation, approval/rejection actions, parallel/sequential approvals, and approval history. No custom code needed. [learn.microsoft.com -- Approvals](https://learn.microsoft.com/en-us/power-automate/get-started-approvals) 5. **Teams "Workflows" app provides simple in-Teams automations.** For basic workflows (post to channel on schedule, notify on form submission), the Workflows app in Teams provides templates without leaving Teams. It's powered by Power Automate under the hood but has a simplified UI. [learn.microsoft.com -- Workflows app](https://learn.microsoft.com/en-us/microsoftteams/platform/m365-apps/publish-app#workflows) 6. **Bot-driven workflow alternative: state machine + Adaptive Card buttons.** For workflows that don't fit Power Automate's model (complex branching, dynamic participants, long-running multi-step processes), implement a state machine in the bot. Each step sends an Adaptive Card with action buttons; button clicks advance the state. Store workflow state in Cosmos DB or similar. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. **Slack `workflow_step` event lifecycle → custom connector action lifecycle.** Slack's workflow step has `edit` (configure step), `save` (persist config), `execute` (run step). Power Automate custom connectors define: action schema (inputs/outputs in OpenAPI), and the runtime HTTP call. There is no separate "edit" flow — the connector schema defines the UI. [learn.microsoft.com -- Connector actions](https://learn.microsoft.com/en-us/connectors/custom-connectors/define-blank#define-the-action) 8. **Slack workflow variables → Power Automate dynamic content.** Slack workflows pass data between steps via variables set in earlier steps. Power Automate uses "dynamic content" — outputs from previous steps that can be referenced in later steps. The data flow model is similar but the syntax is completely different. [learn.microsoft.com -- Dynamic content](https://learn.microsoft.com/en-us/power-automate/use-expressions-in-conditions) 9. **Power Automate flows can call Bot Framework via HTTP.** To integrate your Teams bot into a Power Automate flow, expose REST endpoints on your bot's server and call them from Power Automate's HTTP action. The bot can then send proactive messages based on flow triggers. [learn.microsoft.com -- HTTP connector](https://learn.microsoft.com/en-us/connectors/custom-connectors/) 10. **Slack Workflow Builder is free; Power Automate has licensing tiers.** Slack Workflow Builder is included in all plans. Power Automate has a free tier (limited runs) and premium tiers. Custom connectors require a premium license. Factor licensing into migration planning. [learn.microsoft.com -- Power Automate licensing](https://learn.microsoft.com/en-us/power-platform/admin/pricing-billing-skus) 11. **Reverse direction (Teams → Slack):** For Teams → Slack, Power Automate flows can be mapped to Slack Workflow Builder steps or custom `workflow_step_execute` handlers. Power Automate Approvals map to Slack approval workflows using emoji reactions or interactive message buttons. Power Automate custom connectors map to Slack custom workflow steps registered via `workflow_step` events. Power Automate Recurrence triggers map to Slack Workflow Builder scheduled triggers. ## patterns ### Approval workflow → Power Automate Approvals **Slack Workflow Builder (before):** The Slack workflow is configured in the GUI: 1. Trigger: User submits a form (custom step) 2. Step 1: Send form data to `#approvals` channel 3. Step 2: Wait for `:white_check_mark:` reaction from approver 4. Step 3: Post result to `#completed` channel **Bot code for custom approval step:** ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Custom workflow step execution app.event("workflow_step_execute", async ({ event, client }) => { const { workflow_step } = event; const inputs = workflow_step.inputs; // Post approval request const msg = await client.chat.postMessage({ channel: "#approvals", text: `Approval needed: ${inputs.request_text.value}`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `*Approval Request*\n${inputs.request_text.value}` }, }, { type: "section", text: { type: "mrkdwn", text: "React with :white_check_mark: to approve or :x: to reject." } }, ], }); // Watch for reaction (simplified — real implementation uses reaction_added event) }); ``` **Teams (after) — Power Automate flow (described as JSON definition):** ```json { "definition": { "triggers": { "manual": { "type": "Request", "kind": "Button", "inputs": { "schema": { "type": "object", "properties": { "requestText": { "type": "string", "title": "Request details" }, "requesterEmail": { "type": "string", "title": "Requester email" } } } } } }, "actions": { "Start_approval": { "type": "OpenApiConnection", "inputs": { "host": { "connectionName": "shared_approvals" }, "operationId": "StartAndWaitForAnApproval", "parameters": { "approvalType": "Basic", "ApprovalCreationInput/title": "Approval: @{triggerBody()?['requestText']}", "ApprovalCreationInput/assignedTo": "approver@company.com", "ApprovalCreationInput/details": "@{triggerBody()?['requestText']}" } } }, "Post_result_to_Teams": { "type": "OpenApiConnection", "inputs": { "host": { "connectionName": "shared_teams" }, "operationId": "PostMessageToConversation", "parameters": { "poster": "Flow bot", "location": "Channel", "body/recipient": "completed-channel-id", "body/messageBody": "Request @{outputs('Start_approval')?['body/title']} was @{outputs('Start_approval')?['body/outcome']}" } }, "runAfter": { "Start_approval": ["Succeeded"] } } } } } ``` **Bot-driven alternative (for complex approval logic):** ```typescript import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Approval state machine interface ApprovalRequest { id: string; text: string; requester: string; status: "pending" | "approved" | "rejected"; activityId?: string; } const approvals = new Map<string, ApprovalRequest>(); // Create approval request app.message(/^\/?approve (.+)$/i, async ({ send, activity }) => { const text = activity.text?.replace(/^\/?approve\s+/i, "") ?? ""; const id = `apr_${Date.now()}`; const approval: ApprovalRequest = { id, text, requester: activity.from?.name ?? "Unknown", status: "pending", }; approvals.set(id, approval); const response = await send({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: "Approval Request", weight: "Bolder", size: "Medium" }, { type: "TextBlock", text: `**From:** ${approval.requester}`, wrap: true }, { type: "TextBlock", text: approval.text, wrap: true }, ], actions: [ { type: "Action.Execute", title: "Approve", verb: "approveAction", data: { approvalId: id } }, { type: "Action.Execute", title: "Reject", verb: "rejectAction", data: { approvalId: id } }, ], }, }], }); }); // Handle approval/rejection buttons app.on("card.action" as any, async ({ activity }) => { const data = activity.value?.action?.data ?? activity.value; const approval = approvals.get(data?.approvalId); if (!approval) return { status: 200, body: {} }; const isApprove = data?.verb === "approveAction"; approval.status = isApprove ? "approved" : "rejected"; const reviewer = activity.from?.name ?? "Someone"; // Return updated card (replaces original) return { status: 200, body: { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: "Approval Request", weight: "Bolder", size: "Medium" }, { type: "TextBlock", text: approval.text, wrap: true }, { type: "TextBlock", text: `**${approval.status.toUpperCase()}** by ${reviewer}`, color: isApprove ? "Good" : "Attention", weight: "Bolder", }, ], // No actions — card is now read-only }, }; }); app.start(3978); ``` ### Migration approach comparison | Slack Workflow Feature | Power Automate | Bot-Driven | Teams Workflows App | |---|---|---|---| | Visual builder | Yes (full designer) | No (code) | Yes (simplified) | | Custom steps | Custom connectors | Handler code | No | | Approval flows | Built-in Approvals | Card buttons + state | No | | Scheduled triggers | Recurrence trigger | Timer + proactive | Yes (basic) | | Complex branching | Yes (conditions, loops) | State machine | No | | License cost | Free tier + Premium | Bot hosting cost | Free | | Developer skill needed | Low-code | TypeScript | None | ## pitfalls - **No automated migration**: Every Slack workflow must be manually recreated in Power Automate or bot code. There is no import/export compatibility. Plan for significant manual effort on large workflow portfolios. - **Reaction-based approvals break completely**: Slack workflows commonly use emoji reactions as approval signals. Teams has no equivalent pattern in Power Automate. Use the built-in Approvals connector or Action.Execute card buttons. - **Custom connector licensing**: Power Automate custom connectors (needed to replace Slack custom workflow steps) require a Premium license. The free tier does not support custom connectors. - **Slack workflow variables vs Power Automate dynamic content**: The data passing model is similar in concept but completely different in syntax. Slack uses `{{variable_name}}`; Power Automate uses `@{outputs('step_name')?['property']}`. This is a manual translation. - **Bot-driven workflows require state persistence**: Unlike Power Automate which manages state internally, bot-driven approval workflows need external state storage (Cosmos DB, SQL). Without it, workflow state is lost on bot restart. - **Power Automate flow limits**: Free tier is limited to 750 runs/month. Standard is 10,000/month. High-volume workflows (processing hundreds of requests daily) may require premium plans. ## references - https://learn.microsoft.com/en-us/power-automate/getting-started - https://learn.microsoft.com/en-us/power-automate/get-started-approvals - https://learn.microsoft.com/en-us/connectors/custom-connectors/ - https://learn.microsoft.com/en-us/power-automate/triggers-introduction - https://learn.microsoft.com/en-us/power-automate/use-expressions-in-conditions - https://learn.microsoft.com/en-us/power-platform/admin/pricing-billing-skus - https://github.com/microsoft/teams.ts - https://api.slack.com/workflows — Slack Workflow Builder - https://api.slack.com/workflows/steps — Slack custom workflow steps ## instructions Use this expert when adding cross-platform support in either direction for workflow automation. It covers: Slack Workflow Builder bridged to Power Automate flows, custom workflow steps bridged to Power Automate custom connectors, approval workflows bridged to the Approvals connector, the Teams Workflows app for simple automations, bot-driven workflow alternatives using state machines + Adaptive Cards, and reverse mapping from Power Automate flows back to Slack Workflow Builder steps and custom workflow_step_execute handlers. Pair with `../teams/ui.adaptive-cards-ts.md` for card construction in bot-driven workflows, `../teams/runtime.proactive-messaging-ts.md` for flow-triggered bot messages, and `slack-interactive-responses-to-teams-ts.md` for card replacement patterns in approval flows. ## research Deep Research prompt: "Write a micro expert for bridging Slack Workflow Builder and Microsoft Teams Power Automate / bot-driven orchestration in either direction. Cover: Power Automate as the Teams-side replacement, custom connector creation for Slack custom workflow steps, the built-in Approvals connector for approval flows, the Teams Workflows app for simple automations, bot-driven state machine alternative with Adaptive Card buttons, workflow trigger mapping, variable/dynamic content translation, licensing considerations, and reverse mapping from Power Automate flows back to Slack Workflow Builder steps. Include code examples for bot-driven approvals and a comparison table."
-
-
convert
-
bulk-conversion-strategy-ts.md 12.8 KB
# bulk-conversion-strategy-ts ## purpose Strategy and workflow for large-scale code conversion — converting 100+ source files (Java POJOs, Ruby classes, JS modules) to TypeScript efficiently with prioritized phases, incremental validation, and tooling. ## rules 1. **Never attempt a big-bang conversion.** Convert in phases, ensuring each phase compiles and passes tests before proceeding. A half-converted project that compiles is infinitely better than a fully-converted project that doesn't. 2. **Phase order: Models → Utilities → Core logic → Handlers → Entry point.** Convert bottom-up through the dependency graph. Models have no internal dependencies, so they convert first. Entry points depend on everything, so they convert last. 3. **Prioritize by dependency count.** Run a dependency analysis: files imported by many others convert first (high fan-in). Files that import many others convert last (high fan-out). This minimizes the number of temporary `any` shims. 4. **Use TypeScript's `allowJs: true`** during transition. This lets `.ts` files coexist with unconverted `.js` files. Set `checkJs: false` to avoid type-checking JS files. Remove `allowJs` only when 100% of files are converted. 5. **Create a `@types/source-project` declarations file** for unconverted modules. As you convert models first, other unconverted files may still import them. A `.d.ts` shim keeps the compiler happy during the transition. 6. **Batch similar files.** Group files by pattern (all Lombok `@Data` POJOs, all event handlers, all middleware) and convert each group in one pass. This builds muscle memory and ensures consistency. 7. **Validate each batch immediately.** After converting a batch: (1) `tsc --noEmit` to check types, (2) run relevant tests, (3) commit. Do not accumulate unconverted batches. 8. **Track progress with a conversion manifest.** Maintain a simple JSON or markdown file listing every source file, its status (pending/in-progress/done/skipped), target TS file, and notes. This prevents duplicate work and makes progress visible. 9. **Handle the 80/20 rule.** ~80% of files in a Java project are simple POJOs/models that convert mechanically. ~20% contain complex logic (middleware, async chains, polymorphic factories) that need careful manual conversion. Identify the 20% early and plan extra time. 10. **Establish naming conventions before starting.** Decide once: snake_case API fields stay snake_case or become camelCase? One file per class (Java-style) or group by feature (TS-style)? Barrel exports or direct imports? Document in the conversion manifest. 11. **Write adapter/shim layers for incremental testing.** If the source project has integration tests, create thin adapter layers so converted TS modules can be called from unconverted test harnesses (or vice versa) during transition. 12. **Delete source files after conversion, don't keep both.** Having `User.java` and `User.ts` side by side causes confusion. Once `User.ts` compiles and tests pass, delete `User.java`. The git history preserves the original. ## interview ### Q1 — Naming Conventions ``` question: "How should internal field names be cased in the converted TypeScript code?" header: "Field casing" options: - label: "camelCase (Recommended)" description: "Convert all internal fields to camelCase (standard TS convention). Wire-format fields (API JSON) keep their original casing via serialization mapping." - label: "Keep original casing" description: "Preserve snake_case/PascalCase from the source language as-is. Fewer changes but non-idiomatic TS." - label: "You Decide Everything" description: "Accept recommended defaults for all decisions and skip remaining questions." multiSelect: false ``` ### Q2 — File Organization ``` question: "How should converted files be organized?" header: "File layout" options: - label: "Group by feature (Recommended)" description: "Organize files by feature/module (TS-style). Related types, handlers, and utilities live together." - label: "One file per class" description: "Keep the source language's structure (e.g., Java's one-class-per-file). Familiar but can lead to many small files." multiSelect: false ``` ### Q3 — Export Style ``` question: "How should modules be exported?" header: "Exports" options: - label: "Barrel exports (Recommended)" description: "Each directory gets an index.ts re-exporting its public API. Cleaner imports for consumers." - label: "Direct imports only" description: "Import directly from each file path. No barrel files. Simpler but more verbose import paths." multiSelect: false ``` ### Q4 — Conversion Scope ``` question: "Should we convert everything, or focus on specific modules first?" header: "Scope" options: - label: "Full project (phased)" description: "Convert the entire project in dependency order (models -> utils -> core -> handlers -> entry). Recommended for clean breaks." - label: "Critical path only" description: "Convert only the modules needed for the current feature/migration. Remaining modules use .d.ts shims." - label: "Models + utilities only" description: "Convert data models and shared utilities. Keep handlers/entry points in source language with interop layer." multiSelect: false ``` ### defaults table | Question | Default | |---|---| | Q1 | camelCase for internal, preserve wire-format | | Q2 | Group by feature | | Q3 | Barrel exports | | Q4 | Full project (phased) | ## patterns ### Conversion manifest tracking file ```markdown # Conversion Manifest — java-slack-sdk ## Conventions - API wire-format fields: keep snake_case - Internal fields: camelCase - One interface per model file (may group related types) - Barrel exports via index.ts per directory ## Phase 1: Models (200 files) | Source File | Status | Target File | Notes | |---|---|---|---| | model/block/SectionBlock.java | done | src/models/blocks/section-block.ts | | | model/block/ActionsBlock.java | done | src/models/blocks/actions-block.ts | | | model/block/DividerBlock.java | done | src/models/blocks/divider-block.ts | | | model/event/AppMentionEvent.java | in-progress | src/models/events/app-mention-event.ts | Has nested types | | model/event/MessageEvent.java | pending | src/models/events/message-event.ts | | | ... | | | | ## Phase 2: Utilities (15 files) | Source File | Status | Target File | Notes | |---|---|---|---| ## Phase 3: Core Services (30 files) | Source File | Status | Target File | Notes | |---|---|---|---| ## Phase 4: Handlers (40 files) | Source File | Status | Target File | Notes | |---|---|---|---| ## Phase 5: Entry Points (5 files) | Source File | Status | Target File | Notes | |---|---|---|---| ``` ### Dependency graph analysis for prioritization ```typescript // Script to analyze Java import graph and determine conversion order import { readFileSync, readdirSync } from 'fs'; import { join, relative } from 'path'; interface FileNode { path: string; imports: string[]; // files this file imports importedBy: string[]; // files that import this file (fan-in) } function analyzeJavaImports(srcDir: string): FileNode[] { const files: Map<string, FileNode> = new Map(); // Scan all .java files function scan(dir: string) { for (const entry of readdirSync(dir, { withFileTypes: true })) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { scan(fullPath); } else if (entry.name.endsWith('.java')) { const rel = relative(srcDir, fullPath); const content = readFileSync(fullPath, 'utf-8'); const imports = [...content.matchAll(/^import\s+([\w.]+);/gm)] .map((m) => m[1].replace(/\./g, '/') + '.java'); files.set(rel, { path: rel, imports, importedBy: [] }); } } } scan(srcDir); // Build reverse dependency map (fan-in) for (const [path, node] of files) { for (const imp of node.imports) { const target = files.get(imp); if (target) { target.importedBy.push(path); } } } // Sort: highest fan-in first (most depended-on → convert first) return [...files.values()].sort( (a, b) => b.importedBy.length - a.importedBy.length, ); } // Usage: const ordered = analyzeJavaImports('./java-slack-sdk/slack-api-model/src/main/java'); console.log('Convert in this order:'); ordered.slice(0, 20).forEach((f) => console.log(` ${f.path} (imported by ${f.importedBy.length} files)`), ); ``` ### Batch conversion script for Lombok @Data POJOs ```typescript // Semi-automated: reads Java @Data class, outputs TS interface stub function convertDataClass(javaSource: string): string { const lines = javaSource.split('\n'); const className = lines .find((l) => l.includes('class ')) ?.match(/class\s+(\w+)/)?.[1] ?? 'Unknown'; const fields: { name: string; type: string; serializedName?: string }[] = []; let serializedName: string | undefined; for (const line of lines) { const snMatch = line.match(/@SerializedName\("(\w+)"\)/); if (snMatch) { serializedName = snMatch[1]; continue; } const fieldMatch = line.match( /private\s+(?:final\s+)?(\w+(?:<[\w<>,\s]+>)?)\s+(\w+)\s*;/, ); if (fieldMatch) { fields.push({ type: mapJavaType(fieldMatch[1]), name: serializedName ?? fieldMatch[2], serializedName, }); serializedName = undefined; } } const fieldLines = fields .map((f) => ` ${f.name}: ${f.type};`) .join('\n'); return `export interface ${className} {\n${fieldLines}\n}\n`; } function mapJavaType(javaType: string): string { const map: Record<string, string> = { String: 'string', boolean: 'boolean', Boolean: 'boolean', int: 'number', Integer: 'number', long: 'number', Long: 'number', double: 'number', Double: 'number', float: 'number', Float: 'number', }; if (map[javaType]) return map[javaType]; if (javaType.startsWith('List<')) { const inner = javaType.slice(5, -1); return `${mapJavaType(inner)}[]`; } if (javaType.startsWith('Map<')) { const [k, v] = javaType.slice(4, -1).split(',').map((s) => s.trim()); return `Record<${mapJavaType(k)}, ${mapJavaType(v)}>`; } return javaType; // Keep as-is for custom types (will need manual mapping) } ``` ## pitfalls - **Converting everything before testing anything**: The biggest risk. Convert 5 model files, compile, test, commit. Then the next 5. Never go more than ~20 files without validating. - **Ignoring the dependency graph**: Converting a handler before its model types exist forces you to use `any` everywhere, creating tech debt you'll forget to clean up. - **Inconsistent naming conventions**: If file 1 uses `threadTs` and file 50 uses `thread_ts` for the same field, you'll have runtime bugs. Establish and document conventions in the manifest BEFORE starting. - **Keeping source and target files**: Having `User.java` and `user.ts` in the repo simultaneously leads to confusion about which is authoritative. Delete the source after confirming the target works. - **Automating too much**: Semi-automated scripts (like the POJO converter above) produce ~70% correct output. Always review and adjust. Fully automated conversion produces subtle type errors that are harder to find later. - **Not tracking progress**: After converting 50 of 200 files, it's easy to lose track of what's done. The manifest file is essential for multi-session conversion work. - **Skipping the hard 20%**: It's tempting to convert all easy POJOs and declare victory. The complex files (middleware, async chains, polymorphic factories) are where the real conversion effort lives. Plan them explicitly. - **Breaking the build for days**: Use `allowJs: true` to keep the project buildable throughout. If the project must stay deployable during conversion, maintain a working build at all times. ## references - https://www.typescriptlang.org/tsconfig#allowJs -- allowJs for incremental migration - https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html -- official migration guide - https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html -- .d.ts files for shims ## instructions Use this expert when facing a large-scale conversion (50+ source files). Before converting any code, read this expert to establish the phase order, create a conversion manifest, and run dependency analysis. Pair with the appropriate language expert (`java-to-ts-ts.md`, `ruby-to-ts-ts.md`, or `js-to-ts-ts.md`) for per-file conversion rules, and `json-serialization-ts.md` for serialization-heavy model conversion. ## research Deep Research prompt: "Write a micro expert for large-scale language conversion strategy (100+ files from Java/Ruby/JS to TypeScript). Cover: phased conversion order (models → utils → core → handlers → entry), dependency graph analysis for prioritization, conversion manifest tracking, batch processing patterns, allowJs incremental migration, naming convention decisions, validation checkpoints, and common failure modes in big conversions." -
dependency-mapping-ts.md 8.3 KB
# dependency-mapping-ts ## purpose Cross-language dependency mapping — finding npm/TypeScript equivalents for Ruby gems, Java Maven artifacts, and Python pip packages commonly found in Slack bot projects. ## rules 1. Always check if an `@types/{package}` exists on DefinitelyTyped before declaring a package as untyped. Run `npm info @types/{package}` or search https://www.npmjs.com/~types. 2. Prefer packages with built-in TypeScript types over untyped packages + `@types` shims. A package exporting its own `.d.ts` is better maintained than relying on community type definitions. 3. When no npm equivalent exists for a gem or Maven artifact, first check if the functionality is built into Node.js (e.g., `crypto`, `http`, `fs`, `url`, `path`, `util`). Many small gems/JARs solve problems that Node.js handles natively. 4. For HTTP clients: Ruby `faraday`/`httparty`/`net/http` and Java `OkHttp`/`HttpClient`/`RestTemplate` all map to `fetch` (built-in since Node 18) or `undici` for advanced use cases. Avoid adding `axios` or `got` unless you need interceptors or retry logic. 5. For web frameworks: Ruby `sinatra` → `express` or `fastify`. Ruby `rails` → `express` + individual packages for ORM, validation, etc. Java `Spring Boot` → `express` or `fastify` + middleware. Do NOT look for a single Rails/Spring equivalent — the Node ecosystem is modular. 6. For testing: Ruby `rspec`/`minitest` → `vitest` or `jest`. Java `JUnit`/`TestNG` → `vitest` or `jest`. Java `Mockito` → `vitest` built-in mocking or `jest.fn()`. 7. For environment/config: Ruby `dotenv` → `dotenv`. Java `System.getenv()` → `process.env`. Java Spring `@Value` / `application.properties` → `dotenv` + typed config module. 8. For JSON handling: Ruby `json` (stdlib) and Java `Jackson`/`Gson` → built-in `JSON.parse()`/`JSON.stringify()`. For schema validation, use `zod` or `ajv`. 9. For database access: Ruby `activerecord`/`sequel` → `prisma`, `drizzle`, or `knex`. Java `Hibernate`/`JPA` → `prisma` or `typeorm`. Java `JDBC` → `pg` (Postgres) / `mysql2` / `better-sqlite3` with raw queries. 10. For scheduling/cron: Ruby `clockwork`/`whenever` → `node-cron` or `bullmq`. Java `ScheduledExecutorService`/`Quartz` → `node-cron` or `bullmq`. 11. For logging: Ruby `logger` (stdlib) → `pino` or `winston`. Java `SLF4J`/`Logback`/`Log4j` → `pino` (fast, JSON) or `winston` (flexible transports). 12. When replacing a dependency, verify feature parity. A mapping table entry doesn't mean the npm package covers 100% of the original's API. Identify which features the bot actually uses and confirm the replacement supports them. ## patterns ### Gem → npm mapping table (common Slack bot gems) | Ruby Gem | npm Package | Notes | |---|---|---| | `slack-ruby-bot` | `@slack/bolt` | Different API; rewrite handlers | | `slack-ruby-client` | `@slack/web-api` | Direct API client equivalent | | `sinatra` | `express` | Route syntax differs; see ruby-to-ts-ts.md | | `faraday` / `httparty` | `fetch` (built-in) | No extra dependency needed (Node 18+) | | `json` (stdlib) | `JSON` (built-in) | Native in both; zero effort | | `dotenv` | `dotenv` | Nearly identical API | | `redis` / `redis-rb` | `ioredis` | TS-typed, Promise-based | | `pg` | `pg` + `@types/pg` | Same name, same purpose | | `activerecord` | `prisma` or `drizzle` | Full rewrite of data layer | | `rspec` | `vitest` | Describe/it syntax similar | | `puma` / `unicorn` | N/A | Node handles HTTP serving natively | | `rake` | `tsx` scripts or `npm scripts` | Task runner built into npm | | `erb` | Template literals or `ejs` | For HTML templating only | | `chronic` / `ice_cube` | `date-fns` or `luxon` | Date parsing/recurrence | | `nokogiri` | `cheerio` | HTML/XML parsing | ### Maven → npm mapping table (common Slack bot JARs) | Maven Artifact | npm Package | Notes | |---|---|---| | `com.slack.api:bolt` | `@slack/bolt` | Different API; rewrite handlers | | `com.slack.api:slack-api-client` | `@slack/web-api` | Direct API client equivalent | | `org.springframework.boot:*` | `express` + middleware | No single equivalent; modular | | `com.google.code.gson:gson` | `JSON` (built-in) | Native JSON support | | `com.fasterxml.jackson.core:*` | `JSON` (built-in) + `zod` | Zod for schema validation | | `org.apache.httpcomponents:httpclient` | `fetch` (built-in) | No extra dependency (Node 18+) | | `org.slf4j:slf4j-api` | `pino` or `winston` | Structured logging | | `ch.qos.logback:logback-classic` | `pino` | Fast JSON logger | | `org.junit.jupiter:*` | `vitest` | Test framework | | `org.mockito:*` | `vitest` mocking | Built-in mock support | | `io.github.cdimascio:dotenv-java` | `dotenv` | Same concept | | `com.zaxxer:HikariCP` | N/A | Node uses single-thread; pool via `pg` | | `org.postgresql:postgresql` | `pg` + `@types/pg` | PostgreSQL driver | | `redis.clients:jedis` | `ioredis` | Redis client | | `com.google.guava:guava` | Various / built-in | Most Guava utils are native in JS | ### Dependency audit workflow ```typescript // Step 1: Extract all dependencies from the source project // Ruby: parse Gemfile / Gemfile.lock // Java: parse pom.xml / build.gradle // Step 2: Categorize each dependency type DepCategory = | 'builtin' // Covered by Node.js or TS natively | 'direct-map' // 1:1 npm equivalent exists | 'rewrite' // Functionality exists but API differs significantly | 'eliminate' // Language-specific concern (e.g., thread pools, GC tuning) | 'custom'; // No equivalent; must implement from scratch interface DependencyAudit { source: string; // e.g., "faraday" or "com.google.code.gson:gson" category: DepCategory; target: string; // npm package name or "built-in" notes: string; // Migration notes typesPackage?: string; // @types/* if needed } // Step 3: Install and verify each mapped dependency // npm install {package} @types/{package} // Step 4: Write adapter code for 'rewrite' category deps ``` ## pitfalls - **Don't assume name similarity means API similarity**: Ruby's `redis` gem and npm's `ioredis` serve the same purpose but have completely different APIs. Plan for handler rewrites. - **Check Node.js built-ins first**: Before adding `uuid`, check if `crypto.randomUUID()` suffices. Before adding `axios`, check if `fetch` works. Before adding `path-to-regexp`, check if your framework already includes routing. - **Gem/JAR version matters**: A Ruby project on `slack-ruby-bot 0.10` has a very different API from `0.16`. Check the source project's locked version to understand which features are actually used. - **Transitive dependencies**: Ruby's `Gemfile.lock` and Java's dependency tree include transitive deps. Only map the **direct** dependencies — transitives are handled by the npm package you're switching to. - **Dev dependencies**: Don't forget to map dev-only tools: `rubocop` → `eslint` + `prettier`, `bundler` → `npm`, `mvn` → `npm scripts`, `pry` → Node debugger. - **Web server is implicit**: Ruby needs `puma`/`unicorn`/`thin` as a web server. Java needs `tomcat`/`jetty` (embedded in Spring). Node.js `http` module IS the web server — no additional package needed (Express wraps it). ## references - https://www.npmjs.com/ -- npm package registry (search for equivalents) - https://www.npmjs.com/~types -- DefinitelyTyped @types packages - https://rubygems.org/ -- Ruby gem registry (for understanding source deps) - https://search.maven.org/ -- Maven Central (for understanding source deps) - https://nodejs.org/api/ -- Node.js built-in modules ## instructions Use this expert when auditing and replacing dependencies during a language conversion. Start by extracting all dependencies from the source project (Gemfile, pom.xml, build.gradle, or package.json), then categorize each using the audit workflow pattern. Consult the mapping tables for common equivalents. Pair with the appropriate language conversion expert (`js-to-ts-ts.md`, `ruby-to-ts-ts.md`, or `java-to-ts-ts.md`) for API-level migration guidance. ## research Deep Research prompt: "Write a micro expert for mapping dependencies across languages to TypeScript/npm. Cover: Ruby gems to npm packages, Java Maven artifacts to npm packages, identifying Node.js built-in replacements, @types packages from DefinitelyTyped, dependency audit workflow, and common Slack bot dependency mappings. Include mapping tables for the 15 most common gems and 15 most common Maven artifacts found in chat bot projects." -
index.md 3.5 KB
# convert-router ## purpose Route language-conversion tasks to the minimal set of micro-expert files. Each expert covers rewriting source code from one language into idiomatic TypeScript. ## task clusters ### JS → TypeScript When: converting JavaScript files to TypeScript, adding types, modernizing imports, enabling strict mode Read: - `js-to-ts-ts.md` Depends on: `type-mapping-ts.md` (type system reference) ### Ruby → TypeScript When: rewriting Ruby code in TypeScript, translating Ruby idioms, converting gems to npm Read: - `ruby-to-ts-ts.md` - `dependency-mapping-ts.md` Depends on: `type-mapping-ts.md` (type system reference) ### Java → TypeScript When: rewriting Java code in TypeScript, translating Java OOP patterns, Lombok annotations, CompletableFuture async, converting Maven/Gradle deps to npm Read: - `java-to-ts-ts.md` - `json-serialization-ts.md` - `dependency-mapping-ts.md` Depends on: `type-mapping-ts.md` (type system reference) ### Kotlin → TypeScript When: rewriting Kotlin code in TypeScript, trailing lambdas, SAM conversions, `it` implicit parameter, string templates, `trimIndent()`, null-safety operators (`?.`, `!!`, `?:`), `when` expressions, extension functions, data classes, companion objects, sealed classes, `::class.java` references Read: - `kotlin-to-ts-ts.md` - `java-to-ts-ts.md` (Kotlin uses Java SDK types) - `dependency-mapping-ts.md` Depends on: `type-mapping-ts.md` (type system reference) ### JSON serialization conversion When: converting Gson/Jackson serialization to TypeScript JSON + Zod, polymorphic deserialization, @SerializedName mapping Read: - `json-serialization-ts.md` Depends on: `type-mapping-ts.md` (type system reference) ### Bulk/large-scale conversion When: converting 50+ source files, planning phased conversion, tracking progress across many files Read: - `bulk-conversion-strategy-ts.md` Depends on: The appropriate language-specific expert ### Cross-language dependency mapping When: finding npm equivalents for gems, Maven artifacts, or pip packages Read: - `dependency-mapping-ts.md` ### Cross-language type mapping When: translating type systems between languages, mapping nullable/generic/enum patterns to TypeScript Read: - `type-mapping-ts.md` ### Composite: Full language conversion When: complete end-to-end source rewrite from any supported language to TypeScript Read: - The appropriate language-specific expert (`js-to-ts-ts.md`, `ruby-to-ts-ts.md`, `java-to-ts-ts.md`, or `kotlin-to-ts-ts.md`) - `json-serialization-ts.md` (if Java source with Gson/Jackson) - `bulk-conversion-strategy-ts.md` (if 50+ source files) - `dependency-mapping-ts.md` - `type-mapping-ts.md` Cross-domain deps: If also bridging platforms, pair with `../bridge/index.md` for Slack↔Teams or AWS↔Azure concerns. ## combining rule If a request involves **language conversion** and **platform bridging**, read the language-specific expert here first (to rewrite the source), then route through `../bridge/index.md` for platform-specific mapping. ## file inventory `bulk-conversion-strategy-ts.md` | `dependency-mapping-ts.md` | `java-to-ts-ts.md` | `js-to-ts-ts.md` | `json-serialization-ts.md` | `kotlin-to-ts-ts.md` | `ruby-to-ts-ts.md` | `type-mapping-ts.md` <!-- Updated 2026-02-11: Added json-serialization-ts.md and bulk-conversion-strategy-ts.md for Java SDK conversion support --> <!-- Updated 2026-02-11: Added kotlin-to-ts-ts.md for Kotlin-specific syntax (trailing lambdas, it, string templates, null-safety, when, extension functions, data class, sealed class) --> -
java-to-ts-ts.md 20.6 KB
# java-to-ts-ts ## purpose Rewriting Java source code as idiomatic TypeScript — mapping Java's class-based OOP, generics, annotations, collections, and concurrency patterns to their TypeScript equivalents. ## rules 1. Java class hierarchies map to TypeScript interfaces + classes. Prefer interfaces over abstract classes for defining contracts. Java `implements Interface` maps directly to TypeScript `implements Interface`. Java `extends AbstractClass` maps to TypeScript `extends BaseClass`. 2. Java generics map to TypeScript generics with the same `<T>` syntax. Key difference: Java generics are erased at runtime; TypeScript generics are erased at compile time. Both are structural at their core. Java bounded wildcards (`? extends T`) map to TS constrained generics (`<U extends T>`). Java `? super T` has no direct TS equivalent — use a union or contravariant generic. 3. Java annotations (`@Override`, `@Deprecated`, `@JsonProperty`) have no built-in TS equivalent. Map to: TS decorators (experimental, stage 3), JSDoc comments, or runtime metadata patterns. For simple markers like `@Override`, simply remove them — TS enforces override correctness with the `override` keyword. 4. Java `Optional<T>` maps to `T | null` or `T | undefined`. `Optional.of(x)` → just `x`, `Optional.empty()` → `null`, `Optional.isPresent()` → `!= null`, `Optional.map(fn)` → optional chaining + nullish coalescing (`x?.transform() ?? default`). 5. Java checked exceptions do not exist in TypeScript. Remove `throws` declarations from method signatures. Convert `try/catch` blocks but let unexpected errors propagate naturally. Document thrown errors in JSDoc if important for callers. 6. Java `final` maps to `readonly` for class fields and `const` for local variables. Java `final` on method parameters has no TS equivalent (parameters are already effectively final by convention). 7. Java `static` methods and fields map directly to TypeScript `static`. Java static utility classes (e.g., `Collections`, `Math`) often map better to standalone exported functions rather than a class with all-static members. 8. Java `enum` maps to TypeScript `enum` for simple cases, but prefer string literal unions for most use cases. Java enums with methods and fields → TypeScript `as const` object + associated functions or a class hierarchy. 9. Java `Stream` API maps to TypeScript array methods. `stream().filter().map().collect(Collectors.toList())` becomes `.filter().map()`. `Collectors.toMap()` → `reduce()` or `Object.fromEntries()`. `Collectors.groupingBy()` → `Object.groupBy()` or `reduce()`. 10. Java `synchronized` / `volatile` / `Lock` have no TypeScript equivalent (JS is single-threaded). Remove synchronization primitives entirely. If the Java code uses threads for parallelism, redesign around `Promise.all()`, async/await, or worker threads. 11. Java `Map<K,V>` maps to `Map<K,V>` (JS built-in) or `Record<string, V>` for string-keyed maps. `List<T>` → `T[]` or `Array<T>`. `Set<T>` → `Set<T>`. Java `HashMap`/`TreeMap` distinctions are irrelevant — JS `Map` has insertion-order iteration. 12. Java getter/setter pairs (`getName()`/`setName()`) should be simplified to direct property access in TS. Only use `get`/`set` accessors if validation or side effects are needed. 13. Java `StringBuilder` / string concatenation in loops → template literals or `Array.join()`. TS strings are immutable like Java strings, but template literals handle most interpolation needs. 14. Java package structure (`com.example.app.service`) does NOT map to deeply nested TS folders. Flatten to a pragmatic folder structure: `src/services/`, `src/models/`, etc. Use barrel files (`index.ts`) for clean re-exports. 15. **Lombok `@Data`** generates getters, setters, `equals()`, `hashCode()`, `toString()`, and a required-args constructor. In TypeScript, replace with a plain `interface` (for data-only types) or a `class` with `public` constructor parameters. Remove all generated method equivalents — TS doesn't need them. 16. **Lombok `@Builder`** generates a fluent builder class. Replace with a TypeScript options interface: `new Foo({ bar, baz })` or a factory function. The builder pattern is unnecessary when constructors accept named parameters via object destructuring. 17. **Lombok `@Getter`/`@Setter`** on individual fields → direct `public` property access in TS. If the field was `@Getter` only (read-only), use `readonly`. If `@Setter` has custom logic, use a TS `set` accessor. 18. **Lombok `@AllArgsConstructor`/`@NoArgsConstructor`/`@RequiredArgsConstructor`** → TypeScript constructor with explicit parameters. `@NoArgsConstructor` on a data class → all properties optional or have defaults. `@RequiredArgsConstructor` → constructor with only `final` fields as parameters. 19. **Lombok `@Slf4j`** generates a `private static final Logger log` field. Replace with a module-level logger: `import pino from 'pino'; const log = pino({ name: 'MyClass' });` or accept a logger via constructor injection. 20. **Lombok `@Value`** (immutable `@Data`) → TypeScript `interface` with all `readonly` fields, or use `Readonly<T>` utility type. 21. **`CompletableFuture<T>`** maps to `Promise<T>`. `thenApply(fn)` → `.then(fn)`, `thenCompose(fn)` → `.then(fn)` (Promise auto-flattens), `exceptionally(fn)` → `.catch(fn)`, `thenAccept(fn)` → `.then(fn)` (when return is void). 22. **`CompletableFuture.allOf()`** → `Promise.all()`. `CompletableFuture.anyOf()` → `Promise.race()`. `CompletableFuture.supplyAsync(fn, executor)` → just call the async function directly (no executor needed in single-threaded JS). 23. **`CompletableFuture` chains** should be rewritten as `async/await` for readability. A chain of `.thenApply().thenCompose().exceptionally()` becomes a simple `try { const a = await step1(); const b = await step2(a); } catch (e) { ... }`. 24. **`@FunctionalInterface`** annotations → TypeScript function type aliases. `@FunctionalInterface interface Handler<T> { void handle(T t); }` becomes `type Handler<T> = (t: T) => void`. ## patterns ### Java class hierarchy → TypeScript interfaces + classes ```java // --- Before (Java) --- public interface MessageHandler { void handle(Message message); boolean canHandle(String type); } public abstract class BaseHandler implements MessageHandler { protected final Logger logger; public BaseHandler(Logger logger) { this.logger = logger; } @Override public boolean canHandle(String type) { return getSupportedTypes().contains(type); } protected abstract Set<String> getSupportedTypes(); } public class SlashCommandHandler extends BaseHandler { private final CommandRegistry registry; public SlashCommandHandler(Logger logger, CommandRegistry registry) { super(logger); this.registry = registry; } @Override public void handle(Message message) { String command = message.getText().split(" ")[0]; registry.execute(command, message); } @Override protected Set<String> getSupportedTypes() { return Set.of("slash_command", "block_actions"); } } ``` ```typescript // --- After (TypeScript) --- interface MessageHandler { handle(message: Message): void; canHandle(type: string): boolean; } abstract class BaseHandler implements MessageHandler { constructor(protected readonly logger: Logger) {} canHandle(type: string): boolean { return this.getSupportedTypes().has(type); } abstract handle(message: Message): void; protected abstract getSupportedTypes(): Set<string>; } class SlashCommandHandler extends BaseHandler { constructor( logger: Logger, private readonly registry: CommandRegistry, ) { super(logger); } handle(message: Message): void { const command = message.text.split(' ')[0]; this.registry.execute(command, message); } protected getSupportedTypes(): Set<string> { return new Set(['slash_command', 'block_actions']); } } ``` ### Java Stream API → TypeScript array methods ```java // --- Before (Java) --- import java.util.stream.Collectors; List<UserDTO> activeUsers = users.stream() .filter(u -> u.isActive()) .filter(u -> !u.getRole().equals(Role.GUEST)) .sorted(Comparator.comparing(User::getName)) .map(u -> new UserDTO(u.getName(), u.getEmail())) .collect(Collectors.toList()); Map<String, List<User>> byDepartment = users.stream() .collect(Collectors.groupingBy(User::getDepartment)); Optional<User> admin = users.stream() .filter(u -> u.getRole().equals(Role.ADMIN)) .findFirst(); ``` ```typescript // --- After (TypeScript) --- interface UserDTO { name: string; email: string; } const activeUsers: UserDTO[] = users .filter((u) => u.active) .filter((u) => u.role !== 'guest') .sort((a, b) => a.name.localeCompare(b.name)) .map((u) => ({ name: u.name, email: u.email })); const byDepartment: Record<string, User[]> = Object.groupBy( users, (u) => u.department, ) as Record<string, User[]>; const admin: User | undefined = users.find((u) => u.role === 'admin'); ``` ### Java enum with behavior → TypeScript const object + functions ```java // --- Before (Java) --- public enum Priority { HIGH(1, "High Priority"), MEDIUM(2, "Medium Priority"), LOW(3, "Low Priority"); private final int level; private final String label; Priority(int level, String label) { this.level = level; this.label = label; } public int getLevel() { return level; } public String getLabel() { return label; } public boolean isUrgent() { return this == HIGH; } } ``` ```typescript // --- After (TypeScript) --- const Priority = { HIGH: { level: 1, label: 'High Priority' }, MEDIUM: { level: 2, label: 'Medium Priority' }, LOW: { level: 3, label: 'Low Priority' }, } as const; type PriorityKey = keyof typeof Priority; type PriorityValue = (typeof Priority)[PriorityKey]; function isUrgent(priority: PriorityValue): boolean { return priority === Priority.HIGH; } ``` ### Lombok @Data/@Builder → TypeScript interface + options constructor ```java // --- Before (Java with Lombok) --- import lombok.Builder; import lombok.Data; import lombok.extern.slf4j.Slf4j; @Data @Builder @Slf4j public class SlackMessage { private final String channel; private final String text; private final String threadTs; private final boolean unfurlLinks; private final List<Attachment> attachments; public void send(WebClient client) { log.info("Sending message to {}", channel); client.chatPostMessage(r -> r .channel(channel) .text(text) .threadTs(threadTs) .unfurlLinks(unfurlLinks) .attachments(attachments)); } } // Usage with builder: SlackMessage msg = SlackMessage.builder() .channel("#general") .text("Hello!") .unfurlLinks(false) .build(); msg.send(client); ``` ```typescript // --- After (TypeScript) --- import pino from 'pino'; const log = pino({ name: 'SlackMessage' }); interface SlackMessageOptions { channel: string; text: string; threadTs?: string; unfurlLinks?: boolean; attachments?: Attachment[]; } // Interface replaces @Data — no getters/setters/equals/hashCode/toString needed // Options object replaces @Builder — named params via destructuring class SlackMessage { readonly channel: string; readonly text: string; readonly threadTs?: string; readonly unfurlLinks: boolean; readonly attachments: Attachment[]; constructor({ channel, text, threadTs, unfurlLinks = false, attachments = [], }: SlackMessageOptions) { this.channel = channel; this.text = text; this.threadTs = threadTs; this.unfurlLinks = unfurlLinks; this.attachments = attachments; } send(client: WebClient): void { log.info(`Sending message to ${this.channel}`); client.chat.postMessage({ channel: this.channel, text: this.text, thread_ts: this.threadTs, unfurl_links: this.unfurlLinks, attachments: this.attachments, }); } } // Usage — options object replaces builder chain: const msg = new SlackMessage({ channel: '#general', text: 'Hello!', unfurlLinks: false, }); msg.send(client); ``` ### CompletableFuture chain → async/await ```java // --- Before (Java) --- import java.util.concurrent.CompletableFuture; public class AsyncSlackClient { private final MethodsClient client; private final ExecutorService executor; public CompletableFuture<String> fetchAndNotify(String userId, String channel) { return CompletableFuture.supplyAsync(() -> client.usersInfo(r -> r.user(userId)), executor) .thenApply(response -> response.getUser().getRealName()) .thenCompose(name -> CompletableFuture.supplyAsync( () -> client.chatPostMessage(r -> r.channel(channel).text("Hello " + name)), executor )) .thenApply(response -> response.getTs()) .exceptionally(ex -> { log.error("Failed: {}", ex.getMessage()); return null; }); } public CompletableFuture<List<String>> fetchMultipleUsers(List<String> userIds) { List<CompletableFuture<String>> futures = userIds.stream() .map(id -> CompletableFuture.supplyAsync( () -> client.usersInfo(r -> r.user(id)).getUser().getRealName(), executor )) .collect(Collectors.toList()); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList())); } } ``` ```typescript // --- After (TypeScript) --- class AsyncSlackClient { constructor(private readonly client: WebClient) {} // CompletableFuture chain → simple async/await async fetchAndNotify(userId: string, channel: string): Promise<string | null> { try { const userResponse = await this.client.users.info({ user: userId }); const name = userResponse.user?.real_name ?? 'Unknown'; const msgResponse = await this.client.chat.postMessage({ channel, text: `Hello ${name}`, }); return msgResponse.ts ?? null; } catch (err) { log.error(`Failed: ${(err as Error).message}`); return null; } } // CompletableFuture.allOf → Promise.all async fetchMultipleUsers(userIds: string[]): Promise<string[]> { const results = await Promise.all( userIds.map(async (id) => { const response = await this.client.users.info({ user: id }); return response.user?.real_name ?? 'Unknown'; }), ); return results; } } ``` ### @FunctionalInterface → TypeScript function types ```java // --- Before (Java) --- @FunctionalInterface public interface BoltEventHandler<E extends Event> { Response apply(EventsApiPayload<E> payload, EventContext context) throws Exception; } @FunctionalInterface public interface Middleware { Response apply(Request req, Response resp, MiddlewareChain chain) throws Exception; } // Usage: app.event(AppMentionEvent.class, (payload, ctx) -> { ctx.say("Hello!"); return ctx.ack(); }); ``` ```typescript // --- After (TypeScript) --- // @FunctionalInterface → type alias for the function signature type BoltEventHandler<E extends Event> = ( payload: EventsApiPayload<E>, context: EventContext, ) => Promise<Response>; type Middleware = ( req: Request, resp: Response, chain: MiddlewareChain, ) => Promise<Response>; // Usage — identical lambda syntax: app.event(AppMentionEvent, async (payload, ctx) => { await ctx.say('Hello!'); return ctx.ack(); }); ``` ## pitfalls - **Null vs undefined**: Java has one null; TypeScript has `null` AND `undefined`. Decide on a convention early. Recommendation: use `undefined` for "not provided" (optional params), `null` for "explicitly empty" (API responses). - **No method overloading at runtime**: Java allows multiple methods with the same name but different signatures. TypeScript supports overload signatures but only one implementation. Merge overloads into a single function with union parameter types. - **Access modifiers are compile-time only**: TypeScript's `private`/`protected` are erased at runtime (unlike Java). For true runtime privacy, use `#privateField` (ES2022 private fields). - **No runtime type checking**: Java's `instanceof` checks actual class identity. TypeScript's `instanceof` works for classes but NOT for interfaces (they're erased). Use discriminated unions or type guard functions instead. - **Collections are not auto-imported**: Java's `List`, `Map`, `Set` are imports from `java.util`. TypeScript's `Array`, `Map`, `Set` are global built-ins — no import needed. But helper methods like `Object.groupBy()` may need a polyfill. - **Checked exceptions disappear**: Java forces callers to handle checked exceptions. TypeScript has no mechanism for this. Document important error conditions in JSDoc comments. - **Java `equals()` vs TS `===`**: Java objects use `.equals()` for value comparison. TS `===` compares references for objects. Use deep-equal libraries or compare relevant fields explicitly. - **Thread safety patterns are dead code**: Remove all `synchronized`, `volatile`, `Lock`, `Atomic*` patterns. JS is single-threaded. Keeping them adds confusion with zero benefit. - **Builder pattern is often unnecessary**: Java builders exist because constructors can't have named parameters. TypeScript objects with optional properties serve the same purpose more concisely. - **Over-engineering inheritance**: Java projects often have deep class hierarchies. In TypeScript, prefer composition and interfaces. Flatten hierarchies where possible — if a class exists only to share one method, use a utility function instead. - **Lombok `@Data` on mutable classes**: If the Java class was mutable (setters used), decide whether TS version should be mutable too. Often the answer is no — make properties `readonly` and create new instances instead of mutating. - **Lombok `@Builder.Default`**: Default values in Lombok builders (`@Builder.Default private boolean unfurlLinks = true`) must become explicit defaults in the TS constructor destructuring: `{ unfurlLinks = true }: Options`. - **`CompletableFuture.join()` blocks the thread**: There is NO blocking equivalent in JS. `await` is non-blocking. Code that uses `join()` for synchronous access must be redesigned to be fully async. - **`ExecutorService` thread pools**: Remove entirely. JS is single-threaded. `Promise.all()` provides concurrency for I/O-bound work without thread management. For CPU-bound work, use worker threads only if profiling shows a bottleneck. - **`@FunctionalInterface` with checked exceptions**: Java functional interfaces can declare `throws Exception`. TypeScript function types cannot. Async functions that reject should document their error types in JSDoc but cannot enforce catching at the type level. ## references - https://www.typescriptlang.org/docs/handbook/2/classes.html -- TS classes and inheritance - https://www.typescriptlang.org/docs/handbook/2/generics.html -- TS generics - https://www.typescriptlang.org/docs/handbook/decorators.html -- TS decorators (annotation equivalent) - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map -- JS Map (HashMap equivalent) - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise -- Promise (Future equivalent) ## instructions Use this expert when rewriting Java source code in TypeScript. Start by identifying the Java patterns in use (class hierarchies, generics, annotations, Lombok annotations, Stream API, CompletableFuture chains, functional interfaces, concurrency, Optional) and map each to its TS equivalent. Focus on simplification: flatten unnecessary class hierarchies, replace Lombok @Data/@Builder with interfaces and options objects, remove builder patterns in favor of typed options objects, rewrite CompletableFuture chains as async/await, convert @FunctionalInterface to type aliases, eliminate synchronization code, and convert getters/setters to direct property access. Pair with `dependency-mapping-ts.md` for Maven/Gradle → npm equivalents, `type-mapping-ts.md` for cross-language type reference, and `json-serialization-ts.md` for Gson/Jackson serialization conversion. ## research Deep Research prompt: "Write a micro expert on converting Java to TypeScript. Cover: class hierarchies to interfaces/classes, generics mapping, annotations to decorators, Stream API to array methods, Optional to nullable types, checked exceptions removal, synchronized/volatile removal, enum with behavior to const objects, getter/setter simplification, builder pattern elimination, and package structure flattening. Include 3 worked examples." -
js-to-ts-ts.md 6.2 KB
# js-to-ts-ts ## purpose Converting JavaScript source files to idiomatic TypeScript — adding type annotations, modernizing module syntax, configuring strict compilation, and handling untyped dependencies. ## rules 1. Rename `.js` files to `.ts` (or `.tsx` for JSX). This is the first mechanical step — TypeScript compiles `.ts` files and ignores `.js` by default unless `allowJs` is set. 2. Convert `require()`/`module.exports` to ESM `import`/`export`. `const x = require('y')` becomes `import x from 'y'` (default) or `import { x } from 'y'` (named). `module.exports = { a, b }` becomes `export { a, b }`. 3. Enable `"strict": true` in `tsconfig.json` from the start. Fixing strict errors during conversion is far easier than enabling strict later and facing hundreds of errors at once. 4. Prefer `interface` over `type` for object shapes — interfaces are extensible and produce better error messages. Use `type` for unions, intersections, and mapped types. 5. Replace `/** @type {X} */` JSDoc annotations with inline TypeScript annotations. JSDoc types are redundant once the file is `.ts`. 6. Add explicit return types to exported functions. Internal/private functions can rely on inference, but public API boundaries should have declared types for documentation and refactor safety. 7. Replace `any` with specific types. When the real type is unknown, prefer `unknown` and narrow with type guards. Use `any` only as a temporary escape hatch, marked with `// TODO: type this`. 8. For untyped npm dependencies, install `@types/{package}` from DefinitelyTyped. If no `@types` package exists, create a minimal `declarations.d.ts` with `declare module '{package}'`. 9. Convert dynamic property access patterns (`obj[key]`) to use `Record<string, T>` or an index signature. Slack bots frequently use `payload[field]` patterns that need explicit typing. 10. Replace `arguments` object usage with rest parameters (`...args: T[]`). Replace `Function.prototype.apply/call` patterns with direct invocation or spread syntax. 11. Convert `var` to `const`/`let`. Prefer `const` unless reassignment is needed. 12. Add `as const` assertions to literal objects and arrays that should not be widened (e.g., configuration objects, route tables). ## patterns ### require/module.exports → ESM import/export ```javascript // --- Before (JS) --- const express = require('express'); const { WebClient } = require('@slack/web-api'); const config = require('./config'); function createApp(port) { const app = express(); app.listen(port); return app; } module.exports = { createApp }; ``` ```typescript // --- After (TS) --- import express from 'express'; import { WebClient } from '@slack/web-api'; import config from './config.js'; function createApp(port: number): express.Application { const app = express(); app.listen(port); return app; } export { createApp }; ``` ### Typing callback-heavy patterns ```javascript // --- Before (JS) --- function fetchData(url, callback) { fetch(url) .then(res => res.json()) .then(data => callback(null, data)) .catch(err => callback(err, null)); } ``` ```typescript // --- After (TS) --- interface FetchResult<T> { data: T; status: number; } async function fetchData<T>(url: string): Promise<FetchResult<T>> { const res = await fetch(url); const data: T = await res.json(); return { data, status: res.status }; } ``` ### Starter tsconfig.json for conversion projects ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "./dist", "rootDir": "./src", "declaration": true, "sourceMap": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` ## pitfalls - **`esModuleInterop` required for CJS default imports**: Without it, `import express from 'express'` fails for CommonJS packages. Always enable `esModuleInterop: true`. - **JSON imports need `resolveJsonModule`**: JS code that does `require('./data.json')` won't work in TS without `resolveJsonModule: true` in tsconfig. - **Implicit `any` in callbacks**: Event handler callbacks like `app.on('data', (msg) => ...)` often infer `any` for parameters. Add explicit types: `(msg: IncomingMessage) => ...`. - **`this` context in class methods**: JS classes using `this` in callbacks lose context. Use arrow functions or add explicit `this` parameter types. - **Optional chaining vs truthy checks**: JS code like `if (obj && obj.prop)` can become `obj?.prop` in TS, but be careful with falsy values (`0`, `""`, `false`) — optional chaining only checks `null`/`undefined`. - **Enum vs union**: Don't reflexively convert string constants to `enum`. Prefer string literal unions (`type Status = 'active' | 'inactive'`) unless you need reverse mapping. - **Missing `@types` packages**: Not all npm packages have types. Check with `npm info @types/{package}` before creating manual declarations. - **`export default` vs `export =`**: Some CJS modules use `export = X` in their type definitions. Import these with `import X from 'module'` (with `esModuleInterop`) not `import { X }`. ## references - https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html - https://www.typescriptlang.org/tsconfig -- tsconfig reference - https://github.com/DefinitelyTyped/DefinitelyTyped -- @types packages - https://www.typescriptlang.org/docs/handbook/2/types-from-types.html -- utility types ## instructions Use this expert when converting JavaScript source files to TypeScript. Start by renaming files and converting module syntax, then progressively add types starting from the public API surface inward. Pair with `type-mapping-ts.md` for cross-language type reference and `dependency-mapping-ts.md` if the JS project uses packages that need TS-typed alternatives. ## research Deep Research prompt: "Write a micro expert on converting JavaScript to TypeScript. Cover: require/module.exports to ESM imports, tsconfig strict mode setup, typing callback patterns, handling untyped dependencies with @types and declaration files, common JS idioms that need TS adaptation (var, arguments, dynamic property access), and a starter tsconfig.json for conversion projects." -
json-serialization-ts.md 11 KB
# json-serialization-ts ## purpose Converting Java Gson/Jackson JSON serialization patterns to TypeScript — replacing custom serializers, `@SerializedName` annotations, polymorphic type factories, and schema validation with native `JSON.parse()`/`JSON.stringify()`, Zod schemas, and discriminated unions. ## rules 1. Java's Gson/Jackson are replaced by the built-in `JSON.parse()` and `JSON.stringify()`. No library needed for basic serialization. Add `zod` only when you need runtime schema validation (external API responses, user input). 2. Gson `@SerializedName("snake_case")` on Java fields maps to a Zod schema with `.transform()` for renaming, or simply use the snake_case keys directly in the TypeScript interface if the JSON wire format is snake_case (common in Slack APIs). 3. **Do NOT rename JSON fields to camelCase in the data layer.** If the API sends `thread_ts`, keep `thread_ts` in your interface. Only convert to camelCase at the application boundary if needed. This avoids serialization bugs and keeps types aligned with API docs. 4. Gson `TypeAdapter` / Jackson `@JsonTypeInfo` + `@JsonSubTypes` for polymorphic deserialization → TypeScript discriminated unions with a `type` field + Zod `z.discriminatedUnion()`. This is the most important pattern for Block Kit model conversion. 5. Gson `GsonBuilder().registerTypeAdapterFactory()` for a family of types → a single Zod discriminated union schema that handles all variants. No factory registration needed — Zod validates and narrows in one step. 6. Java `Date`/`Instant` serialized as epoch seconds or ISO strings → parse with `new Date(epoch * 1000)` or `new Date(isoString)`. Use `z.coerce.date()` in Zod for automatic string-to-Date conversion. 7. Gson `@Expose` / Jackson `@JsonIgnore` for selective serialization → TypeScript `Omit<T, 'field'>` utility type at the serialization boundary, or use a `toJSON()` method on classes. 8. Gson null handling (`serializeNulls()`) → JSON.stringify includes `null` by default but omits `undefined`. Use `null` (not `undefined`) for fields that must appear in the wire format. 9. Java `Map<String, Object>` deserialized as a catch-all → `z.record(z.string(), z.unknown())` for validated records, or `Record<string, unknown>` for type-only. 10. Custom Gson deserializers that inspect JSON structure to decide the concrete type → Zod `.transform()` pipelines or preprocess functions that inspect the raw JSON before validating. 11. Jackson `@JsonCreator` / `@JsonProperty` constructor deserialization → just use Zod `.parse()` which returns a plain object matching the schema. No special constructor needed. 12. Large model hierarchies (like Slack's Block Kit: 15+ block types, 20+ element types) should use **one discriminated union per hierarchy level**, not one giant union. This keeps validation fast and error messages readable. ## patterns ### Gson @SerializedName → TypeScript interface with wire-format keys ```java // --- Before (Java with Gson) --- public class SlackUser { @SerializedName("user_id") private String userId; @SerializedName("real_name") private String realName; @SerializedName("is_admin") private boolean isAdmin; @SerializedName("updated") private long updatedTimestamp; // Gson auto-deserializes: {"user_id":"U123","real_name":"Alice","is_admin":true,"updated":1700000000} } ``` ```typescript // --- After (TypeScript) --- // Keep snake_case keys to match the API wire format interface SlackUser { user_id: string; real_name: string; is_admin: boolean; updated: number; } // Parse with no library — JSON.parse returns the right shape const user: SlackUser = JSON.parse(responseBody); // With Zod for runtime validation (recommended for external API data): import { z } from 'zod'; const SlackUserSchema = z.object({ user_id: z.string(), real_name: z.string(), is_admin: z.boolean(), updated: z.number(), }); type SlackUser = z.infer<typeof SlackUserSchema>; const user = SlackUserSchema.parse(JSON.parse(responseBody)); ``` ### Gson polymorphic TypeAdapter → Zod discriminated union ```java // --- Before (Java with Gson) --- // Custom factory for deserializing Block Kit blocks by "type" field public class GsonLayoutBlockFactory implements JsonDeserializer<LayoutBlock> { @Override public LayoutBlock deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext ctx) { String type = json.getAsJsonObject().get("type").getAsString(); switch (type) { case "section": return ctx.deserialize(json, SectionBlock.class); case "actions": return ctx.deserialize(json, ActionsBlock.class); case "divider": return ctx.deserialize(json, DividerBlock.class); case "header": return ctx.deserialize(json, HeaderBlock.class); case "image": return ctx.deserialize(json, ImageBlock.class); case "context": return ctx.deserialize(json, ContextBlock.class); case "input": return ctx.deserialize(json, InputBlock.class); default: throw new JsonParseException("Unknown block type: " + type); } } } // Registration: Gson gson = new GsonBuilder() .registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory()) .create(); List<LayoutBlock> blocks = gson.fromJson(json, new TypeToken<List<LayoutBlock>>(){}.getType()); ``` ```typescript // --- After (TypeScript with Zod) --- import { z } from 'zod'; // Define each block variant schema const SectionBlockSchema = z.object({ type: z.literal('section'), block_id: z.string().optional(), text: z.object({ type: z.string(), text: z.string() }).optional(), fields: z.array(z.object({ type: z.string(), text: z.string() })).optional(), accessory: z.unknown().optional(), }); const ActionsBlockSchema = z.object({ type: z.literal('actions'), block_id: z.string().optional(), elements: z.array(z.unknown()), }); const DividerBlockSchema = z.object({ type: z.literal('divider'), block_id: z.string().optional(), }); const HeaderBlockSchema = z.object({ type: z.literal('header'), block_id: z.string().optional(), text: z.object({ type: z.literal('plain_text'), text: z.string() }), }); const ImageBlockSchema = z.object({ type: z.literal('image'), block_id: z.string().optional(), image_url: z.string().url(), alt_text: z.string(), }); const ContextBlockSchema = z.object({ type: z.literal('context'), block_id: z.string().optional(), elements: z.array(z.unknown()), }); const InputBlockSchema = z.object({ type: z.literal('input'), block_id: z.string().optional(), label: z.object({ type: z.literal('plain_text'), text: z.string() }), element: z.unknown(), }); // Discriminated union replaces the entire TypeAdapter factory const LayoutBlockSchema = z.discriminatedUnion('type', [ SectionBlockSchema, ActionsBlockSchema, DividerBlockSchema, HeaderBlockSchema, ImageBlockSchema, ContextBlockSchema, InputBlockSchema, ]); type LayoutBlock = z.infer<typeof LayoutBlockSchema>; // Usage — replaces Gson.fromJson() + TypeToken const blocks = z.array(LayoutBlockSchema).parse(JSON.parse(jsonString)); // Each block is automatically narrowed by its `type` field ``` ### Gson custom date handling → Zod coerce ```java // --- Before (Java) --- // Custom Gson adapter for epoch seconds GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Instant.class, (JsonDeserializer<Instant>) (json, type, ctx) -> Instant.ofEpochSecond(json.getAsLong())); ``` ```typescript // --- After (TypeScript with Zod) --- const TimestampSchema = z.number().transform((epoch) => new Date(epoch * 1000)); // Or for ISO string dates: const DateStringSchema = z.string().pipe(z.coerce.date()); // In a larger schema: const EventSchema = z.object({ type: z.string(), event_ts: TimestampSchema, created_at: DateStringSchema.optional(), }); ``` ## pitfalls - **Over-validating internal data**: Use Zod at system boundaries (API responses, webhook payloads, user input). Don't validate data you just created yourself — that's wasteful. - **Renaming fields to camelCase**: Resist the urge to transform `thread_ts` to `threadTs` in the data model. Keep wire-format keys to avoid serialization/deserialization bugs and stay aligned with API docs. Transform at the UI/application boundary if needed. - **Gson lenient mode**: Gson's `setLenient(true)` accepts malformed JSON. `JSON.parse()` is strict by default. If the source data is not strict JSON (trailing commas, single quotes), clean it before parsing. - **Losing type narrowing**: Java's polymorphic deserialization returns the base type. TypeScript's discriminated unions + Zod automatically narrow to the specific variant. Leverage this — use `switch (block.type)` and TS will infer the variant type. - **Huge union schemas**: A single `z.discriminatedUnion()` with 30+ variants is slow to compile and produces unreadable errors. Split into hierarchical unions: `LayoutBlock`, `BlockElement`, `TextObject`, etc. - **`null` vs missing key**: Gson distinguishes between `"field": null` and absent `"field"`. In TS, both become `undefined` with `z.optional()`. Use `z.nullable()` if you need to distinguish null from missing. - **Generic type tokens**: Java's `TypeToken<List<LayoutBlock>>` for generic deserialization has no TS equivalent — it's not needed. `z.array(schema).parse(data)` handles generic arrays directly. - **Circular references**: If Java models have circular references (A references B which references A), Gson handles this via lazy deserialization. Zod schemas can use `z.lazy()` for circular types, but redesign to break the cycle if possible. ## references - https://zod.dev/ -- Zod schema validation library - https://github.com/google/gson -- Gson (source library reference) - https://github.com/FasterXML/jackson -- Jackson (source library reference) - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON -- JSON built-in - https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions -- discriminated unions ## instructions Use this expert when converting Java Gson/Jackson serialization code to TypeScript. The most critical pattern is polymorphic deserialization (TypeAdapter factories → Zod discriminated unions), which affects all Block Kit model conversion. Start by identifying all `@SerializedName` fields and custom TypeAdapter/JsonDeserializer classes, then map them to TypeScript interfaces + Zod schemas. Pair with `java-to-ts-ts.md` for general Java→TS conversion, `type-mapping-ts.md` for type system reference, and `../bridge/ui-block-kit-adaptive-cards-ts.md` if converting Block Kit models to Adaptive Cards. ## research Deep Research prompt: "Write a micro expert for converting Java Gson/Jackson JSON serialization to TypeScript. Cover: @SerializedName to interface fields, polymorphic TypeAdapter factories to Zod discriminated unions, custom deserializers to Zod transforms, date/timestamp handling, null semantics, TypeToken generics elimination, and large model hierarchy strategies. Include worked examples for a Block Kit-style type hierarchy with 7+ variants." -
kotlin-to-ts-ts.md 11.2 KB
# kotlin-to-ts-ts ## purpose Rewriting Kotlin source code as idiomatic TypeScript — covering Kotlin-specific syntax (trailing lambdas, null-safety operators, string templates, `it`, `when`, extension functions) that the Java-to-TS expert does not address. ## rules 1. Kotlin string templates (`"Hello $name"`, `"text ${expr}"`) map directly to TypeScript template literals (`` `Hello ${name}` ``, `` `text ${expr}` ``). Multi-line strings with `.trimIndent()` become TS template literals with no call needed — TS template literals already preserve literal indentation. 2. Kotlin trailing lambda syntax (`app.command("/echo") { req, ctx -> ... }`) maps to a callback argument: `app.command("/echo", async (req, ctx) => { ... })`. The lambda body `{ ... }` becomes `async (...) => { ... }` when the target API is async. 3. Kotlin SAM (Single Abstract Method) conversions — where a lambda replaces a single-method interface — map to TypeScript function arguments directly. `app.event(handler)` where `handler` is a lambda becomes `app.on("event", async (ctx) => { ... })`. 4. Kotlin `it` (implicit single-parameter lambda) must be given an explicit name in TS. `list.filter { it.isActive }` becomes `list.filter((item) => item.isActive)`. Choose a meaningful name from context (`req`, `ctx`, `msg`, `user`, etc.). 5. Kotlin null-safety operator `?.` maps to TS optional chaining `?.`. Kotlin `!!` (non-null assertion) maps to TS `!` (non-null assertion). Kotlin elvis `?:` maps to TS nullish coalescing `??`. Examples: `user?.name` → `user?.name`, `value!!` → `value!`, `name ?: "default"` → `name ?? "default"`. 6. Kotlin `when` expressions map to TS `switch` statements or chained ternaries. `when` with no subject (boolean conditions) maps to `if/else if`. `when` with a subject (value matching) maps to `switch`. If used as an expression (assigned to a variable), prefer chained ternaries or an IIFE wrapping a `switch`. 7. Kotlin `val` maps to `const` (for locals) or `readonly` (for class fields). Kotlin `var` maps to `let`. Never use `var` in the TS output — always `const` or `let`. 8. Kotlin `fun` at package level (top-level functions) maps to TS exported functions: `export function myFn() { ... }`. Kotlin does not require a wrapping class for top-level functions, and neither does TS. 9. Kotlin extension functions (`fun String.toSlug(): String`) have no direct TS equivalent. Convert to a standalone utility function: `function toSlug(s: string): string`. If the extension is on a project type, consider adding a method to the class instead. 10. Kotlin `data class` maps to a TypeScript `interface` (for pure data) or a `class` with constructor shorthand (if methods are needed). `data class User(val name: String, val email: String)` → `interface User { readonly name: string; readonly email: string; }`. Destructuring `val (name, email) = user` → `const { name, email } = user`. 11. Kotlin `object` declarations (singletons) map to a plain TS module-level `const` object or a namespace. `object Config { val port = 3000 }` → `const Config = { port: 3000 } as const`. Kotlin `companion object` maps to `static` members on the class or module-level constants. 12. Kotlin `sealed class` / `sealed interface` maps to TS discriminated union types. `sealed class Result` with subclasses `Success` and `Error` → `type Result = { kind: 'success'; value: T } | { kind: 'error'; error: string }`. 13. Kotlin scope functions (`let`, `run`, `apply`, `also`, `with`) should be inlined rather than translated literally. `user?.let { sendEmail(it) }` → `if (user) sendEmail(user)`. `config.apply { port = 3000; host = "localhost" }` → direct property assignments. 14. Kotlin `listOf()`, `mapOf()`, `mutableListOf()`, `mutableMapOf()` map to TS array/object literals: `listOf("a", "b")` → `["a", "b"]`, `mapOf("key" to "value")` → `{ key: "value" }` or `new Map([["key", "value"]])`. 15. Kotlin `for (item in list)` maps to `for (const item of list)`. Kotlin ranges `for (i in 0 until n)` → `for (let i = 0; i < n; i++)`. Kotlin `for (i in 0..n)` (inclusive) → `for (let i = 0; i <= n; i++)`. 16. Kotlin type casts: `as` (unsafe cast) → TS `as` (type assertion). `as?` (safe cast) → TS has no direct equivalent; use a type guard function or conditional check. 17. Kotlin `::class.java` / `SomeClass::class.java` (class references for reflection) should be removed. In the Slack Bolt SDK, `app.event(AppMentionEvent::class.java) { ... }` becomes a string-based route: `app.on("message", async (ctx) => { ... })` in Teams. ## patterns ### Trailing lambda + ack pattern (Slack Bolt → Teams) ```kotlin // --- Before (Kotlin) --- app.command("/echo") { req, ctx -> val text = "You said ${req.payload.text} at <#${req.payload.channelId}|${req.payload.channelName}>" ctx.respond { it.text(text) } ctx.ack() } ``` ```typescript // --- After (TypeScript, Teams SDK) --- app.on('message', async ({ activity, send }) => { const text = `You said ${activity.text}`; await send(text); }); ``` ### Null-safety chain ```kotlin // --- Before (Kotlin) --- val hash = event.event.view?.hash val name = user?.profile?.displayName ?: "Unknown" val id = data!!.userId ``` ```typescript // --- After (TypeScript) --- const hash = event.event.view?.hash; const name = user?.profile?.displayName ?? 'Unknown'; const id = data!.userId; ``` ### String templates + trimIndent ```kotlin // --- Before (Kotlin) --- val view = """ { "type": "home", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Hello ${user.name}! Updated: ${ZonedDateTime.now()}" } } ] } """.trimIndent() ``` ```typescript // --- After (TypeScript) --- const view = { type: 'home' as const, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `Hello ${user.name}! Updated: ${new Date().toISOString()}`, }, }, ], }; // Prefer a typed object over a JSON string when the target SDK accepts objects. // If a raw string is truly needed: const viewJson = JSON.stringify(view); ``` ### When expression → switch ```kotlin // --- Before (Kotlin) --- val response = when (action) { "approve" -> "Approved!" "reject" -> "Rejected." "defer" -> "Deferred to next week." else -> "Unknown action: $action" } ``` ```typescript // --- After (TypeScript) --- let response: string; switch (action) { case 'approve': response = 'Approved!'; break; case 'reject': response = 'Rejected.'; break; case 'defer': response = 'Deferred to next week.'; break; default: response = `Unknown action: ${action}`; } ``` ### Scope function inlining ```kotlin // --- Before (Kotlin) --- val result = config.apply { port = 3000 host = "localhost" } user?.let { ctx.say("Hello ${it.name}") } val mapped = items.map { it.name to it.value }.toMap() ``` ```typescript // --- After (TypeScript) --- const config = { port: 3000, host: 'localhost' }; if (user) { await send(`Hello ${user.name}`); } const mapped = Object.fromEntries(items.map((item) => [item.name, item.value])); ``` ### Object declaration / companion object ```kotlin // --- Before (Kotlin) --- class ResourceLoader { companion object { fun loadAppConfig(name: String = "appConfig.json"): AppConfig { // ... } } } // Usage: ResourceLoader.loadAppConfig() ``` ```typescript // --- After (TypeScript) --- // Companion object → module-level function (no class wrapper needed) export function loadAppConfig(name = 'appConfig.json'): AppConfig { // ... } // Usage: loadAppConfig() ``` ## pitfalls - **Forgetting to name `it`**: Every Kotlin `it` reference must get an explicit TS parameter name. Blindly searching for `it` will produce false positives on English words — search for `{ it.` and `{ it ->` patterns. - **`trimIndent()` on JSON strings**: Kotlin examples often build JSON as `.trimIndent()` multiline strings. In TS, prefer a typed object literal instead of a string. If the target API needs a string, use `JSON.stringify(obj)` for safety over manual template literals. - **`!!` overuse**: Kotlin's `!!` means "throw if null". TS's `!` is only a compile-time assertion — it does NOT throw at runtime. If the Kotlin code relies on `!!` for runtime safety, add an explicit null check instead. - **`as?` safe cast**: Kotlin's `as?` returns `null` if the cast fails. TS's `as` never fails at runtime (it's a compile-time assertion). Translate `as?` to a type guard check, not a bare `as`. - **Trailing lambda position**: Kotlin allows the last lambda argument to be outside the parentheses. In TS, ALL arguments go inside the parentheses. `app.command("/echo") { req, ctx -> }` → `app.command("/echo", async (req, ctx) => { })`. - **`listOf()` / `mapOf()` immutability**: Kotlin's `listOf()` returns an immutable list. TS arrays are mutable by default. If immutability matters, use `as const` or `ReadonlyArray<T>`. - **Class reference syntax**: `SomeClass::class.java` in Kotlin (used for event type registration in Slack Bolt) has no TS equivalent. Replace with the string event name expected by the target SDK. - **Extension functions on primitives**: Kotlin can extend `String`, `Int`, etc. TS cannot extend primitive types. Always convert to standalone functions. - **Destructuring data classes**: Kotlin `val (a, b) = pair` uses `componentN()` functions. TS destructuring uses property names: `const { first, second } = pair`. The names must match. ## references - https://kotlinlang.org/docs/basic-syntax.html — Kotlin syntax reference - https://kotlinlang.org/docs/null-safety.html — Kotlin null-safety operators - https://kotlinlang.org/docs/lambdas.html — Kotlin lambda syntax and SAM conversions - https://kotlinlang.org/docs/scope-functions.html — let, run, apply, also, with - https://kotlinlang.org/docs/data-classes.html — Data classes - https://kotlinlang.org/docs/extensions.html — Extension functions - https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html — TS template literals - https://www.typescriptlang.org/docs/handbook/2/narrowing.html — TS type narrowing and guards ## instructions Use this expert when the source code is Kotlin (`.kt` files). It handles Kotlin-specific syntax that the `java-to-ts-ts.md` expert does not cover: trailing lambdas, `it` implicit parameters, string templates, `trimIndent()`, null-safety operators (`?.`, `!!`, `?:`), `when` expressions, scope functions, extension functions, `data class`, `object`/`companion object`, sealed classes, and `::class.java` references. For Java SDK types, generics, collections, and Lombok patterns, pair with `java-to-ts-ts.md`. For type system mapping, pair with `type-mapping-ts.md`. ## research Deep Research prompt: "Write a micro expert on converting Kotlin to TypeScript. Cover: string templates to template literals, trailing lambda syntax to callback arguments, SAM conversions, it implicit parameter, null-safety operators (?. !! ?:) to optional chaining/nullish coalescing/non-null assertion, when expressions to switch, val/var to const/let, extension functions to utility functions, data class to interface, object declarations to module constants, sealed class to discriminated unions, scope functions (let/run/apply/also/with) inlining, and class reference syntax removal. Include 4-5 worked side-by-side examples." -
ruby-to-ts-ts.md 9.7 KB
# ruby-to-ts-ts ## purpose Rewriting Ruby source code as idiomatic TypeScript — mapping Ruby language constructs, OOP patterns, metaprogramming, and common idioms to their TypeScript equivalents. ## rules 1. Ruby blocks (`do...end` / `{ |x| }`) map to arrow functions. `array.each { |item| puts item }` becomes `array.forEach((item) => console.log(item))`. Ruby's `yield` inside methods maps to calling a callback parameter. 2. Ruby mixins (`include Module`) map to TypeScript interfaces + composition. Do NOT use class inheritance to simulate mixins — use interface implementation with helper functions or the mixin pattern (`applyMixins`). 3. Ruby duck typing maps to TypeScript structural typing. If Ruby code checks `obj.respond_to?(:method)`, define an interface with that method and use a type guard: `function hasMethod(obj: unknown): obj is HasMethod`. 4. Ruby `attr_accessor :name` maps to a class property with TypeScript accessor shorthand: `constructor(public name: string) {}` or explicit `get`/`set` pairs if logic is needed. 5. Ruby symbols (`:name`) map to string literal types or enum members. A method accepting `type: :admin | :user` becomes `type: 'admin' | 'user'` in TypeScript. 6. Ruby hashes (`{ key: value }`) map to TypeScript objects or `Record<string, T>`. Named-parameter hashes (`def method(opts = {})`) become destructured typed parameters: `function method({ key1, key2 }: Options)`. 7. Ruby `nil` maps to `null` or `undefined`. Use `null` for explicit absence and `undefined` for optional/missing. Ruby's `&.` safe navigator maps to optional chaining (`?.`). 8. Ruby `begin/rescue/ensure` maps to `try/catch/finally`. Ruby's typed rescue (`rescue TypeError => e`) maps to catching and narrowing: `catch (e) { if (e instanceof TypeError) ... }`. 9. Ruby open classes and monkey-patching have NO TypeScript equivalent. Redesign as wrapper functions, decorator patterns, or module augmentation (`declare module` for extending third-party types). 10. Ruby metaprogramming (`define_method`, `method_missing`, `send`) has no direct equivalent. Replace `define_method` loops with computed property patterns or factory functions. Replace `method_missing` with `Proxy` objects (sparingly) or explicit handler maps. 11. Ruby's `Enumerable` methods map to JavaScript array methods: `map`→`map`, `select`→`filter`, `reject`→`filter` (inverted), `reduce`→`reduce`, `detect`/`find`→`find`, `flat_map`→`flatMap`, `each_with_object`→`reduce`, `group_by`→custom `groupBy` or `Object.groupBy()`. 12. Ruby string interpolation `"Hello #{name}"` maps to template literals `` `Hello ${name}` ``. 13. Ruby `Proc.new` / `lambda` / `->` all map to arrow functions. Ruby's distinction between procs and lambdas (arity checking, return behavior) disappears — TypeScript arrow functions always behave like Ruby lambdas. 14. Ruby modules used as namespaces map to TypeScript modules (files) with named exports. Do NOT use TypeScript `namespace` keyword — use ES module `export` instead. ## patterns ### Ruby class with mixins → TypeScript interface + composition ```ruby # --- Before (Ruby) --- module Greetable def greet "Hello, I'm #{name}" end end module Trackable def track(event) puts "Tracking #{event} for #{name}" end end class User include Greetable include Trackable attr_accessor :name, :email def initialize(name, email) @name = name @email = email end end user = User.new("Alice", "alice@example.com") puts user.greet user.track("login") ``` ```typescript // --- After (TypeScript) --- interface Greetable { name: string; greet(): string; } function greetMixin<T extends { name: string }>(obj: T): T & Greetable { return Object.assign(obj, { greet() { return `Hello, I'm ${obj.name}`; }, }); } interface Trackable { name: string; track(event: string): void; } function trackMixin<T extends { name: string }>(obj: T): T & Trackable { return Object.assign(obj, { track(event: string) { console.log(`Tracking ${event} for ${obj.name}`); }, }); } class User { constructor( public name: string, public email: string, ) {} } // Apply mixins function createUser(name: string, email: string): User & Greetable & Trackable { const user = new User(name, email); return trackMixin(greetMixin(user)); } const user = createUser("Alice", "alice@example.com"); console.log(user.greet()); user.track("login"); ``` ### Ruby hash options / keyword args → TypeScript typed parameters ```ruby # --- Before (Ruby) --- class SlackNotifier def initialize(opts = {}) @webhook_url = opts[:webhook_url] || ENV['SLACK_WEBHOOK'] @channel = opts[:channel] || '#general' @username = opts[:username] || 'bot' end def notify(message, opts = {}) icon = opts.fetch(:icon_emoji, ':robot_face:') thread_ts = opts[:thread_ts] # ... send notification end end notifier = SlackNotifier.new(webhook_url: 'https://...', channel: '#alerts') notifier.notify('Deploy complete', icon_emoji: ':rocket:') ``` ```typescript // --- After (TypeScript) --- interface SlackNotifierOptions { webhookUrl?: string; channel?: string; username?: string; } interface NotifyOptions { iconEmoji?: string; threadTs?: string; } class SlackNotifier { private readonly webhookUrl: string; private readonly channel: string; private readonly username: string; constructor({ webhookUrl = process.env.SLACK_WEBHOOK ?? '', channel = '#general', username = 'bot', }: SlackNotifierOptions = {}) { this.webhookUrl = webhookUrl; this.channel = channel; this.username = username; } notify(message: string, { iconEmoji = ':robot_face:', threadTs }: NotifyOptions = {}): void { // ... send notification } } const notifier = new SlackNotifier({ webhookUrl: 'https://...', channel: '#alerts' }); notifier.notify('Deploy complete', { iconEmoji: ':rocket:' }); ``` ### Ruby Enumerable → TypeScript array methods ```ruby # --- Before (Ruby) --- users = get_users() active_admins = users .select { |u| u.active? } .reject { |u| u.guest? } .select { |u| u.role == :admin } .map { |u| { name: u.name, email: u.email } } .sort_by { |h| h[:name] } ``` ```typescript // --- After (TypeScript) --- interface User { name: string; email: string; active: boolean; guest: boolean; role: 'admin' | 'user' | 'guest'; } const users: User[] = getUsers(); const activeAdmins = users .filter((u) => u.active) .filter((u) => !u.guest) .filter((u) => u.role === 'admin') .map((u) => ({ name: u.name, email: u.email })) .sort((a, b) => a.name.localeCompare(b.name)); ``` ## pitfalls - **Ruby truthiness vs JS truthiness**: In Ruby, only `nil` and `false` are falsy. In JS/TS, `0`, `""`, `NaN`, `null`, `undefined`, and `false` are all falsy. Ruby code like `if count` (truthy when 0) must become `if (count !== null && count !== undefined)` in TS. - **Ruby `==` is value equality; JS `===` is identity for objects**: Ruby `==` on strings/numbers compares values. TS `===` on primitives works the same, but on objects it compares references. Deep equality requires a library or custom check. - **`each` return value**: Ruby's `each` returns the original array. JS `forEach` returns `undefined`. Don't chain after `forEach`. - **Ruby ranges (`1..10`)**: No TS equivalent. Use `Array.from({ length: 10 }, (_, i) => i + 1)` or a simple `for` loop. - **String is mutable in Ruby, immutable in JS**: Ruby `str.gsub!` mutates in place. TS strings are immutable — always reassign: `str = str.replace(...)`. - **Ruby exception hierarchy**: Ruby has `StandardError`, `RuntimeError`, etc. TS/JS only has `Error`. Use custom error classes extending `Error` if you need a hierarchy. - **Snake_case to camelCase**: Ruby uses `snake_case` for methods/variables. TypeScript convention is `camelCase`. Convert all identifiers, but keep API payloads in their original format (e.g., Slack payloads use `snake_case`). - **Ruby `require` is file-level, not scoped**: All Ruby `require` statements load globally. TS `import` is scoped to the file. This means Ruby's implicit global availability must become explicit imports in every file that uses the dependency. - **Sinatra/Rack → Express**: Ruby Sinatra routes (`get '/' do ... end`) map to Express (`app.get('/', (req, res) => { ... })`). The middleware patterns are similar but request/response APIs differ completely. ## references - https://www.typescriptlang.org/docs/handbook/2/classes.html -- TS classes - https://www.typescriptlang.org/docs/handbook/2/objects.html -- structural typing - https://www.typescriptlang.org/docs/handbook/mixins.html -- mixin pattern - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array -- JS array methods (Enumerable equivalents) - https://ruby-doc.org/core/Enumerable.html -- Ruby Enumerable reference (for mapping) ## instructions Use this expert when rewriting Ruby source code in TypeScript. Start by identifying the Ruby constructs in use (classes, modules/mixins, blocks, metaprogramming, Enumerable chains) and map each to its TS equivalent using the rules above. Pay special attention to truthiness differences, mixin patterns, and naming convention changes (snake_case → camelCase). Pair with `dependency-mapping-ts.md` for gem → npm package equivalents, and `type-mapping-ts.md` for cross-language type reference. ## research Deep Research prompt: "Write a micro expert on converting Ruby to TypeScript. Cover: blocks to arrow functions, mixins to interfaces/composition, duck typing to structural typing, attr_accessor to class properties, symbol to string literals, hash options to typed parameters, metaprogramming alternatives, Enumerable methods to array methods, exception handling, truthiness differences, and naming convention conversion (snake_case to camelCase). Include 3 worked examples." -
type-mapping-ts.md 10.1 KB
# type-mapping-ts ## purpose Cross-language type system mapping reference — translating type concepts from JavaScript, Ruby, and Java into idiomatic TypeScript types, covering primitives, nullability, generics, collections, enums, and structural patterns. ## rules 1. Map primitive types using the canonical table below. TypeScript uses lowercase for primitives (`string`, `number`, `boolean`) — never use the wrapper types (`String`, `Number`, `Boolean`). 2. Nullable types: Java `@Nullable T` and Ruby's implicit nil-ability both map to `T | null`. For optional parameters/properties, use `T | undefined` (or the `?` optional marker). Distinguish between "explicitly null" and "not provided". 3. Generic type parameters use the same `<T>` syntax across Java and TypeScript. Ruby has no generics — infer types from usage patterns and add explicit generic parameters during conversion. 4. Collection types: Java `List<T>` → `T[]`, Java `Map<K,V>` → `Map<K,V>` or `Record<string, V>`, Java `Set<T>` → `Set<T>`. Ruby `Array` → `T[]`, Ruby `Hash` → `Record<string, T>` or `Map`. 5. Union types (`A | B`) are TypeScript's killer feature with no direct Java or Ruby equivalent. Use them liberally to replace: Java method overloading, Ruby duck-typed parameters that accept multiple types, and stringly-typed fields. 6. Discriminated unions replace Java's visitor pattern and Ruby's case-when on class type. Add a `type` or `kind` literal field to each variant for exhaustive narrowing. 7. TypeScript `unknown` is safer than `any`. Use `unknown` for values from external sources (API responses, user input, parsed JSON) and narrow with type guards. Reserve `any` for temporary migration scaffolding. 8. Ruby symbols (`:name`) and Java string constants (`public static final String`) both map to string literal types: `type Role = 'admin' | 'user' | 'guest'`. 9. Java `void` maps to TypeScript `void`. Ruby methods that return `nil` implicitly map to `void` return type (or `T | undefined` if the nil return is meaningful). 10. Tuple types (`[string, number]`) are useful when converting Ruby methods that return multiple values (`return name, age`) or Java `Pair<A, B>` / `Map.Entry<K, V>`. 11. Use `readonly` modifier for properties that were `final` in Java or `freeze`-d in Ruby. Use `Readonly<T>` utility type for deeply immutable objects. 12. Index signatures (`[key: string]: T`) replace Java's `Map<String, Object>` and Ruby's open hashes when the key set is not known at compile time. ## patterns ### Primitive type mapping table | Concept | Java | Ruby | JavaScript | TypeScript | |---|---|---|---|---| | String | `String` | `String` | `string` | `string` | | Integer | `int` / `Integer` | `Integer` / `Fixnum` | `number` | `number` | | Float | `double` / `Double` / `float` | `Float` | `number` | `number` | | Big integer | `BigInteger` / `long` | `Bignum` | `bigint` | `bigint` | | Boolean | `boolean` / `Boolean` | `TrueClass`/`FalseClass` | `boolean` | `boolean` | | Null | `null` | `nil` | `null` | `null` | | Undefined | N/A | N/A | `undefined` | `undefined` | | Void | `void` | implicit nil | `undefined` | `void` | | Any/Object | `Object` | `Object` | `any` | `unknown` (preferred) or `any` | | Byte array | `byte[]` | `String` (binary) | `Uint8Array` | `Uint8Array` or `Buffer` | | Date/Time | `LocalDateTime` / `Instant` | `Time` / `DateTime` | `Date` | `Date` or `Temporal` (stage 3) | | Regex | `Pattern` | `Regexp` | `RegExp` | `RegExp` | | Symbol | N/A | `Symbol` (`:name`) | `symbol` / string | string literal type | ### Collection type mapping table | Concept | Java | Ruby | TypeScript | |---|---|---|---| | Ordered list | `List<T>` / `ArrayList<T>` | `Array` | `T[]` or `Array<T>` | | Fixed-size tuple | `Pair<A,B>` / `record` (Java 16+) | `[a, b]` array | `[A, B]` tuple | | Key-value map (string keys) | `Map<String, V>` | `Hash` | `Record<string, V>` | | Key-value map (any keys) | `Map<K, V>` | `Hash` | `Map<K, V>` | | Unique set | `Set<T>` / `HashSet<T>` | `Set` | `Set<T>` | | Queue | `Queue<T>` / `Deque<T>` | `Array` (push/shift) | `T[]` (push/shift) | | Immutable list | `List.of()` / `Collections.unmodifiable` | `freeze` | `readonly T[]` or `ReadonlyArray<T>` | | Immutable map | `Map.of()` | `freeze` | `Readonly<Record<string, V>>` | ### Nullability pattern mapping ```typescript // Java Optional<T> → TypeScript // Java: Optional<User> findUser(String id) // Ruby: def find_user(id) → User or nil // TS: function findUser(id: string): User | null { const user = db.get(id); return user ?? null; } // Java Optional chain → TypeScript optional chaining // Java: user.flatMap(u -> u.getAddress()).map(a -> a.getCity()).orElse("Unknown") // Ruby: user&.address&.city || "Unknown" // TS: const city = user?.address?.city ?? 'Unknown'; // Java @Nullable parameter → TypeScript optional parameter // Java: void send(String msg, @Nullable String channel) // Ruby: def send(msg, channel = nil) // TS: function send(msg: string, channel?: string): void { const target = channel ?? '#general'; // ... } ``` ### Discriminated union (replaces Java visitor / Ruby case-when on type) ```java // --- Java (before) --- // Visitor pattern with 3 message types public interface MessageVisitor { void visit(TextMessage msg); void visit(CardMessage msg); void visit(FileMessage msg); } public abstract class Message { public abstract void accept(MessageVisitor visitor); } ``` ```ruby # --- Ruby (before) --- # Case-when on class type case message when TextMessage handle_text(message) when CardMessage handle_card(message) when FileMessage handle_file(message) end ``` ```typescript // --- TypeScript (after) --- // Discriminated union replaces both patterns interface TextMessage { kind: 'text'; content: string; } interface CardMessage { kind: 'card'; cardJson: Record<string, unknown>; } interface FileMessage { kind: 'file'; url: string; mimeType: string; } type Message = TextMessage | CardMessage | FileMessage; function handleMessage(msg: Message): void { switch (msg.kind) { case 'text': console.log(msg.content); // TS narrows to TextMessage break; case 'card': renderCard(msg.cardJson); // TS narrows to CardMessage break; case 'file': downloadFile(msg.url); // TS narrows to FileMessage break; } // Exhaustive — adding a new variant causes a compile error } ``` ### Generics mapping ```java // --- Java (before) --- public class Repository<T extends Entity> { private final Map<String, T> store = new HashMap<>(); public Optional<T> findById(String id) { return Optional.ofNullable(store.get(id)); } public List<T> findAll(Predicate<T> filter) { return store.values().stream() .filter(filter) .collect(Collectors.toList()); } } ``` ```typescript // --- TypeScript (after) --- interface Entity { id: string; } class Repository<T extends Entity> { private readonly store = new Map<string, T>(); findById(id: string): T | null { return this.store.get(id) ?? null; } findAll(filter: (item: T) => boolean): T[] { return [...this.store.values()].filter(filter); } } ``` ## pitfalls - **`number` covers both int and float**: TypeScript has no integer type. If integer precision matters (IDs, counters), document the expectation or use `bigint` for very large values. - **`null` vs `undefined` confusion**: Pick a convention. Recommendation: `undefined` for "optional/missing" (function params, object properties), `null` for "explicitly empty" (API responses, database NULLs). - **Wrapper types**: Never use `String`, `Number`, `Boolean` as types in TypeScript. Always use lowercase `string`, `number`, `boolean`. - **Java `int` overflow**: Java `int` is 32-bit; TypeScript `number` is 64-bit float. Values above `Number.MAX_SAFE_INTEGER` (2^53-1) lose precision. Use `bigint` if the Java code relies on exact large integer arithmetic. - **Ruby's open type system**: Ruby allows adding methods to any object at runtime. TypeScript's type system is closed at compile time. Methods discovered via `method_missing` or `define_method` must be predefined in interfaces. - **Enum pitfalls**: TypeScript numeric enums have reverse mapping (`Priority[1] === 'HIGH'`), which is usually unexpected. Prefer string literal unions or `as const` objects. - **Generic variance**: Java has `? extends T` (covariant) and `? super T` (contravariant). TypeScript uses structural subtyping and generally infers variance. Explicit variance annotations (`in`/`out` modifiers) exist but are rarely needed. - **Date handling**: Java's `java.time` and Ruby's `Time`/`DateTime` are far richer than JS `Date`. For serious date work, use `date-fns` or `luxon` rather than relying on the built-in `Date`. ## references - https://www.typescriptlang.org/docs/handbook/2/everyday-types.html -- basic types - https://www.typescriptlang.org/docs/handbook/2/narrowing.html -- type narrowing and guards - https://www.typescriptlang.org/docs/handbook/2/generics.html -- generics - https://www.typescriptlang.org/docs/handbook/utility-types.html -- Readonly, Partial, Pick, etc. - https://www.typescriptlang.org/docs/handbook/2/types-from-types.html -- advanced type construction ## instructions Use this expert as a cross-language type reference when converting from any source language to TypeScript. Consult the primitive and collection mapping tables first, then use the nullability and generics patterns for complex type scenarios. This expert is a dependency of all three language-specific conversion experts — they reference it for type translation questions. Pair with the appropriate language expert (`js-to-ts-ts.md`, `ruby-to-ts-ts.md`, or `java-to-ts-ts.md`) for language-specific idiom conversion beyond types. ## research Deep Research prompt: "Write a micro expert for cross-language type mapping to TypeScript. Cover: primitive type mapping from Java/Ruby/JS to TS, collection type mapping (List, Map, Set, Queue), nullability patterns (Optional, nil, null/undefined), generic type parameter translation, discriminated unions replacing visitor/case-when patterns, enum mapping strategies, and common type system pitfalls when converting from statically-typed (Java) and dynamically-typed (Ruby/JS) languages."
-
-
deploy
-
aws-bot-deploy-ts.md 14.6 KB
# aws-bot-deploy-ts ## purpose Step-by-step deployment of a Slack bot or Teams bot to AWS. Covers AWS CLI setup, IAM configuration, compute provisioning (Lambda + API Gateway / EC2 / ECS Fargate), environment configuration, and verification. Teams bots on AWS still require an Azure Bot Service registration for the Bot Framework messaging endpoint. ## rules 1. **Install prerequisites.** You need: Node.js 20 LTS, AWS CLI v2, and optionally AWS SAM CLI (`pip install aws-sam-cli`) or AWS CDK (`npm install -g aws-cdk`). Verify with `aws --version` and `node --version`. [docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 2. **Configure AWS credentials.** Run `aws configure` and enter your IAM access key, secret key, default region, and output format. For SSO-enabled organizations, use `aws sso login` instead. Verify with `aws sts get-caller-identity`. [docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) 3. **Create an IAM user or role for the bot.** The bot's execution role needs permissions for: CloudWatch Logs (logging), Secrets Manager or SSM Parameter Store (credentials), and any other AWS services it accesses. Use least-privilege — don't give the bot AdministratorAccess. [docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) 4. **Create a Slack API app at api.slack.com.** Under "OAuth & Permissions", install the app to your workspace and copy the Bot User OAuth Token (`xoxb-...`). Under "Basic Information", copy the Signing Secret. For Socket Mode, also create an App-Level Token (`xapp-...`). [api.slack.com/authentication/basics](https://api.slack.com/authentication/basics) 5. **Choose your compute target.** Lambda + API Gateway (serverless, event-driven — best for HTTP-mode Slack bots), EC2 or Elastic Beanstalk (always-on — required for Socket Mode, good for Teams bots), or ECS Fargate (containerized, production-grade). [docs.aws.amazon.com/lambda/latest/dg/welcome.html](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) 6. **For Lambda: use SAM or CDK to define the stack.** A SAM template defines the Lambda function + API Gateway in YAML. `sam build && sam deploy --guided` handles packaging, uploading, and CloudFormation stack creation. [docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) 7. **For Lambda: handle the Slack 3-second ack deadline.** Lambda cold starts can take 1-5 seconds. Use provisioned concurrency (`ProvisionedConcurrencyConfig` in SAM) to keep warm instances, or use the async pattern: immediately return 200 to ack, then process via SQS + a second Lambda. [docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html](https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html) 8. **Socket Mode cannot run on Lambda.** Socket Mode requires a persistent WebSocket connection — Lambda functions are ephemeral. Use EC2, Elastic Beanstalk, or ECS Fargate for Socket Mode bots. HTTP-mode Slack bots work fine on Lambda. 9. **Store secrets in Secrets Manager or SSM Parameter Store.** Never put `SLACK_BOT_TOKEN` or `CLIENT_SECRET` in Lambda environment variables in plaintext for production. Use the SDK to fetch secrets at runtime: `const client = new SecretsManagerClient({}); const secret = await client.send(new GetSecretValueCommand({ SecretId: "bot/slack" }))`. [docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) 10. **For Teams bots on AWS: you still need Azure Bot Service.** Register an App Registration in Entra ID (Azure AD), create a Bot Service resource, and set the messaging endpoint to your AWS URL (e.g., `https://<api-id>.execute-api.<region>.amazonaws.com/api/messages`). Configure `MicrosoftAppId` and `MicrosoftAppPassword` in your AWS environment. [learn.microsoft.com/azure/bot-service/bot-service-quickstart-registration](https://learn.microsoft.com/azure/bot-service/bot-service-quickstart-registration) 11. **Configure Slack app URLs after deployment.** Once your API Gateway or EC2 instance is live, set the Event Subscriptions Request URL and Interactivity URL in the Slack app dashboard to your endpoint (e.g., `https://<api-id>.execute-api.<region>.amazonaws.com/slack/events`). Slack sends a verification challenge immediately — the app must be running. 12. **Set up CloudWatch alarms for error monitoring.** Create alarms for Lambda errors (`Errors` metric > 0), API Gateway 5xx responses, and invocation duration. Use `aws cloudwatch put-metric-alarm` or define them in your SAM/CDK template. [docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html) ## interview ### Q1 — Compute Target ``` question: "Which AWS compute target do you want to deploy to?" header: "Compute" options: - label: "Lambda + API Gateway (Recommended)" description: "Serverless, pay-per-invocation. Best for HTTP-mode Slack bots. Cannot use Socket Mode." - label: "EC2 / Elastic Beanstalk" description: "Always-on VM. Supports Socket Mode, good for Teams bots. ~$8/month for t3.micro." - label: "ECS / Fargate" description: "Containerized, production-grade. Auto-scaling, no server management. Good for high-traffic bots." - label: "You Decide Everything" description: "Use Lambda + API Gateway (recommended default) and skip remaining questions." multiSelect: false ``` ### Q2 — Infrastructure as Code ``` question: "How do you want to define your infrastructure?" header: "IaC" options: - label: "AWS SAM (Recommended)" description: "YAML templates for Lambda + API Gateway. sam build && sam deploy — simple and well-documented." - label: "AWS CDK" description: "Define infrastructure in TypeScript. Full AWS resource control. More flexible but more setup." - label: "Manual CLI" description: "Step-by-step aws CLI commands. Learn exactly what resources are created." - label: "You Decide Everything" description: "Use AWS SAM (recommended default) and skip remaining questions." multiSelect: false ``` ### defaults table | Question | Default | |---|---| | Q1 | Lambda + API Gateway | | Q2 | AWS SAM | ## patterns ### Slack bot on Lambda with SAM ```yaml # template.yaml (SAM template) AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Slack bot on Lambda Globals: Function: Timeout: 30 Runtime: nodejs20.x MemorySize: 256 Resources: SlackBotFunction: Type: AWS::Serverless::Function Properties: Handler: dist/lambda.handler CodeUri: . Events: SlackEvents: Type: HttpApi Properties: Path: /slack/events Method: POST Environment: Variables: SLACK_SECRET_NAME: bot/slack # reference, not the actual secret Policies: - SecretsManagerReadWrite Outputs: SlackEndpoint: Description: "URL for Slack Event Subscriptions" Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/slack/events" ``` ```bash # Deploy the SAM stack sam build sam deploy --guided \ --stack-name slack-bot \ --capabilities CAPABILITY_IAM \ --resolve-s3 # Output shows the API Gateway URL — use it as Slack Request URL ``` ```typescript // src/lambda.ts — Lambda handler wrapping Bolt import { App, AwsLambdaReceiver } from "@slack/bolt"; const awsReceiver = new AwsLambdaReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET!, }); const app = new App({ token: process.env.SLACK_BOT_TOKEN!, receiver: awsReceiver, }); app.message("hello", async ({ say }) => { await say("Hi from Lambda!"); }); export const handler = async (event: any, context: any, callback: any) => { const handler = await awsReceiver.start(); return handler(event, context, callback); }; ``` ### Slack bot on EC2 with Elastic Beanstalk ```bash # 1. Install EB CLI pip install awsebcli # 2. Initialize the project eb init slack-bot --platform "Node.js 20" --region us-east-1 # 3. Create the environment eb create slack-bot-env --single --instance-types t3.micro # 4. Set environment variables eb setenv \ SLACK_BOT_TOKEN=xoxb-your-token \ SLACK_SIGNING_SECRET=your-signing-secret \ SLACK_APP_TOKEN=xapp-your-app-token \ PORT=8080 # 5. Deploy eb deploy # 6. Get the URL eb status # shows CNAME: slack-bot-env.us-east-1.elasticbeanstalk.com # Configure Slack Request URL: # https://slack-bot-env.us-east-1.elasticbeanstalk.com/slack/events ``` ### Teams bot on AWS (Lambda + Azure Bot Service) ```bash # Step 1: Deploy to AWS (same as Slack bot SAM pattern, but different routes) # In template.yaml, use Path: /api/messages instead of /slack/events # Step 2: Register in Azure (required for Teams) az login APP_ID=$(az ad app create --display-name "MyBot-AWS" --query appId -o tsv) APP_SECRET=$(az ad app credential reset --id $APP_ID --query password -o tsv) TENANT_ID=$(az account show --query tenantId -o tsv) az bot create \ --resource-group rg-mybot \ --name mybot-aws \ --app-type SingleTenant \ --appid $APP_ID \ --tenant-id $TENANT_ID az bot msteams create --resource-group rg-mybot --name mybot-aws # Step 3: Set the messaging endpoint to your AWS URL API_URL="https://abc123.execute-api.us-east-1.amazonaws.com/api/messages" az bot update --resource-group rg-mybot --name mybot-aws --endpoint $API_URL # Step 4: Add Azure credentials to AWS Lambda environment aws lambda update-function-configuration \ --function-name MyTeamsBot \ --environment "Variables={MicrosoftAppId=$APP_ID,MicrosoftAppPassword=$APP_SECRET,MicrosoftAppTenantId=$TENANT_ID}" ``` ### Socket Mode on EC2 (long-running process) ```bash # Socket Mode requires a persistent WebSocket — use EC2 or ECS, not Lambda # 1. Launch an EC2 instance (Amazon Linux 2023, t3.micro) aws ec2 run-instances \ --image-id ami-0c02fb55956c7d316 \ --instance-type t3.micro \ --key-name my-key \ --security-group-ids sg-xxx \ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=slack-bot}]' # 2. SSH in and install Node.js ssh -i my-key.pem ec2-user@<public-ip> curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - sudo yum install -y nodejs # 3. Clone, install, build git clone https://github.com/your-org/your-bot.git cd your-bot && npm install && npm run build # 4. Set environment variables export SLACK_BOT_TOKEN=xoxb-your-token export SLACK_APP_TOKEN=xapp-your-app-token export SLACK_SIGNING_SECRET=your-signing-secret # 5. Run with PM2 for process management npm install -g pm2 pm2 start dist/index.js --name slack-bot pm2 save pm2 startup # auto-restart on reboot ``` ## pitfalls - **Lambda cold starts causing Slack ack timeout.** Node.js Lambda cold starts take 1-5 seconds. If your handler does any work before calling `ack()`, you'll exceed the 3-second Slack deadline. Use provisioned concurrency, or ack immediately and process asynchronously. - **Socket Mode on Lambda.** Socket Mode requires a persistent WebSocket connection. Lambda functions are ephemeral — they spin down after the request completes. Use EC2, Elastic Beanstalk, or ECS Fargate for Socket Mode. - **Forgetting Azure Bot Service for Teams bots.** Even though your bot runs on AWS, Teams bots require an Azure Bot Service resource with the messaging endpoint pointing to your AWS URL. Without it, Teams cannot discover or route messages to your bot. - **API Gateway default timeout.** API Gateway has a 29-second integration timeout. For most bot handlers this is fine, but long-running AI inference calls may exceed it. Use async invocation patterns for heavy processing. - **Lambda function URL vs API Gateway.** Function URLs are simpler (no API Gateway needed) but lack WAF, throttling, and custom domain support. Use API Gateway for production bots that need rate limiting or custom domains. - **Missing IAM permissions for Secrets Manager.** If your Lambda execution role doesn't include `secretsmanager:GetSecretValue`, the bot crashes when trying to fetch credentials. Add the policy to the SAM template or IAM role. - **Elastic Beanstalk port mismatch.** EB expects your app to listen on port 8080 by default (configurable). If your bot hardcodes port 3000, the health check fails and EB marks the instance unhealthy. Always use `process.env.PORT`. ## references - https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html - https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html - https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html - https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html - https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_nodejs.html - https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html - https://api.slack.com/authentication/basics - https://slack.dev/bolt-js/deployments/aws-lambda - https://learn.microsoft.com/azure/bot-service/bot-service-quickstart-registration ## instructions This expert walks through deploying a bot to AWS from scratch — from installing the CLI to verifying a test message. Use it when a developer says "deploy my bot to AWS", "set up Lambda hosting", or "get my bot running on AWS". Covers Slack bots (Lambda or EC2), Teams bots (requires Azure Bot Service + AWS hosting), and Socket Mode considerations. Pair with: `../slack/runtime.bolt-foundations-ts.md` (Bolt app setup for receiver selection), `../slack/bolt-oauth-distribution-ts.md` (OAuth for multi-workspace Slack apps), `../security/secrets-ts.md` (secrets best practices), `../bridge/infra-compute-ts.md` (if comparing AWS compute options with Azure equivalents), `azure-bot-deploy-ts.md` (if also needing Azure Bot Service for Teams). ## research Deep Research prompt: "Write a micro expert on deploying a Slack Bolt.js or Microsoft Teams bot to AWS. Cover: AWS CLI v2 installation, aws configure / aws sso login, IAM role creation for bot execution, Lambda + API Gateway deployment with SAM (template.yaml, sam build, sam deploy), AwsLambdaReceiver from @slack/bolt, EC2 deployment with PM2 for Socket Mode, Elastic Beanstalk for managed EC2, ECS Fargate for containerized bots, Secrets Manager for credential storage, CloudWatch alarms for error monitoring, provisioned concurrency for cold start mitigation, Teams-on-AWS pattern (Azure Bot Service pointing to AWS endpoint), and Slack app URL configuration. Provide 3-4 canonical deployment examples and 5-7 common pitfalls." -
aws-cli-reference-ts.md 52.4 KB
# aws-cli-reference-ts ## purpose Comprehensive reference of all AWS CLI (`aws`) command groups a developer needs for creating, reading, updating, and deleting resources in a bot or AI agent project on AWS. Use as a lookup companion to `aws-bot-deploy-ts.md` (step-by-step deployment) — this file maps every relevant CLI surface so you know what commands exist. ## rules 1. **This is a reference, not a tutorial.** For step-by-step deployment walkthroughs, see `aws-bot-deploy-ts.md`. This file catalogs every `aws` command group relevant to bot/agent projects. 2. **Always authenticate first.** Every command below assumes you have run `aws configure` (or `aws sso login`) and verified with `aws sts get-caller-identity`. [docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) 3. **Region matters.** Most commands operate in your configured default region. Override per-command with `--region <region>`, or set globally with `export AWS_DEFAULT_REGION=us-east-1`. --- ## 1. IAM (`aws iam`) — Identity & Access Management Every bot needs an execution role with least-privilege permissions. ### Roles | Command | Purpose | |---|---| | `aws iam create-role --role-name <name> --assume-role-policy-document file://trust.json` | Create execution role for Lambda/ECS/EC2 bot | | `aws iam get-role --role-name <name>` | Read role details including ARN | | `aws iam list-roles` | List all roles | | `aws iam update-role --role-name <name> --max-session-duration 7200` | Update role session duration | | `aws iam update-assume-role-policy --role-name <name> --policy-document file://trust.json` | Update who can assume the role | | `aws iam delete-role --role-name <name>` | Delete a role (must detach policies first) | ### Policies | Command | Purpose | |---|---| | `aws iam create-policy --policy-name <name> --policy-document file://policy.json` | Create custom policy for bot permissions | | `aws iam get-policy --policy-arn <arn>` | Read policy metadata | | `aws iam get-policy-version --policy-arn <arn> --version-id v1` | Read actual policy document | | `aws iam list-policies --scope Local` | List custom policies | | `aws iam create-policy-version --policy-arn <arn> --policy-document file://policy.json --set-as-default` | Update policy (creates new version) | | `aws iam delete-policy --policy-arn <arn>` | Delete policy | ### Attach/Detach Policies to Roles | Command | Purpose | |---|---| | `aws iam attach-role-policy --role-name <name> --policy-arn <arn>` | Attach managed policy to role | | `aws iam list-attached-role-policies --role-name <name>` | List policies on a role | | `aws iam detach-role-policy --role-name <name> --policy-arn <arn>` | Remove policy from role | | `aws iam put-role-policy --role-name <name> --policy-name <name> --policy-document file://policy.json` | Attach inline policy | | `aws iam delete-role-policy --role-name <name> --policy-name <name>` | Delete inline policy | ### Instance Profiles (for EC2 bots) | Command | Purpose | |---|---| | `aws iam create-instance-profile --instance-profile-name <name>` | Create instance profile for EC2 | | `aws iam add-role-to-instance-profile --instance-profile-name <name> --role-name <name>` | Link role to instance profile | | `aws iam remove-role-from-instance-profile --instance-profile-name <name> --role-name <name>` | Unlink role | | `aws iam delete-instance-profile --instance-profile-name <name>` | Delete instance profile | Reference: [docs.aws.amazon.com/cli/latest/reference/iam](https://docs.aws.amazon.com/cli/latest/reference/iam) --- ## 2. Lambda (`aws lambda`) — Serverless Bot Hosting ### Functions | Command | Purpose | |---|---| | `aws lambda create-function --function-name <name> --runtime nodejs20.x --role <arn> --handler index.handler --zip-file fileb://function.zip` | Create bot function | | `aws lambda get-function --function-name <name>` | Read function config and code location | | `aws lambda get-function-configuration --function-name <name>` | Read runtime config only | | `aws lambda list-functions` | List all functions | | `aws lambda update-function-code --function-name <name> --zip-file fileb://function.zip` | Deploy new bot code | | `aws lambda update-function-code --function-name <name> --image-uri <ecr-uri>` | Deploy from container image | | `aws lambda update-function-configuration --function-name <name> --timeout 30 --memory-size 256 --environment "Variables={KEY=value}"` | Update runtime settings | | `aws lambda delete-function --function-name <name>` | Delete function | ### Invocation & Testing | Command | Purpose | |---|---| | `aws lambda invoke --function-name <name> --payload file://event.json output.json` | Invoke synchronously (test) | | `aws lambda invoke --function-name <name> --invocation-type Event --payload file://event.json output.json` | Invoke async (fire-and-forget) | ### Event Source Mappings (SQS trigger for async bot processing) | Command | Purpose | |---|---| | `aws lambda create-event-source-mapping --function-name <name> --event-source-arn <sqs-arn> --batch-size 10` | Connect SQS queue to Lambda | | `aws lambda list-event-source-mappings --function-name <name>` | List triggers | | `aws lambda update-event-source-mapping --uuid <id> --batch-size 5` | Update trigger | | `aws lambda delete-event-source-mapping --uuid <id>` | Remove trigger | ### Permissions (resource-based policy) | Command | Purpose | |---|---| | `aws lambda add-permission --function-name <name> --statement-id apigateway --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn <api-arn>` | Allow API Gateway to invoke | | `aws lambda get-policy --function-name <name>` | Read resource policy | | `aws lambda remove-permission --function-name <name> --statement-id apigateway` | Revoke permission | ### Aliases & Versions (deployment strategy) | Command | Purpose | |---|---| | `aws lambda publish-version --function-name <name>` | Publish immutable version | | `aws lambda create-alias --function-name <name> --name prod --function-version 3` | Create alias pointing to version | | `aws lambda update-alias --function-name <name> --name prod --function-version 4` | Shift alias to new version | | `aws lambda delete-alias --function-name <name> --name prod` | Delete alias | ### Function URL (alternative to API Gateway) | Command | Purpose | |---|---| | `aws lambda create-function-url-config --function-name <name> --auth-type NONE` | Create public HTTPS endpoint | | `aws lambda get-function-url-config --function-name <name>` | Read URL config | | `aws lambda update-function-url-config --function-name <name> --auth-type AWS_IAM` | Update auth type | | `aws lambda delete-function-url-config --function-name <name>` | Delete URL endpoint | ### Layers | Command | Purpose | |---|---| | `aws lambda publish-layer-version --layer-name <name> --zip-file fileb://layer.zip --compatible-runtimes nodejs20.x` | Publish shared dependency layer | | `aws lambda list-layers` | List available layers | | `aws lambda delete-layer-version --layer-name <name> --version-number 1` | Delete layer version | Reference: [docs.aws.amazon.com/cli/latest/reference/lambda](https://docs.aws.amazon.com/cli/latest/reference/lambda) --- ## 3. API Gateway — HTTP Endpoints for Bots ### HTTP API (`aws apigatewayv2`) — Recommended for bot webhooks | Command | Purpose | |---|---| | `aws apigatewayv2 create-api --name <name> --protocol-type HTTP` | Create HTTP API | | `aws apigatewayv2 get-api --api-id <id>` | Read API details | | `aws apigatewayv2 get-apis` | List APIs | | `aws apigatewayv2 update-api --api-id <id> --name <new-name>` | Update API | | `aws apigatewayv2 delete-api --api-id <id>` | Delete API | ### Integrations | Command | Purpose | |---|---| | `aws apigatewayv2 create-integration --api-id <id> --integration-type AWS_PROXY --integration-uri <lambda-arn> --payload-format-version 2.0` | Connect Lambda backend | | `aws apigatewayv2 get-integration --api-id <id> --integration-id <id>` | Read integration | | `aws apigatewayv2 update-integration --api-id <id> --integration-id <id> --timeout-in-millis 10000` | Update integration | | `aws apigatewayv2 delete-integration --api-id <id> --integration-id <id>` | Remove integration | ### Routes | Command | Purpose | |---|---| | `aws apigatewayv2 create-route --api-id <id> --route-key "POST /slack/events" --target integrations/<integration-id>` | Create route for Slack events | | `aws apigatewayv2 get-routes --api-id <id>` | List routes | | `aws apigatewayv2 update-route --api-id <id> --route-id <id> --route-key "POST /slack/interactions"` | Update route | | `aws apigatewayv2 delete-route --api-id <id> --route-id <id>` | Delete route | ### Stages & Deployment | Command | Purpose | |---|---| | `aws apigatewayv2 create-stage --api-id <id> --stage-name prod --auto-deploy` | Create stage with auto-deploy | | `aws apigatewayv2 get-stages --api-id <id>` | List stages | | `aws apigatewayv2 update-stage --api-id <id> --stage-name prod --stage-variables env=production` | Update stage variables | | `aws apigatewayv2 delete-stage --api-id <id> --stage-name prod` | Delete stage | ### Custom Domain | Command | Purpose | |---|---| | `aws apigatewayv2 create-domain-name --domain-name bot.example.com --domain-name-configurations CertificateArn=<acm-arn>` | Map custom domain | | `aws apigatewayv2 create-api-mapping --api-id <id> --domain-name bot.example.com --stage prod` | Map domain to stage | | `aws apigatewayv2 delete-domain-name --domain-name bot.example.com` | Remove custom domain | ### REST API (`aws apigateway`) — When you need request validation, API keys, usage plans | Command | Purpose | |---|---| | `aws apigateway create-rest-api --name <name> --endpoint-configuration types=REGIONAL` | Create REST API | | `aws apigateway get-rest-api --rest-api-id <id>` | Read API | | `aws apigateway get-rest-apis` | List REST APIs | | `aws apigateway delete-rest-api --rest-api-id <id>` | Delete API | | `aws apigateway get-resources --rest-api-id <id>` | List resources/paths | | `aws apigateway create-resource --rest-api-id <id> --parent-id <root-id> --path-part slack` | Create path segment | | `aws apigateway put-method --rest-api-id <id> --resource-id <id> --http-method POST --authorization-type NONE` | Create method | | `aws apigateway put-integration --rest-api-id <id> --resource-id <id> --http-method POST --type AWS_PROXY --integration-http-method POST --uri <lambda-invoke-arn>` | Connect to Lambda | | `aws apigateway create-deployment --rest-api-id <id> --stage-name prod` | Deploy changes | Reference: [docs.aws.amazon.com/cli/latest/reference/apigatewayv2](https://docs.aws.amazon.com/cli/latest/reference/apigatewayv2) --- ## 4. EC2 (`aws ec2`) — VM Hosting for Socket Mode Bots ### Instances | Command | Purpose | |---|---| | `aws ec2 run-instances --image-id <ami> --instance-type t3.micro --key-name <key> --security-group-ids <sg-id> --subnet-id <subnet-id> --iam-instance-profile Name=<profile> --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=slack-bot}]"` | Launch bot instance | | `aws ec2 describe-instances --filters "Name=tag:Name,Values=slack-bot"` | Find bot instances | | `aws ec2 describe-instance-status --instance-ids <id>` | Check instance health | | `aws ec2 start-instances --instance-ids <id>` | Start stopped instance | | `aws ec2 stop-instances --instance-ids <id>` | Stop instance (preserve state) | | `aws ec2 reboot-instances --instance-ids <id>` | Reboot instance | | `aws ec2 terminate-instances --instance-ids <id>` | Delete instance permanently | ### Key Pairs (SSH access) | Command | Purpose | |---|---| | `aws ec2 create-key-pair --key-name <name> --query "KeyMaterial" --output text > key.pem` | Create SSH key pair | | `aws ec2 describe-key-pairs` | List key pairs | | `aws ec2 delete-key-pair --key-name <name>` | Delete key pair | ### Security Groups (firewall) | Command | Purpose | |---|---| | `aws ec2 create-security-group --group-name bot-sg --description "Bot security group" --vpc-id <vpc-id>` | Create security group | | `aws ec2 authorize-security-group-ingress --group-id <sg-id> --protocol tcp --port 443 --cidr 0.0.0.0/0` | Allow inbound HTTPS | | `aws ec2 describe-security-groups --group-ids <sg-id>` | Read rules | | `aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 22 --cidr 0.0.0.0/0` | Remove inbound rule | | `aws ec2 delete-security-group --group-id <sg-id>` | Delete security group | ### VPC Basics | Command | Purpose | |---|---| | `aws ec2 describe-vpcs` | List VPCs | | `aws ec2 describe-subnets --filters "Name=vpc-id,Values=<vpc-id>"` | List subnets in VPC | ### AMI (machine images) | Command | Purpose | |---|---| | `aws ec2 describe-images --owners amazon --filters "Name=name,Values=al2023-ami-*-x86_64"` | Find Amazon Linux AMI | | `aws ec2 create-image --instance-id <id> --name "bot-snapshot"` | Create AMI from running instance | Reference: [docs.aws.amazon.com/cli/latest/reference/ec2](https://docs.aws.amazon.com/cli/latest/reference/ec2) --- ## 5. ECS (`aws ecs`) — Containerized Bot Hosting ### Clusters | Command | Purpose | |---|---| | `aws ecs create-cluster --cluster-name <name> --capacity-providers FARGATE --default-capacity-provider-strategy capacityProvider=FARGATE,weight=1` | Create Fargate cluster | | `aws ecs describe-clusters --clusters <name>` | Read cluster details | | `aws ecs list-clusters` | List clusters | | `aws ecs delete-cluster --cluster <name>` | Delete cluster (must be empty) | ### Task Definitions (container blueprint) | Command | Purpose | |---|---| | `aws ecs register-task-definition --cli-input-json file://task-def.json` | Create/update task definition | | `aws ecs describe-task-definition --task-definition <name>` | Read latest task def | | `aws ecs describe-task-definition --task-definition <name>:<revision>` | Read specific revision | | `aws ecs list-task-definitions --family-prefix <name>` | List revisions | | `aws ecs deregister-task-definition --task-definition <name>:<revision>` | Deactivate revision | ### Services (long-running bot) | Command | Purpose | |---|---| | `aws ecs create-service --cluster <name> --service-name <name> --task-definition <name> --desired-count 1 --launch-type FARGATE --network-configuration "awsvpcConfiguration={subnets=[<subnet>],securityGroups=[<sg>],assignPublicIp=ENABLED}"` | Create service | | `aws ecs describe-services --cluster <name> --services <name>` | Read service status | | `aws ecs list-services --cluster <name>` | List services | | `aws ecs update-service --cluster <name> --service <name> --desired-count 2` | Scale service | | `aws ecs update-service --cluster <name> --service <name> --task-definition <name>:<new-rev> --force-new-deployment` | Deploy new version | | `aws ecs delete-service --cluster <name> --service <name> --force` | Delete service | ### Tasks (individual containers) | Command | Purpose | |---|---| | `aws ecs run-task --cluster <name> --task-definition <name> --launch-type FARGATE --network-configuration "awsvpcConfiguration={...}"` | Run one-off task | | `aws ecs list-tasks --cluster <name> --service-name <name>` | List running tasks | | `aws ecs describe-tasks --cluster <name> --tasks <task-arn>` | Read task details | | `aws ecs stop-task --cluster <name> --task <task-arn> --reason "manual stop"` | Stop a running task | | `aws ecs execute-command --cluster <name> --task <task-arn> --container <name> --interactive --command "/bin/sh"` | Exec into running container | Reference: [docs.aws.amazon.com/cli/latest/reference/ecs](https://docs.aws.amazon.com/cli/latest/reference/ecs) --- ## 6. Elastic Beanstalk (`aws elasticbeanstalk`) — Managed Hosting | Command | Purpose | |---|---| | `aws elasticbeanstalk create-application --application-name <name>` | Create application | | `aws elasticbeanstalk describe-applications --application-names <name>` | Read application | | `aws elasticbeanstalk update-application --application-name <name> --description "Slack bot"` | Update application | | `aws elasticbeanstalk delete-application --application-name <name> --terminate-env-by-force` | Delete application | | `aws elasticbeanstalk create-application-version --application-name <name> --version-label v1 --source-bundle S3Bucket=<bucket>,S3Key=<key>` | Upload version | | `aws elasticbeanstalk create-environment --application-name <name> --environment-name prod --solution-stack-name "64bit Amazon Linux 2023 v6.1.0 running Node.js 20" --option-settings file://options.json` | Create environment | | `aws elasticbeanstalk describe-environments --application-name <name>` | Read environment status | | `aws elasticbeanstalk update-environment --environment-name <name> --version-label v2` | Deploy new version | | `aws elasticbeanstalk terminate-environment --environment-name <name>` | Delete environment | | `aws elasticbeanstalk list-platform-versions --filters "Type=PlatformName,Operator=contains,Values=Node.js"` | Find supported platforms | Reference: [docs.aws.amazon.com/cli/latest/reference/elasticbeanstalk](https://docs.aws.amazon.com/cli/latest/reference/elasticbeanstalk) --- ## 7. App Runner (`aws apprunner`) — Simplified Container Hosting | Command | Purpose | |---|---| | `aws apprunner create-service --service-name <name> --source-configuration file://source-config.json` | Create service from ECR image or GitHub | | `aws apprunner describe-service --service-arn <arn>` | Read service details and URL | | `aws apprunner list-services` | List services | | `aws apprunner update-service --service-arn <arn> --source-configuration file://source-config.json` | Update source/config | | `aws apprunner delete-service --service-arn <arn>` | Delete service | | `aws apprunner start-deployment --service-arn <arn>` | Trigger manual deployment | | `aws apprunner pause-service --service-arn <arn>` | Pause (stop billing for compute) | | `aws apprunner resume-service --service-arn <arn>` | Resume paused service | | `aws apprunner associate-custom-domain --service-arn <arn> --domain-name bot.example.com` | Map custom domain | | `aws apprunner disassociate-custom-domain --service-arn <arn> --domain-name bot.example.com` | Remove custom domain | Reference: [docs.aws.amazon.com/cli/latest/reference/apprunner](https://docs.aws.amazon.com/cli/latest/reference/apprunner) --- ## 8. Secrets Manager (`aws secretsmanager`) — Bot Credentials | Command | Purpose | |---|---| | `aws secretsmanager create-secret --name bot/slack --secret-string '{"SLACK_BOT_TOKEN":"xoxb-...","SLACK_SIGNING_SECRET":"..."}'` | Store bot credentials | | `aws secretsmanager get-secret-value --secret-id bot/slack` | Read secret value | | `aws secretsmanager describe-secret --secret-id bot/slack` | Read metadata (no value) | | `aws secretsmanager list-secrets --filters Key=name,Values=bot/` | List secrets | | `aws secretsmanager update-secret --secret-id bot/slack --secret-string '{"SLACK_BOT_TOKEN":"xoxb-new"}'` | Update secret value | | `aws secretsmanager rotate-secret --secret-id bot/slack --rotation-lambda-arn <arn>` | Trigger rotation | | `aws secretsmanager delete-secret --secret-id bot/slack --recovery-window-in-days 7` | Soft delete (recoverable) | | `aws secretsmanager delete-secret --secret-id bot/slack --force-delete-without-recovery` | Hard delete (immediate) | | `aws secretsmanager restore-secret --secret-id bot/slack` | Recover soft-deleted secret | Reference: [docs.aws.amazon.com/cli/latest/reference/secretsmanager](https://docs.aws.amazon.com/cli/latest/reference/secretsmanager) --- ## 9. SSM Parameter Store (`aws ssm`) — Configuration & Secrets | Command | Purpose | |---|---| | `aws ssm put-parameter --name /bot/config/log-level --value "info" --type String` | Create string parameter | | `aws ssm put-parameter --name /bot/secrets/api-key --value "sk-..." --type SecureString` | Create encrypted parameter | | `aws ssm get-parameter --name /bot/config/log-level` | Read parameter | | `aws ssm get-parameter --name /bot/secrets/api-key --with-decryption` | Read encrypted parameter | | `aws ssm get-parameters-by-path --path /bot/ --recursive --with-decryption` | Read all params under path | | `aws ssm describe-parameters --parameter-filters "Key=Name,Option=BeginsWith,Values=/bot/"` | List parameters (metadata only) | | `aws ssm put-parameter --name /bot/config/log-level --value "debug" --type String --overwrite` | Update parameter | | `aws ssm delete-parameter --name /bot/config/log-level` | Delete parameter | | `aws ssm delete-parameters --names /bot/config/log-level /bot/config/timeout` | Batch delete | Reference: [docs.aws.amazon.com/cli/latest/reference/ssm](https://docs.aws.amazon.com/cli/latest/reference/ssm) --- ## 10. CloudWatch & Logs (`aws cloudwatch`, `aws logs`) — Monitoring ### CloudWatch Metrics & Alarms | Command | Purpose | |---|---| | `aws cloudwatch put-metric-alarm --alarm-name bot-errors --metric-name Errors --namespace AWS/Lambda --statistic Sum --period 300 --threshold 5 --comparison-operator GreaterThanThreshold --evaluation-periods 1 --alarm-actions <sns-arn> --dimensions Name=FunctionName,Value=<func>` | Create error alarm | | `aws cloudwatch describe-alarms --alarm-names bot-errors` | Read alarm config | | `aws cloudwatch list-metrics --namespace AWS/Lambda --dimensions Name=FunctionName,Value=<func>` | List available metrics | | `aws cloudwatch get-metric-statistics --namespace AWS/Lambda --metric-name Duration --dimensions Name=FunctionName,Value=<func> --start-time <time> --end-time <time> --period 3600 --statistics Average` | Query metric data | | `aws cloudwatch put-metric-data --namespace BotMetrics --metric-name MessagesProcessed --value 1 --unit Count` | Publish custom metric | | `aws cloudwatch delete-alarms --alarm-names bot-errors` | Delete alarm | ### CloudWatch Logs | Command | Purpose | |---|---| | `aws logs create-log-group --log-group-name /aws/lambda/slack-bot` | Create log group | | `aws logs describe-log-groups --log-group-name-prefix /aws/lambda/` | List log groups | | `aws logs put-retention-policy --log-group-name /aws/lambda/slack-bot --retention-in-days 30` | Set log retention | | `aws logs delete-log-group --log-group-name /aws/lambda/slack-bot` | Delete log group | | `aws logs describe-log-streams --log-group-name /aws/lambda/slack-bot --order-by LastEventTime --descending --limit 5` | List recent log streams | | `aws logs get-log-events --log-group-name /aws/lambda/slack-bot --log-stream-name <stream>` | Read log events | | `aws logs filter-log-events --log-group-name /aws/lambda/slack-bot --filter-pattern "ERROR"` | Search logs for errors | | `aws logs tail /aws/lambda/slack-bot --follow` | Live tail logs | | `aws logs put-metric-filter --log-group-name /aws/lambda/slack-bot --filter-name bot-errors --filter-pattern "ERROR" --metric-transformations metricName=BotErrors,metricNamespace=BotMetrics,metricValue=1` | Create metric from log pattern | Reference: [docs.aws.amazon.com/cli/latest/reference/cloudwatch](https://docs.aws.amazon.com/cli/latest/reference/cloudwatch) --- ## 11. S3 (`aws s3` / `aws s3api`) — Artifact Storage & Bot State ### High-Level Commands (`aws s3`) | Command | Purpose | |---|---| | `aws s3 mb s3://my-bot-artifacts` | Create bucket | | `aws s3 ls` | List buckets | | `aws s3 ls s3://my-bot-artifacts/` | List objects in bucket | | `aws s3 cp function.zip s3://my-bot-artifacts/deploys/function.zip` | Upload file | | `aws s3 cp s3://my-bot-artifacts/deploys/function.zip ./function.zip` | Download file | | `aws s3 sync ./dist s3://my-bot-artifacts/deploys/latest/` | Sync directory to S3 | | `aws s3 rm s3://my-bot-artifacts/deploys/function.zip` | Delete object | | `aws s3 rb s3://my-bot-artifacts --force` | Delete bucket and all contents | | `aws s3 presign s3://my-bot-artifacts/files/report.pdf --expires-in 3600` | Generate pre-signed URL | ### Low-Level Commands (`aws s3api`) | Command | Purpose | |---|---| | `aws s3api create-bucket --bucket <name> --region us-east-1` | Create bucket (us-east-1) | | `aws s3api create-bucket --bucket <name> --region us-west-2 --create-bucket-configuration LocationConstraint=us-west-2` | Create bucket (other regions) | | `aws s3api put-bucket-versioning --bucket <name> --versioning-configuration Status=Enabled` | Enable versioning | | `aws s3api put-bucket-encryption --bucket <name> --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms"}}]}'` | Enable encryption | | `aws s3api put-public-access-block --bucket <name> --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true` | Block public access | Reference: [docs.aws.amazon.com/cli/latest/reference/s3](https://docs.aws.amazon.com/cli/latest/reference/s3) --- ## 12. DynamoDB (`aws dynamodb`) — Conversation State ### Tables | Command | Purpose | |---|---| | `aws dynamodb create-table --table-name bot-conversations --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE --billing-mode PAY_PER_REQUEST` | Create table (on-demand) | | `aws dynamodb describe-table --table-name bot-conversations` | Read table details | | `aws dynamodb list-tables` | List tables | | `aws dynamodb update-table --table-name bot-conversations --billing-mode PAY_PER_REQUEST` | Switch to on-demand | | `aws dynamodb update-time-to-live --table-name bot-conversations --time-to-live-specification Enabled=true,AttributeName=ttl` | Enable TTL (auto-expire old state) | | `aws dynamodb delete-table --table-name bot-conversations` | Delete table | ### Items (CRUD) | Command | Purpose | |---|---| | `aws dynamodb put-item --table-name bot-conversations --item '{"pk":{"S":"user#U123"},"sk":{"S":"conv#2026-02-28"},"state":{"S":"awaiting_input"}}'` | Create/overwrite item | | `aws dynamodb get-item --table-name bot-conversations --key '{"pk":{"S":"user#U123"},"sk":{"S":"conv#2026-02-28"}}'` | Read item by key | | `aws dynamodb query --table-name bot-conversations --key-condition-expression "pk = :pk" --expression-attribute-values '{":pk":{"S":"user#U123"}}'` | Query items by partition key | | `aws dynamodb update-item --table-name bot-conversations --key '{"pk":{"S":"user#U123"},"sk":{"S":"conv#2026-02-28"}}' --update-expression "SET #s = :s" --expression-attribute-names '{"#s":"state"}' --expression-attribute-values '{":s":{"S":"completed"}}'` | Update specific attributes | | `aws dynamodb delete-item --table-name bot-conversations --key '{"pk":{"S":"user#U123"},"sk":{"S":"conv#2026-02-28"}}'` | Delete item | ### Batch Operations | Command | Purpose | |---|---| | `aws dynamodb batch-write-item --request-items file://batch-write.json` | Batch write (up to 25 items) | | `aws dynamodb batch-get-item --request-items file://batch-get.json` | Batch read (up to 100 items) | Reference: [docs.aws.amazon.com/cli/latest/reference/dynamodb](https://docs.aws.amazon.com/cli/latest/reference/dynamodb) --- ## 13. SQS (`aws sqs`) — Async Message Processing For Lambda bots that need to ack Slack within 3 seconds and process asynchronously. | Command | Purpose | |---|---| | `aws sqs create-queue --queue-name bot-events` | Create standard queue | | `aws sqs create-queue --queue-name bot-events.fifo --attributes FifoQueue=true,ContentBasedDeduplication=true` | Create FIFO queue (ordered) | | `aws sqs create-queue --queue-name bot-events-dlq` | Create dead-letter queue | | `aws sqs get-queue-url --queue-name bot-events` | Get queue URL | | `aws sqs get-queue-attributes --queue-url <url> --attribute-names All` | Read queue config | | `aws sqs list-queues --queue-name-prefix bot-` | List queues | | `aws sqs set-queue-attributes --queue-url <url> --attributes '{"VisibilityTimeout":"60","RedrivePolicy":"{\"deadLetterTargetArn\":\"<dlq-arn>\",\"maxReceiveCount\":\"3\"}"}'` | Configure DLQ redrive | | `aws sqs send-message --queue-url <url> --message-body '{"event":"message","text":"hello"}'` | Send message | | `aws sqs purge-queue --queue-url <url>` | Delete all messages | | `aws sqs delete-queue --queue-url <url>` | Delete queue | Reference: [docs.aws.amazon.com/cli/latest/reference/sqs](https://docs.aws.amazon.com/cli/latest/reference/sqs) --- ## 14. SNS (`aws sns`) — Notifications & Alerts | Command | Purpose | |---|---| | `aws sns create-topic --name bot-alerts` | Create topic | | `aws sns list-topics` | List topics | | `aws sns get-topic-attributes --topic-arn <arn>` | Read topic details | | `aws sns delete-topic --topic-arn <arn>` | Delete topic | | `aws sns subscribe --topic-arn <arn> --protocol email --notification-endpoint ops@example.com` | Subscribe email | | `aws sns subscribe --topic-arn <arn> --protocol lambda --notification-endpoint <lambda-arn>` | Subscribe Lambda | | `aws sns list-subscriptions-by-topic --topic-arn <arn>` | List subscribers | | `aws sns unsubscribe --subscription-arn <arn>` | Remove subscriber | | `aws sns publish --topic-arn <arn> --subject "Bot Error" --message "Lambda function failed"` | Publish to topic | Reference: [docs.aws.amazon.com/cli/latest/reference/sns](https://docs.aws.amazon.com/cli/latest/reference/sns) --- ## 15. ECR (`aws ecr`) — Container Registry | Command | Purpose | |---|---| | `aws ecr create-repository --repository-name slack-bot --image-scanning-configuration scanOnPush=true` | Create repository | | `aws ecr describe-repositories --repository-names slack-bot` | Read repository | | `aws ecr list-images --repository-name slack-bot` | List images | | `aws ecr describe-images --repository-name slack-bot --image-ids imageTag=latest` | Read image details | | `aws ecr get-login-password --region us-east-1 \| docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com` | Authenticate Docker to ECR | | `aws ecr put-lifecycle-policy --repository-name slack-bot --lifecycle-policy-text file://lifecycle.json` | Set image cleanup policy | | `aws ecr batch-delete-image --repository-name slack-bot --image-ids imageTag=old` | Delete images | | `aws ecr delete-repository --repository-name slack-bot --force` | Delete repository and images | ### Typical Docker Push Flow ```bash aws ecr get-login-password | docker login --username AWS --password-stdin <account>.dkr.ecr.<region>.amazonaws.com docker build -t slack-bot . docker tag slack-bot:latest <account>.dkr.ecr.<region>.amazonaws.com/slack-bot:latest docker push <account>.dkr.ecr.<region>.amazonaws.com/slack-bot:latest ``` Reference: [docs.aws.amazon.com/cli/latest/reference/ecr](https://docs.aws.amazon.com/cli/latest/reference/ecr) --- ## 16. CloudFormation (`aws cloudformation`) — Infrastructure as Code | Command | Purpose | |---|---| | `aws cloudformation create-stack --stack-name bot-infra --template-body file://template.yaml --capabilities CAPABILITY_IAM` | Create stack | | `aws cloudformation describe-stacks --stack-name bot-infra` | Read stack status and outputs | | `aws cloudformation describe-stack-resources --stack-name bot-infra` | List resources in stack | | `aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE` | List active stacks | | `aws cloudformation update-stack --stack-name bot-infra --template-body file://template.yaml --capabilities CAPABILITY_IAM` | Update stack | | `aws cloudformation create-change-set --stack-name bot-infra --change-set-name update-v2 --template-body file://template.yaml --capabilities CAPABILITY_IAM` | Preview changes | | `aws cloudformation execute-change-set --stack-name bot-infra --change-set-name update-v2` | Apply change set | | `aws cloudformation delete-stack --stack-name bot-infra` | Delete stack and resources | | `aws cloudformation describe-stack-events --stack-name bot-infra` | Read deployment events/errors | | `aws cloudformation validate-template --template-body file://template.yaml` | Validate template syntax | | `aws cloudformation wait stack-create-complete --stack-name bot-infra` | Wait for creation to finish | Reference: [docs.aws.amazon.com/cli/latest/reference/cloudformation](https://docs.aws.amazon.com/cli/latest/reference/cloudformation) --- ## 17. STS (`aws sts`) — Identity Verification | Command | Purpose | |---|---| | `aws sts get-caller-identity` | Verify current identity (account, user, ARN) | | `aws sts assume-role --role-arn <arn> --role-session-name bot-deploy` | Assume a role (cross-account or elevated) | | `aws sts get-session-token --duration-seconds 3600` | Get temporary credentials | | `aws sts decode-authorization-message --encoded-message <msg>` | Decode IAM denial message | Reference: [docs.aws.amazon.com/cli/latest/reference/sts](https://docs.aws.amazon.com/cli/latest/reference/sts) --- ## 18. Route 53 (`aws route53`) — Custom Domain for Bot Endpoints ### Hosted Zones | Command | Purpose | |---|---| | `aws route53 create-hosted-zone --name example.com --caller-reference $(date +%s)` | Create hosted zone | | `aws route53 list-hosted-zones` | List hosted zones | | `aws route53 get-hosted-zone --id <zone-id>` | Read zone details | | `aws route53 delete-hosted-zone --id <zone-id>` | Delete zone | ### DNS Records | Command | Purpose | |---|---| | `aws route53 change-resource-record-sets --hosted-zone-id <id> --change-batch file://dns-change.json` | Create/update/delete records | | `aws route53 list-resource-record-sets --hosted-zone-id <id>` | List records | | `aws route53 test-dns-answer --hosted-zone-id <id> --record-name bot.example.com --record-type A` | Test DNS resolution | ### Example `dns-change.json` (CNAME to API Gateway) ```json { "Changes": [{ "Action": "UPSERT", "ResourceRecordSet": { "Name": "bot.example.com", "Type": "CNAME", "TTL": 300, "ResourceRecords": [{"Value": "abc123.execute-api.us-east-1.amazonaws.com"}] } }] } ``` Reference: [docs.aws.amazon.com/cli/latest/reference/route53](https://docs.aws.amazon.com/cli/latest/reference/route53) --- ## 19. ACM (`aws acm`) — SSL Certificates | Command | Purpose | |---|---| | `aws acm request-certificate --domain-name bot.example.com --validation-method DNS` | Request certificate | | `aws acm describe-certificate --certificate-arn <arn>` | Read cert status and validation info | | `aws acm list-certificates` | List certificates | | `aws acm list-certificates --certificate-statuses ISSUED` | List only issued certs | | `aws acm delete-certificate --certificate-arn <arn>` | Delete certificate | | `aws acm wait certificate-validated --certificate-arn <arn>` | Wait for validation to complete | ### DNS Validation Flow ```bash # 1. Request certificate CERT_ARN=$(aws acm request-certificate --domain-name bot.example.com --validation-method DNS --query CertificateArn --output text) # 2. Get CNAME validation record aws acm describe-certificate --certificate-arn $CERT_ARN --query "Certificate.DomainValidationOptions[0].ResourceRecord" # 3. Create validation CNAME in Route 53 (using change-resource-record-sets) # 4. Wait for validation aws acm wait certificate-validated --certificate-arn $CERT_ARN ``` Reference: [docs.aws.amazon.com/cli/latest/reference/acm](https://docs.aws.amazon.com/cli/latest/reference/acm) --- ## 20. Bedrock (`aws bedrock` / `aws bedrock-agent`) — AI Agents on AWS ### Foundation Model Discovery | Command | Purpose | |---|---| | `aws bedrock list-foundation-models` | List all available models | | `aws bedrock list-foundation-models --by-provider anthropic` | List Anthropic models | | `aws bedrock get-foundation-model --model-identifier anthropic.claude-3-sonnet-20240229-v1:0` | Get model details | ### Model Invocation (`aws bedrock-runtime`) | Command | Purpose | |---|---| | `aws bedrock-runtime invoke-model --model-id anthropic.claude-3-sonnet-20240229-v1:0 --content-type application/json --body file://prompt.json output.json` | Invoke model (sync) | | `aws bedrock-runtime invoke-model-with-response-stream --model-id <model-id> --content-type application/json --body file://prompt.json output.json` | Invoke model (streaming) | | `aws bedrock-runtime converse --model-id <model-id> --messages file://messages.json` | Multi-turn conversation (Converse API) | ### Bedrock Agents (`aws bedrock-agent`) | Command | Purpose | |---|---| | `aws bedrock-agent create-agent --agent-name slack-ai-agent --agent-resource-role-arn <arn> --foundation-model anthropic.claude-3-sonnet-20240229-v1:0 --instruction "You are a helpful Slack bot."` | Create agent | | `aws bedrock-agent get-agent --agent-id <id>` | Read agent config | | `aws bedrock-agent list-agents` | List agents | | `aws bedrock-agent update-agent --agent-id <id> --agent-name <name> --agent-resource-role-arn <arn> --foundation-model <model> --instruction "Updated instructions"` | Update agent | | `aws bedrock-agent delete-agent --agent-id <id>` | Delete agent | | `aws bedrock-agent prepare-agent --agent-id <id>` | Prepare agent for use (required after changes) | ### Agent Action Groups (tool use) | Command | Purpose | |---|---| | `aws bedrock-agent create-agent-action-group --agent-id <id> --agent-version DRAFT --action-group-name slack-actions --action-group-executor lambda=<lambda-arn> --api-schema s3=<s3-uri>` | Add tools/actions to agent | | `aws bedrock-agent list-agent-action-groups --agent-id <id> --agent-version DRAFT` | List action groups | | `aws bedrock-agent update-agent-action-group --agent-id <id> --agent-version DRAFT --action-group-id <id> --action-group-name <name>` | Update action group | | `aws bedrock-agent delete-agent-action-group --agent-id <id> --agent-version DRAFT --action-group-id <id>` | Delete action group | ### Agent Knowledge Bases | Command | Purpose | |---|---| | `aws bedrock-agent create-knowledge-base --name bot-knowledge --role-arn <arn> --knowledge-base-configuration type=VECTOR,vectorKnowledgeBaseConfiguration={embeddingModelArn=<model-arn>} --storage-configuration file://storage.json` | Create knowledge base | | `aws bedrock-agent list-knowledge-bases` | List knowledge bases | | `aws bedrock-agent get-knowledge-base --knowledge-base-id <id>` | Read knowledge base | | `aws bedrock-agent associate-agent-knowledge-base --agent-id <id> --agent-version DRAFT --knowledge-base-id <kb-id> --description "Bot documentation"` | Connect KB to agent | | `aws bedrock-agent create-data-source --knowledge-base-id <id> --name docs --data-source-configuration type=S3,s3Configuration={bucketArn=<arn>}` | Add data source to KB | | `aws bedrock-agent start-ingestion-job --knowledge-base-id <id> --data-source-id <id>` | Sync data into KB | | `aws bedrock-agent delete-knowledge-base --knowledge-base-id <id>` | Delete knowledge base | ### Agent Aliases & Invocation | Command | Purpose | |---|---| | `aws bedrock-agent create-agent-alias --agent-id <id> --agent-alias-name prod` | Create alias for deployment | | `aws bedrock-agent list-agent-aliases --agent-id <id>` | List aliases | | `aws bedrock-agent update-agent-alias --agent-id <id> --agent-alias-id <alias-id> --agent-alias-name prod` | Update alias | | `aws bedrock-agent delete-agent-alias --agent-id <id> --agent-alias-id <alias-id>` | Delete alias | ### Agent Runtime (`aws bedrock-agent-runtime`) | Command | Purpose | |---|---| | `aws bedrock-agent-runtime invoke-agent --agent-id <id> --agent-alias-id <alias-id> --session-id <session> --input-text "What are our team policies?"` | Invoke agent | | `aws bedrock-agent-runtime retrieve --knowledge-base-id <id> --retrieval-query text="deployment process"` | Query knowledge base directly | | `aws bedrock-agent-runtime retrieve-and-generate --input text="How do I deploy?" --retrieve-and-generate-configuration file://rag-config.json` | RAG query | ### Guardrails | Command | Purpose | |---|---| | `aws bedrock create-guardrail --name bot-guardrail --blocked-input-messaging "I cannot process that." --blocked-outputs-messaging "Response filtered." --content-policy-config file://content-policy.json` | Create guardrail | | `aws bedrock get-guardrail --guardrail-identifier <id>` | Read guardrail | | `aws bedrock list-guardrails` | List guardrails | | `aws bedrock update-guardrail --guardrail-identifier <id> --name bot-guardrail --blocked-input-messaging "..." --blocked-outputs-messaging "..."` | Update guardrail | | `aws bedrock delete-guardrail --guardrail-identifier <id>` | Delete guardrail | Reference: [docs.aws.amazon.com/cli/latest/reference/bedrock](https://docs.aws.amazon.com/cli/latest/reference/bedrock) --- ## 21. Lex V2 (`aws lexv2-models`) — AWS Native Bot Framework ### Bots | Command | Purpose | |---|---| | `aws lexv2-models create-bot --bot-name slack-bot --role-arn <arn> --data-privacy '{"childDirected":false}' --idle-session-ttl-in-seconds 300` | Create bot | | `aws lexv2-models describe-bot --bot-id <id>` | Read bot config | | `aws lexv2-models list-bots` | List bots | | `aws lexv2-models update-bot --bot-id <id> --bot-name <name> --role-arn <arn> --data-privacy '{"childDirected":false}' --idle-session-ttl-in-seconds 300` | Update bot | | `aws lexv2-models delete-bot --bot-id <id> --skip-resource-in-use-check` | Delete bot | ### Intents | Command | Purpose | |---|---| | `aws lexv2-models create-intent --bot-id <id> --bot-version DRAFT --locale-id en_US --intent-name OrderFood` | Create intent | | `aws lexv2-models list-intents --bot-id <id> --bot-version DRAFT --locale-id en_US` | List intents | | `aws lexv2-models update-intent --bot-id <id> --bot-version DRAFT --locale-id en_US --intent-id <id> --intent-name OrderFood --sample-utterances file://utterances.json --fulfillment-code-hook '{"enabled":true}'` | Update intent with utterances and Lambda fulfillment | | `aws lexv2-models delete-intent --bot-id <id> --bot-version DRAFT --locale-id en_US --intent-id <id>` | Delete intent | ### Slots (parameters) | Command | Purpose | |---|---| | `aws lexv2-models create-slot --bot-id <id> --bot-version DRAFT --locale-id en_US --intent-id <id> --slot-name FoodType --slot-type-id AMAZON.FreeFormInput --value-elicitation-setting file://elicitation.json` | Create slot | | `aws lexv2-models list-slots --bot-id <id> --bot-version DRAFT --locale-id en_US --intent-id <id>` | List slots | | `aws lexv2-models delete-slot --bot-id <id> --bot-version DRAFT --locale-id en_US --intent-id <id> --slot-id <id>` | Delete slot | ### Build & Deploy | Command | Purpose | |---|---| | `aws lexv2-models build-bot-locale --bot-id <id> --bot-version DRAFT --locale-id en_US` | Build bot (compile NLU model) | | `aws lexv2-models create-bot-version --bot-id <id> --bot-version-locale-specification '{"en_US":{"sourceBotVersion":"DRAFT"}}'` | Create immutable version | | `aws lexv2-models create-bot-alias --bot-id <id> --bot-alias-name prod --bot-version 1` | Create alias | | `aws lexv2-models update-bot-alias --bot-id <id> --bot-alias-id <alias-id> --bot-alias-name prod --bot-version 2` | Point alias to new version | | `aws lexv2-models delete-bot-alias --bot-id <id> --bot-alias-id <alias-id>` | Delete alias | ### Runtime (`aws lexv2-runtime`) | Command | Purpose | |---|---| | `aws lexv2-runtime recognize-text --bot-id <id> --bot-alias-id <alias-id> --locale-id en_US --session-id user-123 --text "I want to order pizza"` | Send text to bot | | `aws lexv2-runtime get-session --bot-id <id> --bot-alias-id <alias-id> --locale-id en_US --session-id user-123` | Read session state | | `aws lexv2-runtime delete-session --bot-id <id> --bot-alias-id <alias-id> --locale-id en_US --session-id user-123` | Clear session | Reference: [docs.aws.amazon.com/cli/latest/reference/lexv2-models](https://docs.aws.amazon.com/cli/latest/reference/lexv2-models) --- ## patterns ### Minimum viable Slack bot on Lambda — full CRUD flow ```bash # 1. Verify identity aws sts get-caller-identity # 2. Create IAM role for Lambda cat > trust.json << 'TRUST' { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole" }] } TRUST ROLE_ARN=$(aws iam create-role --role-name slack-bot-role \ --assume-role-policy-document file://trust.json --query Role.Arn --output text) aws iam attach-role-policy --role-name slack-bot-role \ --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole aws iam attach-role-policy --role-name slack-bot-role \ --policy-arn arn:aws:iam::aws:policy/SecretsManagerReadWrite # 3. Store secrets aws secretsmanager create-secret --name bot/slack \ --secret-string '{"SLACK_BOT_TOKEN":"xoxb-...","SLACK_SIGNING_SECRET":"..."}' # 4. Create Lambda function npm run build && cd dist && zip -r ../function.zip . && cd .. aws lambda create-function --function-name slack-bot \ --runtime nodejs20.x --role $ROLE_ARN --handler lambda.handler \ --zip-file fileb://function.zip --timeout 30 --memory-size 256 # 5. Create HTTP API + Lambda integration API_ID=$(aws apigatewayv2 create-api --name slack-bot-api \ --protocol-type HTTP --query ApiId --output text) INTEGRATION_ID=$(aws apigatewayv2 create-integration --api-id $API_ID \ --integration-type AWS_PROXY \ --integration-uri arn:aws:lambda:us-east-1:$(aws sts get-caller-identity --query Account --output text):function:slack-bot \ --payload-format-version 2.0 --query IntegrationId --output text) aws apigatewayv2 create-route --api-id $API_ID \ --route-key "POST /slack/events" --target integrations/$INTEGRATION_ID aws apigatewayv2 create-stage --api-id $API_ID --stage-name '$default' --auto-deploy # 6. Grant API Gateway permission to invoke Lambda aws lambda add-permission --function-name slack-bot \ --statement-id apigateway --action lambda:InvokeFunction \ --principal apigateway.amazonaws.com \ --source-arn "arn:aws:execute-api:us-east-1:$(aws sts get-caller-identity --query Account --output text):$API_ID/*" # 7. Get endpoint URL for Slack app configuration echo "Slack Request URL: https://$API_ID.execute-api.us-east-1.amazonaws.com/slack/events" # 8. Set up monitoring aws cloudwatch put-metric-alarm --alarm-name slack-bot-errors \ --metric-name Errors --namespace AWS/Lambda --statistic Sum \ --period 300 --threshold 5 --comparison-operator GreaterThanThreshold \ --evaluation-periods 1 --dimensions Name=FunctionName,Value=slack-bot ``` ### Minimum viable Bedrock agent wired to a Slack bot ```bash # 1. Create Bedrock agent AGENT_ID=$(aws bedrock-agent create-agent \ --agent-name slack-ai-agent \ --agent-resource-role-arn $ROLE_ARN \ --foundation-model anthropic.claude-3-sonnet-20240229-v1:0 \ --instruction "You are a helpful assistant in a Slack workspace." \ --query agent.agentId --output text) # 2. Prepare and create alias aws bedrock-agent prepare-agent --agent-id $AGENT_ID ALIAS_ID=$(aws bedrock-agent create-agent-alias \ --agent-id $AGENT_ID --agent-alias-name prod \ --query agentAlias.agentAliasId --output text) # 3. Add a knowledge base (optional) KB_ID=$(aws bedrock-agent create-knowledge-base \ --name company-docs --role-arn $ROLE_ARN \ --knowledge-base-configuration type=VECTOR,vectorKnowledgeBaseConfiguration={embeddingModelArn=arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v1} \ --storage-configuration file://storage.json \ --query knowledgeBase.knowledgeBaseId --output text) aws bedrock-agent associate-agent-knowledge-base \ --agent-id $AGENT_ID --agent-version DRAFT \ --knowledge-base-id $KB_ID --description "Company documentation" aws bedrock-agent prepare-agent --agent-id $AGENT_ID # 4. Test invocation aws bedrock-agent-runtime invoke-agent \ --agent-id $AGENT_ID --agent-alias-id $ALIAS_ID \ --session-id test-session --input-text "Hello, what can you help me with?" ``` ### Teardown (delete everything) ```bash # Delete compute aws lambda delete-function --function-name slack-bot aws apigatewayv2 delete-api --api-id $API_ID # Delete secrets aws secretsmanager delete-secret --secret-id bot/slack --force-delete-without-recovery # Delete IAM (detach policies first) aws iam detach-role-policy --role-name slack-bot-role \ --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole aws iam detach-role-policy --role-name slack-bot-role \ --policy-arn arn:aws:iam::aws:policy/SecretsManagerReadWrite aws iam delete-role --role-name slack-bot-role # Delete Bedrock agent aws bedrock-agent delete-agent-alias --agent-id $AGENT_ID --agent-alias-id $ALIAS_ID aws bedrock-agent delete-agent --agent-id $AGENT_ID # Delete monitoring aws cloudwatch delete-alarms --alarm-names slack-bot-errors ``` ### List all resources in a bot project ```bash # Lambda functions aws lambda list-functions --query "Functions[?starts_with(FunctionName,'slack-bot')]" --output table # API Gateway aws apigatewayv2 get-apis --query "Items[?Name=='slack-bot-api']" --output table # Secrets aws secretsmanager list-secrets --filters Key=name,Values=bot/ --output table # Bedrock agents aws bedrock-agent list-agents --output table # CloudWatch alarms aws cloudwatch describe-alarms --alarm-name-prefix slack-bot --output table ``` ## service selection by architecture | Architecture | Primary Services | |---|---| | **Lambda webhook bot** (Slack Events API) | Lambda, API Gateway (HTTP API), Secrets Manager, CloudWatch Logs, IAM, SQS (for async) | | **Lambda + Bedrock AI agent** | Lambda, API Gateway, Bedrock (agent + runtime), Secrets Manager, DynamoDB (state), S3 (knowledge base), IAM | | **ECS Fargate long-running bot** (Socket Mode) | ECS, ECR, Secrets Manager, CloudWatch Logs, IAM | | **EC2 Socket Mode bot** | EC2, IAM (instance profile), SSM Parameter Store, CloudWatch Logs | | **App Runner bot** | App Runner, ECR, Secrets Manager, CloudWatch Logs | | **Elastic Beanstalk bot** | Elastic Beanstalk, S3 (deploy artifacts), CloudWatch Logs, IAM | | **Lex conversational bot** | Lex V2, Lambda (fulfillment), DynamoDB (state), CloudWatch Logs | | **Full production deployment** | All above + CloudFormation, Route 53, ACM, SNS (alerts) | ## pitfalls - **IAM role must exist before Lambda.** The `create-function` call fails if the role ARN doesn't exist yet. After `create-role`, wait a few seconds for propagation before creating the Lambda. - **Detach policies before deleting roles.** `aws iam delete-role` fails if any policies are still attached. Always `detach-role-policy` for each managed policy and `delete-role-policy` for each inline policy first. - **API Gateway permission on Lambda.** Creating the API Gateway and integration doesn't automatically grant invoke permission. You must run `aws lambda add-permission` — without it, API Gateway returns 500 errors. - **Secrets Manager soft-delete.** By default, `delete-secret` schedules deletion after 30 days. Recreating a secret with the same name fails during this window. Use `--force-delete-without-recovery` for immediate deletion, or `restore-secret` to recover. - **Lambda cold starts vs Slack 3-second ack.** Node.js Lambda cold starts can take 1-5 seconds. If your handler does work before calling `ack()`, you exceed the deadline. Use provisioned concurrency or the async SQS pattern. - **ECS needs ECR auth refresh.** The ECR login token expires after 12 hours. CI/CD pipelines must call `aws ecr get-login-password` before every `docker push`. - **DynamoDB on-demand vs provisioned.** On-demand (`PAY_PER_REQUEST`) is best for unpredictable bot traffic. Provisioned with auto-scaling saves cost at high, steady throughput but requires capacity planning. - **CloudFormation stack deletion order.** Stacks with resources that have deletion protection (S3 buckets with objects, DynamoDB tables) will fail to delete. Empty buckets and remove protection before deleting the stack. - **Bedrock model access.** Foundation models require explicit enablement in your account. Check `aws bedrock list-foundation-models` and request access via the console before trying to invoke. - **Region availability for Bedrock.** Not all Bedrock models are available in all regions. Anthropic Claude models are typically available in `us-east-1` and `us-west-2`. ## instructions This expert is a reference catalog of all AWS CLI commands relevant to bot and agent development. Use it when a developer asks "what aws commands do I need for X?" or needs to look up the CLI surface for a specific AWS service. For step-by-step deployment instructions, defer to `aws-bot-deploy-ts.md`. Pair with: `aws-bot-deploy-ts.md` (step-by-step deployment), `../security/secrets-ts.md` (secrets best practices), `../bridge/infra-compute-ts.md` (compute comparisons with Azure), `azure-cli-reference-ts.md` (Azure equivalent). ## research Deep Research prompt: "Catalog all AWS CLI command groups a developer would need for creating, reading, updating, and deleting resources in a bot/agent project on AWS. Include: IAM (roles, policies, instance profiles), Lambda (functions, aliases, layers, function URLs), API Gateway (HTTP API and REST API), EC2 (instances, security groups, key pairs), ECS (clusters, task definitions, services), Elastic Beanstalk, App Runner, Secrets Manager, SSM Parameter Store, CloudWatch (alarms, metrics, logs), S3, DynamoDB, SQS, SNS, ECR, CloudFormation, STS, Route 53, ACM, Bedrock (foundation models, agents, knowledge bases, guardrails), and Lex V2 (bots, intents, slots). For each group, list the key CRUD commands and their purpose." -
azure-bot-deploy-ts.md 15.4 KB
# azure-bot-deploy-ts ## purpose Step-by-step deployment of a Slack bot, Teams bot, or dual bot to Azure. Covers CLI setup, App Registration, Bot Service registration, compute provisioning (App Service / Functions / Container Apps), environment configuration, and verification. ## rules 1. **Install prerequisites before anything else.** You need: Node.js 20 LTS, Azure CLI (`az`), and optionally the Agents Toolkit CLI (`npm install -g @microsoft/m365agentstoolkit-cli`). Verify with `az --version` and `node --version`. [learn.microsoft.com/cli/azure/install-azure-cli](https://learn.microsoft.com/cli/azure/install-azure-cli) 2. **Authenticate and set the target subscription.** Run `az login` to open browser auth, then `az account set --subscription <subscription-id>`. All subsequent commands use this subscription. [learn.microsoft.com/cli/azure/authenticate-azure-cli](https://learn.microsoft.com/cli/azure/authenticate-azure-cli) 3. **Create a resource group to contain all bot resources.** `az group create --name <rg-name> --location <region>`. Use a region close to your users (e.g., `eastus`, `westeurope`). All subsequent resources go in this group. [learn.microsoft.com/azure/azure-resource-manager/management/manage-resource-groups-cli](https://learn.microsoft.com/azure/azure-resource-manager/management/manage-resource-groups-cli) 4. **Register an Entra ID App Registration.** This is the bot's identity — required for both Teams and Slack bots on Azure. `az ad app create --display-name <bot-name>` returns an `appId` (client ID). Then create a secret: `az ad app credential reset --id <appId>`. Save the `password` — it's the CLIENT_SECRET and is only shown once. [learn.microsoft.com/entra/identity-platform/quickstart-register-app](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) 5. **Create an Azure Bot Service resource (Teams bots).** `az bot create --resource-group <rg> --name <bot-name> --app-type SingleTenant --appid <appId> --tenant-id <tenantId>`. Set the messaging endpoint to `https://<app-name>.azurewebsites.net/api/messages`. [learn.microsoft.com/azure/bot-service/bot-service-quickstart-registration](https://learn.microsoft.com/azure/bot-service/bot-service-quickstart-registration) 6. **Connect the Bot Service to Teams.** `az bot msteams create --resource-group <rg> --name <bot-name>`. Without this, Teams cannot reach your bot even if it's deployed and running. [learn.microsoft.com/azure/bot-service/channel-connect-teams](https://learn.microsoft.com/azure/bot-service/channel-connect-teams) 7. **For Slack bots on Azure, skip Bot Service.** Deploy as a plain web app with the Bolt HTTP receiver. Configure the Slack app's Event Subscriptions Request URL and Interactivity URL to `https://<app-name>.azurewebsites.net/slack/events`. 8. **Choose your compute target.** App Service (recommended — always-on, simple), Azure Functions Premium (serverless with warm instances), or Container Apps (containerized workloads with scale-to-zero). Avoid Functions Consumption plan for bots — cold starts exceed Slack's 3-second ack deadline and Teams' response expectations. [learn.microsoft.com/azure/app-service/overview](https://learn.microsoft.com/azure/app-service/overview) 9. **Provision the compute resource with Node.js 20 LTS.** For App Service: `az webapp create --resource-group <rg> --plan <plan-name> --name <app-name> --runtime "NODE:20-lts"`. For Functions: `az functionapp create ... --runtime node --runtime-version 20`. [learn.microsoft.com/azure/app-service/quickstart-nodejs](https://learn.microsoft.com/azure/app-service/quickstart-nodejs) 10. **Configure App Settings with all required environment variables.** `az webapp config appsettings set --resource-group <rg> --name <app-name> --settings MicrosoftAppId=<appId> MicrosoftAppPassword=<secret> MicrosoftAppTenantId=<tenantId> PORT=3978`. For Slack, add `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, and `SLACK_APP_TOKEN`. [learn.microsoft.com/azure/app-service/configure-common](https://learn.microsoft.com/azure/app-service/configure-common) 11. **Build and deploy.** Run `npm run build` locally, then zip deploy: `az webapp deploy --resource-group <rg> --name <app-name> --src-path <zip-path> --type zip`. For Functions: `func azure functionapp publish <app-name>`. [learn.microsoft.com/azure/app-service/deploy-zip](https://learn.microsoft.com/azure/app-service/deploy-zip) 12. **Enable Always On for App Service.** `az webapp config set --resource-group <rg> --name <app-name> --always-on true`. Without this, the app goes idle after 20 minutes and the next request cold-starts. For Functions Premium, configure Always Ready instances instead. [learn.microsoft.com/azure/app-service/configure-common](https://learn.microsoft.com/azure/app-service/configure-common) 13. **Verify the deployment.** Check the health endpoint: `curl https://<app-name>.azurewebsites.net/api/health`. Then send a test message in Teams or Slack. Check App Service logs: `az webapp log tail --resource-group <rg> --name <app-name>`. [learn.microsoft.com/azure/app-service/troubleshoot-diagnostic-logs](https://learn.microsoft.com/azure/app-service/troubleshoot-diagnostic-logs) 14. **Agents Toolkit fast path (Teams bots).** Instead of steps 3-12, run `atk provision` (creates App Registration, Bot Service, App Service, and all config) then `atk deploy` (builds and deploys). Two commands replace the entire manual process. Requires an `m365agents.yml` in your project. [learn.microsoft.com/microsoftteams/platform/toolkit/toolkit-cli](https://learn.microsoft.com/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli) ## interview ### Q1 — Compute Target ``` question: "Which Azure compute target do you want to deploy to?" header: "Compute" options: - label: "App Service (Recommended)" description: "Always-on web app. Simplest deployment, good for most bots. ~$13/month for B1 plan." - label: "Azure Functions Premium" description: "Serverless with warm instances. Auto-scales, pay-per-execution + base cost. Good for variable traffic." - label: "Container Apps" description: "Containerized deployment with Dapr support. Scale-to-zero. Good for microservice architectures." - label: "You Decide Everything" description: "Use App Service (recommended default) and skip remaining questions." multiSelect: false ``` ### Q2 — Deployment Method ``` question: "How do you want to deploy?" header: "Method" options: - label: "Agents Toolkit CLI (Recommended)" description: "atk provision + atk deploy — automates App Registration, Bot Service, App Service, and manifest sideloading in two commands." - label: "Manual az CLI" description: "Full control, step-by-step. Learn exactly what resources are created and how they connect." - label: "You Decide Everything" description: "Use Agents Toolkit CLI (recommended default) and skip remaining questions." multiSelect: false ``` ### defaults table | Question | Default | |---|---| | Q1 | App Service | | Q2 | Agents Toolkit CLI | ## patterns ### End-to-end manual Azure deployment (Teams bot) ```bash # 1. Prerequisites az --version # Verify Azure CLI installed node --version # Verify Node.js 20+ # 2. Authenticate az login az account set --subscription "My Subscription" # 3. Create resource group az group create --name rg-mybot --location eastus # 4. App Registration (bot identity) APP_ID=$(az ad app create --display-name "MyBot" --query appId -o tsv) APP_SECRET=$(az ad app credential reset --id $APP_ID --query password -o tsv) TENANT_ID=$(az account show --query tenantId -o tsv) echo "Save these values:" echo " APP_ID=$APP_ID" echo " APP_SECRET=$APP_SECRET" echo " TENANT_ID=$TENANT_ID" # 5. Create Azure Bot Service az bot create \ --resource-group rg-mybot \ --name mybot-bot \ --app-type SingleTenant \ --appid $APP_ID \ --tenant-id $TENANT_ID # 6. Connect Teams channel az bot msteams create --resource-group rg-mybot --name mybot-bot # 7. Create App Service plan + web app az appservice plan create \ --resource-group rg-mybot \ --name mybot-plan \ --sku B1 \ --is-linux az webapp create \ --resource-group rg-mybot \ --plan mybot-plan \ --name mybot-app \ --runtime "NODE:20-lts" # 8. Configure environment variables az webapp config appsettings set \ --resource-group rg-mybot \ --name mybot-app \ --settings \ MicrosoftAppId=$APP_ID \ MicrosoftAppPassword=$APP_SECRET \ MicrosoftAppTenantId=$TENANT_ID \ PORT=3978 # 9. Enable Always On az webapp config set --resource-group rg-mybot --name mybot-app --always-on true # 10. Update Bot Service messaging endpoint az bot update \ --resource-group rg-mybot \ --name mybot-bot \ --endpoint "https://mybot-app.azurewebsites.net/api/messages" # 11. Build and deploy npm run build cd dist && zip -r ../deploy.zip . && cd .. az webapp deploy --resource-group rg-mybot --name mybot-app --src-path deploy.zip --type zip # 12. Verify curl https://mybot-app.azurewebsites.net/api/health az webapp log tail --resource-group rg-mybot --name mybot-app ``` ### Agents Toolkit fast path (Teams bot) ```bash # 1. Install Agents Toolkit CLI npm install -g @microsoft/m365agentstoolkit-cli@beta # 2. Provision all Azure resources (App Registration, Bot Service, App Service) atk provision --env dev --resource-group <rg> --region <region> -i false # 3. Build and deploy atk deploy --env dev -i false # 4. Sideload to Teams for testing # Get TEAMS_APP_ID from env/.env.dev, open: # https://teams.microsoft.com/l/app/$TEAMS_APP_ID?installAppPackage=true&webjoin=true # That's it — two commands from zero to running bot in Teams. # m365agents.yml in your project defines the resource topology. ``` ### Slack bot on Azure App Service ```bash # 1. Create resource group + App Service (no Bot Service needed) az group create --name rg-slackbot --location eastus az appservice plan create \ --resource-group rg-slackbot \ --name slackbot-plan \ --sku B1 \ --is-linux az webapp create \ --resource-group rg-slackbot \ --plan slackbot-plan \ --name slackbot-app \ --runtime "NODE:20-lts" # 2. Configure Slack credentials az webapp config appsettings set \ --resource-group rg-slackbot \ --name slackbot-app \ --settings \ SLACK_BOT_TOKEN=xoxb-your-token \ SLACK_SIGNING_SECRET=your-signing-secret \ SLACK_APP_TOKEN=xapp-your-app-token \ PORT=3000 # 3. Enable Always On + deploy az webapp config set --resource-group rg-slackbot --name slackbot-app --always-on true npm run build && cd dist && zip -r ../deploy.zip . && cd .. az webapp deploy --resource-group rg-slackbot --name slackbot-app --src-path deploy.zip --type zip # 4. Configure Slack app URLs at api.slack.com: # Event Subscriptions Request URL: https://slackbot-app.azurewebsites.net/slack/events # Interactivity Request URL: https://slackbot-app.azurewebsites.net/slack/events # Slash Command Request URL: https://slackbot-app.azurewebsites.net/slack/events ``` ### Dual bot on Azure (Slack + Teams on shared Express) ```bash # Follow the Teams bot manual deployment (pattern 1), then add Slack config: az webapp config appsettings set \ --resource-group rg-mybot \ --name mybot-app \ --settings \ SLACK_BOT_TOKEN=xoxb-your-token \ SLACK_SIGNING_SECRET=your-signing-secret \ SLACK_APP_TOKEN=xapp-your-app-token # The shared Express server mounts: # Teams: POST /api/messages # Slack: POST /slack/events # Both work on the same App Service instance. # Configure Slack app URLs at api.slack.com: # Event Subscriptions: https://mybot-app.azurewebsites.net/slack/events # Interactivity: https://mybot-app.azurewebsites.net/slack/events ``` ## pitfalls - **Forgetting to set the Bot Service messaging endpoint.** After creating the App Service, you must update the Bot Service with `az bot update --endpoint`. Without this, Teams messages never reach your code. The endpoint format is `https://<app-name>.azurewebsites.net/api/messages`. - **Using Consumption plan for bots.** Azure Functions Consumption plan has cold starts of 5-15 seconds. This exceeds Slack's 3-second ack deadline and causes Teams timeout errors. Use App Service (Always On) or Functions Premium (Always Ready) instead. - **CLIENT_SECRET expiration.** Entra ID app secrets expire by default after 6 months. Set a calendar reminder. Rotate by creating a new secret (`az ad app credential reset`) and updating the App Setting before the old one expires. - **Port mismatch.** App Service injects the `PORT` environment variable (usually 8080). Your bot code must listen on `process.env.PORT`. If you hardcode port 3978, the app starts but App Service can't route traffic to it. - **Deploying without building.** `az webapp deploy --type zip` deploys whatever is in the zip. If you skip `npm run build`, you're deploying source TypeScript, not compiled JavaScript. The app crashes with syntax errors. - **Forgetting Always On.** Without `--always-on true`, App Service idles after 20 minutes. The first request after idle takes 10-30 seconds to cold-start, causing timeout errors in both Slack and Teams. - **Missing Teams channel on Bot Service.** Running `az bot create` creates the Bot Service but doesn't connect it to Teams. You must also run `az bot msteams create`. Without it, Teams shows "This app is not responding." - **Slack Request URL verification failure.** When you enter the Request URL in the Slack app dashboard, Slack immediately sends a verification challenge. Your app must already be deployed and running. Configure the URL after deployment, not before. ## references - https://learn.microsoft.com/cli/azure/install-azure-cli - https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - https://learn.microsoft.com/azure/bot-service/bot-service-quickstart-registration - https://learn.microsoft.com/azure/bot-service/channel-connect-teams - https://learn.microsoft.com/azure/app-service/quickstart-nodejs - https://learn.microsoft.com/azure/app-service/deploy-zip - https://learn.microsoft.com/azure/app-service/configure-common - https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli - https://learn.microsoft.com/azure/app-service/troubleshoot-diagnostic-logs ## instructions This expert walks through deploying a bot to Azure from scratch — from installing the CLI to verifying a test message. Use it when a developer says "deploy my bot to Azure", "set up Azure hosting", or "get my bot running in production on Azure". Covers Teams bots (with Bot Service + App Registration), Slack bots (plain App Service), and dual bots (shared Express). Pair with: `../teams/project.scaffold-files-ts.md` (project structure before deployment), `../teams/runtime.manifest-ts.md` (Teams manifest for sideloading after deployment), `../security/secrets-ts.md` (secrets best practices), `../bridge/infra-compute-ts.md` (if comparing Azure compute options with AWS equivalents). ## research Deep Research prompt: "Write a micro expert on deploying a Slack Bolt.js or Microsoft Teams bot to Azure. Cover: Azure CLI installation, az login, resource group creation, Entra ID App Registration (client ID + secret), Azure Bot Service creation and Teams channel connection, App Service provisioning with Node.js 20 LTS, environment variable configuration via App Settings, zip deployment, Always On configuration, Agents Toolkit CLI (atk provision + atk deploy) as a fast path, Slack-on-Azure configuration (Event Subscriptions URL), dual bot deployment on shared Express, and common deployment verification steps. Provide 3-4 canonical bash script examples and 6-8 common pitfalls." -
azure-cli-reference-ts.md 16.9 KB
# azure-cli-reference-ts ## purpose Comprehensive reference of all Azure CLI (`az`) command groups a developer needs for creating, reading, updating, and deleting resources in a bot or AI agent project on Azure. Use as a lookup companion to `azure-bot-deploy-ts.md` (step-by-step deployment) — this file maps every relevant CLI surface so you know what commands exist. ## rules 1. **This is a reference, not a tutorial.** For step-by-step deployment walkthroughs, see `azure-bot-deploy-ts.md`. This file catalogs every `az` command group relevant to bot/agent projects. 2. **Always authenticate first.** Every command below assumes you have run `az login` and `az account set --subscription <id>`. [learn.microsoft.com/cli/azure/authenticate-azure-cli](https://learn.microsoft.com/cli/azure/authenticate-azure-cli) 3. **Resource group is required for almost everything.** Most commands take `--resource-group <rg>`. Create one with `az group create` before provisioning any resources. --- ## 1. Bot Service (`az bot`) The core resource for registering and managing a bot on Azure. | Command | Purpose | |---|---| | `az bot create` | Register a new v4 SDK bot (requires `--app-type`, `--appid`, `--name`, `--resource-group`) | | `az bot show` | Get bot details | | `az bot update` | Update bot properties (endpoint, description, display name) | | `az bot delete` | Delete a bot registration | | `az bot download` | Download bot source code | | `az bot publish` | Publish to the bot's associated App Service | | `az bot prepare-deploy` | Add deployment config files | Reference: [learn.microsoft.com/cli/azure/bot](https://learn.microsoft.com/cli/azure/bot) ### Bot Channels Connect a bot to messaging platforms. Each channel group has `create`, `delete`, `show` sub-commands: | Channel Group | Platform | |---|---| | `az bot msteams` | Microsoft Teams | | `az bot slack` | Slack | | `az bot directline` | DirectLine (web/custom clients) | | `az bot webchat` | Web Chat embed | | `az bot facebook` | Facebook Messenger | | `az bot telegram` | Telegram | | `az bot email` | Email | | `az bot sms` | SMS | | `az bot kik` | Kik | | `az bot skype` | Skype | Reference: [learn.microsoft.com/cli/azure/bot/msteams](https://learn.microsoft.com/cli/azure/bot/msteams) ### Bot Auth (`az bot authsetting`) Manage OAuth connection settings on a bot: | Command | Purpose | |---|---| | `az bot authsetting create` | Create an OAuth connection | | `az bot authsetting show` | View a connection | | `az bot authsetting list` | List all connections | | `az bot authsetting delete` | Delete a connection | | `az bot authsetting list-providers` | List available OAuth providers | Reference: [learn.microsoft.com/cli/azure/bot/authsetting](https://learn.microsoft.com/cli/azure/bot/authsetting) --- ## 2. AI Foundry Agents (`az cognitiveservices agent`) For hosted AI agents via Azure AI Foundry: | Command | Purpose | |---|---| | `az cognitiveservices agent create` | Create hosted agent from container image or source | | `az cognitiveservices agent show` | Get agent details | | `az cognitiveservices agent update` | Update agent deployment | | `az cognitiveservices agent delete` | Delete agent version(s) | | `az cognitiveservices agent list` | List agents | | `az cognitiveservices agent list-versions` | List all versions of an agent | | `az cognitiveservices agent start` | Start agent deployment | | `az cognitiveservices agent stop` | Stop agent deployment | | `az cognitiveservices agent status` | Check deployment status | | `az cognitiveservices agent delete-deployment` | Delete a deployment | | `az cognitiveservices agent logs` | View container logs | Reference: [learn.microsoft.com/cli/azure/cognitiveservices/agent](https://learn.microsoft.com/cli/azure/cognitiveservices/agent) --- ## 3. Azure OpenAI / Cognitive Services (`az cognitiveservices account`) Manage the AI backend your bot/agent calls: | Command | Purpose | |---|---| | `az cognitiveservices account create` | Create an Azure OpenAI / Cognitive Services account | | `az cognitiveservices account show` | View account details | | `az cognitiveservices account update` | Update account settings | | `az cognitiveservices account delete` | Delete account | | `az cognitiveservices account list` | List all accounts in subscription | | `az cognitiveservices account keys list` | Get API keys | | `az cognitiveservices account keys regenerate` | Rotate keys | | `az cognitiveservices account deployment create` | Deploy a model (e.g., GPT-4o) | | `az cognitiveservices account deployment show` | View deployment | | `az cognitiveservices account deployment list` | List deployments | | `az cognitiveservices account deployment delete` | Remove deployment | | `az cognitiveservices model list` | List available models | Reference: [learn.microsoft.com/cli/azure/cognitiveservices/account](https://learn.microsoft.com/cli/azure/cognitiveservices/account) --- ## 4. App Registration / Identity (`az ad app`, `az ad sp`, `az identity`) Every bot needs an app registration for authentication. ### App Registration (`az ad app`) | Command | Purpose | |---|---| | `az ad app create` | Create Entra ID app registration (gets the appId for bot) | | `az ad app show` | View app details | | `az ad app update` | Update app properties | | `az ad app delete` | Delete app registration | | `az ad app list` | List apps | | `az ad app credential reset` | Reset password/certificate | | `az ad app credential list` | List credentials | | `az ad app credential delete` | Remove a credential | | `az ad app permission` | Manage OAuth2 API permissions | Reference: [learn.microsoft.com/cli/azure/ad/app](https://learn.microsoft.com/cli/azure/ad/app) ### Managed Identity (`az identity`) | Command | Purpose | |---|---| | `az identity create` | Create a user-assigned managed identity | | `az identity show` | View identity details | | `az identity list` | List identities | | `az identity delete` | Delete identity | Reference: [learn.microsoft.com/cli/azure/identity](https://learn.microsoft.com/cli/azure/identity) ### Role Assignments (`az role assignment`) | Command | Purpose | |---|---| | `az role assignment create` | Grant a role (e.g., "Cognitive Services OpenAI User") | | `az role assignment list` | List current assignments | | `az role assignment delete` | Revoke a role | Reference: [learn.microsoft.com/cli/azure/role/assignment](https://learn.microsoft.com/cli/azure/role/assignment) --- ## 5. Hosting / Compute ### Web App (`az webapp`) Traditional bot hosting on App Service: | Command | Purpose | |---|---| | `az webapp create` | Create App Service for bot | | `az webapp show` / `list` / `delete` / `update` | Standard CRUD | | `az webapp start` / `stop` / `restart` | Lifecycle management | | `az webapp deploy` | Deploy artifact (zip, war, jar) | | `az webapp up` | Create + deploy from local workspace | | `az webapp deployment source` | Configure source control deployment | | `az webapp deployment github-actions` | Configure CI/CD via GitHub Actions | | `az webapp deployment slot` | Manage staging slots | | `az webapp config` | App settings, connection strings, runtime | | `az webapp identity` | Assign managed identity to web app | | `az webapp log` | View/configure logs | Reference: [learn.microsoft.com/cli/azure/webapp](https://learn.microsoft.com/cli/azure/webapp) ### App Service Plan (`az appservice plan`) | Command | Purpose | |---|---| | `az appservice plan create` | Create hosting plan (defines SKU/pricing tier) | | `az appservice plan show` / `list` / `update` / `delete` | Standard CRUD | Reference: [learn.microsoft.com/cli/azure/appservice/plan](https://learn.microsoft.com/cli/azure/appservice/plan) ### Function App (`az functionapp`) Serverless bot hosting: | Command | Purpose | |---|---| | `az functionapp create` | Create a function app | | `az functionapp show` / `list` / `delete` / `update` | Standard CRUD | | `az functionapp start` / `stop` / `restart` | Lifecycle | | `az functionapp deploy` | Deploy artifact | | `az functionapp config` | App settings, runtime config | | `az functionapp identity` | Managed identity | | `az functionapp keys` | Manage function keys | | `az functionapp log` | View logs | Reference: [learn.microsoft.com/cli/azure/functionapp](https://learn.microsoft.com/cli/azure/functionapp) ### Container App (`az containerapp`) Containerized bot hosting: | Command | Purpose | |---|---| | `az containerapp create` | Create container app | | `az containerapp show` / `list` / `delete` / `update` | Standard CRUD | | `az containerapp up` | Create + deploy (handles ACR, env, etc.) | | `az containerapp env` | Manage Container Apps environments | | `az containerapp secret` | Manage secrets | | `az containerapp identity` | Managed identity | | `az containerapp ingress` | Configure ingress / traffic | | `az containerapp revision` | Manage revisions | | `az containerapp logs` | View logs | | `az containerapp job` | Manage background jobs | Reference: [learn.microsoft.com/cli/azure/containerapp](https://learn.microsoft.com/cli/azure/containerapp) --- ## 6. Infrastructure & Resource Management ### Resource Groups (`az group`) | Command | Purpose | |---|---| | `az group create` | Create resource group (logical container for all bot resources) | | `az group show` / `list` / `delete` / `update` | Standard CRUD | | `az group exists` | Check existence | | `az group export` | Export as ARM template | Reference: [learn.microsoft.com/cli/azure/group](https://learn.microsoft.com/cli/azure/group) ### Subscriptions (`az account`) | Command | Purpose | |---|---| | `az account list` | List subscriptions | | `az account set` | Switch active subscription | | `az account show` | Show current subscription | Reference: [learn.microsoft.com/cli/azure/account](https://learn.microsoft.com/cli/azure/account) --- ## 7. Secrets & Configuration (`az keyvault`) | Command | Purpose | |---|---| | `az keyvault create` / `show` / `list` / `delete` | Vault CRUD | | `az keyvault secret set` | Store a secret (API keys, connection strings) | | `az keyvault secret show` | Retrieve a secret | | `az keyvault secret list` | List secrets | | `az keyvault secret delete` | Delete a secret | | `az keyvault set-policy` | Grant access to the bot's identity | Reference: [learn.microsoft.com/cli/azure/keyvault](https://learn.microsoft.com/cli/azure/keyvault) --- ## 8. Storage & State ### Storage Account (`az storage account`) For bot state and blob storage: | Command | Purpose | |---|---| | `az storage account create` | Create storage account | | `az storage account show` / `list` / `delete` / `update` | Standard CRUD | | `az storage account show-connection-string` | Get connection string | | `az storage account keys list` | Get access keys | Reference: [learn.microsoft.com/cli/azure/storage/account](https://learn.microsoft.com/cli/azure/storage/account) ### Cosmos DB (`az cosmosdb`) For bot conversation state: | Command | Purpose | |---|---| | `az cosmosdb create` | Create Cosmos DB account | | `az cosmosdb show` / `list` / `delete` / `update` | Standard CRUD | | `az cosmosdb keys list` | Get access keys | | `az cosmosdb sql database create` | Create a SQL API database | | `az cosmosdb sql container create` | Create a container | Reference: [learn.microsoft.com/cli/azure/cosmosdb](https://learn.microsoft.com/cli/azure/cosmosdb) --- ## 9. Monitoring & Diagnostics (`az monitor`) | Command | Purpose | |---|---| | `az monitor log-analytics workspace create` | Create Log Analytics workspace | | `az monitor diagnostic-settings create` | Enable diagnostics on bot resources | | `az monitor metrics list` | View resource metrics | | `az monitor activity-log list` | View activity logs | | `az monitor action-group create` | Set up alert notifications | Reference: [learn.microsoft.com/cli/azure/monitor](https://learn.microsoft.com/cli/azure/monitor) --- ## patterns ### Minimum viable bot/agent CRUD flow The numbered steps below show the typical order for provisioning a complete bot project from scratch using only `az` commands: ```bash # 1. Resource group az group create --name rg-mybot --location eastus # 2. App registration + secret APP_ID=$(az ad app create --display-name "MyBot" --query appId -o tsv) APP_SECRET=$(az ad app credential reset --id $APP_ID --query password -o tsv) TENANT_ID=$(az account show --query tenantId -o tsv) # 3. Hosting plan az appservice plan create --resource-group rg-mybot --name mybot-plan --sku B1 --is-linux # 4. Web app (or functionapp / containerapp) az webapp create --resource-group rg-mybot --plan mybot-plan --name mybot-app --runtime "NODE:20-lts" # 5. Bot registration az bot create --resource-group rg-mybot --name mybot-bot \ --app-type SingleTenant --appid $APP_ID --tenant-id $TENANT_ID # 6. Connect channels az bot msteams create --resource-group rg-mybot --name mybot-bot # az bot slack create --resource-group rg-mybot --name mybot-bot ... # 7. AI backend (Azure OpenAI) az cognitiveservices account create --resource-group rg-mybot --name mybot-openai \ --kind OpenAI --sku S0 --location eastus az cognitiveservices account deployment create --resource-group rg-mybot \ --name mybot-openai --deployment-name gpt-4o \ --model-name gpt-4o --model-version "2024-08-06" --model-format OpenAI \ --sku-name Standard --sku-capacity 10 # 8. Secrets management az keyvault create --resource-group rg-mybot --name mybot-kv --location eastus az keyvault secret set --vault-name mybot-kv --name "AppSecret" --value "$APP_SECRET" # 9. Wire up permissions (managed identity → OpenAI) az webapp identity assign --resource-group rg-mybot --name mybot-app PRINCIPAL_ID=$(az webapp identity show --resource-group rg-mybot --name mybot-app --query principalId -o tsv) OPENAI_ID=$(az cognitiveservices account show --resource-group rg-mybot --name mybot-openai --query id -o tsv) az role assignment create --assignee $PRINCIPAL_ID \ --role "Cognitive Services OpenAI User" --scope $OPENAI_ID # 10. Observability az monitor diagnostic-settings create --resource rg-mybot/mybot-app \ --name mybot-diag --logs '[{"enabled":true,"category":"AppServiceHTTPLogs"}]' \ --workspace <log-analytics-workspace-id> ``` ### Teardown (delete everything) ```bash # Delete the entire resource group and all resources within it az group delete --name rg-mybot --yes --no-wait # Delete the app registration separately (it lives in Entra ID, not the resource group) az ad app delete --id $APP_ID ``` ### List all resources in a bot project ```bash # See everything in the resource group az resource list --resource-group rg-mybot --output table # Check bot channel connections az bot show --resource-group rg-mybot --name mybot-bot --query "properties.enabledChannels" # Check OpenAI deployments az cognitiveservices account deployment list --resource-group rg-mybot --name mybot-openai --output table ``` ## pitfalls - **App Registration lives outside the resource group.** Deleting the resource group does not delete the Entra ID app registration. Always clean up with `az ad app delete --id <appId>` separately. - **Key Vault soft-delete.** Deleted vaults are retained for 90 days by default. Recreating a vault with the same name fails until you purge it: `az keyvault purge --name <vault-name>`. - **Cognitive Services region availability.** Not all Azure OpenAI models are available in all regions. Check `az cognitiveservices model list --location <region>` before creating the account. - **Role assignment propagation delay.** After `az role assignment create`, it can take up to 5 minutes for the assignment to propagate. If your bot gets 403 errors immediately after setup, wait and retry. - **Managed identity vs app secret.** Prefer managed identity (`az webapp identity assign`) over storing `MicrosoftAppPassword` in app settings. Managed identities rotate automatically and never expire. - **Container Apps require an environment.** You must create a Container Apps environment (`az containerapp env create`) before creating a container app. The environment defines the Log Analytics workspace and networking. ## instructions This expert is a reference catalog of all Azure CLI commands relevant to bot and agent development. Use it when a developer asks "what az commands do I need for X?" or needs to look up the CLI surface for a specific Azure service. For step-by-step deployment instructions, defer to `azure-bot-deploy-ts.md`. Pair with: `azure-bot-deploy-ts.md` (step-by-step deployment), `../security/secrets-ts.md` (secrets best practices), `../bridge/infra-compute-ts.md` (compute comparisons). ## research Deep Research prompt: "Catalog all Azure CLI (`az`) command groups a developer would need for creating, reading, updating, and deleting resources in a bot/agent project on Azure. Include: bot service (az bot), bot channels, bot auth settings, AI Foundry agents (az cognitiveservices agent), Azure OpenAI (az cognitiveservices account), app registration (az ad app), managed identity (az identity), role assignments, hosting (webapp, functionapp, containerapp), resource groups, subscriptions, Key Vault, storage accounts, Cosmos DB, and monitoring. For each group, list the key CRUD commands and their purpose." -
index.md 4.6 KB
# deploy-router ## purpose Route deployment tasks to the correct cloud-specific expert. Handles the initial cloud provider interview, then loads the matching micro-expert for step-by-step provisioning and deployment. ## interview ### Q1 — Cloud Provider ``` question: "Which cloud provider are you deploying to?" header: "Cloud" options: - label: "Azure (Recommended)" description: "Deploy to Azure App Service, Functions, or Container Apps. Required for Teams bots (Bot Framework registration lives in Azure). Also works for Slack bots." - label: "AWS" description: "Deploy to AWS Lambda, EC2, or ECS/Fargate. Native choice for Slack bots. Teams bots on AWS still require an Azure Bot Service registration." - label: "You Decide Everything" description: "Accept recommended defaults for all decisions and skip remaining questions." multiSelect: false ``` ### Q2 — Bot Platform ``` question: "Which bot platform are you deploying?" header: "Platform" options: - label: "Teams bot" description: "Microsoft Teams bot using Teams SDK / Bot Framework. Requires Azure Bot Service registration regardless of hosting cloud." - label: "Slack bot" description: "Slack app using @slack/bolt. Requires Slack API app configuration." - label: "Both (dual bot)" description: "Single server hosting both Slack and Teams bots. Deploy once, configure both platforms." - label: "You Decide Everything" description: "Accept recommended defaults for all decisions and skip remaining questions." multiSelect: false ``` ### defaults table | Question | Default | |---|---| | Q1 | Azure | | Q2 | Teams bot | ## task clusters ### Deploy to Azure When: deploying a bot to Azure, Azure App Service, Azure Functions, Azure Container Apps, `az` CLI, `az login`, Azure Bot registration, App Registration, Entra ID, `atk provision`, `atk deploy`, Agents Toolkit deploy, deploy Teams bot, deploy Slack bot to Azure Read: - `azure-bot-deploy-ts.md` Cross-domain deps: `../teams/project.scaffold-files-ts.md` (project structure), `../teams/runtime.manifest-ts.md` (Teams manifest for sideloading), `../teams/dev.debug-test-ts.md` (Agents Toolkit reference), `../security/secrets-ts.md` (secrets hygiene), `../bridge/infra-compute-ts.md` (if also migrating from AWS) Note: For Agents Toolkit automated deployment (alternative to manual Azure CLI), see `../teams/toolkit.lifecycle-cli.md`. ### Azure CLI Reference When: looking up Azure CLI commands, "what az commands do I need", az bot commands, az cognitiveservices commands, az ad app commands, az webapp commands, az containerapp commands, az keyvault commands, Azure CLI CRUD reference, list all az commands for bots Read: - `azure-cli-reference-ts.md` Cross-domain deps: `azure-bot-deploy-ts.md` (step-by-step deployment), `../security/secrets-ts.md` (secrets hygiene) ### Deploy to AWS When: deploying a bot to AWS, Lambda, EC2, ECS, Elastic Beanstalk, Fargate, AWS CLI, `aws configure`, API Gateway, CloudFormation, SAM, CDK, deploy Slack bot to AWS Read: - `aws-bot-deploy-ts.md` Cross-domain deps: `../slack/bolt-oauth-distribution-ts.md` (Slack OAuth for multi-workspace), `../security/secrets-ts.md` (secrets hygiene), `../bridge/infra-compute-ts.md` (if also deploying to Azure) ### AWS CLI Reference When: looking up AWS CLI commands, "what aws commands do I need", aws lambda commands, aws ecs commands, aws bedrock commands, aws iam commands, aws secretsmanager commands, aws dynamodb commands, aws sqs commands, AWS CLI CRUD reference, list all aws commands for bots, Bedrock agents, Lex bots Read: - `aws-cli-reference-ts.md` Cross-domain deps: `aws-bot-deploy-ts.md` (step-by-step deployment), `../security/secrets-ts.md` (secrets hygiene) ### Deploy Both (Dual Bot) When: deploying a dual-platform bot to the cloud, deploy to both Azure and AWS, single server deployment for both Slack and Teams Read: - `azure-bot-deploy-ts.md` - `aws-bot-deploy-ts.md` Cross-domain deps: `../bridge/cross-platform-architecture-ts.md` (shared Express architecture) ## combining rule If deploying a **dual bot** (Slack + Teams), read both cloud experts. The **Azure expert always applies for Teams bots** even when primary hosting is AWS — Bot Service registration is Azure-only. If deploying a **Slack-only bot**, either cloud works independently. ## file inventory `aws-bot-deploy-ts.md` | `aws-cli-reference-ts.md` | `azure-bot-deploy-ts.md` | `azure-cli-reference-ts.md` <!-- Created 2026-02-28: Deploy domain with cloud provider interview, Azure and AWS deployment experts --> <!-- Updated 2026-03-01: Added cross-reference to teams/toolkit.lifecycle-cli.md as Agents Toolkit alternative to manual Azure deployment -->
-
-
models
-
anthropic-ts.md 7.8 KB
# anthropic-ts ## purpose Configuring and calling Anthropic Claude models from TypeScript using the official SDK. Covers direct API usage, tool use, streaming, vision, and integration patterns for bots. ## rules 1. **Use the official `@anthropic-ai/sdk` package.** `npm install @anthropic-ai/sdk`. This is the only supported TypeScript/JavaScript SDK. [docs.anthropic.com/en/docs/build-with-claude/getting-started](https://docs.anthropic.com/en/docs/build-with-claude/getting-started) 2. **The SDK reads `ANTHROPIC_API_KEY` from the environment by default.** You can also pass it explicitly: `new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })`. [docs.anthropic.com/en/api/client-sdks](https://docs.anthropic.com/en/api/client-sdks) 3. **Use the Messages API, not the legacy Completions API.** All Claude models use `client.messages.create()`. The legacy `client.completions.create()` is deprecated. [docs.anthropic.com/en/api/messages](https://docs.anthropic.com/en/api/messages) 4. **Always specify `max_tokens`.** Unlike OpenAI, Anthropic requires `max_tokens` on every request. There is no default. Omitting it throws an error. [docs.anthropic.com/en/api/messages](https://docs.anthropic.com/en/api/messages) 5. **System messages go in the `system` parameter, not in `messages`.** Claude uses a top-level `system` string, not a `{ role: 'system', content: '...' }` message. Putting system content in `messages` with role `'system'` will error. [docs.anthropic.com/en/docs/build-with-claude/prompt-caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) 6. **Use `model` IDs like `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001`.** Check the model names page for the latest IDs. Aliases like `claude-3-5-sonnet-latest` are available but less predictable. [docs.anthropic.com/en/docs/about-claude/models](https://docs.anthropic.com/en/docs/about-claude/models) 7. **Tool use follows the `tools` + `tool_use` / `tool_result` pattern.** Define tools in the request, receive `tool_use` content blocks in the response, execute them, then send `tool_result` blocks back. [docs.anthropic.com/en/docs/build-with-claude/tool-use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) 8. **Streaming uses `client.messages.stream()`.** Returns a `MessageStream` with event-driven or async iteration. Use `stream.on('text', (text) => ...)` for incremental text or `for await (const event of stream)` for full events. [docs.anthropic.com/en/api/messages-streaming](https://docs.anthropic.com/en/api/messages-streaming) 9. **Handle rate limits with exponential backoff.** The SDK throws `RateLimitError` (HTTP 429). Implement retry logic with backoff, or use the SDK's built-in `maxRetries` option (defaults to 2). [docs.anthropic.com/en/api/rate-limits](https://docs.anthropic.com/en/api/rate-limits) 10. **For Bedrock-hosted Claude, use the Bedrock expert instead.** This expert covers direct Anthropic API access. If you're accessing Claude through AWS Bedrock, see `bedrock-ts.md` — the SDK and auth are completely different. ## patterns ### Basic chat completion ```typescript import Anthropic from '@anthropic-ai/sdk'; const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, maxRetries: 3, timeout: 30000, }); const response = await client.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, system: 'You are a helpful assistant in a Slack workspace.', messages: [ { role: 'user', content: userMessage }, ], }); const reply = response.content[0].type === 'text' ? response.content[0].text : ''; ``` ### Streaming ```typescript const stream = client.messages.stream({ model: 'claude-sonnet-4-6', max_tokens: 1024, system: 'You are a helpful assistant.', messages: [{ role: 'user', content: userMessage }], }); stream.on('text', (text) => { process.stdout.write(text); }); const finalMessage = await stream.finalMessage(); ``` ### Tool use (function calling) ```typescript const response = await client.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, tools: [{ name: 'get_weather', description: 'Get current weather for a location', input_schema: { type: 'object', properties: { location: { type: 'string', description: 'City name' }, }, required: ['location'], }, }], messages: [{ role: 'user', content: 'What is the weather in Seattle?' }], }); // Check for tool use in the response const toolUseBlock = response.content.find((b) => b.type === 'tool_use'); if (toolUseBlock && toolUseBlock.type === 'tool_use') { const result = await executeFunction(toolUseBlock.name, toolUseBlock.input); // Send tool result back const followUp = await client.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, tools: [/* same tools */], messages: [ { role: 'user', content: 'What is the weather in Seattle?' }, { role: 'assistant', content: response.content }, { role: 'user', content: [{ type: 'tool_result', tool_use_id: toolUseBlock.id, content: JSON.stringify(result), }], }, ], }); } ``` ### Multi-turn conversation ```typescript const conversationHistory: Anthropic.MessageParam[] = []; async function chat(userInput: string): Promise<string> { conversationHistory.push({ role: 'user', content: userInput }); const response = await client.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, system: 'You are a helpful Slack bot.', messages: conversationHistory, }); const assistantText = response.content .filter((b) => b.type === 'text') .map((b) => b.text) .join(''); conversationHistory.push({ role: 'assistant', content: response.content }); return assistantText; } ``` ## pitfalls - **Putting system message in `messages` array.** Claude requires `system` as a top-level parameter. `{ role: 'system', content: '...' }` in `messages` will error. - **Forgetting `max_tokens`.** Anthropic requires this on every request. Unlike OpenAI, there's no default. - **Response content is an array, not a string.** `response.content` is `ContentBlock[]`. Each block has a `type` (`text`, `tool_use`). Don't treat it as a plain string. - **Tool result must reference `tool_use_id`.** When sending `tool_result` blocks, you must include the exact `tool_use_id` from the model's response. Mismatches cause errors. - **`stop_reason` vs `finish_reason`.** Anthropic uses `stop_reason` (values: `end_turn`, `max_tokens`, `stop_sequence`, `tool_use`). Don't confuse with OpenAI's `finish_reason`. - **Model ID format differs from OpenAI.** Anthropic uses IDs like `claude-sonnet-4-6`, not `gpt-4o`. Check the models page for current IDs. ## references - [Anthropic TypeScript SDK — GitHub](https://github.com/anthropics/anthropic-sdk-typescript) - [Messages API Reference](https://docs.anthropic.com/en/api/messages) - [Tool Use Guide](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) - [Streaming Guide](https://docs.anthropic.com/en/api/messages-streaming) - [Claude Models](https://docs.anthropic.com/en/docs/about-claude/models) ## instructions This expert covers direct Anthropic API usage from TypeScript. Use it when the developer is calling Claude models via the Anthropic API (not through Bedrock). For Bedrock-hosted Claude, see `bedrock-ts.md` instead. Pair with: `bedrock-ts.md` (if also using Bedrock), `openai-azure-openai-ts.md` (if mixing providers), `../security/secrets-ts.md` (API key management). ## research Deep Research prompt: "Write a micro expert on using the @anthropic-ai/sdk TypeScript package. Cover: client initialization, Messages API, system messages, max_tokens requirement, streaming with MessageStream, tool use (defining tools, handling tool_use blocks, sending tool_result), multi-turn conversations, vision/image input, prompt caching, rate limits and retries, error handling, and model selection guidance." -
bedrock-ts.md 9.9 KB
# bedrock-ts ## purpose Calling AI models hosted on AWS Bedrock from TypeScript. Covers the Converse API (multi-model), Bedrock Agents, Knowledge Bases, guardrails, and IAM-based authentication. Supports Anthropic Claude, Meta Llama, Cohere, Amazon Titan, and other Bedrock-hosted models. ## rules 1. **Use `@aws-sdk/client-bedrock-runtime` for model invocation.** This is the primary package for calling models. `npm install @aws-sdk/client-bedrock-runtime`. For agent/KB management, use `@aws-sdk/client-bedrock-agent` and `@aws-sdk/client-bedrock-agent-runtime`. [docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/bedrock-runtime](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/bedrock-runtime) 2. **Prefer the Converse API over InvokeModel.** `ConverseCommand` provides a unified interface across all Bedrock models — same request/response format regardless of provider. `InvokeModelCommand` requires provider-specific request bodies. [docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html) 3. **Authentication uses AWS IAM, not API keys.** Bedrock uses standard AWS credential resolution: environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`), IAM roles, SSO, or credential files. No Anthropic/OpenAI-style API keys. [docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html) 4. **Model IDs follow the format `provider.model-name`.** Examples: `anthropic.claude-3-5-sonnet-20241022-v2:0`, `meta.llama3-2-90b-instruct-v1:0`, `amazon.titan-text-express-v1`. Check the Bedrock console for available model IDs in your region. [docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html](https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html) 5. **You must enable model access before use.** Go to the Bedrock console → Model access → Request access for each model. This is a one-time per-account setup. Without it, API calls return `AccessDeniedException`. [docs.aws.amazon.com/bedrock/latest/userguide/model-access.html](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) 6. **Region matters for model availability.** Not all models are available in all regions. Claude is typically in `us-east-1` and `us-west-2`. Llama and Titan have broader availability. Check the model access page in your target region. 7. **Use `ConverseStreamCommand` for streaming.** Returns a stream of events. Iterate with `for await (const event of response.stream)` and check `event.contentBlockDelta?.delta?.text` for incremental text. [docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call-streaming.html](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call-streaming.html) 8. **Tool use with Converse API follows a unified format.** Define `toolConfig` with `tools` array. The model returns `toolUse` content blocks. Send `toolResult` blocks back. This works identically across all Bedrock models that support tool use. [docs.aws.amazon.com/bedrock/latest/userguide/tool-use.html](https://docs.aws.amazon.com/bedrock/latest/userguide/tool-use.html) 9. **Bedrock Agents provide autonomous tool orchestration.** Unlike raw tool use (where your code manages the loop), Bedrock Agents handle the tool-call loop internally. You invoke the agent and get a final answer. Use `@aws-sdk/client-bedrock-agent-runtime` with `InvokeAgentCommand`. [docs.aws.amazon.com/bedrock/latest/userguide/agents.html](https://docs.aws.amazon.com/bedrock/latest/userguide/agents.html) 10. **Attach guardrails to filter content.** Pass `guardrailConfig: { guardrailIdentifier, guardrailVersion }` in the Converse request to apply content filters, denied topics, and PII redaction. [docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html) ## patterns ### Converse API — basic chat completion ```typescript import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; const client = new BedrockRuntimeClient({ region: 'us-east-1' }); const response = await client.send(new ConverseCommand({ modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0', messages: [ { role: 'user', content: [{ text: userMessage }], }, ], system: [{ text: 'You are a helpful assistant.' }], inferenceConfig: { maxTokens: 1024, temperature: 0.7, }, })); const reply = response.output?.message?.content?.[0]?.text ?? ''; ``` ### Converse API — streaming ```typescript import { BedrockRuntimeClient, ConverseStreamCommand } from '@aws-sdk/client-bedrock-runtime'; const client = new BedrockRuntimeClient({ region: 'us-east-1' }); const response = await client.send(new ConverseStreamCommand({ modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0', messages: [{ role: 'user', content: [{ text: userMessage }] }], system: [{ text: 'You are a helpful assistant.' }], inferenceConfig: { maxTokens: 1024 }, })); for await (const event of response.stream!) { if (event.contentBlockDelta?.delta?.text) { process.stdout.write(event.contentBlockDelta.delta.text); } } ``` ### Tool use with Converse API ```typescript const response = await client.send(new ConverseCommand({ modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0', messages: [{ role: 'user', content: [{ text: 'What is the weather in Seattle?' }] }], toolConfig: { tools: [{ toolSpec: { name: 'get_weather', description: 'Get current weather for a location', inputSchema: { json: { type: 'object', properties: { location: { type: 'string', description: 'City name' } }, required: ['location'], }, }, }, }], }, })); // Check for tool use const toolUseBlock = response.output?.message?.content?.find((b) => b.toolUse); if (toolUseBlock?.toolUse) { const result = await executeFunction(toolUseBlock.toolUse.name!, toolUseBlock.toolUse.input); const followUp = await client.send(new ConverseCommand({ modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0', messages: [ { role: 'user', content: [{ text: 'What is the weather in Seattle?' }] }, { role: 'assistant', content: response.output!.message!.content! }, { role: 'user', content: [{ toolResult: { toolUseId: toolUseBlock.toolUse.toolUseId!, content: [{ text: JSON.stringify(result) }], }, }], }, ], toolConfig: { tools: [/* same tools */] }, })); } ``` ### Invoke a Bedrock Agent ```typescript import { BedrockAgentRuntimeClient, InvokeAgentCommand } from '@aws-sdk/client-bedrock-agent-runtime'; const agentClient = new BedrockAgentRuntimeClient({ region: 'us-east-1' }); const response = await agentClient.send(new InvokeAgentCommand({ agentId: process.env.BEDROCK_AGENT_ID, agentAliasId: process.env.BEDROCK_AGENT_ALIAS_ID, sessionId: `session-${userId}`, inputText: userMessage, })); let agentReply = ''; for await (const event of response.completion!) { if (event.chunk?.bytes) { agentReply += new TextDecoder().decode(event.chunk.bytes); } } ``` ## pitfalls - **`AccessDeniedException` on first call.** You must enable model access in the Bedrock console before API calls work. This is per-account, per-region. - **Wrong region.** Claude models are often only available in `us-east-1` and `us-west-2`. Creating a client in `eu-west-1` will fail for Claude. - **Using `InvokeModel` instead of `Converse`.** `InvokeModel` requires provider-specific JSON payloads (Anthropic format, Titan format, etc.). `Converse` abstracts this — always prefer it. - **Content block structure.** Converse API messages use `content: [{ text: '...' }]` (array of content blocks), not `content: '...'` (plain string). Missing the array wrapper causes validation errors. - **Forgetting `system` is an array.** Converse API takes `system: [{ text: '...' }]`, not `system: '...'`. - **Agent session management.** Bedrock Agents maintain conversation state per `sessionId`. Use consistent session IDs for multi-turn conversations, unique IDs for fresh conversations. - **IAM policy missing Bedrock permissions.** The calling role needs `bedrock:InvokeModel`, `bedrock:Converse`, or `bedrock:InvokeAgent`. Without these, you get authorization errors. ## references - [AWS SDK for JS v3 — BedrockRuntimeClient](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/bedrock-runtime) - [Converse API — User Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html) - [Bedrock Tool Use](https://docs.aws.amazon.com/bedrock/latest/userguide/tool-use.html) - [Bedrock Agents](https://docs.aws.amazon.com/bedrock/latest/userguide/agents.html) - [Bedrock Model IDs](https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html) - [Bedrock Guardrails](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html) ## instructions This expert covers AWS Bedrock model invocation and agent usage from TypeScript. Use it when the developer is calling models through Bedrock (not the direct Anthropic API). For direct Anthropic API access, see `anthropic-ts.md`. Pair with: `anthropic-ts.md` (direct Anthropic comparison), `../deploy/aws-cli-reference-ts.md` (Bedrock CLI commands for provisioning), `../deploy/aws-bot-deploy-ts.md` (Lambda/ECS deployment), `../security/secrets-ts.md` (IAM auth patterns). ## research Deep Research prompt: "Write a micro expert on using AWS Bedrock from TypeScript. Cover: @aws-sdk/client-bedrock-runtime Converse API vs InvokeModel, ConverseStreamCommand for streaming, tool use with toolConfig, IAM authentication, model ID formats, Bedrock Agents with InvokeAgentCommand, Knowledge Bases with RetrieveAndGenerateCommand, guardrails, model access enablement, region availability, and error handling." -
foundry-cloud-ts.md 8.1 KB
# foundry-cloud-ts ## purpose Using Azure AI Foundry (cloud) and the Azure AI model catalog for serverless model inference. Covers Model-as-a-Service (MaaS) deployments, the Azure AI Inference SDK, GitHub Models, and connecting to Foundry-hosted models from TypeScript. ## rules 1. **Azure AI Foundry provides serverless model endpoints (Model-as-a-Service).** No GPU provisioning needed — deploy a model from the catalog and get an HTTPS endpoint with pay-per-token billing. Available models include Phi-4, Llama, Mistral, Cohere, and more. [learn.microsoft.com/azure/ai-studio/how-to/deploy-models-serverless](https://learn.microsoft.com/azure/ai-studio/how-to/deploy-models-serverless) 2. **MaaS endpoints are OpenAI-compatible.** The deployed endpoint exposes `/v1/chat/completions` with the standard OpenAI request/response format. Use the `openai` npm package with a custom `baseURL` and the endpoint's API key. [learn.microsoft.com/azure/ai-studio/reference/reference-model-inference-chat-completions](https://learn.microsoft.com/azure/ai-studio/reference/reference-model-inference-chat-completions) 3. **Alternatively, use `@azure-rest/ai-inference` for the Azure AI Inference SDK.** This TypeScript SDK provides a typed client for Azure AI model endpoints. `npm install @azure-rest/ai-inference`. It supports chat completions, embeddings, and image generation. [learn.microsoft.com/azure/ai-studio/reference/reference-model-inference-api](https://learn.microsoft.com/azure/ai-studio/reference/reference-model-inference-api) 4. **Authentication uses either API key or Entra ID token.** MaaS endpoints accept an API key in the `Authorization: Bearer <key>` header. For managed identity, use `@azure/identity` to get a token. [learn.microsoft.com/azure/ai-studio/how-to/deploy-models-serverless](https://learn.microsoft.com/azure/ai-studio/how-to/deploy-models-serverless) 5. **GitHub Models provides free-tier access to the same model catalog.** Use `https://models.inference.ai.azure.com` as the base URL with a GitHub personal access token as the API key. Great for prototyping before deploying to your own Azure subscription. [docs.github.com/en/github-models](https://docs.github.com/en/github-models) 6. **Deploy models via the Azure AI Foundry portal or CLI.** In the portal: AI Foundry → Model catalog → Deploy. Via CLI: `az cognitiveservices account deployment create` for Azure OpenAI models, or use the AI Foundry portal for MaaS models. [learn.microsoft.com/azure/ai-studio/how-to/deploy-models-serverless](https://learn.microsoft.com/azure/ai-studio/how-to/deploy-models-serverless) 7. **Each deployment gets a unique endpoint URL and API key.** The endpoint URL format is `https://<deployment-name>.<region>.models.ai.azure.com`. Copy the endpoint and key from the deployment details page. 8. **Streaming works via standard SSE.** Pass `stream: true` in the request body. The response is `text/event-stream` with `data: {...}` chunks, identical to OpenAI streaming format. 9. **Some catalog models support tool use.** Check the model card in the catalog for "Function calling" or "Tool use" support. The API format matches OpenAI's `tools` / `tool_choice` parameters. 10. **For Azure OpenAI models (GPT-4o, etc.), use the Azure OpenAI Service instead.** Foundry MaaS is for non-OpenAI models (Phi, Llama, Mistral, etc.). GPT-4o goes through the Azure OpenAI resource, not MaaS. See `openai-azure-openai-ts.md` for that. ## patterns ### Connect via OpenAI SDK (simplest) ```typescript import OpenAI from 'openai'; // MaaS endpoint from Azure AI Foundry deployment const client = new OpenAI({ baseURL: process.env.AZURE_AI_ENDPOINT, // e.g., https://my-phi4.eastus.models.ai.azure.com/v1 apiKey: process.env.AZURE_AI_API_KEY, timeout: 30000, }); const response = await client.chat.completions.create({ model: 'phi-4', // model name from deployment messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: userMessage }, ], temperature: 0.7, max_tokens: 1000, }); const reply = response.choices[0].message.content; ``` ### Connect via Azure AI Inference SDK ```typescript import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'; import { AzureKeyCredential } from '@azure/core-auth'; const client = ModelClient( process.env.AZURE_AI_ENDPOINT!, new AzureKeyCredential(process.env.AZURE_AI_API_KEY!), ); const response = await client.path('/chat/completions').post({ body: { messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: userMessage }, ], temperature: 0.7, max_tokens: 1000, }, }); if (isUnexpected(response)) { throw new Error(`API error: ${response.status} ${response.body.error?.message}`); } const reply = response.body.choices[0].message.content; ``` ### GitHub Models (free prototyping) ```typescript import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'https://models.inference.ai.azure.com', apiKey: process.env.GITHUB_TOKEN, // GitHub personal access token }); const response = await client.chat.completions.create({ model: 'Phi-4', // model name from GitHub Models catalog messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: userMessage }, ], }); ``` ### Dev/prod switching (GitHub Models → Azure AI Foundry) ```typescript import OpenAI from 'openai'; function createClient(): OpenAI { if (process.env.NODE_ENV === 'production') { return new OpenAI({ baseURL: process.env.AZURE_AI_ENDPOINT, apiKey: process.env.AZURE_AI_API_KEY, }); } // Free tier for development return new OpenAI({ baseURL: 'https://models.inference.ai.azure.com', apiKey: process.env.GITHUB_TOKEN, }); } ``` ## pitfalls - **Confusing Foundry MaaS with Azure OpenAI.** MaaS is for non-OpenAI models (Phi, Llama, Mistral). GPT-4o uses Azure OpenAI Service with a different SDK path. Don't mix them up. - **Wrong base URL format.** MaaS endpoints already include the path. When using the `openai` SDK, set `baseURL` to the endpoint URL plus `/v1`. Check the deployment details page for the exact URL. - **GitHub Models rate limits.** The free tier has aggressive rate limits. For production, deploy your own model in Azure AI Foundry. - **Model-specific quirks.** Different models have different context windows, token limits, and feature support. Phi-4 supports function calling; some models don't. Check the model card. - **Endpoint key rotation.** MaaS API keys can be regenerated in the portal. After rotation, update all services using the old key. - **Region availability.** Not all models are available in all Azure regions. Check the model catalog for your region before deploying. ## references - [Azure AI Foundry — Model Catalog](https://learn.microsoft.com/azure/ai-studio/how-to/model-catalog-overview) - [Deploy Serverless Models (MaaS)](https://learn.microsoft.com/azure/ai-studio/how-to/deploy-models-serverless) - [Azure AI Inference SDK — npm](https://www.npmjs.com/package/@azure-rest/ai-inference) - [Azure AI Model Inference API](https://learn.microsoft.com/azure/ai-studio/reference/reference-model-inference-api) - [GitHub Models](https://docs.github.com/en/github-models) ## instructions This expert covers Azure AI Foundry cloud (Model-as-a-Service) and GitHub Models. Use it when the developer wants to call non-OpenAI models (Phi, Llama, Mistral) hosted on Azure's serverless infrastructure. For OpenAI/GPT models on Azure, see `openai-azure-openai-ts.md`. For running models locally, see `foundry-local-ts.md`. Pair with: `openai-azure-openai-ts.md` (Azure OpenAI for GPT models), `foundry-local-ts.md` (local development), `../deploy/azure-cli-reference-ts.md` (provisioning), `../security/secrets-ts.md` (key management). ## research Deep Research prompt: "Write a micro expert on Azure AI Foundry Model-as-a-Service (MaaS) for TypeScript developers. Cover: model catalog overview, serverless deployment, OpenAI-compatible endpoints, connecting with the openai npm package, @azure-rest/ai-inference SDK, GitHub Models as a free-tier option, API key vs Entra ID auth, streaming, function calling support by model, dev-to-prod patterns, and deployment via portal and CLI." -
foundry-local-ts.md 10.6 KB
# foundry-local-ts ## purpose Running AI models locally on-device using Azure AI Foundry Local. Covers the `foundry` CLI for model management, the OpenAI-compatible REST API for code integration, and patterns for using local models in bot development and testing. ## rules 1. **Install Foundry Local via package manager.** Windows: `winget install Microsoft.FoundryLocal`. macOS: `brew tap microsoft/foundrylocal && brew install foundrylocal`. Verify with `foundry --version`. [learn.microsoft.com/azure/foundry-local/get-started](https://learn.microsoft.com/azure/foundry-local/get-started) 2. **Foundry Local exposes an OpenAI-compatible API.** The local service runs at `http://localhost:<port>/v1/chat/completions`. Use the standard `openai` npm package pointed at this endpoint — no custom SDK needed. [learn.microsoft.com/azure/foundry-local/reference/reference-rest](https://learn.microsoft.com/azure/foundry-local/reference/reference-rest) 3. **The port is dynamic.** Foundry Local assigns a random port each time the service starts. Always discover it via `foundry service status` and parse the endpoint URL. Do not hard-code port numbers. [learn.microsoft.com/azure/foundry-local/reference/reference-cli](https://learn.microsoft.com/azure/foundry-local/reference/reference-cli) 4. **Use aliases to let Foundry auto-select the best variant.** Running `foundry model run phi-4-mini` auto-selects the GPU/NPU/CPU variant matching your hardware. Use the full model ID (e.g., `phi-4-mini-instruct-cuda-gpu`) only when you need a specific variant. 5. **Models are downloaded on first use and cached locally.** The first `foundry model run <model>` downloads the model (can take minutes). Subsequent runs use the cache. Manage the cache with `foundry cache list`, `foundry cache remove <model>`, and `foundry cache cd <path>`. 6. **No API key required.** Foundry Local runs entirely on-device with no authentication. When connecting the `openai` SDK, set `apiKey` to any non-empty string (the SDK requires it but Foundry ignores it). 7. **The API supports streaming, function calling, and tool use.** The `/v1/chat/completions` endpoint supports `stream: true`, `tools`, `function_call`, and all standard OpenAI chat completion parameters. Not all models support all features — check `supportsToolCalling` in the model catalog. 8. **Use Foundry Local for development and testing, cloud for production.** Local models are smaller and less capable than cloud models. Use them to iterate on prompts, test function calling logic, and develop offline — then switch to a cloud provider for production. 9. **Monitor loaded models with `foundry service ps`.** Models consume significant RAM/VRAM. Unload models you're not using: `foundry model unload <model>`. Use `foundry service diag` to view service logs. 10. **If the service fails, restart it.** Common error: "Request to local service failed." Fix with `foundry service restart`. This resolves port binding issues after sleep/hibernate. ## cli reference ### Model commands | Command | Purpose | |---|---| | `foundry model run <model>` | Download (if needed) and run a model with interactive chat | | `foundry model list` | List all available models in the catalog | | `foundry model list --filter device=GPU` | Filter models by device type (CPU, GPU, NPU) | | `foundry model list --filter task=chat-completion` | Filter by task type | | `foundry model list --filter provider=CUDAExecutionProvider` | Filter by execution provider | | `foundry model list --filter alias=phi*` | Filter by alias with wildcard | | `foundry model list --filter device=!GPU` | Exclude GPU models (negation) | | `foundry model info <model>` | Show detailed model information | | `foundry model info <model> --license` | Show model license | | `foundry model download <model>` | Download without running | | `foundry model load <model>` | Load into service memory | | `foundry model unload <model>` | Unload from service memory | ### Service commands | Command | Purpose | |---|---| | `foundry service start` | Start the Foundry Local service | | `foundry service stop` | Stop the service | | `foundry service restart` | Restart the service (fixes port issues) | | `foundry service status` | Show status and endpoint URL | | `foundry service ps` | List currently loaded models | | `foundry service diag` | View service logs | | `foundry service set <options>` | Configure service settings | ### Cache commands | Command | Purpose | |---|---| | `foundry cache list` | List cached (downloaded) models | | `foundry cache location` | Show cache directory path | | `foundry cache cd <path>` | Change cache directory | | `foundry cache remove <model>` | Remove a model from cache | ## patterns ### Connect with OpenAI SDK (TypeScript) ```typescript import OpenAI from 'openai'; import { execSync } from 'child_process'; // Discover the dynamic endpoint function getFoundryEndpoint(): string { const output = execSync('foundry service status', { encoding: 'utf-8' }); const match = output.match(/http:\/\/localhost:\d+/); if (!match) throw new Error('Foundry Local is not running. Run: foundry service restart'); return match[0]; } const endpoint = getFoundryEndpoint(); const client = new OpenAI({ baseURL: `${endpoint}/v1`, apiKey: 'not-needed', // required by SDK but ignored by Foundry }); const response = await client.chat.completions.create({ model: 'phi-4-mini', // use the alias — Foundry resolves to loaded variant messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: userMessage }, ], temperature: 0.7, max_tokens: 500, }); const reply = response.choices[0].message.content; ``` ### Streaming with Foundry Local ```typescript const stream = await client.chat.completions.create({ model: 'phi-4-mini', messages: [{ role: 'user', content: userMessage }], stream: true, }); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content; if (delta) process.stdout.write(delta); } ``` ### Dev/prod model switching (Foundry Local → Azure OpenAI) ```typescript import OpenAI, { AzureOpenAI } from 'openai'; function createModelClient(): OpenAI { if (process.env.NODE_ENV === 'production') { return new AzureOpenAI({ apiKey: process.env.AZURE_OPENAI_API_KEY, endpoint: process.env.AZURE_OPENAI_ENDPOINT, apiVersion: '2024-10-21', deployment: process.env.AZURE_OPENAI_DEPLOYMENT, }); } // Local development — use Foundry Local const endpoint = getFoundryEndpoint(); return new OpenAI({ baseURL: `${endpoint}/v1`, apiKey: 'not-needed', }); } const client = createModelClient(); // Same chat.completions.create() call works for both ``` ### Function calling with Foundry Local ```typescript // Check model supports tool calling first // foundry model info phi-4-mini → look for supportsToolCalling: true const response = await client.chat.completions.create({ model: 'phi-4-mini', messages: [{ role: 'user', content: 'What is the weather in Seattle?' }], tools: [{ type: 'function', function: { name: 'get_weather', description: 'Get current weather for a location', parameters: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'], }, }, }], tool_choice: 'auto', }); ``` ### Quick model setup for bot development ```bash # 1. Start a model foundry model run phi-4-mini # 2. In another terminal, verify the endpoint foundry service status # Output: http://localhost:5272 (port varies) # 3. Test with curl curl http://localhost:5272/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"phi-4-mini","messages":[{"role":"user","content":"Hello!"}]}' # 4. List available models foundry model list --filter task=chat-completion # 5. Check what's loaded foundry service ps # 6. Unload when done foundry model unload phi-4-mini ``` ## pitfalls - **Hard-coding the port.** Foundry Local assigns a random port on each service start. Always discover it via `foundry service status`. Code that assumes port 5272 will break after a restart. - **"Request to local service failed."** The service lost its port binding (common after sleep/hibernate). Fix with `foundry service restart`. - **Large models on limited hardware.** Models like `gpt-oss-20b` need 16+ GB VRAM. Check model sizes with `foundry model info <model>` before downloading. Start with `qwen2.5-0.5b` for testing on low-end hardware. - **Expecting cloud-quality responses from local models.** Local models (Phi-4 mini, Qwen 2.5) are smaller and less capable than GPT-4o or Claude. Use them for testing interaction patterns, not evaluating response quality. - **Not unloading models.** Each loaded model consumes RAM/VRAM. If your machine slows down, check `foundry service ps` and unload unused models. - **Tool calling on models that don't support it.** Not all local models support function calling. Check `supportsToolCalling` in `foundry model info <model>`. Phi-4 mini supports it; smaller models may not. - **Forgetting `apiKey` in the OpenAI SDK.** The SDK constructor requires `apiKey` even though Foundry ignores it. Pass any non-empty string. ## references - [Foundry Local — Get Started](https://learn.microsoft.com/azure/foundry-local/get-started) - [Foundry Local CLI Reference](https://learn.microsoft.com/azure/foundry-local/reference/reference-cli) - [Foundry Local REST API Reference](https://learn.microsoft.com/azure/foundry-local/reference/reference-rest) - [Foundry Local GitHub](https://github.com/microsoft/Foundry-Local) - [OpenAI Node.js SDK](https://github.com/openai/openai-node) ## instructions This expert covers running AI models locally with Azure AI Foundry Local. Use it when the developer wants to run models on-device for development, testing, or offline scenarios. Foundry Local's OpenAI-compatible API means the same `openai` SDK code works for both local and cloud models. Pair with: `openai-azure-openai-ts.md` (cloud counterpart — same SDK), `oss-openai-compatible-ts.md` (other local model servers like Ollama), `foundry-cloud-ts.md` (Azure AI Foundry cloud deployment). ## research Deep Research prompt: "Write a micro expert on Azure AI Foundry Local for TypeScript developers. Cover: installation (winget/brew), foundry CLI commands (model run/list/info/download/load/unload, service start/stop/restart/status/ps/diag, cache list/location/cd/remove), the OpenAI-compatible REST API at /v1/chat/completions, connecting with the openai npm package, dynamic port discovery, model aliases vs model IDs, hardware-specific variant selection, function calling support, streaming, dev-to-prod patterns (Foundry Local for dev, Azure OpenAI for prod), and common troubleshooting." -
index.md 5.8 KB
# models-router ## purpose Route AI model integration tasks to the correct provider-specific expert. Covers configuring, calling, and managing AI models from any supported provider: Anthropic, OpenAI, Azure OpenAI, AWS Bedrock, Azure AI Foundry (cloud), Foundry Local, and OpenAI-compatible OSS endpoints (Ollama, vLLM, LM Studio, etc.). ## interview ### Q1 — Model Provider ``` question: "Which AI model provider are you working with?" header: "Provider" options: - label: "OpenAI / Azure OpenAI (Recommended)" description: "GPT-4o, GPT-4, GPT-3.5. Works with both OpenAI API and Azure OpenAI Service. Best Teams AI SDK support." - label: "Anthropic (Claude)" description: "Claude 4, Claude 3.5 Sonnet, Claude 3 Haiku. Direct API or via AWS Bedrock." - label: "AWS Bedrock" description: "Managed access to Anthropic, Meta Llama, Cohere, Amazon Titan, and other models. Uses AWS IAM auth." - label: "Foundry / OSS Local" description: "Azure AI Foundry (cloud or local), Ollama, vLLM, LM Studio, or any OpenAI-compatible endpoint for open-source models." multiSelect: false ``` ### Q2 — Use Case ``` question: "What are you building with the model?" header: "Use case" options: - label: "Bot / agent with chat completions" description: "Chat-style interaction in a Slack or Teams bot. May include function calling / tool use." - label: "RAG / knowledge retrieval" description: "Retrieve-then-generate pattern with embeddings, vector stores, or knowledge bases." - label: "Standalone API integration" description: "Direct API calls from a service — not tied to a specific bot framework." - label: "You decide everything" description: "Use recommended defaults and skip remaining questions." multiSelect: false ``` ### defaults table | Question | Default | |---|---| | Q1 | OpenAI / Azure OpenAI | | Q2 | Bot / agent with chat completions | ## task clusters ### OpenAI / Azure OpenAI When: OpenAI, Azure OpenAI, GPT-4o, GPT-4, GPT-3.5, `openai` npm package, `@azure/openai`, chat completions, OpenAI API key, Azure OpenAI endpoint, deployment name, `apiVersion` Read: - `openai-azure-openai-ts.md` Cross-domain deps: `../teams/ai.model-setup-ts.md` (Teams AI SDK model config), `../deploy/azure-cli-reference-ts.md` (az cognitiveservices for Azure OpenAI provisioning), `../security/secrets-ts.md` (API key management) ### Anthropic (Claude) When: Anthropic, Claude, Claude 4, Claude 3.5, Claude 3, `@anthropic-ai/sdk`, Anthropic API, `ANTHROPIC_API_KEY`, Messages API, tool use with Claude Read: - `anthropic-ts.md` Cross-domain deps: `../security/secrets-ts.md` (API key management) ### AWS Bedrock When: Bedrock, AWS Bedrock, `@aws-sdk/client-bedrock-runtime`, Bedrock agents, Bedrock Converse API, Bedrock Knowledge Bases, hosted Anthropic, hosted Llama, Amazon Titan, Bedrock guardrails Read: - `bedrock-ts.md` Cross-domain deps: `../deploy/aws-cli-reference-ts.md` (aws bedrock CLI commands), `../deploy/aws-bot-deploy-ts.md` (Lambda/ECS deployment), `../security/secrets-ts.md` (IAM auth) ### Azure AI Foundry (Cloud) When: Azure AI Foundry, AI Foundry, Foundry Models, Azure AI model catalog, model-as-a-service, MaaS, serverless API, Azure AI Studio, Foundry cloud, GitHub Models Read: - `foundry-cloud-ts.md` Cross-domain deps: `../deploy/azure-cli-reference-ts.md` (az cognitiveservices for provisioning), `../security/secrets-ts.md` (API key management) ### Foundry Local When: Foundry Local, `foundry` CLI, `flocal`, local model, local inference, run model locally, on-device AI, ONNX, Phi-4, Qwen, `foundry model run`, `foundry model list`, `foundry service`, offline AI Read: - `foundry-local-ts.md` ### OpenAI-Compatible OSS Endpoints When: Ollama, vLLM, LM Studio, llama.cpp, text-generation-inference, TGI, LocalAI, OpenAI-compatible, self-hosted model, open-source model, Llama, Mistral, DeepSeek, local LLM, `/v1/chat/completions` custom endpoint, custom base URL Read: - `oss-openai-compatible-ts.md` ### Transformers.js (In-Process Inference) When: Transformers.js, `@huggingface/transformers`, in-process inference, browser inference, WASM inference, WebGPU inference, local embeddings, local classification, local NER, local summarization, pipeline API, HuggingFace Hub, ONNX in browser, serverless ML, no-server AI, feature extraction, zero-shot classification, sentiment analysis, token classification, offline embeddings Read: - `transformers-js-ts.md` Cross-domain deps: `openai-azure-openai-ts.md` (hybrid pattern — local preprocessing + cloud LLM) ### Multi-Provider / Provider Abstraction When: multiple models, fallback model, model routing, provider abstraction, LangChain, LiteLLM, Vercel AI SDK, `ai` npm package, model switching, cost optimization, A/B test models Read: - `openai-azure-openai-ts.md` - `anthropic-ts.md` - `oss-openai-compatible-ts.md` ## combining rule If the developer uses **multiple providers** (e.g., Claude for reasoning + GPT-4o for function calling, or Foundry Local for dev + Azure OpenAI for prod), load all relevant provider experts. The multi-provider cluster above covers this. If integrating a model into a **Teams bot**, always also read `../teams/ai.model-setup-ts.md` — it covers the `OpenAIChatModel` wrapper that Teams AI SDK uses. If integrating a model into a **Slack bot**, the provider experts here cover direct SDK usage — Slack Bolt doesn't have a built-in AI layer, so you wire models directly. ## file inventory `anthropic-ts.md` | `bedrock-ts.md` | `foundry-cloud-ts.md` | `foundry-local-ts.md` | `openai-azure-openai-ts.md` | `oss-openai-compatible-ts.md` | `transformers-js-ts.md` <!-- Created 2026-02-28: Models domain for AI model provider integration (Anthropic, OpenAI, Azure OpenAI, Bedrock, Foundry, OSS) --> <!-- Updated 2026-02-28: Added transformers-js-ts.md for in-process inference via @huggingface/transformers (embeddings, classification, NER, summarization in Node.js/browser without a server) --> -
openai-azure-openai-ts.md 8.3 KB
# openai-azure-openai-ts ## purpose Configuring and calling OpenAI and Azure OpenAI models from TypeScript. Covers direct SDK usage (not Teams AI SDK — see `../teams/ai.model-setup-ts.md` for that), authentication patterns, streaming, function calling, and environment variable management. ## rules 1. **Use the official `openai` npm package for both OpenAI and Azure OpenAI.** The same SDK supports both — Azure is just a configuration difference. `npm install openai`. [github.com/openai/openai-node](https://github.com/openai/openai-node) 2. **For plain OpenAI, pass `apiKey` only.** The SDK reads `OPENAI_API_KEY` from the environment by default. You can also pass it explicitly: `new OpenAI({ apiKey: process.env.OPENAI_API_KEY })`. [platform.openai.com/docs/api-reference](https://platform.openai.com/docs/api-reference) 3. **For Azure OpenAI, use `AzureOpenAI` from the same package.** Import `AzureOpenAI` and provide `endpoint`, `apiVersion`, and `deployment`. The deployment name replaces the `model` parameter in API calls. [learn.microsoft.com/azure/ai-services/openai/quickstart](https://learn.microsoft.com/azure/ai-services/openai/quickstart) 4. **Azure OpenAI requires `apiVersion`.** Use a stable version like `2024-10-21`. Omitting it produces request path errors. Check the docs for the latest stable version. [learn.microsoft.com/azure/ai-services/openai/reference](https://learn.microsoft.com/azure/ai-services/openai/reference) 5. **For Azure Managed Identity, use `@azure/identity`.** Pass a `DefaultAzureCredential` token provider instead of an API key. This eliminates key management entirely: `azureADTokenProvider: () => credential.getToken('https://cognitiveservices.azure.com/.default')`. [learn.microsoft.com/azure/active-directory/managed-identities-azure-resources](https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources) 6. **Always set a timeout.** The SDK defaults to no timeout. Set `timeout: 30000` (30s) for chat completions, higher for image generation. Stalled endpoints will hang your bot indefinitely without this. 7. **Use streaming for long responses.** Call `client.chat.completions.create({ ..., stream: true })` to get an `AsyncIterable<ChatCompletionChunk>`. This lets your bot send progressive updates instead of waiting for the full response. [platform.openai.com/docs/api-reference/chat/create](https://platform.openai.com/docs/api-reference/chat/create) 8. **Use structured outputs for reliable JSON.** Pass `response_format: { type: 'json_schema', json_schema: { ... } }` to guarantee valid JSON output. Available on GPT-4o and later. [platform.openai.com/docs/guides/structured-outputs](https://platform.openai.com/docs/guides/structured-outputs) 9. **Function calling uses `tools` and `tool_choice`.** Define tools as an array of `{ type: 'function', function: { name, description, parameters } }`. The model returns `tool_calls` in its response — your code executes them and sends results back. [platform.openai.com/docs/guides/function-calling](https://platform.openai.com/docs/guides/function-calling) 10. **Store all secrets in environment variables.** Use `dotenv` for local dev. Never hard-code API keys. For production, use Key Vault (Azure) or Secrets Manager (AWS). [npmjs.com/package/dotenv](https://www.npmjs.com/package/dotenv) ## patterns ### OpenAI chat completion ```typescript import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, timeout: 30000, }); const response = await client.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: userMessage }, ], temperature: 0.7, max_tokens: 1000, }); const reply = response.choices[0].message.content; ``` ### Azure OpenAI chat completion ```typescript import { AzureOpenAI } from 'openai'; const client = new AzureOpenAI({ apiKey: process.env.AZURE_OPENAI_API_KEY, endpoint: process.env.AZURE_OPENAI_ENDPOINT, apiVersion: '2024-10-21', deployment: process.env.AZURE_OPENAI_DEPLOYMENT, timeout: 30000, }); const response = await client.chat.completions.create({ model: '', // ignored — deployment determines the model messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: userMessage }, ], }); ``` ### Azure OpenAI with Managed Identity (keyless) ```typescript import { AzureOpenAI } from 'openai'; import { DefaultAzureCredential, getBearerTokenProvider } from '@azure/identity'; const credential = new DefaultAzureCredential(); const tokenProvider = getBearerTokenProvider(credential, 'https://cognitiveservices.azure.com/.default'); const client = new AzureOpenAI({ azureADTokenProvider: tokenProvider, endpoint: process.env.AZURE_OPENAI_ENDPOINT, apiVersion: '2024-10-21', deployment: process.env.AZURE_OPENAI_DEPLOYMENT, }); ``` ### Streaming ```typescript const stream = await client.chat.completions.create({ model: 'gpt-4o', messages, stream: true, }); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content; if (delta) process.stdout.write(delta); } ``` ### Function calling (tool use) ```typescript const response = await client.chat.completions.create({ model: 'gpt-4o', messages, tools: [{ type: 'function', function: { name: 'get_weather', description: 'Get current weather for a location', parameters: { type: 'object', properties: { location: { type: 'string', description: 'City name' }, }, required: ['location'], }, }, }], tool_choice: 'auto', }); if (response.choices[0].message.tool_calls) { for (const call of response.choices[0].message.tool_calls) { const args = JSON.parse(call.function.arguments); const result = await executeFunction(call.function.name, args); messages.push(response.choices[0].message); messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) }); } // Send tool results back for the model to generate a final response const finalResponse = await client.chat.completions.create({ model: 'gpt-4o', messages, tools }); } ``` ## pitfalls - **Using `model` name with Azure OpenAI.** Azure uses the deployment name, not the model name. `model: 'gpt-4o'` fails — use the deployment name from Azure portal. - **Forgetting `apiVersion` for Azure.** Every Azure OpenAI request requires `apiVersion` in the path. Omitting it gives a cryptic 404 or path error. - **Stale tokens with Managed Identity.** The `getBearerTokenProvider` from `@azure/identity` handles caching/refresh internally. Don't wrap it in your own caching layer. - **Mixing `maxTokens` and `max_tokens`.** The SDK uses snake_case (`max_tokens`) matching the API. Don't use camelCase. - **Not handling `finish_reason: 'length'`.** If the response was truncated, `finish_reason` is `'length'` not `'stop'`. Check this to decide whether to send a follow-up request. - **Streaming without error handling.** Wrap `for await` in try/catch — network errors mid-stream throw from the iterator. ## references - [OpenAI Node.js SDK — GitHub](https://github.com/openai/openai-node) - [OpenAI API Reference — Chat Completions](https://platform.openai.com/docs/api-reference/chat/create) - [Azure OpenAI Quickstart — TypeScript](https://learn.microsoft.com/azure/ai-services/openai/quickstart) - [Azure OpenAI REST API Reference](https://learn.microsoft.com/azure/ai-services/openai/reference) - [Structured Outputs Guide](https://platform.openai.com/docs/guides/structured-outputs) ## instructions This expert covers direct SDK usage of OpenAI and Azure OpenAI from TypeScript. Use it when the developer is calling the OpenAI API directly (not through Teams AI SDK). For Teams AI SDK's `OpenAIChatModel` wrapper, see `../teams/ai.model-setup-ts.md` instead. Pair with: `../teams/ai.model-setup-ts.md` (Teams AI SDK model layer), `../security/secrets-ts.md` (API key management), `../deploy/azure-cli-reference-ts.md` (provisioning Azure OpenAI via CLI). ## research Deep Research prompt: "Write a micro expert on using the OpenAI Node.js SDK (TypeScript) for both OpenAI and Azure OpenAI. Cover: client initialization, AzureOpenAI class, apiVersion requirements, Managed Identity with @azure/identity, chat completions, streaming, function calling / tool use, structured outputs, timeout configuration, error handling, and retry patterns." -
oss-openai-compatible-ts.md 10.7 KB
# oss-openai-compatible-ts ## purpose Connecting to self-hosted open-source models via OpenAI-compatible endpoints. Covers Ollama, vLLM, LM Studio, llama.cpp (server mode), text-generation-inference (TGI), LocalAI, and any other server that implements the `/v1/chat/completions` API. ## rules 1. **All OpenAI-compatible servers use the same API contract.** The `/v1/chat/completions` endpoint accepts the same request body as OpenAI's API. Use the `openai` npm package with a custom `baseURL` — no special SDKs needed. [github.com/openai/openai-node](https://github.com/openai/openai-node) 2. **Set `baseURL` to the server's endpoint.** Common defaults: Ollama `http://localhost:11434/v1`, LM Studio `http://localhost:1234/v1`, vLLM `http://localhost:8000/v1`, llama.cpp `http://localhost:8080/v1`. Always confirm the actual URL — ports may differ. 3. **Set `apiKey` to any non-empty string.** Most local servers don't require auth but the OpenAI SDK constructor requires `apiKey`. Pass `'not-needed'` or `'ollama'`. 4. **Model names are server-specific.** Ollama uses names like `llama3.2`, `mistral`, `phi4`. vLLM uses the model path. LM Studio uses whatever you loaded. Check the server's model list endpoint: `GET /v1/models`. 5. **Feature support varies by server and model.** Not all servers support streaming, tool use, JSON mode, or vision. Test capabilities before relying on them. Ollama supports tool use for some models; llama.cpp has limited function calling. 6. **Ollama is the easiest local server to start with.** Install: `curl -fsSL https://ollama.com/install.sh | sh` (Linux/macOS) or download from [ollama.com](https://ollama.com). Pull a model: `ollama pull llama3.2`. It auto-starts and exposes the OpenAI-compatible API. [ollama.com](https://ollama.com) 7. **vLLM is best for production self-hosting.** Optimized for throughput with continuous batching, PagedAttention, and tensor parallelism. Requires NVIDIA GPU. `pip install vllm && vllm serve meta-llama/Llama-3.2-3B-Instruct`. [docs.vllm.ai](https://docs.vllm.ai) 8. **LM Studio provides a desktop GUI with an API server.** Download from [lmstudio.ai](https://lmstudio.ai). Load a model in the GUI, then start the server tab. Good for developers who prefer a visual interface. 9. **Always set timeouts.** Local models can be slow, especially on CPU. Set `timeout: 120000` (2 minutes) or higher for large models. Streaming helps surface partial results while the model generates. 10. **Use the same code for local and cloud.** Since the API contract is identical to OpenAI's, you can swap between local OSS models and cloud providers by changing `baseURL` and `apiKey`. This enables local dev with cloud prod deployment. ## patterns ### Ollama ```typescript import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama', // required by SDK, ignored by Ollama timeout: 120000, // local models can be slow }); const response = await client.chat.completions.create({ model: 'llama3.2', // must match an installed Ollama model messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: userMessage }, ], temperature: 0.7, }); const reply = response.choices[0].message.content; ``` ### vLLM ```typescript import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'http://localhost:8000/v1', apiKey: 'not-needed', timeout: 60000, }); const response = await client.chat.completions.create({ model: 'meta-llama/Llama-3.2-3B-Instruct', // model path as served by vLLM messages: [{ role: 'user', content: userMessage }], }); ``` ### LM Studio ```typescript import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'http://localhost:1234/v1', apiKey: 'lm-studio', }); // List available models (whatever is loaded in LM Studio) const models = await client.models.list(); console.log(models.data.map((m) => m.id)); const response = await client.chat.completions.create({ model: models.data[0].id, // use the first loaded model messages: [{ role: 'user', content: userMessage }], }); ``` ### Universal provider abstraction ```typescript import OpenAI, { AzureOpenAI } from 'openai'; interface ModelConfig { provider: 'openai' | 'azure' | 'ollama' | 'vllm' | 'foundry-local' | 'custom'; baseURL?: string; apiKey?: string; model: string; // Azure-specific endpoint?: string; apiVersion?: string; deployment?: string; } function createClient(config: ModelConfig): OpenAI { switch (config.provider) { case 'openai': return new OpenAI({ apiKey: config.apiKey }); case 'azure': return new AzureOpenAI({ apiKey: config.apiKey, endpoint: config.endpoint, apiVersion: config.apiVersion ?? '2024-10-21', deployment: config.deployment, }); case 'ollama': return new OpenAI({ baseURL: config.baseURL ?? 'http://localhost:11434/v1', apiKey: 'ollama', timeout: 120000, }); case 'vllm': return new OpenAI({ baseURL: config.baseURL ?? 'http://localhost:8000/v1', apiKey: 'not-needed', timeout: 60000, }); case 'foundry-local': return new OpenAI({ baseURL: config.baseURL, // from foundry service status apiKey: 'not-needed', }); case 'custom': return new OpenAI({ baseURL: config.baseURL, apiKey: config.apiKey ?? 'not-needed', }); } } // Usage — same chat.completions.create() call works for all providers const client = createClient({ provider: process.env.MODEL_PROVIDER as any, baseURL: process.env.MODEL_BASE_URL, apiKey: process.env.MODEL_API_KEY, model: process.env.MODEL_NAME!, }); ``` ### Streaming with any OpenAI-compatible server ```typescript const stream = await client.chat.completions.create({ model: 'llama3.2', messages: [{ role: 'user', content: userMessage }], stream: true, }); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content; if (delta) process.stdout.write(delta); } ``` ### Ollama quick setup (bash) ```bash # Install Ollama curl -fsSL https://ollama.com/install.sh | sh # Pull a model ollama pull llama3.2 # List installed models ollama list # Test the API curl http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"llama3.2","messages":[{"role":"user","content":"Hello!"}]}' # Pull more models ollama pull mistral ollama pull phi4 ollama pull deepseek-r1:1.5b ``` ## server selection guide | Server | Best For | GPU Required? | OpenAI API? | Tool Calling? | Setup Effort | |---|---|---|---|---|---| | **Ollama** | Desktop dev, quick prototyping | No (GPU optional) | Yes (`/v1`) | Some models | Minimal — one-line install | | **vLLM** | Production self-hosting, high throughput | Yes (NVIDIA) | Yes (`/v1`) | Yes | Medium — pip install | | **LM Studio** | Desktop GUI, non-CLI users | No (GPU optional) | Yes (`/v1`) | Limited | Minimal — desktop app | | **llama.cpp** | Ultra-lightweight CPU inference | No | Yes (server mode) | Limited | Medium — build from source or use pre-built | | **TGI** | Production HuggingFace models, Docker | Yes (NVIDIA) | Yes (`/v1`) | Yes | Medium — Docker-based | | **LocalAI** | Drop-in OpenAI replacement, multi-model | No (GPU optional) | Yes (`/v1`) | Yes | Medium — Docker or binary | | **Foundry Local** | Microsoft ecosystem, ONNX models | No (GPU/NPU optional) | Yes (`/v1`) | Some models | Minimal — winget/brew | ### Where models come from Most OSS inference servers pull models from **Hugging Face Hub** (`huggingface.co`). Ollama has its own registry (`ollama.com/library`) that wraps HF models with optimized configs. vLLM and TGI use HF model paths directly (e.g., `meta-llama/Llama-3.2-3B-Instruct`). Foundry Local uses the Azure ML model registry. You don't need the **Hugging Face Transformers** Python library to use these models from TypeScript — the inference servers handle model loading internally. HF Transformers is only needed if you're running models directly in Python. ## pitfalls - **Port conflicts.** If multiple local servers are running, they may fight for the same port. Check with `lsof -i :<port>` or `netstat -an | grep <port>` before starting a server. - **Assuming feature parity with OpenAI.** Local models may not support JSON mode, tool use, vision, or structured outputs. Test the specific feature with your chosen model before building on it. - **Model name mismatches.** Each server has its own model naming convention. Ollama uses `llama3.2`, vLLM uses the full HuggingFace path `meta-llama/Llama-3.2-3B-Instruct`. Check `GET /v1/models` for the exact names. - **CPU inference is slow.** CPU-only inference for 7B+ models can take 10-60 seconds per response. Use streaming so the user sees partial results. For acceptable speed, use a GPU or smaller models (1-3B parameters). - **Out of memory.** Large models (13B+) need 16+ GB RAM/VRAM. If the server crashes or hangs, the model is too large for your hardware. Try a quantized variant (e.g., Q4_K_M in llama.cpp) or a smaller model. - **Ollama auto-unloads idle models.** By default, Ollama unloads models after 5 minutes of inactivity. The next request will have a cold-start delay while the model reloads. Set `OLLAMA_KEEP_ALIVE=-1` to keep models loaded. - **vLLM requires NVIDIA GPU.** vLLM only supports NVIDIA GPUs with CUDA. For AMD GPUs, use ROCm builds. For CPU-only, use Ollama or llama.cpp instead. ## references - [Ollama](https://ollama.com) - [vLLM Documentation](https://docs.vllm.ai) - [LM Studio](https://lmstudio.ai) - [llama.cpp — Server Mode](https://github.com/ggerganov/llama.cpp/tree/master/examples/server) - [text-generation-inference (TGI)](https://github.com/huggingface/text-generation-inference) - [LocalAI](https://localai.io) - [OpenAI Node.js SDK](https://github.com/openai/openai-node) ## instructions This expert covers connecting to any OpenAI-compatible model server from TypeScript. Use it when the developer is running self-hosted open-source models (Ollama, vLLM, LM Studio, llama.cpp, TGI) or any custom server implementing the OpenAI chat completions API. Pair with: `foundry-local-ts.md` (Microsoft's local model server), `openai-azure-openai-ts.md` (cloud OpenAI for production), `anthropic-ts.md` (if mixing providers). ## research Deep Research prompt: "Write a micro expert on connecting TypeScript applications to OpenAI-compatible self-hosted model servers. Cover: Ollama (install, pull, serve, API), vLLM (serve, model paths, GPU requirements), LM Studio (server mode), llama.cpp server mode, text-generation-inference (TGI), connecting with the openai npm package via custom baseURL, model naming conventions by server, feature support matrix (streaming, tool use, JSON mode, vision), provider abstraction patterns, timeout and performance considerations, and dev-to-prod patterns." -
transformers-js-ts.md 11.6 KB
# transformers-js-ts ## purpose Running AI models directly in Node.js or the browser using Transformers.js — no server, no API calls, no network. Covers the `@huggingface/transformers` pipeline API for text generation, embeddings, classification, summarization, and other tasks using ONNX models via WASM/WebGPU. ## rules 1. **Transformers.js runs models in-process, not via an API.** Unlike Ollama, vLLM, or Foundry Local (which run a server you call via HTTP), Transformers.js loads ONNX models directly into your Node.js process or browser tab. No separate server to manage. `npm install @huggingface/transformers`. [huggingface.co/docs/transformers.js](https://huggingface.co/docs/transformers.js) 2. **Use the `pipeline` API for most tasks.** `pipeline(task, model?)` returns a callable function. Supported tasks include `text-generation`, `text-classification`, `feature-extraction` (embeddings), `summarization`, `translation`, `question-answering`, `token-classification` (NER), `zero-shot-classification`, `automatic-speech-recognition`, and more. [huggingface.co/docs/transformers.js/api/pipelines](https://huggingface.co/docs/transformers.js/api/pipelines) 3. **Models must be ONNX-format and tagged `transformers.js` on HuggingFace Hub.** Not every HF model works — look for the `transformers.js` library tag. Find compatible models at `huggingface.co/models?library=transformers.js`. Popular choices: `Xenova/all-MiniLM-L6-v2` (embeddings), `Xenova/distilbert-base-uncased-finetuned-sst-2-english` (classification), `onnx-community/Qwen2.5-0.5B-Instruct` (text generation). 4. **Models are downloaded and cached on first use.** The first call to `pipeline()` downloads the model from HF Hub. Subsequent calls use the local cache. For Node.js, models are cached in the filesystem. For browsers, they're cached in browser storage. 5. **Use `device: 'webgpu'` for GPU acceleration in browsers.** By default, models run on CPU via WASM. WebGPU provides significant speedups but is still experimental in some browsers. Node.js uses CPU (WASM) by default. 6. **Use quantized models for performance.** Set `dtype: 'q4'` or `dtype: 'q8'` to load quantized variants. Quantized models are smaller and faster but slightly less accurate. Default is `q8` for WASM and `fp32` for WebGPU. 7. **This is NOT for large chat models.** Transformers.js is best for small, task-specific models (embeddings, classification, NER, summarization). Running LLM-class text generation (7B+ params) is extremely slow in WASM. For chat with large models, use a server-based solution (Ollama, Foundry Local, vLLM) or a cloud API. 8. **Best use cases in bots: embeddings, classification, and preprocessing.** Use Transformers.js for tasks that run before or after calling a large model — e.g., compute embeddings for RAG search, classify user intent locally, detect PII/sentiment, or summarize context before sending to a cloud LLM. 9. **Works in both Node.js and browsers.** The same code runs in both environments. For server-side bots, import normally. For browser-based bots (web chat widgets), import in a web worker to avoid blocking the UI thread. 10. **Models are large downloads.** Even quantized models are 50-500MB. Budget for initial download time and disk/storage space. Use `env.cacheDir` to control where models are cached in Node.js. ## patterns ### Text classification (sentiment analysis) ```typescript import { pipeline } from '@huggingface/transformers'; const classifier = await pipeline('text-classification', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english'); const result = await classifier('I love this product!'); // [{ label: 'POSITIVE', score: 0.9998 }] // Use in a bot to classify user sentiment before responding const sentiment = result[0].label; // 'POSITIVE' or 'NEGATIVE' ``` ### Embeddings for RAG (feature extraction) ```typescript import { pipeline } from '@huggingface/transformers'; const embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { dtype: 'q8', // quantized for speed }); // Generate embeddings for semantic search const embedding = await embedder('How do I reset my password?', { pooling: 'mean', normalize: true, }); // embedding.data is a Float32Array — use for cosine similarity search const vector = Array.from(embedding.data); ``` ### Text generation (small models only) ```typescript import { pipeline } from '@huggingface/transformers'; const generator = await pipeline('text-generation', 'onnx-community/Qwen2.5-0.5B-Instruct', { dtype: 'q4', // quantized for speed }); const result = await generator('What is the capital of France?', { max_new_tokens: 100, temperature: 0.7, }); console.log(result[0].generated_text); ``` ### Zero-shot classification (intent detection) ```typescript import { pipeline } from '@huggingface/transformers'; const classifier = await pipeline('zero-shot-classification', 'Xenova/mobilebert-uncased-mnli'); const result = await classifier('I need to cancel my order', { candidate_labels: ['order status', 'cancellation', 'returns', 'billing', 'general inquiry'], }); // result.labels = ['cancellation', 'returns', 'order status', ...] // result.scores = [0.87, 0.05, 0.04, ...] const detectedIntent = result.labels[0]; // 'cancellation' ``` ### Named entity recognition (NER) ```typescript import { pipeline } from '@huggingface/transformers'; const ner = await pipeline('token-classification', 'Xenova/bert-base-NER'); const entities = await ner('John Smith works at Microsoft in Seattle.'); // [ // { entity: 'B-PER', word: 'John', score: 0.99 }, // { entity: 'I-PER', word: 'Smith', score: 0.99 }, // { entity: 'B-ORG', word: 'Microsoft', score: 0.99 }, // { entity: 'B-LOC', word: 'Seattle', score: 0.99 }, // ] ``` ### Summarization (condense context before sending to LLM) ```typescript import { pipeline } from '@huggingface/transformers'; const summarizer = await pipeline('summarization', 'Xenova/distilbart-cnn-6-6'); const summary = await summarizer(longDocument, { max_length: 130, min_length: 30, }); // Use the summary as context in a cloud LLM call to save tokens const condensedContext = summary[0].summary_text; ``` ### Hybrid pattern: local preprocessing + cloud LLM ```typescript import { pipeline } from '@huggingface/transformers'; import OpenAI from 'openai'; // Initialize local models once at startup const intentClassifier = await pipeline('zero-shot-classification', 'Xenova/mobilebert-uncased-mnli'); const embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2'); // Initialize cloud LLM const llm = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); async function handleMessage(userMessage: string) { // Step 1: Classify intent locally (free, fast, no network) const intent = await intentClassifier(userMessage, { candidate_labels: ['question', 'complaint', 'request', 'greeting'], }); // Step 2: Generate embedding locally for RAG search const embedding = await embedder(userMessage, { pooling: 'mean', normalize: true }); const relevantDocs = await searchVectorStore(Array.from(embedding.data)); // Step 3: Send to cloud LLM with context (only pay for the final generation) const response = await llm.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: `User intent: ${intent.labels[0]}. Relevant docs: ${relevantDocs}` }, { role: 'user', content: userMessage }, ], }); return response.choices[0].message.content; } ``` ### Configure cache directory (Node.js) ```typescript import { env } from '@huggingface/transformers'; // Set custom cache directory (default: ~/.cache/huggingface) env.cacheDir = '/path/to/model-cache'; // Disable remote model downloads (use only cached models) env.allowRemoteModels = false; // Use local models from a specific directory env.localModelPath = '/path/to/local-models'; ``` ### Browser web worker (avoid blocking UI) ```typescript // worker.ts — runs in a web worker import { pipeline } from '@huggingface/transformers'; let classifier: any = null; self.onmessage = async (event) => { if (!classifier) { classifier = await pipeline('text-classification', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english', { device: 'webgpu', // GPU acceleration in browser }); } const result = await classifier(event.data.text); self.postMessage(result); }; ``` ## pitfalls - **Trying to run large chat models.** Transformers.js via WASM is far too slow for 7B+ parameter models. Use it for small task-specific models (50-500M params). For LLM chat, use Ollama, Foundry Local, or a cloud API. - **Forgetting models need the `transformers.js` tag.** Not every HuggingFace model has an ONNX export. Filter models at `huggingface.co/models?library=transformers.js` to find compatible ones. - **Blocking the main thread in browsers.** Model loading and inference are CPU-intensive. In browsers, always run Transformers.js in a **web worker** to avoid freezing the UI. - **First-run download surprise.** The first call to `pipeline()` downloads the model (50-500MB). In production, pre-download models during build/deployment, or set `env.allowRemoteModels = false` and bundle models locally. - **Memory pressure in Node.js.** Each loaded model consumes significant RAM. Loading multiple models simultaneously can exhaust memory. Reuse pipeline instances — don't create new ones per request. - **Assuming OpenAI API compatibility.** Transformers.js has its own `pipeline()` API — it does NOT expose an OpenAI-compatible endpoint. You can't use the `openai` npm package with it. It's a completely different integration pattern. - **WebGPU browser support.** WebGPU is still experimental in some browsers. Chrome/Edge have the best support. Firefox and Safari support varies. Always fall back to WASM (`device: 'cpu'`). ## references - [Transformers.js Documentation](https://huggingface.co/docs/transformers.js) - [Transformers.js GitHub](https://github.com/huggingface/transformers.js) - [@huggingface/transformers on npm](https://www.npmjs.com/package/@huggingface/transformers) - [Compatible Models on HuggingFace Hub](https://huggingface.co/models?library=transformers.js) - [Pipeline API Reference](https://huggingface.co/docs/transformers.js/api/pipelines) - [WebGPU Guide](https://huggingface.co/docs/transformers.js/guides/webgpu) ## instructions This expert covers running AI models in-process using Transformers.js. Use it when the developer wants to run inference directly in Node.js or the browser without a server — for embeddings, classification, NER, summarization, or small text generation. This is fundamentally different from server-based solutions (Ollama, Foundry Local, vLLM) which expose an HTTP API. Best bot use cases: local embeddings for RAG, intent classification, sentiment analysis, PII detection, summarization as preprocessing before a cloud LLM call. Pair with: `openai-azure-openai-ts.md` (cloud LLM for the hybrid pattern), `oss-openai-compatible-ts.md` (server-based alternatives for larger models), `foundry-local-ts.md` (Foundry Local also uses ONNX but as a server). ## research Deep Research prompt: "Write a micro expert on Transformers.js (@huggingface/transformers) for TypeScript developers building bots. Cover: installation, pipeline API, supported tasks (text-generation, text-classification, feature-extraction/embeddings, summarization, translation, token-classification/NER, zero-shot-classification, question-answering), model selection from HuggingFace Hub (transformers.js tag), ONNX format requirement, quantization (q4/q8/fp16/fp32), WebGPU vs WASM backends, Node.js vs browser differences, web worker pattern for browsers, cache configuration, hybrid patterns (local preprocessing + cloud LLM), and limitations vs server-based inference."
-
-
security
-
index.md 1.2 KB
# security-router ## purpose Route security-hardening tasks to the minimal set of micro-expert files. Read only the clusters that match the user's request. ## task clusters ### Input Validation When: sanitizing user input, preventing injection, XSS prevention, content validation, PII handling Read: - `input-validation-ts.md` Cross-domain deps: `../teams/ui.adaptive-cards-ts.md` (card action payloads that need validation), `../teams/ai.function-calling-implementation-ts.md` (AI function parameter validation) ### Secrets Management When: secrets, credentials, API keys, Key Vault, environment variables, secret rotation Read: - `secrets-ts.md` Cross-domain deps: `../teams/runtime.app-init-ts.md` (App constructor credentials), `../bridge/infra-secrets-config-ts.md` (only if bridging between AWS and Azure) ### General Hardening When: broad security review, security audit, hardening checklist, defense in depth Read: - `input-validation-ts.md` - `secrets-ts.md` Cross-domain deps: `../teams/mcp.security-ts.md` (only if using MCP) ## combining rule If a request covers both input validation and secrets, read both files (same as "General Hardening"). ## file inventory `input-validation-ts.md` | `secrets-ts.md` -
input-validation-ts.md 13.8 KB
# input-validation-ts ## purpose Validating user input from messages, Adaptive Card submissions, and task module forms in Teams bots to prevent injection, data corruption, and unexpected behavior. ## rules 1. Always validate `activity.value` server-side after Adaptive Card `Action.Submit` and `Action.Execute`. The JSON payload can be tampered with by clients -- never trust that field names, types, or values match the card definition. Use a schema validator like zod before processing. [learn.microsoft.com -- Cards actions](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-actions) 2. Define zod schemas for every card action and dialog submission payload. Parse with `schema.safeParse(activity.value)` and handle validation failures with a user-friendly error message. Never use `schema.parse()` in handlers -- uncaught `ZodError` exceptions will crash the handler. [github.com/colinhacks/zod](https://github.com/colinhacks/zod) 3. Validate `activity.text` from user messages before using it in database queries, API calls, or AI prompts. Apply content length limits (Teams messages can be up to 28 KB), strip or escape control characters, and reject messages that exceed expected bounds. [learn.microsoft.com -- Message size limits](https://learn.microsoft.com/en-us/microsoftteams/limits-specifications-teams) 4. Use Adaptive Card input element validation properties (`isRequired`, `regex`, `errorMessage`) as a first layer of client-side validation. These provide immediate feedback to users but are NOT a security boundary -- the server must re-validate all inputs because clients can bypass card-level validation. [adaptivecards.io -- Input.Text](https://adaptivecards.io/explorer/Input.Text.html) 5. Be aware that `Input.ChoiceSet` values are always strings in `activity.value`, even when they appear numeric. A choice with `"value": "42"` arrives as the string `"42"`, not the number `42`. Always use explicit type coercion (`parseInt()`, `Number()`) or zod transforms (`z.coerce.number()`) when numeric values are expected. [adaptivecards.io -- Input.ChoiceSet](https://adaptivecards.io/explorer/Input.ChoiceSet.html) 6. Sanitize user input before rendering it in Adaptive Card `TextBlock` elements to prevent XSS-like injection. While Teams sanitizes most HTML, markdown rendering in cards can be abused with crafted links or misleading formatting. Strip or escape markdown syntax (`[]()`, `**`, `#`) in user-provided text displayed in cards. [learn.microsoft.com -- Format cards](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format) 7. When using AI function calling, validate that user-influenced parameters passed to tool functions do not enable command injection or unauthorized data access. If the LLM generates a function call with parameters derived from user input, validate those parameters against an allowlist or schema before execution. [OWASP -- Injection](https://owasp.org/www-community/Injection_Flaws) 8. Implement content length limits for all text inputs. Set `maxLength` on `Input.Text` elements in cards (client-side enforcement), and enforce the same limit server-side. Reject payloads larger than expected to prevent denial-of-service from oversized submissions. [adaptivecards.io -- Input.Text](https://adaptivecards.io/explorer/Input.Text.html) 9. Validate the `verb` or routing identifier in `activity.value.data` before dispatching card actions. An attacker could submit a crafted payload with an unexpected verb to reach unintended handlers. Verify that the verb matches a known set of registered actions. [github.com/microsoft/teams-ai](https://github.com/microsoft/teams-ai) 10. Log validation failures for security monitoring but never log the raw invalid input if it may contain PII or malicious payloads. Log the validation error type and field name, not the value. Use structured logging with Application Insights custom events for audit trails. [learn.microsoft.com -- Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/api-custom-events-metrics) ## patterns ### Zod validation for Adaptive Card submissions ```typescript import { z } from "zod"; import { App } from "@microsoft/teams.apps"; // Define schemas for each card action's expected payload const feedbackSchema = z.object({ verb: z.literal("submitFeedback"), userName: z.string().min(1).max(100), rating: z.coerce.number().int().min(1).max(5), // ChoiceSet values are strings! comments: z.string().max(2000).optional().default(""), followUp: z.enum(["true", "false"]), // Input.Toggle values are strings }); const approvalSchema = z.object({ verb: z.literal("approve"), requestId: z.string().uuid(), approverNote: z.string().max(500).optional().default(""), }); // Type-safe handler with validation const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, }); app.on("card.action", async ({ activity, send }) => { const raw = activity.value?.action?.data; // Route by verb with validation if (raw?.verb === "submitFeedback") { const result = feedbackSchema.safeParse(raw); if (!result.success) { await send("Invalid submission. Please check your inputs and try again."); // Log error type, not the raw value console.error("Validation failed for submitFeedback:", result.error.issues.map(i => i.path.join("."))); return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.activity.message", value: "Validation error." } }; } const data = result.data; // Safe to use: data.userName, data.rating (number), data.comments, data.followUp await send(`Thanks ${data.userName}! Rating: ${data.rating}/5`); return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.activity.message", value: "Feedback received!" } }; } if (raw?.verb === "approve") { const result = approvalSchema.safeParse(raw); if (!result.success) { return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.activity.message", value: "Invalid approval data." } }; } // Process approval with validated data... } return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.activity.message", value: "Unknown action." } }; }); ``` ### Message text validation and sanitization ```typescript import { z } from "zod"; import { App } from "@microsoft/teams.apps"; // Schema for validating message text before processing const messageSchema = z.object({ text: z .string() .min(1, "Message cannot be empty") .max(4000, "Message too long") .transform((val) => val.trim()), }); // Sanitize user text before embedding in Adaptive Card TextBlocks function sanitizeForCard(text: string): string { return text .replace(/\[([^\]]*)\]\(([^)]*)\)/g, "$1") // Strip markdown links .replace(/[*_~`#]/g, "") // Strip markdown formatting .replace(/</g, "<") // Escape HTML .replace(/>/g, ">") .slice(0, 2000); // Enforce length limit } const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, }); app.on("message", async ({ send, activity }) => { const result = messageSchema.safeParse({ text: activity.text }); if (!result.success) { await send("I could not process your message. Please try a shorter message."); return; } const cleanText = result.data.text; // Safe to use in AI prompt // const aiResponse = await prompt.send(cleanText); // Safe to embed in a card const safeForCard = sanitizeForCard(cleanText); await send({ type: "message", attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.5", body: [{ type: "TextBlock", text: `You said: ${safeForCard}`, wrap: true }], }, }], }); }); ``` ### AI function parameter validation ```typescript import { z } from "zod"; import { ChatPrompt } from "@microsoft/teams.ai"; import { OpenAIChatModel } from "@microsoft/teams.openai"; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY!, model: "gpt-4o", }); // Allowlist of valid database table names the LLM can query const ALLOWED_TABLES = ["tickets", "users", "projects"] as const; const querySchema = z.object({ table: z.enum(ALLOWED_TABLES), filter: z.string().max(200).regex(/^[a-zA-Z0-9\s=<>'"%_.-]+$/), // No SQL injection characters limit: z.coerce.number().int().min(1).max(100).default(10), }); const prompt = new ChatPrompt({ model, instructions: "You help users query project data. Use the queryData function.", }).function( "queryData", "Query a database table with filters", { type: "object", properties: { table: { type: "string", description: "Table name: tickets, users, or projects" }, filter: { type: "string", description: "Filter expression" }, limit: { type: "number", description: "Max rows to return (1-100)" }, }, required: ["table", "filter"], }, async (params: { table: string; filter: string; limit?: number }) => { // Validate LLM-generated parameters before executing const result = querySchema.safeParse(params); if (!result.success) { return { error: "Invalid query parameters. Please try a different query." }; } const { table, filter, limit } = result.data; // Now safe to use in a database query // return await db.query(table, filter, limit); return { table, filter, limit, results: [] }; }, ); ``` ## pitfalls - **Trusting client-side card validation**: Adaptive Card `isRequired`, `regex`, and `errorMessage` properties are enforced by the Teams client UI only. An attacker sending crafted HTTP requests to `/api/messages` can bypass all client-side validation. Always re-validate server-side. - **ChoiceSet type coercion surprises**: All `Input.ChoiceSet` values arrive as strings. Comparing `activity.value.rating === 5` will always be `false` because the value is `"5"`. Use `z.coerce.number()` or explicit `parseInt()` to convert. - **Input.Toggle boolean mismatch**: `Input.Toggle` sends `"true"` or `"false"` as strings (matching `valueOn`/`valueOff`), not actual booleans. Use `z.enum(["true", "false"]).transform(v => v === "true")` to convert to boolean. - **Missing verb in action data**: If an `Action.Submit` has no `data` object or no `verb` key, the handler cannot route the action. An attacker could also submit a payload with a `verb` that matches a different handler. Validate verb presence and value. - **Logging PII in validation errors**: Logging the full `activity.value` on validation failure may expose user PII (names, emails, free-text input). Log only the schema path and error type, not the submitted values. - **Oversized payloads causing OOM**: Without content length limits, a malicious client could submit extremely large text values. While Teams has message size limits (~28 KB), card action payloads should still be validated for reasonable sizes. - **Markdown injection in card display**: User-provided text rendered in `TextBlock` with `"markdown": true` (the default in some contexts) can include formatted links that disguise phishing URLs. Sanitize or disable markdown for user-supplied content. - **AI function calling with unsanitized parameters**: The LLM may pass user-influenced strings directly to function parameters. If a function executes shell commands, SQL queries, or API calls, validate parameters against strict schemas and allowlists. ## references - [Zod documentation](https://zod.dev/) - [Teams: Cards and card actions](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-actions) - [Adaptive Cards Input.Text](https://adaptivecards.io/explorer/Input.Text.html) - [Adaptive Cards Input.ChoiceSet](https://adaptivecards.io/explorer/Input.ChoiceSet.html) - [Teams: Format cards in Teams](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format) - [OWASP Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html) - [Teams message size limits](https://learn.microsoft.com/en-us/microsoftteams/limits-specifications-teams) - [Teams AI Library GitHub](https://github.com/microsoft/teams-ai) ## instructions This expert covers input validation for Microsoft Teams bots built with the Teams AI SDK v2 in TypeScript. Use it when you need to: - Validate `activity.value` from Adaptive Card `Action.Submit` and `Action.Execute` submissions - Define zod schemas for card action payloads and dialog form data - Sanitize `activity.text` from user messages before processing, storage, or AI prompts - Handle type coercion for `Input.ChoiceSet` (always strings), `Input.Toggle` (string booleans), and `Input.Number` - Prevent injection attacks in card rendering (markdown/XSS) and AI function calling (command injection) - Implement server-side validation that mirrors and enforces card-level `isRequired` and `regex` constraints - Set content length limits and validate payload sizes Pair with `../teams/ui.adaptive-cards-ts.md` for understanding card action payloads that need validation, and `../teams/ai.function-calling-implementation-ts.md` for AI function parameter validation. ## research Deep Research prompt: "Write a micro expert on input validation for Teams bots (TypeScript). Cover validating activity.value from card actions and dialog submissions using zod, handling type coercion for ChoiceSet (string values), sanitizing activity.text for injection prevention, validating AI function calling parameters, server-side enforcement beyond client card validation (isRequired, regex), content length limits, and secure error logging without PII exposure. Include zod schema patterns and sanitization utility examples." -
secrets-ts.md 13.9 KB
# secrets-ts ## purpose Secrets management best practices for Teams bots: environment variables, Key Vault, managed identity, and credential hygiene across development and production environments. ## rules 1. Never commit secrets to source control. Add `.env` to `.gitignore` before the first commit. Create a `.env.example` file with placeholder values and comments documenting each required variable. Scan repositories with tools like `git-secrets` or GitHub secret scanning to catch accidental commits. [OWASP -- Hard-coded credentials](https://owasp.org/www-community/vulnerabilities/Use_of_hard-coded_password) 2. A Teams bot requires at minimum three secrets for Azure Bot registration: `CLIENT_ID` (Azure AD app registration ID), `CLIENT_SECRET` (app credential), and `TENANT_ID` (Azure AD tenant). These are configured in the `App` constructor via `clientId`, `clientSecret`, and `tenantId` options. [learn.microsoft.com -- Azure Bot registration](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration) 3. Use Azure Key Vault for all secrets in production environments. Store `CLIENT_SECRET`, `OPENAI_API_KEY`, database connection strings, and any other sensitive values in Key Vault. Access them via Key Vault references in App Settings or programmatically with `@azure/keyvault-secrets`. [learn.microsoft.com -- Key Vault overview](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) 4. Use managed identity (system-assigned or user-assigned) for zero-secret production deployments. The Teams SDK supports `managedIdentityClientId: "system"` or a specific client ID, eliminating the need for `CLIENT_SECRET` entirely. This also works for accessing Key Vault, Cosmos DB, and Blob Storage without connection strings. [learn.microsoft.com -- Managed identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) 5. Apply the least-privilege principle to Azure AD app registrations. Grant only the Microsoft Graph permissions the bot actually needs (e.g., `User.Read` for profile access, not `Directory.ReadWrite.All`). Use delegated permissions where possible (user-level) rather than application permissions (admin-level). Review and remove unused permissions quarterly. [learn.microsoft.com -- Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-overview) 6. Rotate `CLIENT_SECRET` before expiration. Azure AD app credentials can be set with 6-month, 12-month, or 24-month expiration. Create a new credential before the old one expires, update Key Vault, verify the bot works, then remove the old credential. Automate this with Key Vault rotation policies and Event Grid notifications. [learn.microsoft.com -- Credential rotation](https://learn.microsoft.com/en-us/azure/key-vault/secrets/tutorial-rotation) 7. Secure API keys (`OPENAI_API_KEY`, `AZURE_OPENAI_API_KEY`) with the same rigor as bot credentials. Store in Key Vault, access via managed identity or Key Vault references, and set usage limits/quotas on the OpenAI/Azure OpenAI side to limit blast radius if a key is compromised. [learn.microsoft.com -- Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview) 8. Never log secrets or tokens. Implement log scrubbing to redact patterns matching API keys, JWTs, and connection strings. The Teams SDK `ConsoleLogger` does not automatically redact secrets -- wrap or post-process log output if it might contain token values from error stack traces. [OWASP -- Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html) 9. Use the Teams SDK `token` option for custom credential factories when managed identity does not fit your architecture. The token factory pattern `token: (config) => getToken()` lets you integrate with custom secret stores or token services without hardcoding credentials. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. For local development, use `.env` files loaded via `dotenv` (included in the project template via `node -r dotenv/config .`). Keep local `.env` secrets separate from production secrets. Use Azure CLI login (`az login`) with `DefaultAzureCredential` to access Key Vault and other Azure services locally without storing production secrets on dev machines. [learn.microsoft.com -- DefaultAzureCredential](https://learn.microsoft.com/en-us/azure/developer/javascript/sdk/authentication/credential-chains) ## patterns ### Secure .env setup and gitignore configuration ```shell # .gitignore — always include these .env .env.local .env.*.local *.pem *.key credentials.json # .env.example — commit this to document required variables # Azure Bot Registration (required) CLIENT_ID=<your-azure-ad-app-id> CLIENT_SECRET=<your-azure-ad-app-secret> TENANT_ID=<your-azure-ad-tenant-id> # OpenAI (required for AI features) OPENAI_API_KEY=<your-openai-api-key> # Azure OpenAI (alternative to OpenAI) # AZURE_OPENAI_API_KEY=<your-azure-openai-key> # AZURE_OPENAI_ENDPOINT=<https://your-resource.openai.azure.com> # AZURE_OPENAI_API_VERSION=2024-02-01 # AZURE_OPENAI_MODEL_DEPLOYMENT_NAME=<your-deployment-name> # Application Insights (optional) # APPLICATIONINSIGHTS_CONNECTION_STRING=<your-connection-string> # Port (optional, default 3978) PORT=3978 ``` ```typescript // src/index.ts — Local development with dotenv // Run with: node -r dotenv/config . // Or: tsx watch -r dotenv/config src/index.ts import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger("my-bot", { level: "debug" }), }); app.on("message", async ({ send }) => { await send("Bot is running with env-based secrets."); }); app.start(process.env.PORT || 3978); ``` ### Managed identity for zero-secret production ```typescript // src/index.ts — Production deployment with managed identity import { App } from "@microsoft/teams.apps"; import { ConsoleLogger } from "@microsoft/teams.common"; import { DefaultAzureCredential } from "@azure/identity"; import { SecretClient } from "@azure/keyvault-secrets"; // Option 1: Managed identity for bot authentication (no CLIENT_SECRET) const app = new App({ clientId: process.env.CLIENT_ID, tenantId: process.env.TENANT_ID, managedIdentityClientId: "system", // or process.env.MANAGED_IDENTITY_CLIENT_ID logger: new ConsoleLogger("my-bot", { level: "info" }), }); // Option 2: Managed identity for Key Vault access (fetch other secrets) async function getSecret(name: string): Promise<string> { const credential = new DefaultAzureCredential(); const client = new SecretClient(process.env.KEY_VAULT_URL!, credential); const secret = await client.getSecret(name); return secret.value!; } // Option 3: Custom token factory for advanced scenarios // const app = new App({ // clientId: process.env.CLIENT_ID, // tenantId: process.env.TENANT_ID, // token: async (config) => { // // Fetch token from custom secret store or token service // const credential = new DefaultAzureCredential(); // const tokenResponse = await credential.getToken(config.scopes); // return tokenResponse.token; // }, // }); app.on("message", async ({ send }) => { await send("Running with managed identity - zero secrets in config!"); }); app.start(process.env.PORT || 3978); ``` ### Log scrubbing to prevent secret leakage ```typescript // src/utils/log-scrubber.ts const SECRET_PATTERNS: RegExp[] = [ // Azure AD client secrets (40+ character base64-like strings) /[A-Za-z0-9~._-]{34,}/g, // JWT tokens /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, // Connection strings with keys /AccountKey=[^;]+/gi, // OpenAI API keys /sk-[A-Za-z0-9]{20,}/g, // Generic key=value patterns for known secret keys /(client.?secret|api.?key|password|token|credential)\s*[:=]\s*\S+/gi, ]; export function scrubSecrets(message: string): string { let scrubbed = message; for (const pattern of SECRET_PATTERNS) { scrubbed = scrubbed.replace(pattern, "[REDACTED]"); } return scrubbed; } // Usage with a custom logger wrapper: import { ILogger } from "@microsoft/teams.common"; export class ScrubLogger implements ILogger { constructor(private inner: ILogger) {} error(message: string, ...args: unknown[]): void { this.inner.error(scrubSecrets(message), ...args.map(a => typeof a === "string" ? scrubSecrets(a) : a)); } warn(message: string, ...args: unknown[]): void { this.inner.warn(scrubSecrets(message), ...args.map(a => typeof a === "string" ? scrubSecrets(a) : a)); } info(message: string, ...args: unknown[]): void { this.inner.info(scrubSecrets(message), ...args.map(a => typeof a === "string" ? scrubSecrets(a) : a)); } debug(message: string, ...args: unknown[]): void { this.inner.debug(scrubSecrets(message), ...args.map(a => typeof a === "string" ? scrubSecrets(a) : a)); } log(message: string, ...args: unknown[]): void { this.inner.log(scrubSecrets(message), ...args.map(a => typeof a === "string" ? scrubSecrets(a) : a)); } child(name: string): ILogger { return new ScrubLogger(this.inner.child(name)); } } // src/index.ts // import { ScrubLogger } from "./utils/log-scrubber.js"; // import { ConsoleLogger } from "@microsoft/teams.common"; // const app = new App({ // logger: new ScrubLogger(new ConsoleLogger("my-bot", { level: "debug" })), // }); ``` ## pitfalls - **Committing .env to git history**: Even if `.env` is in `.gitignore`, it may already be in git history from an earlier commit. Use `git rm --cached .env` to remove it from tracking, and consider rotating all secrets that were ever committed. Use `git log --all -- .env` to check. - **CLIENT_SECRET expiration**: Azure AD app credentials expire. If the bot stops authenticating unexpectedly, check the credential expiration date in Azure Portal > App registrations > Certificates & secrets. Set calendar reminders or automate rotation. - **Over-permissioned app registration**: Granting broad Graph permissions (e.g., `Directory.ReadWrite.All`) to a bot that only needs to read user profiles (`User.Read`) violates least-privilege. If the bot's credentials are compromised, the blast radius includes all granted permissions. - **Managed identity not available locally**: `managedIdentityClientId` only works on Azure compute. For local development, fall back to `clientId` + `clientSecret` from `.env`. Use conditional configuration based on `NODE_ENV` or presence of `MANAGED_IDENTITY_CLIENT_ID`. - **Logging JWT tokens in error messages**: When auth fails, error messages and stack traces may include full JWT tokens. These tokens grant access until they expire (typically 1 hour). Use log scrubbing to redact JWT patterns. - **Key Vault access denied in new deployments**: After enabling managed identity on App Service, the Key Vault access policy must also be configured. Without it, all Key Vault references resolve to empty strings, and the bot fails silently or crashes at startup. - **Sharing secrets across environments**: Using the same `CLIENT_SECRET` or `OPENAI_API_KEY` in development, staging, and production means compromising one environment compromises all. Use separate credentials per environment. - **OpenAI API key without usage limits**: A leaked `OPENAI_API_KEY` without spending limits can result in significant unexpected costs. Set monthly spending caps in the OpenAI dashboard and use project-scoped API keys where available. ## references - [Azure Key Vault overview](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) - [Managed identities for Azure resources](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) - [Azure Bot registration](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration) - [Key Vault references for App Service](https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references) - [DefaultAzureCredential documentation](https://learn.microsoft.com/en-us/azure/developer/javascript/sdk/authentication/credential-chains) - [Key Vault secret rotation](https://learn.microsoft.com/en-us/azure/key-vault/secrets/tutorial-rotation) - [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-overview) - [OWASP Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html) ## instructions This expert covers secrets management for Microsoft Teams bots built with the Teams AI SDK v2 in TypeScript. Use it when you need to: - Set up `.env` files and `.gitignore` for safe local development with bot credentials - Understand the required secrets for Teams bot registration (`CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`) - Configure Azure Key Vault for production secret storage - Implement managed identity for zero-secret deployments (eliminating `CLIENT_SECRET` in production) - Use the Teams SDK `managedIdentityClientId` or `token` factory for secure authentication - Rotate `CLIENT_SECRET` and other credentials safely - Secure API keys (`OPENAI_API_KEY`) with usage limits and proper storage - Prevent secret leakage in logs with scrubbing patterns - Apply least-privilege to Azure AD app registrations and Key Vault access policies Pair with `../teams/runtime.app-init-ts.md` for App constructor credential configuration, and `../bridge/infra-secrets-config-ts.md` when bridging secrets between AWS and Azure. ## research Deep Research prompt: "Write a micro expert on secrets management for Node/TypeScript Teams bots. Cover local .env handling with dotenv, required bot secrets (CLIENT_ID, CLIENT_SECRET, TENANT_ID), Azure Key Vault for production, managed identity for zero-secret deployments, the Teams SDK managedIdentityClientId and token factory options, CLIENT_SECRET rotation patterns, securing OPENAI_API_KEY, least-privilege Azure AD app registrations, log scrubbing to prevent secret leakage, and a .env.example template. Include code examples for each pattern."
-
-
slack
-
bolt-assistant-ts.md 9.9 KB
# bolt-assistant-ts ## purpose Slack Assistant container patterns for Bolt.js — `Assistant` class configuration, thread lifecycle handlers (`threadStarted`, `userMessage`, `threadContextChanged`), utility functions (`say`, `setStatus`, `setSuggestedPrompts`, `setTitle`), and thread context storage. ## rules 1. **Provide `threadStarted` and `userMessage` handlers (required).** These are the minimum callbacks for an Assistant. `threadContextChanged` is optional and defaults to saving context via the thread context store. [slack.dev/bolt-js/concepts/assistant](https://slack.dev/bolt-js/concepts/assistant) 2. **Call `setStatus()` to show typing indicators.** Use `await setStatus("Thinking...")` at the start of `userMessage` to show the user that processing is happening. The status clears automatically when the bot sends a reply via `say()`, or pass an empty string to clear manually. [api.slack.com/docs/assistants](https://api.slack.com/docs/assistants) 3. **Use `setSuggestedPrompts()` in `threadStarted`.** Provide up to 4 preset prompts with `title` and `message` properties. This helps users discover what the assistant can do. An optional top-level `title` parameter labels the prompt group (defaults to "Try these prompts:"). [api.slack.com/docs/assistants](https://api.slack.com/docs/assistants) 4. **Use `setTitle()` to label conversation threads.** Call `setTitle(summary)` after processing the first `userMessage` to give the thread a meaningful name in the sidebar. [api.slack.com/docs/assistants](https://api.slack.com/docs/assistants) 5. **Thread context is NOT included in `userMessage` events.** The `message` event payload does not carry thread context. Call `await getThreadContext()` in `userMessage` handlers to retrieve the current context (channel_id, team_id, enterprise_id). [slack.dev/bolt-js/concepts/assistant](https://slack.dev/bolt-js/concepts/assistant) 6. **Use `saveThreadContext()` in `threadStarted`.** The initial thread context (which channel the user was viewing) arrives in the `threadStarted` event. Save it immediately so `userMessage` handlers can retrieve it later. [slack.dev/bolt-js/concepts/assistant](https://slack.dev/bolt-js/concepts/assistant) 7. **The default `AssistantThreadContextStore` uses message metadata.** Context is persisted by updating the bot's first message in the thread with metadata. This survives app restarts without external storage. For production, implement a custom store backed by a database. [slack.dev/bolt-js/concepts/assistant](https://slack.dev/bolt-js/concepts/assistant) 8. **Register the assistant with `app.assistant(assistant)`.** This adds middleware that intercepts `assistant_thread_started`, `assistant_thread_context_changed`, and thread messages. The middleware stops propagation — registered `app.message()` handlers will NOT fire for assistant threads. [slack.dev/bolt-js/concepts/assistant](https://slack.dev/bolt-js/concepts/assistant) 9. **`threadContextChanged` fires when the user switches channels.** The updated context is in `event.assistant_thread.context`. The default behavior (when handler is omitted) automatically calls `saveThreadContext()` to persist the new context. 10. **All handlers receive the same utility set.** Every handler gets: `say`, `getThreadContext`, `saveThreadContext`, `setStatus`, `setSuggestedPrompts`, `setTitle`, plus the standard `client`, `context`, and `logger`. ## patterns ### Basic assistant with suggested prompts and status ```typescript import { App, Assistant } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, appToken: process.env.SLACK_APP_TOKEN!, socketMode: true, }); const assistant = new Assistant({ threadStarted: async ({ say, setSuggestedPrompts, saveThreadContext }) => { await saveThreadContext(); await say("Hi! How can I help?"); await setSuggestedPrompts({ title: "Try one of these:", prompts: [ { title: "Summarize", message: "Summarize this channel's recent messages" }, { title: "Draft", message: "Help me draft a message" }, { title: "Search", message: "Search our docs for..." }, ], }); }, userMessage: async ({ message, say, setStatus, setTitle, getThreadContext }) => { await setStatus("Thinking..."); const context = await getThreadContext(); const channelId = context?.channel_id; const userText = message.text ?? ""; // Set thread title from first message await setTitle(userText.slice(0, 50)); // Business logic / AI call here... const answer = await generateAnswer(userText, channelId); await say(answer); }, }); app.assistant(assistant); async function generateAnswer(text: string, channelId?: string): Promise<string> { return `You asked: "${text}" (from channel ${channelId ?? "unknown"})`; } (async () => { await app.start(); console.log("Assistant is running"); })(); ``` ### Custom thread context store backed by a database ```typescript import { type AssistantThreadContextStore, type AllAssistantMiddlewareArgs, type AssistantThreadContext } from "@slack/bolt"; const contextStore: AssistantThreadContextStore = { async get({ payload }: AllAssistantMiddlewareArgs): Promise<AssistantThreadContext> { const threadTs = "assistant_thread" in payload ? payload.assistant_thread.thread_ts : payload.thread_ts; const row = await db.query("SELECT context FROM assistant_threads WHERE thread_ts = $1", [threadTs]); return row?.context ?? {}; }, async save({ payload }: AllAssistantMiddlewareArgs): Promise<void> { const threadTs = "assistant_thread" in payload ? payload.assistant_thread.thread_ts : payload.thread_ts; const context = "assistant_thread" in payload ? payload.assistant_thread.context : {}; await db.query( "INSERT INTO assistant_threads (thread_ts, context) VALUES ($1, $2) ON CONFLICT (thread_ts) DO UPDATE SET context = $2", [threadTs, context] ); }, }; const assistant = new Assistant({ threadContextStore: contextStore, threadStarted: async ({ saveThreadContext, say }) => { await saveThreadContext(); // uses custom store await say("Hello! I'm ready to help."); }, userMessage: async ({ getThreadContext, say, setStatus }) => { await setStatus("Processing..."); const ctx = await getThreadContext(); // reads from custom store await say(`Context channel: ${ctx?.channel_id}`); }, }); ``` ### Handling context changes with custom logic ```typescript const assistant = new Assistant({ threadStarted: async ({ saveThreadContext, say, setSuggestedPrompts }) => { await saveThreadContext(); await say("I'll adapt to whatever channel you're viewing."); await setSuggestedPrompts({ prompts: [ { title: "What's happening?", message: "What's the latest in this channel?" }, ], }); }, userMessage: async ({ getThreadContext, say, setStatus }) => { await setStatus("Looking up context..."); const ctx = await getThreadContext(); await say(`You're currently viewing <#${ctx?.channel_id ?? "unknown"}>.`); }, threadContextChanged: async ({ event, saveThreadContext, logger }) => { const newChannel = event.assistant_thread.context?.channel_id; logger.info(`User switched to channel: ${newChannel}`); await saveThreadContext(); // persist the new context }, }); ``` ## pitfalls - **Forgetting `saveThreadContext()` in `threadStarted`**: Without saving, `getThreadContext()` in `userMessage` returns empty/stale data. The initial context is only available in the `threadStarted` event. - **Assistant middleware blocks `app.message()` handlers**: Once `app.assistant()` is registered, messages in assistant threads are consumed by the Assistant middleware and do NOT propagate to `app.message()` listeners. Don't register duplicate handlers. - **`setSuggestedPrompts` limit of 4**: Passing more than 4 prompts causes the API to reject the call. Keep it to 4 or fewer. - **Default context store requires bot to post first**: The `DefaultThreadContextStore` saves context as metadata on the bot's first message. If `threadStarted` doesn't call `say()`, there's no message to attach metadata to, and context storage fails silently. - **No `ack()` in assistant handlers**: Unlike actions/commands, assistant handlers don't have an `ack()` function. Events are fire-and-forget from Slack's perspective. - **`message.text` can be undefined**: Always handle the case where `message.text` is `undefined` (e.g., when the user sends only an attachment). ## references - https://api.slack.com/docs/assistants - https://slack.dev/bolt-js/concepts/assistant - https://github.com/slackapi/bolt-js/blob/main/src/Assistant.ts - https://github.com/slackapi/bolt-js/blob/main/src/AssistantThreadContextStore.ts ## instructions This expert covers the Slack Assistant container for Bolt.js in TypeScript. Use it when you need to: create an AI assistant that lives in Slack's assistant panel; handle thread lifecycle events (threadStarted, userMessage, threadContextChanged); use utility functions for status indicators, suggested prompts, and thread titles; implement custom thread context stores for production persistence; and understand the middleware behavior that separates assistant threads from regular message handlers. Pair with `runtime.bolt-foundations-ts.md` for general Bolt app setup and `ui.block-kit-ts.md` for rich message formatting within assistant responses. ## research Deep Research prompt: "Write a micro expert on the Slack Assistant container in Bolt.js TypeScript. Cover: Assistant class constructor (threadStarted, userMessage, threadContextChanged, threadContextStore), utility functions (say, setStatus, setSuggestedPrompts, setTitle, getThreadContext, saveThreadContext), AssistantThreadContextStore interface (get, save), DefaultThreadContextStore message metadata pattern, app.assistant() registration and middleware behavior, thread lifecycle event payloads, and common patterns for AI-powered assistants. Provide 2-3 canonical TypeScript examples." -
bolt-events-ts.md 9.3 KB
# bolt-events-ts ## purpose Events API patterns for Slack Bolt.js — `app.event()` registration, event type payloads, retry handling, built-in event middleware (`ignoreSelf`, `directMention`), and event-vs-message handler selection. ## rules 1. **Use `app.event(eventType, handler)` for all non-message events.** Register handlers with the event type string (e.g., `"reaction_added"`, `"member_joined_channel"`, `"app_home_opened"`). Bolt routes events by matching `event.type`. [slack.dev/bolt-js/concepts/event-listening](https://slack.dev/bolt-js/concepts/event-listening) 2. **Use `app.message()` for message events, not `app.event("message")`.** `app.message()` provides text pattern matching (string/RegExp), `say()`, and subtype-aware filtering. `app.event("message")` works but lacks these conveniences. Bolt throws an error if you pass `"message"` with subtype patterns to `app.event()`. [slack.dev/bolt-js/concepts/message-listening](https://slack.dev/bolt-js/concepts/message-listening) 3. **Events do NOT have `ack()`.** Unlike commands, actions, and views, event handlers receive no `ack` function. Bolt automatically acknowledges event webhooks with HTTP 200 before your handler runs. [slack.dev/bolt-js/concepts/event-listening](https://slack.dev/bolt-js/concepts/event-listening) 4. **`say()` is only available for events with channel context.** Events like `app_mention`, `message`, and `member_joined_channel` include a channel, so `say()` works. Events like `team_join` or `app_home_opened` may not — use `client.chat.postMessage()` with an explicit channel. [slack.dev/bolt-js/concepts/event-listening](https://slack.dev/bolt-js/concepts/event-listening) 5. **`ignoreSelf` is enabled by default.** Bolt filters out events generated by your own bot (matching `bot_id` for messages, `user` for other events). This prevents infinite loops. Disable with `ignoreSelf: false` in App constructor if you need to process your own events. [slack.dev/bolt-js/concepts/event-listening](https://slack.dev/bolt-js/concepts/event-listening) 6. **Use `context.retryNum` and `context.retryReason` to detect retries.** Slack retries event delivery if your server doesn't respond with 200 in time. Check `context.retryNum` to skip duplicate processing or log retry attempts. [api.slack.com/events-api#retries](https://api.slack.com/events-api#retries) 7. **Event type can be matched with RegExp.** Pass a RegExp to `app.event()` for pattern matching: `app.event(/^member_/, handler)` matches both `member_joined_channel` and `member_left_channel`. Matches are stored in `context.matches`. [slack.dev/bolt-js/concepts/event-listening](https://slack.dev/bolt-js/concepts/event-listening) 8. **Subscribe to events in the Slack app dashboard.** `app.event()` in code does nothing if the event type isn't enabled in "Event Subscriptions" in your app's configuration. Both code and dashboard must agree. [api.slack.com/events-api#subscriptions](https://api.slack.com/events-api#subscriptions) 9. **The `body` property contains the full event envelope.** It includes `team_id`, `api_app_id`, `event_id`, `event_time`, and `authorizations`. The `event` property is just the inner event payload. Use `body` when you need workspace-level metadata. [api.slack.com/events-api#event_type_structure](https://api.slack.com/events-api#event_type_structure) 10. **URL verification is handled automatically by Bolt receivers.** Both `HTTPReceiver` and `ExpressReceiver` respond to `{ type: "url_verification" }` challenges without any handler code. You don't need to implement this yourself. [slack.dev/bolt-js/concepts/event-listening](https://slack.dev/bolt-js/concepts/event-listening) ## patterns ### Common event handlers ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // React to app mentions app.event("app_mention", async ({ event, say }) => { await say(`Hey <@${event.user}>, you mentioned me!`); }); // Welcome new team members app.event("team_join", async ({ event, client }) => { // team_join has no channel context — use client directly await client.chat.postMessage({ channel: "#general", text: `Welcome to the team, <@${event.user.id}>! :wave:`, }); }); // Track reactions app.event("reaction_added", async ({ event, client }) => { if (event.reaction === "white_check_mark" && event.item.type === "message") { await client.chat.postMessage({ channel: event.item.channel, thread_ts: event.item.ts, text: `<@${event.user}> marked this as done :white_check_mark:`, }); } }); // Update App Home when opened app.event("app_home_opened", async ({ event, client }) => { if (event.tab !== "home") return; await client.views.publish({ user_id: event.user, view: { type: "home", blocks: [ { type: "section", text: { type: "mrkdwn", text: `*Welcome home, <@${event.user}>!*` }, }, { type: "divider" }, { type: "section", text: { type: "mrkdwn", text: "Here's what you can do..." }, }, ], }, }); }); // Channel membership changes (RegExp pattern) app.event(/^member_(joined|left)_channel$/, async ({ event, say, context }) => { const action = context.matches?.[1]; // "joined" or "left" await say(`<@${event.user}> ${action} the channel.`); }); ``` ### Retry-aware event handler ```typescript app.event("app_mention", async ({ event, say, context }) => { // Skip retries to avoid duplicate processing if (context.retryNum !== undefined) { console.log(`Skipping retry #${context.retryNum}: ${context.retryReason}`); return; } // Process normally await say(`Got your mention, <@${event.user}>!`); }); ``` ### Event handler with listener middleware ```typescript import { App, directMention } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Only fires when message starts with @bot mention app.message(directMention(), async ({ message, say }) => { await say(`You said: ${(message as any).text}`); }); // Custom listener middleware: only process DMs const onlyDMs = async ({ event, next }: { event: any; next: () => Promise<void> }) => { if (event.channel_type === "im") { await next(); } }; app.event("message", onlyDMs, async ({ event, say }) => { await say?.("Got your DM!"); }); ``` ## pitfalls - **Not subscribing in the dashboard**: `app.event("reaction_added")` in code does nothing if `reaction_added` isn't enabled in "Event Subscriptions" in your Slack app settings. You'll see no errors — events just don't arrive. - **Using `say()` in events without channel context**: `team_join`, `tokens_revoked`, `app_uninstalled`, and some other events don't have a channel. Calling `say()` throws "channel not found". Use `client.chat.postMessage()` with an explicit channel. - **Infinite loops from bot's own events**: If you disable `ignoreSelf` and your handler posts a message that triggers the same event, you get an infinite loop. Always check `event.bot_id` or `event.user` against your bot's identity when `ignoreSelf` is off. - **Processing retries as new events**: Slack retries after ~10 seconds, ~1 minute, and ~5 minutes if your server doesn't respond with 200 quickly. Without checking `context.retryNum`, you'll process the same event multiple times. Use idempotency keys or skip retries. - **Confusing `event` and `body`**: The `event` property is the inner event payload. The `body` property wraps it with envelope metadata (`team_id`, `event_id`, etc.). Use `event` for the event data and `body` when you need workspace context. - **Missing scopes for events**: Each event type requires specific OAuth scopes. For example, `channels:history` for public channel messages, `im:history` for DMs, `reactions:read` for reactions. Missing scopes cause silent failure — no events arrive. ## references - https://api.slack.com/events-api - https://api.slack.com/events - https://slack.dev/bolt-js/concepts/event-listening - https://slack.dev/bolt-js/concepts/message-listening - https://api.slack.com/scopes - https://github.com/slackapi/bolt-js ## instructions This expert covers the Slack Events API integration in Bolt.js TypeScript. Use it when you need to: handle non-message events with app.event(); understand event payloads and the event envelope structure; implement retry-aware handlers; use built-in middleware like ignoreSelf and directMention; build App Home views with app_home_opened; track reactions, team joins, and channel membership changes; and understand the relationship between event subscriptions in the dashboard and handler registration in code. Pair with `runtime.bolt-foundations-ts.md` for general handler context and `runtime.ack-rules-ts.md` to understand why events don't have ack(). ## research Deep Research prompt: "Write a micro expert on the Slack Events API in Bolt.js TypeScript. Cover: app.event() registration (string and RegExp), common event types (app_mention, reaction_added, team_join, app_home_opened, member_joined/left_channel), event payload shapes, retry handling (context.retryNum, context.retryReason), built-in middleware (ignoreSelf, directMention), say() availability per event type, event envelope (body) vs inner event, URL verification auto-handling, and dashboard subscription requirements. Provide 2-3 canonical TypeScript examples." -
bolt-java.md 8.9 KB
# bolt-java ## purpose Slack Bolt for Java SDK patterns — app initialization, listener registration, context objects, and Web API usage for Tier 3 Java projects. ## rules 1. Add the `com.slack.api:bolt` dependency (and optionally `bolt-servlet`, `bolt-jetty`, or `bolt-socket-mode`) via Maven or Gradle. The SDK is modular: `slack-api-client` for Web API, `slack-api-model` for data models, `bolt` for the app framework. [github.com/slackapi/java-slack-sdk](https://github.com/slackapi/java-slack-sdk) 2. Initialize the App with `new App()` (reads `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` from env) or with `new App(AppConfig.builder().singleTeamBotToken(token).signingSecret(secret).build())`. The `AppConfig` uses Lombok `@Builder` for fluent configuration. [github.com/slackapi/java-slack-sdk/wiki](https://github.com/slackapi/java-slack-sdk/wiki) 3. Register listeners using lambda syntax: `app.command("/cmd", (req, ctx) -> ctx.ack())`. All handlers receive two parameters — a typed `Request` and a typed `Context`. The handler returns a `Response` object. This replaces the TS single-context-object pattern. [github.com/slackapi/java-slack-sdk/wiki](https://github.com/slackapi/java-slack-sdk/wiki) 4. All listeners support string or `Pattern` (regex) matching: `app.command("/hello", handler)` for exact match, `app.command(Pattern.compile("^/hello.*"), handler)` for regex. Message listeners do substring matching by default: `app.message("hello", handler)` matches any message containing "hello". [java-slack-sdk source: App.java] 5. Always return `ctx.ack()` from handlers — Java Bolt uses return values, not void handlers. Return `ctx.ack()`, `ctx.ack("text")`, `ctx.ack(blocks)`, or for views: `ctx.ackWithErrors(errorMap)`, `ctx.ackWithUpdate(view)`, `ctx.ackWithPush(view)`. [java-slack-sdk source: Context.java] 6. Access the Web API client via `ctx.client()`. Method names match the Slack API but use **camelCase**: `ctx.client().chatPostMessage(r -> r.channel(ch).text(msg))`. All methods use a **request configurator lambda** (`r -> r.field(value)`), not positional arguments. [java-slack-sdk source: MethodsClient] 7. Use the **static import helpers** for Block Kit: `import static com.slack.api.model.block.Blocks.*`, `import static com.slack.api.model.block.element.BlockElements.*`, `import static com.slack.api.model.block.composition.BlockCompositions.*`. Build blocks with `asBlocks(section(...), divider(), actions(...))`. [java-slack-sdk source: Blocks.java] 8. Build modal views with `View.builder().type("modal").callbackId("id").title(viewTitle(t -> t.text("Title"))).blocks(blocks).submit(viewSubmit(s -> s.text("Submit"))).build()`. Open with `ctx.client().viewsOpen(r -> r.triggerId(ctx.getTriggerId()).view(view))`. [java-slack-sdk source: View.java] 9. For Socket Mode, use `SocketModeApp` from `bolt-socket-mode`: `new SocketModeApp(appToken, app).start()`. The `SLACK_APP_TOKEN` env var is auto-loaded if not passed explicitly. [java-slack-sdk source: SocketModeApp.java] 10. For Spring Boot, extend `SlackAppServlet` from `bolt-jakarta-servlet` and annotate with `@WebServlet("/slack/events")`. Register the `App` as a Spring `@Bean`. [java-slack-sdk bolt-spring-boot-examples] 11. For OAuth multi-workspace apps, configure `AppConfig.builder().clientId(...).clientSecret(...).scope("chat:write,commands")` and call `app.asOAuthApp(true)`. Implement `InstallationService` for custom token storage. The built-in `FileInstallationService` stores tokens on disk. [java-slack-sdk source: InstallationService.java] 12. Use `app.executorService().submit(() -> { ... })` for async background work after ack. Java Bolt handlers are synchronous — ack first within 3 seconds, then submit long-running work to the executor. [java-slack-sdk source: App.java] ## patterns ### Slash command that opens a modal ```java import com.slack.api.bolt.App; import com.slack.api.bolt.AppConfig; import com.slack.api.model.view.View; import static com.slack.api.model.block.Blocks.*; import static com.slack.api.model.block.element.BlockElements.*; import static com.slack.api.model.block.composition.BlockCompositions.*; import static com.slack.api.model.view.Views.*; App app = new App(AppConfig.builder() .singleTeamBotToken(System.getenv("SLACK_BOT_TOKEN")) .signingSecret(System.getenv("SLACK_SIGNING_SECRET")) .build()); app.command("/task", (req, ctx) -> { ctx.client().viewsOpen(r -> r .triggerId(ctx.getTriggerId()) .view(View.builder() .type("modal") .callbackId("task_modal") .title(viewTitle(t -> t.text("Create Task"))) .submit(viewSubmit(s -> s.text("Create"))) .blocks(asBlocks( input(i -> i .blockId("title_block") .label(plainText("Title")) .element(plainTextInput(pti -> pti .actionId("title_input"))) ) )) .build() ) ); return ctx.ack(); }); app.viewSubmission("task_modal", (req, ctx) -> { var values = req.getPayload().getView().getState().getValues(); var titleMap = values.get("title_block"); var title = titleMap.get("title_input").getValue(); if (title == null || title.length() < 3) { return ctx.ackWithErrors(Map.of("title_block", "Title too short")); } return ctx.ack(); }); ``` ### Event handler with proactive message ```java import com.slack.api.model.event.AppMentionEvent; app.event(AppMentionEvent.class, (req, ctx) -> { var event = req.getEvent(); ctx.client().chatPostMessage(r -> r .channel(event.getChannel()) .threadTs(event.getTs()) .text("Thanks for the mention, <@" + event.getUser() + ">!") ); return ctx.ack(); }); ``` ### Socket Mode with Spring Boot ```java import com.slack.api.bolt.App; import com.slack.api.bolt.socket_mode.SocketModeApp; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SlackConfig { @Bean public App slackApp() { App app = new App(); app.command("/ping", (req, ctx) -> ctx.ack("pong!")); app.message("hello", (req, ctx) -> { ctx.say("Hey there!"); return ctx.ack(); }); return app; } @Bean public SocketModeApp socketModeApp(App app) throws Exception { var socketApp = new SocketModeApp( System.getenv("SLACK_APP_TOKEN"), app ); socketApp.startAsync(); return socketApp; } } ``` ## pitfalls - **Returning `null` vs `ctx.ack()`**: Java handlers must return a `Response`. Returning `null` skips the response — the user sees a timeout error. Always return `ctx.ack()`. - **Request configurator pattern**: All API methods use `r -> r.field(value)` lambdas, not method arguments. Writing `chatPostMessage(channel, text)` won't compile. - **Static import confusion**: Block Kit builders require static imports from three separate classes (`Blocks`, `BlockElements`, `BlockCompositions`). Missing any causes compilation errors on helper methods like `section()`, `plainText()`, `asBlocks()`. - **No Teams SDK for Java**: Bot Framework Java was archived at end of 2025 with no replacement. For the Teams side in Tier 3 Java projects, use REST API patterns from `experts/bridge/rest-only-integration-ts.md`. - **Async work after ack**: Java handlers are synchronous. For work taking >3 seconds, call `ctx.ack()` first, then submit work to `app.executorService()`. Do not block the handler thread. ## references - https://github.com/slackapi/java-slack-sdk - https://github.com/slackapi/java-slack-sdk/wiki - https://slack.dev/java-slack-sdk/guides/bolt-basics ## instructions This expert covers Slack Bolt for Java — the Java equivalent of `@slack/bolt`. Use it for Tier 3 Java projects that need Slack SDK patterns. Java projects have SDK support for Slack but not Teams. For the Teams side, pair with `experts/bridge/rest-only-integration-ts.md` to implement REST-based Teams integration. Pair with: `bridge/rest-only-integration-ts.md` for REST-based Teams integration (the unsupported side). TS Bolt experts for conceptual architecture reference. ## research Deep Research prompt: "Write a micro expert on Slack Bolt for Java. Cover App/AppConfig initialization (builder pattern, env vars), listener registration (lambda syntax, request + context params, string/Pattern matching), Context subclasses (SlashCommandContext, ActionContext, EventContext, ViewSubmissionContext), Response return pattern (ack, ackWithErrors, ackWithUpdate), MethodsClient API calls (request configurator lambdas, camelCase methods), Block Kit builders (static imports from Blocks/BlockElements/BlockCompositions), View builder, Socket Mode (SocketModeApp), Spring Boot integration (SlackAppServlet, @Bean), OAuth (InstallationService, AppConfig OAuth fields), and executor for async work. Source from java-slack-sdk source code." -
bolt-oauth-distribution-ts.md 10.9 KB
# bolt-oauth-distribution-ts ## purpose OAuth and multi-workspace app distribution for Slack Bolt.js — `InstallProvider` configuration, `InstallationStore` interface, OAuth flow, `authorize` callback, scope management, state verification, and receiver OAuth route setup. ## rules 1. **Provide `clientId`, `clientSecret`, and `stateSecret` for OAuth apps.** These enable the built-in OAuth flow with install page and callback handling. The `stateSecret` must be at least 16 characters for CSRF protection. Without all three, Bolt uses single-workspace mode. [slack.dev/bolt-js/concepts/authenticating-oauth](https://slack.dev/bolt-js/concepts/authenticating-oauth) 2. **Implement `InstallationStore` for production.** The default `MemoryInstallationStore` loses data on restart. Implement `storeInstallation`, `fetchInstallation`, and `deleteInstallation` with database persistence. [slack.dev/bolt-js/concepts/authenticating-oauth](https://slack.dev/bolt-js/concepts/authenticating-oauth) 3. **Handle both team-level and enterprise-level installations.** Check `installation.isEnterpriseInstall`: if true, key by `enterprise.id`; if false, key by `team.id`. Enterprise Grid org-wide installs have `team` undefined. [slack.dev/bolt-js/concepts/authenticating-oauth](https://slack.dev/bolt-js/concepts/authenticating-oauth) 4. **`scopes` defines bot token permissions.** Pass an array of scope strings (e.g., `["chat:write", "commands", "channels:history"]`) at the App or receiver level. These determine what the bot token can do. [api.slack.com/scopes](https://api.slack.com/scopes) 5. **`installerOptions.userScopes` defines user token permissions.** User scopes grant permissions on behalf of the installing user. The resulting `installation.user.token` (xoxp-...) is separate from the bot token (xoxb-...). [api.slack.com/scopes](https://api.slack.com/scopes) 6. **`fetchInstallation` is called for every incoming event.** The built-in `authorize` function queries your `InstallationStore` for each event to resolve the bot token. Keep this lookup fast — use caching or an indexed database. [slack.dev/bolt-js/concepts/authorization](https://slack.dev/bolt-js/concepts/authorization) 7. **Use custom `authorize` for advanced token resolution.** Instead of `InstallationStore`, pass an `authorize` function that receives `{ teamId, enterpriseId, userId, isEnterpriseInstall }` and returns `{ botToken, botId, botUserId }`. [slack.dev/bolt-js/concepts/authorization](https://slack.dev/bolt-js/concepts/authorization) 8. **Don't mix `token` with OAuth.** Single-workspace apps use `token` directly. OAuth/multi-workspace apps use `clientId` + `clientSecret` + `installationStore`. Providing both causes undefined behavior. [slack.dev/bolt-js/concepts/authenticating-oauth](https://slack.dev/bolt-js/concepts/authenticating-oauth) 9. **OAuth routes are registered automatically.** `HTTPReceiver` and `ExpressReceiver` create `GET /slack/install` (Add to Slack page) and `GET /slack/oauth_redirect` (callback). Customize paths via `installerOptions.installPath` and `installerOptions.redirectUriPath`. [slack.dev/bolt-js/concepts/authenticating-oauth](https://slack.dev/bolt-js/concepts/authenticating-oauth) 10. **Handle `tokens_revoked` and `app_uninstalled` events.** Subscribe to these events and call `installationStore.deleteInstallation()` to clean up. Without this, revoked tokens cause auth errors on every event from that workspace. [api.slack.com/events/tokens_revoked](https://api.slack.com/events/tokens_revoked) ## patterns ### Multi-workspace OAuth app with database-backed store ```typescript import { App, type Installation, type InstallationQuery } from "@slack/bolt"; const app = new App({ signingSecret: process.env.SLACK_SIGNING_SECRET!, clientId: process.env.SLACK_CLIENT_ID!, clientSecret: process.env.SLACK_CLIENT_SECRET!, stateSecret: process.env.SLACK_STATE_SECRET!, scopes: ["chat:write", "commands", "channels:history", "app_mentions:read"], installationStore: { storeInstallation: async (installation: Installation) => { if (installation.isEnterpriseInstall && installation.enterprise) { await db.set(`install:${installation.enterprise.id}`, installation); return; } if (installation.team) { await db.set(`install:${installation.team.id}`, installation); return; } throw new Error("Failed saving installation: no team or enterprise ID"); }, fetchInstallation: async (query: InstallationQuery<boolean>) => { if (query.isEnterpriseInstall && query.enterpriseId) { return await db.get(`install:${query.enterpriseId}`); } if (query.teamId) { return await db.get(`install:${query.teamId}`); } throw new Error("Failed fetching installation"); }, deleteInstallation: async (query: InstallationQuery<boolean>) => { if (query.isEnterpriseInstall && query.enterpriseId) { await db.delete(`install:${query.enterpriseId}`); return; } if (query.teamId) { await db.delete(`install:${query.teamId}`); return; } throw new Error("Failed deleting installation"); }, }, }); // Clean up on uninstall app.event("app_uninstalled", async ({ context, body }) => { const teamId = body.team_id; const enterpriseId = body.enterprise_id; console.log(`App uninstalled from team ${teamId}`); // deleteInstallation is called automatically by the built-in authorize }); // Clean up on token revocation app.event("tokens_revoked", async ({ event, context }) => { console.log(`Tokens revoked: ${JSON.stringify(event.tokens)}`); }); (async () => { await app.start(3000); console.log("OAuth app running — visit http://localhost:3000/slack/install"); })(); ``` ### Custom authorize function (alternative to InstallationStore) ```typescript import { App, type AuthorizeResult } from "@slack/bolt"; const app = new App({ signingSecret: process.env.SLACK_SIGNING_SECRET!, authorize: async ({ teamId, enterpriseId, isEnterpriseInstall }): Promise<AuthorizeResult> => { const key = isEnterpriseInstall ? enterpriseId : teamId; const installation = await db.get(`install:${key}`); if (!installation) { throw new Error(`No installation found for ${key}`); } return { botToken: installation.bot.token, botId: installation.bot.id, botUserId: installation.bot.userId, teamId, enterpriseId, }; }, }); ``` ### ExpressReceiver with custom OAuth routes and additional endpoints ```typescript import { App, ExpressReceiver } from "@slack/bolt"; const receiver = new ExpressReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET!, clientId: process.env.SLACK_CLIENT_ID!, clientSecret: process.env.SLACK_CLIENT_SECRET!, stateSecret: process.env.SLACK_STATE_SECRET!, scopes: ["chat:write", "commands"], installerOptions: { installPath: "/slack/install", redirectUriPath: "/slack/oauth_redirect", directInstall: false, // show Add to Slack page (vs redirect immediately) userScopes: ["chat:write"], // optional user token scopes }, installationStore: myDatabaseStore, }); // Add custom routes on the same Express app receiver.router.get("/health", (_req, res) => res.send("OK")); receiver.router.get("/api/installations", async (_req, res) => { const count = await db.count("installations"); res.json({ count }); }); const app = new App({ receiver }); (async () => { await app.start(3000); console.log("App with OAuth running on :3000"); })(); ``` ## pitfalls - **`MemoryInstallationStore` loses data on restart**: The default store is in-memory only. Every restart requires users to re-install your app. Always implement a persistent `installationStore` for production. - **Missing `stateSecret` with OAuth**: If you provide `clientId` and `clientSecret` but forget `stateSecret` (and don't set `stateVerification: false`), the app throws on startup. Always provide a `stateSecret` of at least 16 characters. - **Enterprise Grid keying**: Org-wide Enterprise Grid installs have `installation.team` as `undefined`. If you only key by `team.id`, enterprise installs fail. Always check `isEnterpriseInstall` first. - **Slow `fetchInstallation` blocks event processing**: Since `fetchInstallation` runs for every incoming event, a slow database query adds latency to all interactions. Index your lookup key and consider caching. - **Not handling `tokens_revoked`**: When a user revokes your app's tokens, Slack sends this event. If you don't delete the installation, subsequent events fail with invalid token errors. Subscribe to `tokens_revoked` and `app_uninstalled`. - **Mixing `token` with `clientId`/`clientSecret`**: Single-workspace mode (`token`) and OAuth mode (`clientId`/`clientSecret`) are mutually exclusive. Using both causes Bolt to use the OAuth path but may ignore your `token`. - **`redirectUri` mismatch**: The redirect URI in your code must exactly match the one registered in the Slack app settings (including trailing slashes). Mismatches cause OAuth to fail with a cryptic error. - **State cookie domain issues**: In development with tunneling tools (ngrok, Cloudflare Tunnel), the state cookie may not be sent back if the domain changes between install and callback. Test OAuth flow end-to-end in your tunneling setup. ## references - https://slack.dev/bolt-js/concepts/authenticating-oauth - https://slack.dev/bolt-js/concepts/authorization - https://api.slack.com/authentication/oauth-v2 - https://api.slack.com/scopes - https://api.slack.com/events/app_uninstalled - https://api.slack.com/events/tokens_revoked - https://github.com/slackapi/bolt-js - https://github.com/slackapi/node-slack-sdk/tree/main/packages/oauth ## instructions This expert covers Slack OAuth and multi-workspace app distribution in Bolt.js TypeScript. Use it when you need to: set up OAuth with InstallProvider for distributing your app to multiple workspaces; implement a persistent InstallationStore with database backing; configure bot and user scopes; handle the authorize callback for custom token resolution; set up OAuth routes on HTTPReceiver or ExpressReceiver; manage app uninstalls and token revocations; and handle Enterprise Grid org-wide installations. Pair with `runtime.bolt-foundations-ts.md` for general App setup and `runtime.ack-rules-ts.md` for how token resolution affects handler execution. ## research Deep Research prompt: "Write a micro expert on Slack OAuth and multi-workspace app distribution in Bolt.js TypeScript. Cover: InstallProvider configuration (clientId, clientSecret, stateSecret), InstallationStore interface (storeInstallation, fetchInstallation, deleteInstallation), OAuth flow (authorize URL, callback, token exchange), authorize callback for custom token resolution, bot scopes vs user scopes, HTTPReceiver and ExpressReceiver OAuth route setup, state verification (stateSecret, stateStore), Enterprise Grid org-wide installs, token revocation handling, and production deployment considerations. Provide 2-3 canonical TypeScript examples." -
bolt-python.md 9.5 KB
# bolt-python ## purpose Slack Bolt for Python SDK patterns — translating TypeScript Bolt concepts to Python equivalents using `slack_bolt`. ## rules 1. Import the sync App from `slack_bolt` or the async App from `slack_bolt.async_app`. Use `AsyncApp` for FastAPI or any async framework; use sync `App` for Flask or Django. [slack.dev/bolt-python/concepts](https://slack.dev/bolt-python/concepts) 2. Initialize the App with `token` and `signing_secret`, or let Bolt auto-load from `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` environment variables. Unlike TS Bolt, Python Bolt reads env vars automatically when no args are passed. [slack.dev/bolt-python/tutorial/getting-started](https://slack.dev/bolt-python/tutorial/getting-started) 3. Register listeners using decorator syntax: `@app.message("pattern")`, `@app.command("/cmd")`, `@app.action("action_id")`, `@app.shortcut("callback_id")`, `@app.view("callback_id")`, `@app.event("event_type")`, `@app.options("action_id")`. This replaces the TS method-call pattern. [slack.dev/bolt-python/concepts](https://slack.dev/bolt-python/concepts) 4. Python Bolt uses **argument injection** — handlers declare only the parameters they need (`ack`, `say`, `respond`, `client`, `body`, `event`, `command`, `action`, `shortcut`, `view`, `context`, `logger`). Bolt inspects the function signature and injects matching values. This replaces the TS destructured context object. [slack.dev/bolt-python/concepts/listener-functions](https://slack.dev/bolt-python/concepts/listener-functions) 5. Use `ack()` the same way as TS. For view submissions, pass `response_action="errors"` and `errors={"block_id": "message"}` as keyword arguments instead of the TS object form `ack({ response_action: 'errors', errors })`. [slack.dev/bolt-python/concepts/acknowledge](https://slack.dev/bolt-python/concepts/acknowledge) 6. Use `client` (a `WebClient` instance from `slack_sdk`) for API calls. Method names use **snake_case**: `client.chat_postMessage()`, `client.views_open()`, `client.users_info()`, `client.files_upload_v2()`. This maps directly from the TS camelCase equivalents. [slack.dev/python-slack-sdk/web](https://slack.dev/python-slack-sdk/web) 7. Pass keyword arguments to API methods, not request configurators. TS uses `client.chat.postMessage({ channel, text })`. Python uses `client.chat_postMessage(channel=channel, text=text)`. No nested method namespaces — methods are flat on the client. [slack.dev/python-slack-sdk/web](https://slack.dev/python-slack-sdk/web) 8. For Socket Mode, use `SocketModeHandler` from `slack_bolt.adapter.socket_mode`. Pass the `App` and `app_token`. Call `handler.start()` to block. This replaces the TS `socketMode: true` constructor option. [slack.dev/bolt-python/concepts/socket-mode](https://slack.dev/bolt-python/concepts/socket-mode) 9. For Flask, wrap the App with `SlackRequestHandler` from `slack_bolt.adapter.flask`. For FastAPI, use `AsyncSlackRequestHandler` from `slack_bolt.adapter.fastapi` with `AsyncApp`. Django, Bottle, Sanic, Tornado, Starlette, and ASGI adapters also exist. [slack.dev/bolt-python/concepts/adapters](https://slack.dev/bolt-python/concepts/adapters) 10. Use regex patterns with `re.compile()`: `@app.message(re.compile(r"^hello"))`. Same capability as TS RegExp patterns but uses Python's `re` module. [slack.dev/bolt-python/concepts/message-listening](https://slack.dev/bolt-python/concepts/message-listening) 11. Access event data through the injected `event` or `body` dict — these are plain Python dicts, not typed objects. Use `event["user"]`, `body["trigger_id"]`, `view["state"]["values"]`. No TypeScript interfaces — rely on Slack API documentation for field shapes. [api.slack.com/events](https://api.slack.com/events) 12. For middleware, use `@app.middleware` decorator or pass `middleware=[fn]` to individual listeners. Middleware functions receive injected args including `next` (or `next_` to avoid shadowing the builtin). Call `next()` to continue the chain. [slack.dev/bolt-python/concepts/middleware](https://slack.dev/bolt-python/concepts/middleware) 13. Both sync and async Apps support the same listener types. Async handlers must use `async def` and `await` all utility calls (`await ack()`, `await say()`, `await client.chat_postMessage()`). Sync handlers use regular `def` and direct calls. [slack.dev/bolt-python/concepts/async](https://slack.dev/bolt-python/concepts/async) ## patterns ### Basic app with message, command, and action handlers ```python import os import re from slack_bolt import App app = App( token=os.environ["SLACK_BOT_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"], ) @app.message(re.compile(r"^hello")) def handle_hello(message, say): say(f"Hey <@{message['user']}>!") @app.command("/status") def handle_status(ack, command, respond): ack("Checking status...") status = get_system_status() respond(response_type="in_channel", text=f"Status: {status}") @app.action("approve_button") def handle_approve(ack, body, client): ack() client.chat_update( channel=body["channel"]["id"], ts=body["message"]["ts"], text=f"Approved by <@{body['user']['id']}>", ) if __name__ == "__main__": app.start(port=3000) ``` ### Async app with FastAPI ```python import os from slack_bolt.async_app import AsyncApp from slack_bolt.adapter.fastapi import AsyncSlackRequestHandler from fastapi import FastAPI, Request app = AsyncApp( token=os.environ["SLACK_BOT_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"], ) @app.command("/ticket") async def handle_ticket(ack, command, client): await ack() await client.views_open( trigger_id=command["trigger_id"], view={ "type": "modal", "callback_id": "ticket_modal", "title": {"type": "plain_text", "text": "Create Ticket"}, "submit": {"type": "plain_text", "text": "Create"}, "blocks": [ { "type": "input", "block_id": "title_block", "label": {"type": "plain_text", "text": "Title"}, "element": { "type": "plain_text_input", "action_id": "title_input", }, } ], }, ) @app.view("ticket_modal") async def handle_submission(ack, view, client): title = view["state"]["values"]["title_block"]["title_input"]["value"] if len(title) < 3: await ack(response_action="errors", errors={"title_block": "Too short"}) return await ack() fastapi_app = FastAPI() handler = AsyncSlackRequestHandler(app) @fastapi_app.post("/slack/events") async def slack_events(req: Request): return await handler.handle(req) ``` ### Socket Mode ```python import os from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler app = App(token=os.environ["SLACK_BOT_TOKEN"]) @app.event("app_mention") def handle_mention(event, say): say(f"You mentioned me! <@{event['user']}>") if __name__ == "__main__": handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]) handler.start() ``` ## pitfalls - **Using `next` instead of `next_` in middleware**: `next` shadows Python's builtin. Use `next_` as the parameter name to avoid confusion. Both work for injection. - **Forgetting `await` in async handlers**: Unlike sync App where `say("text")` works directly, async App requires `await say("text")`. Missing `await` silently drops the call. - **Dict access vs typed objects**: Python payloads are plain dicts. `event["user"]` not `event.user`. KeyError on missing fields — use `.get("key")` for optional fields. - **Method name casing**: Python SDK uses `snake_case` for API methods (`chat_postMessage` not `chatPostMessage`, `views_open` not `viewsOpen`). The TS camelCase habit causes `AttributeError`. - **Flask vs FastAPI mismatch**: Using sync `App` with FastAPI's async handler or `AsyncApp` with Flask causes runtime errors. Match sync/async consistently. ## references - https://slack.dev/bolt-python/concepts - https://slack.dev/bolt-python/tutorial/getting-started - https://slack.dev/python-slack-sdk/web - https://github.com/slackapi/bolt-python ## instructions This expert covers Slack Bolt for Python — the Python equivalent of `@slack/bolt`. Use it when building Slack apps in Python (Tier 2 or standalone), translating TypeScript Bolt patterns to Python, or setting up Python web framework adapters (Flask, FastAPI, Django). All TS Bolt experts (`runtime.bolt-foundations-ts.md`, `runtime.ack-rules-ts.md`, etc.) provide the architectural patterns — this expert provides the Python API mappings. Pair with: `runtime.bolt-foundations-ts.md` for conceptual architecture (translate to Python). `bolt-oauth-distribution-ts.md` for OAuth concepts (translate to Python). `bridge/python-cross-platform.md` for unified Python server with both Slack and Teams. ## research Deep Research prompt: "Write a micro expert mapping Slack Bolt TypeScript patterns to Python equivalents using slack_bolt. Cover App initialization (sync vs async), decorator-based listener registration (@app.message, @app.command, @app.action, @app.shortcut, @app.view, @app.event), argument injection (ack, say, respond, client, body, context, logger), WebClient snake_case methods (chat_postMessage, views_open), Socket Mode setup (SocketModeHandler), web framework adapters (Flask SlackRequestHandler, FastAPI AsyncSlackRequestHandler), middleware patterns, regex matching with re.compile(), and key differences from TS (dict access, no types, sync/async split). Source from bolt-python source code and slack.dev docs." -
cli.app-management.md 7.9 KB
# cli.app-management ## purpose App lifecycle management (`slack app`), collaborator administration (`slack collaborator`), and workspace installation controls via the Slack CLI. ## rules 1. **`slack app install` installs the production app to a workspace.** After `slack deploy`, use this to install the app in additional workspaces. The app must be deployed before it can be installed. 2. **`slack app uninstall` removes the app from a workspace.** Revokes all tokens and permissions. Users lose access to the app's triggers, functions, and datastores in that workspace. 3. **`slack app delete` permanently removes the app.** Deletes the app registration from Slack's platform. This is irreversible — all triggers, datastores, and installations are lost. Requires confirmation. 4. **`slack app link` associates an existing app with a project.** If you have an app created outside the CLI (e.g., via api.slack.com), link it to your local project for CLI management. Updates `.slack/project.json`. 5. **`slack app unlink` disconnects an app from the project.** Removes the app-to-project mapping in `.slack/project.json`. The app still exists on Slack's platform — it's just no longer managed by this project directory. 6. **`slack app list` shows apps linked to this project.** Displays app IDs, workspace names, and deployment status (dev vs deployed). Useful for multi-workspace setups. 7. **`slack app settings` opens the app config in a browser.** Navigates to `api.slack.com/apps/<id>` for the app's web-based settings page. Useful for configuring features not exposed via CLI. 8. **`slack collaborator add` grants another developer access.** They can run CLI commands against the app (deploy, run, trigger management). Requires the collaborator's Slack email or user ID. 9. **`slack collaborator remove` revokes a collaborator's access.** They can no longer deploy, run, or manage the app via the CLI. 10. **`slack collaborator list` shows all collaborators.** Displays user IDs, emails, and permission levels for the app. 11. **`slack collaborator update` changes a collaborator's role.** Modify permissions (e.g., read-only vs full access) for an existing collaborator. 12. **Multi-workspace management is first-class.** One project can have multiple app installations across workspaces. Each workspace gets its own app ID in `.slack/project.json`. Use `--team` to target specific workspaces. ## patterns ### Pattern 1: App lifecycle workflow ```bash # Deploy the app first (creates/updates the production app) slack deploy # Install to additional workspaces slack app install --team T0SECOND_WS # List all linked apps and their workspaces slack app list # Output: # App ID Team Status # A01234 (dev) MyWorkspace (T001) Development # A05678 MyWorkspace (T001) Deployed # A09012 OtherWorkspace (T002) Deployed # View app settings in browser slack app settings --app A05678 # Uninstall from a workspace (keeps app, removes from workspace) slack app uninstall --team T0SECOND_WS # Permanently delete the app (irreversible!) slack app delete --app A05678 # CLI prompts: "Are you sure? This cannot be undone." → confirm ``` ### Pattern 2: Collaborator management ```bash # Add a collaborator by email slack collaborator add --email alice@example.com # Add by Slack user ID slack collaborator add --user U0ALICE # List all collaborators slack collaborator list # Output: # User ID Email Role # U0OWNER owner@example.com Owner # U0ALICE alice@example.com Collaborator # U0BOB bob@example.com Collaborator # Update collaborator role slack collaborator update --user U0ALICE --role viewer # Remove a collaborator slack collaborator remove --user U0ALICE ``` ### Pattern 3: Multi-workspace project setup ```bash # Authenticate with multiple workspaces slack auth login # Primary workspace slack auth login # Secondary workspace (repeat login) slack auth list # Shows both workspaces # Deploy to primary workspace slack deploy --team T0PRIMARY # Deploy to secondary workspace (creates a separate app registration) slack deploy --team T0SECONDARY # Create triggers per workspace (triggers are workspace-scoped) slack trigger create --trigger-def triggers/greeting.ts --team T0PRIMARY slack trigger create --trigger-def triggers/greeting.ts --team T0SECONDARY # Monitor activity per workspace slack activity --team T0PRIMARY slack activity --team T0SECONDARY # Project config tracks both cat .slack/project.json # Shows app IDs for both workspaces ``` ### Pattern 4: Linking and unlinking existing apps ```bash # Link an app created via api.slack.com to this project slack app link --app A0EXISTING --team T0WORKSPACE # Updates .slack/project.json with the app mapping # Unlink without deleting the app slack app unlink --app A0EXISTING # Removes from .slack/project.json, app still exists on Slack # Initialize a project and link in one step cd existing-code/ slack project init slack app link --app A0EXISTING --team T0WORKSPACE slack deploy # Now deploys to the linked app ``` ## pitfalls - **`slack app delete` is irreversible** — All data, triggers, and installations are permanently destroyed. Double-check the app ID before confirming. There is no undo. - **Confusing uninstall with delete** — `uninstall` removes from a workspace (reversible via re-install). `delete` destroys the app entirely. - **Deploying to wrong workspace** — Without `--team`, deploys to the default workspace. Always verify with `slack auth list` which workspace is active. - **Collaborators vs workspace members** — Collaborators are developers who can manage the app via CLI. Workspace members are end users who interact with the app. These are different permission systems. - **Unlinked app still exists** — `slack app unlink` only removes the local project mapping. The app continues running on Slack's platform. To fully remove, use `slack app delete`. - **Triggers are workspace-scoped** — When deploying to multiple workspaces, you must create triggers separately in each workspace. They don't automatically propagate. - **Stale `.slack/project.json` after manual changes** — If you delete an app via the web UI, the local project.json still references it. Run `slack app list` and `slack app unlink` to clean up. - **Collaborator email must be a Slack account** — The person must have a Slack account in the workspace. External emails without Slack accounts can't be added. ## references - [slack app reference](https://tools.slack.dev/cli/reference/slack_app/) - [App installation](https://tools.slack.dev/cli/guides/installing-an-app/) - [Collaborator management](https://tools.slack.dev/cli/reference/slack_collaborator/) - [Multi-workspace apps](https://tools.slack.dev/cli/guides/deploying-to-slack/) - [slack app link](https://tools.slack.dev/cli/reference/slack_app_link/) ## instructions Do a web search for: - "Slack CLI app install uninstall delete link management 2025" - "Slack CLI collaborator add remove permissions" - "Slack CLI multi-workspace deployment project.json" Pair with: - `cli.local-dev-deploy.md` — deploy before install, dev vs production apps - `cli.getting-started.md` — auth and project setup before app management - `cli.manifest-triggers.md` — triggers must be created per workspace after install - `bolt-oauth-distribution-ts.md` — OAuth distribution for multi-workspace Bolt apps ## research Deep Research prompt: "Write a micro expert on Slack CLI app management and collaboration. Cover app lifecycle (install, uninstall, delete, link, unlink, list, settings), collaborator management (add, remove, list, update), multi-workspace deployment (--team flag, separate app IDs per workspace, workspace-scoped triggers), project.json workspace mapping, app link/unlink for existing apps, and the difference between dev and deployed app installations. Include canonical patterns for: app lifecycle workflow, collaborator management, multi-workspace setup, linking existing apps." -
cli.datastore-env.md 8.6 KB
# cli.datastore-env ## purpose Datastore CRUD operations (`slack datastore`), environment variable management (`slack env`), and external authentication provider configuration (`slack external-auth`) via the Slack CLI. ## rules 1. **Datastores are typed key-value stores on Slack's platform.** Defined in the app manifest with a schema (attributes + primary key). Data persists across deployments. No external database needed for simple use cases. 2. **`slack datastore put` writes a single record.** Accepts `--datastore <name>` and `--item` with a JSON object matching the datastore schema. Overwrites the record if the primary key already exists. 3. **`slack datastore get` reads a single record.** Requires `--datastore <name>` and `--item` with the primary key field. Returns the full record as JSON. 4. **`slack datastore delete` removes a single record.** Requires `--datastore <name>` and `--item` with the primary key. The record is permanently removed. 5. **`slack datastore query` retrieves multiple records.** Supports `--expression` for filter expressions, `--expression-values` for parameter binding, and `--limit` for result caps. Uses DynamoDB-style filter syntax. 6. **Bulk operations exist for batch work.** `slack datastore bulk-put`, `slack datastore bulk-get`, `slack datastore bulk-delete` accept arrays of items. More efficient than looping single operations. 7. **`slack datastore count` returns the record count.** Useful for monitoring data growth and verifying bulk operations completed. 8. **`slack datastore update` modifies specific fields.** Updates individual attributes without replacing the entire record. Requires the primary key and the fields to update. 9. **`slack env add` sets an environment variable.** Environment variables are encrypted secrets stored on Slack's platform. Access them in functions via `env.get("VAR_NAME")`. Use for API keys, tokens, and configuration. 10. **`slack env list` shows all environment variables.** Displays variable names (not values) for the app. Values are encrypted and not retrievable via the CLI. 11. **`slack env remove` deletes an environment variable.** The variable is permanently removed and no longer available to functions. 12. **`slack external-auth` configures OAuth2 providers.** Set up external auth providers (Google, GitHub, etc.) that your app's functions can use to make authenticated API calls on behalf of users. ## patterns ### Pattern 1: Datastore definition in manifest ```typescript // datastores/users.ts — Deno datastore definition import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts"; export const UsersDatastore = DefineDatastore({ name: "users", primary_key: "user_id", attributes: { user_id: { type: Schema.slack.types.user_id }, display_name: { type: Schema.types.string }, score: { type: Schema.types.number }, joined_at: { type: Schema.types.string }, // ISO 8601 timestamp active: { type: Schema.types.boolean }, }, }); // Register in manifest.ts: // datastores: [UsersDatastore], // botScopes: ["datastore:read", "datastore:write"], ``` ### Pattern 2: Datastore CRUD via CLI ```bash # Put (create/upsert) a single record slack datastore put --datastore users \ --item '{"user_id": "U0123", "display_name": "Alice", "score": 42, "active": true}' # Get a single record by primary key slack datastore get --datastore users \ --item '{"user_id": "U0123"}' # Output: { "user_id": "U0123", "display_name": "Alice", "score": 42, "active": true } # Update specific fields slack datastore update --datastore users \ --item '{"user_id": "U0123", "score": 50}' # Delete a record slack datastore delete --datastore users \ --item '{"user_id": "U0123"}' # Query with filter expression slack datastore query --datastore users \ --expression "score > :min_score AND active = :is_active" \ --expression-values '{ ":min_score": 10, ":is_active": true }' \ --limit 20 # Count all records slack datastore count --datastore users # Output: 47 # Bulk put multiple records slack datastore bulk-put --datastore users \ --items '[ {"user_id": "U001", "display_name": "Alice", "score": 42, "active": true}, {"user_id": "U002", "display_name": "Bob", "score": 38, "active": true}, {"user_id": "U003", "display_name": "Charlie", "score": 55, "active": false} ]' # Bulk get multiple records slack datastore bulk-get --datastore users \ --items '[{"user_id": "U001"}, {"user_id": "U002"}]' # Bulk delete slack datastore bulk-delete --datastore users \ --items '[{"user_id": "U001"}, {"user_id": "U002"}]' ``` ### Pattern 3: Environment variables ```bash # Add an environment variable (encrypted on Slack's platform) slack env add MY_API_KEY # CLI prompts for the value interactively (not shown in terminal) # Or provide value inline (less secure — appears in shell history) slack env add MY_API_KEY --value "sk-abc123..." # List all env vars (names only — values are encrypted) slack env list # Output: # MY_API_KEY # DATABASE_URL # WEBHOOK_SECRET # Remove an env var slack env remove MY_API_KEY # Access in function code (Deno example): # const apiKey = env.get("MY_API_KEY"); ``` ### Pattern 4: External auth provider setup ```bash # Add an external OAuth2 provider slack external-auth add # Interactive wizard prompts for: # Provider name: google # Client ID: your-client-id # Client Secret: your-client-secret # Authorization URL: https://accounts.google.com/o/oauth2/v2/auth # Token URL: https://oauth2.googleapis.com/token # Scopes: email profile # List configured providers slack external-auth list # Remove a provider slack external-auth remove --provider google # Select a provider token for a user (used in trigger/workflow setup) slack external-auth select-auth --provider google ``` ## pitfalls - **Missing `datastore:read` / `datastore:write` scopes** — Datastores won't work without these bot scopes in the manifest. Add them before deploying. - **Query expression syntax errors** — Filter expressions use DynamoDB-style syntax. Attribute names are bare, values use `:placeholder` binding. Test queries in dev before deploying. - **Expecting SQL-like queries** — Datastores support basic filter expressions, not JOINs, GROUP BY, or complex aggregations. For complex queries, fetch data and process in function code. - **Bulk operations item limit** — Bulk commands have limits on items per request. For very large datasets, batch in chunks. - **Env var values not retrievable** — `slack env list` only shows names. You cannot read back the stored value. Keep a local record of what you set. - **Env vars scoped to deployment target** — Dev app (`slack run`) and deployed app (`slack deploy`) may use different env var stores. Set vars for both if needed. - **Setting env vars via `--value` flag** — The value appears in shell history. Prefer the interactive prompt for secrets. - **Confusing datastore put with update** — `put` replaces the entire record. `update` modifies specific fields. Use `update` to change one attribute without resending the full object. ## references - [Datastores overview](https://tools.slack.dev/cli/guides/datastores/) - [slack datastore reference](https://tools.slack.dev/cli/reference/slack_datastore/) - [Environment variables](https://tools.slack.dev/cli/guides/environment-variables/) - [slack env reference](https://tools.slack.dev/cli/reference/slack_env/) - [External auth providers](https://tools.slack.dev/cli/guides/external-auth/) - [Query expressions](https://api.slack.com/automation/datastores/query-expressions) ## instructions Do a web search for: - "Slack CLI datastore put get query bulk operations 2025" - "Slack CLI env add environment variables encrypted" - "Slack CLI external-auth OAuth2 provider configuration" Pair with: - `cli.manifest-triggers.md` — datastores are declared in the manifest - `cli.local-dev-deploy.md` — env vars apply to deployed apps - `cli.getting-started.md` — project must be set up before using datastores - `runtime.bolt-foundations-ts.md` — accessing datastores and env vars in function code ## research Deep Research prompt: "Write a micro expert on Slack CLI datastore and environment management. Cover datastore CRUD (put, get, delete, update, query, count), bulk operations (bulk-put, bulk-get, bulk-delete), query expression syntax (DynamoDB-style filters), datastore definition in manifest.ts (DefineDatastore, Schema types, primary_key), environment variables (env add/list/remove, encrypted storage, accessing in functions), external auth provider configuration (OAuth2 setup, provider management). Include canonical patterns for: datastore definition, full CRUD operations, query with filter expressions, env var workflow, external auth setup." -
cli.getting-started.md 8 KB
# cli.getting-started ## purpose Slack CLI installation, authentication, project scaffolding, system diagnostics, and the `.slack/` configuration directory. ## rules 1. **Install the Slack CLI from the official release.** On macOS use Homebrew (`brew install slack-cli`), on Windows use the PowerShell installer, on Linux download the tarball from GitHub releases. Verify with `slack version`. 2. **`slack auth login` authenticates via browser OAuth.** Opens a browser to the Slack OAuth consent screen. After approval, the CLI stores tokens locally in `~/.slack/`. You can authenticate with multiple workspaces. 3. **`slack auth list` shows authenticated workspaces.** Lists all logged-in accounts with workspace names and team IDs. The active workspace is marked. Use `--team` flag on any command to target a specific workspace. 4. **`slack create` scaffolds a new project from a template.** Alias for `slack project create`. Launches an interactive wizard to pick a template (blank, AI agent, sample app). Supports `--template <url>` for custom templates and Deno, Node.js, or Python runtimes. 5. **`slack project init` initializes an existing directory.** Links an existing codebase to the Slack platform by creating `.slack/` config files. Use when you already have app code. 6. **`slack project samples` lists available templates.** Shows the full catalog of sample templates from the Slack sample repository. Use `slack create <name> --template <url>` to clone one. 7. **`slack doctor` diagnoses system setup.** Checks installed runtimes (Deno, Node, Python), CLI version, authentication status, and project configuration. Run this first when debugging setup issues. 8. **The `.slack/` directory holds project config.** Created at the project root. Contains `project.json` (app IDs, team IDs, runtime info) and `cli-config.json` (SDK hooks). This directory is auto-generated — don't manually create it. 9. **`project.json` maps environments to app IDs.** Each workspace gets its own app registration. The CLI manages this mapping automatically when you `run` or `deploy` to different workspaces. 10. **`cli-config.json` defines SDK hooks.** Hooks are shell commands the CLI executes for lifecycle events: `get-manifest`, `build`, `start`, `deploy`, `validate`. The SDK scaffolding sets these up — you rarely edit them directly. 11. **System-level config lives in `~/.slack/`.** Contains `apps.json` (auth tokens), `global-config.json` (user preferences like trust settings), and `system-id` (unique machine identifier for telemetry). 12. **`slack upgrade` updates the CLI to the latest version.** The CLI also checks for updates in the background on every run. Suppress with `SLACK_SKIP_UPDATE=1` or `--skip-update`. 13. **Global flags apply to all commands.** Key flags: `--token` (pre-provide auth token), `--team` (target workspace), `--app` (target app ID), `--no-color` (disable colors), `--debug` (verbose logging), `--force` (bypass confirmations). ## patterns ### Pattern 1: First-time setup workflow ```bash # Step 1: Install the CLI # macOS: brew install slack-cli # Windows (PowerShell as admin): # irm https://downloads.slack-edge.com/slack-cli/install-windows.ps1 | iex # Linux: # curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash # Step 2: Verify installation slack version # Step 3: Authenticate with your workspace slack auth login # → Opens browser → Approve OAuth → Done # Step 4: Verify authentication slack auth list # Shows: workspace name, team ID, user # Step 5: Check system health slack doctor # Verifies: CLI version, auth, runtimes (Deno/Node/Python) ``` ### Pattern 2: Create a new project ```bash # Interactive wizard — pick template and runtime slack create my-bot # From a specific template URL slack create my-bot --template https://github.com/slack-samples/deno-hello-world # Create an AI agent app slack project create agent my-agent # Initialize existing code as a Slack project cd existing-app/ slack project init # List available sample templates slack project samples ``` ### Pattern 3: Project directory structure after scaffolding ``` my-bot/ ├── .slack/ │ ├── project.json # App IDs, team IDs, runtime config │ └── cli-config.json # SDK hook definitions ├── manifest.ts # App manifest (Deno) or slack.json (Node/Python) ├── functions/ # Custom function definitions ├── workflows/ # Workflow definitions ├── triggers/ # Trigger definitions ├── datastores/ # Datastore schemas ├── deno.jsonc / package.json / requirements.txt # Runtime deps └── README.md ``` ```json // .slack/project.json — auto-managed by the CLI { "app_id": "A0123456789", "team_id": "T0123456789", "runtime": "deno" } ``` ```json // .slack/cli-config.json — SDK hooks { "hooks": { "get-manifest": "deno run -q --allow-read --allow-net jsr:@anthropic/slack-cli-hooks/mod.ts --manifest", "build": "deno run -q --allow-read --allow-net jsr:@anthropic/slack-cli-hooks/mod.ts --build", "start": "deno run -q --allow-read --allow-net jsr:@anthropic/slack-cli-hooks/mod.ts --start", "deploy": "deno run -q --allow-read --allow-net jsr:@anthropic/slack-cli-hooks/mod.ts --deploy" } } ``` ## pitfalls - **Skipping `slack doctor` when setup fails** — It catches missing runtimes, stale auth, and config issues. Always run it first. - **Manually creating `.slack/` files** — The CLI generates and manages these. Manual edits can corrupt the project state. Use CLI commands instead. - **Forgetting `--team` with multiple workspaces** — Without `--team`, the CLI uses the default workspace. When working with multiple, always specify the target. - **Using `slack login` instead of `slack auth login`** — `login` is an alias that works, but be aware the full command is `auth login` when reading docs. - **Running `slack create` inside an existing project** — Creates a nested project. Run it in the parent directory or use `slack project init` for existing code. - **Stale auth tokens** — Tokens expire. If commands fail with auth errors, run `slack auth login` again. Use `slack auth list` to check status. - **Corporate proxy blocking OAuth flow** — The browser-based login requires HTTPS access to `slack.com`. If blocked, use `slack auth login --token xoxp-...` with a manually obtained token. - **CI/CD without browser access** — Use ticket-based login: `slack auth login --no-prompt --ticket <T> --challenge <C>` for headless environments. ## references - [Slack CLI quickstart](https://tools.slack.dev/cli/getting-started/) - [Install the Slack CLI](https://tools.slack.dev/cli/install/) - [Authentication](https://tools.slack.dev/cli/authorization/) - [slack create reference](https://tools.slack.dev/cli/reference/slack_create/) - [Project configuration](https://tools.slack.dev/cli/guides/project-structure/) - [Slack CLI GitHub repo](https://github.com/slackapi/slack-cli) ## instructions Do a web search for: - "Slack CLI install auth login quickstart 2025" - "slack create project template Deno Node Python" - "Slack CLI .slack project.json cli-config.json hooks" Pair with: - `cli.local-dev-deploy.md` — local development and deployment after project creation - `cli.manifest-triggers.md` — manifest configuration for the scaffolded project - `runtime.bolt-foundations-ts.md` — Bolt SDK patterns used within CLI-managed projects - `runtime.socket-mode-ts.md` — Socket Mode setup for local development ## research Deep Research prompt: "Write a micro expert on Slack CLI getting started (installation, authentication, project scaffolding). Cover CLI installation methods (Homebrew, PowerShell, tarball), slack auth login/logout/list, slack create and project create/init/samples, slack doctor diagnostics, .slack/ config directory (project.json, cli-config.json), SDK hooks system, global flags (--token, --team, --app, --debug), system-level config (~/.slack/), and slack upgrade. Include canonical patterns for: first-time setup workflow, project creation variants, scaffolded directory structure." -
cli.local-dev-deploy.md 8.4 KB
# cli.local-dev-deploy ## purpose Local development with `slack run`, production deployment with `slack deploy`, activity monitoring, and the SDK hooks system that powers both workflows. ## rules 1. **`slack run` starts a local development server.** Installs a development version of the app to the workspace, starts the local bot process, and tunnels traffic from Slack to your machine. Code changes trigger automatic rebuilds via file watching. 2. **`slack run` uses Socket Mode under the hood.** The CLI creates a WebSocket tunnel — no public URL or ngrok needed. Your bot receives events over the socket connection and responds locally. 3. **The dev app is separate from the deployed app.** `slack run` creates a development app installation (suffixed with `(dev)` in Slack). It has its own app ID, separate from the production `slack deploy` installation. 4. **`--cleanup` uninstalls the dev app on exit.** By default the dev app persists between sessions. Use `slack run --cleanup` to automatically uninstall when you stop the dev server. Useful for keeping workspaces tidy. 5. **`slack deploy` pushes code to Slack's hosted infrastructure.** Packages your app code, uploads it to Slack's platform, and installs or updates the production app in the target workspace. The app runs on Slack's managed Deno/Node/Python runtime. 6. **Always validate before deploying.** `slack deploy` runs manifest validation automatically, but run `slack manifest validate` separately during development to catch issues early. 7. **`slack activity` streams real-time app logs.** Shows function executions, errors, and system events. Use `--level debug` for verbose output, `--level info` for standard, or `--level error` for errors only. Essential for debugging deployed apps. 8. **Activity log levels control verbosity.** Levels: `debug` (all output including SDK internals), `info` (function starts/completions), `warn` (non-fatal issues), `error` (failures only). Default is `info`. 9. **Hooks power the CLI's lifecycle commands.** The `.slack/cli-config.json` defines hooks (`get-manifest`, `build`, `start`, `deploy`, `validate`) that the CLI calls during `run` and `deploy`. The SDK sets these up — you typically don't edit them. 10. **`slack run` supports `--activity-level`** to control log verbosity during local development. Combines the run server with activity monitoring in one terminal. 11. **Deployment targets the active workspace.** Use `--team` to specify which workspace receives the deployment. Without it, the CLI uses the default authenticated workspace. 12. **`slack deploy` is idempotent for updates.** Running it again re-deploys with the latest code. The app ID stays the same — existing triggers and installations are preserved. ## patterns ### Pattern 1: Local development workflow ```bash # Start local dev server (creates dev app, watches for changes) slack run # With activity level for debugging slack run --activity-level debug # Auto-cleanup dev app when stopping (Ctrl+C) slack run --cleanup # Target a specific workspace slack run --team T0123456789 # Typical terminal output: # ⚡ App is running in development mode # Connected, awaiting events # my-bot (dev) A0123456789 T0123456789 # SDK: deno-slack-sdk 2.x # Visit https://app.slack.com/client/T0123456789 to use your app ``` ### Pattern 2: Production deployment ```bash # Deploy to Slack's hosted platform slack deploy # Target a specific workspace slack deploy --team T0123456789 # Typical deployment output: # 📦 Packaging my-bot... # 🔐 Validating manifest... # ✅ my-bot deployed to workspace MyWorkspace # App ID: A0123456789 # Dashboard: https://api.slack.com/apps/A0123456789 # After deploying, create triggers for users to interact with the app slack trigger create --trigger-def triggers/greeting_trigger.ts ``` ### Pattern 3: Activity monitoring ```bash # Stream live activity logs for deployed app slack activity # Filter by log level slack activity --level debug # Everything slack activity --level info # Function starts/completions slack activity --level error # Errors only # Target a specific app slack activity --app A0123456789 # Target a specific workspace slack activity --team T0123456789 # Tail mode (follows new logs, Ctrl+C to stop) # This is the default behavior — activity streams continuously ``` ### Pattern 4: Hooks system (cli-config.json) ```json // .slack/cli-config.json — hooks executed by the CLI { "hooks": { "get-manifest": "deno run -q --config=deno.jsonc --allow-read --allow-net jsr:@anthropic/slack-cli-hooks/mod.ts --manifest", "build": "deno run -q --config=deno.jsonc --allow-read --allow-net jsr:@anthropic/slack-cli-hooks/mod.ts --build", "start": "deno run -q --config=deno.jsonc --allow-read --allow-net jsr:@anthropic/slack-cli-hooks/mod.ts --start", "deploy": "deno run -q --config=deno.jsonc --allow-read --allow-net jsr:@anthropic/slack-cli-hooks/mod.ts --deploy", "validate": "deno run -q --config=deno.jsonc --allow-read --allow-net jsr:@anthropic/slack-cli-hooks/mod.ts --validate" } } ``` ``` Hook execution flow: slack run: 1. get-manifest → reads manifest.ts, returns JSON manifest 2. build → compiles/bundles the app code 3. start → starts the local dev server (file change detected → re-runs build + restart) slack deploy: 1. get-manifest → reads manifest.ts, returns JSON manifest 2. validate → checks manifest against Slack schema 3. build → compiles/bundles the app code 4. deploy → packages and uploads to Slack platform ``` ## pitfalls - **Forgetting to create triggers after deploy** — `slack deploy` installs the app but users can't interact with it until you create triggers (`slack trigger create`). The dev app from `slack run` may auto-create triggers, but production requires explicit setup. - **Confusing dev app with deployed app** — `slack run` and `slack deploy` create separate app installations. Triggers, datastores, and configs are independent between them. - **Running `slack run` without auth** — The CLI needs an active login. Run `slack auth login` first if you see auth errors. - **Deploying untested code** — Always `slack run` and test locally before `slack deploy`. Deployed apps are immediately live. - **Ignoring activity logs** — Deployed functions fail silently from the user's perspective. Monitor `slack activity` after deploying to catch runtime errors. - **Not specifying `--team` with multiple workspaces** — Deploys to the default workspace, which may not be your intended target. - **Editing hooks manually** — The SDK scaffolding sets up hooks correctly. Manual edits to `.slack/cli-config.json` can break the build/deploy pipeline. - **Expecting ngrok for local dev** — `slack run` uses Socket Mode (WebSocket), not HTTP tunneling. No public URL is needed or created. ## references - [slack run reference](https://tools.slack.dev/cli/reference/slack_run/) - [slack deploy reference](https://tools.slack.dev/cli/reference/slack_deploy/) - [slack activity reference](https://tools.slack.dev/cli/reference/slack_activity/) - [Local development guide](https://tools.slack.dev/cli/guides/developing-locally/) - [Deploying to Slack](https://tools.slack.dev/cli/guides/deploying-to-slack/) - [Hooks system](https://tools.slack.dev/cli/guides/hooks/) ## instructions Do a web search for: - "Slack CLI slack run local development Socket Mode 2025" - "Slack CLI slack deploy hosted platform production" - "Slack CLI activity logs monitoring debugging" Pair with: - `cli.getting-started.md` — project setup before running or deploying - `cli.manifest-triggers.md` — triggers must be created after deployment - `runtime.socket-mode-ts.md` — Socket Mode concepts used by `slack run` - `cli.app-management.md` — app install/uninstall lifecycle ## research Deep Research prompt: "Write a micro expert on Slack CLI local development and deployment. Cover slack run (local dev server, Socket Mode tunnel, file watching, hot reload, --cleanup, --activity-level), slack deploy (packaging, uploading to Slack platform, idempotent updates), slack activity (log streaming, --level debug/info/warn/error), the hooks system in cli-config.json (get-manifest, build, start, deploy, validate), dev app vs deployed app separation, and deployment workflow (validate → build → deploy → create triggers). Include canonical patterns for: local dev workflow, production deployment, activity monitoring, hooks configuration." -
cli.manifest-triggers.md 9.9 KB
# cli.manifest-triggers ## purpose App manifest management (`slack manifest`), workflow trigger CRUD (`slack trigger`), and custom function distribution (`slack function`) via the Slack CLI. ## rules 1. **The manifest defines the entire app surface.** It declares functions, workflows, triggers, datastores, bot user, OAuth scopes, slash commands, shortcuts, and outgoing domains. Written in TypeScript (`manifest.ts` for Deno) or JSON (`slack.json` for Node/Python). 2. **`slack manifest validate` checks the manifest against Slack's schema.** Catches missing fields, invalid types, scope conflicts, and structural errors. Run early and often during development. 3. **`slack manifest info` shows the remote manifest.** Displays the manifest as stored on Slack's servers for the deployed app. Useful for comparing local vs remote state. 4. **Triggers connect workflows to user actions.** A trigger defines how users invoke a workflow — via shortcut, slash command, event, schedule, or webhook. Without triggers, deployed workflows are unreachable. 5. **`slack trigger create` registers a new trigger.** Accepts a `--trigger-def` flag pointing to a trigger definition file (TypeScript or JSON). The definition specifies the trigger type, linked workflow, and inputs. 6. **Trigger types: shortcut, event, scheduled, webhook.** Shortcut triggers appear in the Slack UI shortcuts menu. Event triggers fire on platform events. Scheduled triggers run on a cron or at a specific time. Webhook triggers expose an HTTP URL. 7. **`slack trigger list` shows all triggers for the app.** Displays trigger IDs, types, names, and linked workflows. Use `--team` to filter by workspace. 8. **`slack trigger update` modifies an existing trigger.** Use `--trigger-id` to target and `--trigger-def` to provide the updated definition. The trigger ID is preserved. 9. **`slack trigger delete` removes a trigger.** Requires `--trigger-id`. The linked workflow remains deployed — only the entry point is removed. 10. **`slack trigger info` shows trigger details.** Displays the full trigger configuration including inputs, workflow reference, and access permissions. 11. **`slack trigger access` controls who can invoke a trigger.** Set access to everyone in the workspace, specific users, specific channels, or specific orgs. Default varies by trigger type. 12. **`slack function distribute` shares custom functions.** Makes functions from your app available to other apps and Workflow Builder. Distributed functions appear in the workspace's function catalog. 13. **Trigger definitions are separate files.** Store in a `triggers/` directory. Each file exports a trigger definition object with `type`, `name`, `workflow`, and `inputs` fields. ## patterns ### Pattern 1: Manifest structure (Deno TypeScript) ```typescript // manifest.ts — Deno Slack app manifest import { Manifest } from "deno-slack-sdk/mod.ts"; import { GreetingWorkflow } from "./workflows/greeting.ts"; import { GreetingFunction } from "./functions/greeting.ts"; import { UsersDatastore } from "./datastores/users.ts"; export default Manifest({ name: "my-bot", description: "A helpful Slack bot", icon: "assets/icon.png", functions: [GreetingFunction], workflows: [GreetingWorkflow], datastores: [UsersDatastore], outgoingDomains: ["api.example.com"], // External API allowlist botScopes: [ "commands", "chat:write", "chat:write.public", "channels:read", "datastore:read", "datastore:write", ], }); ``` ### Pattern 2: Trigger CRUD workflow ```bash # Create a trigger from a definition file slack trigger create --trigger-def triggers/greeting_trigger.ts # Output: ⚡ Trigger created # Trigger ID: Ft0123456789 # Type: shortcut # Name: Send Greeting # Shortcut URL: https://slack.com/shortcuts/Ft0123456789/... # List all triggers for the app slack trigger list # Shows table of: ID, Type, Name, Workflow # Get details on a specific trigger slack trigger info --trigger-id Ft0123456789 # Update a trigger definition slack trigger update --trigger-id Ft0123456789 \ --trigger-def triggers/greeting_trigger_v2.ts # Set trigger access permissions slack trigger access --trigger-id Ft0123456789 \ --everyone # All workspace members # Or: --users U001,U002 # Specific users # Or: --channels C001,C002 # Specific channels # Delete a trigger slack trigger delete --trigger-id Ft0123456789 ``` ### Pattern 3: Trigger definition files ```typescript // triggers/greeting_trigger.ts — shortcut trigger import { Trigger } from "deno-slack-api/types.ts"; import { GreetingWorkflow } from "../workflows/greeting.ts"; import { TriggerTypes, TriggerContextData } from "deno-slack-api/mod.ts"; const greetingTrigger: Trigger<typeof GreetingWorkflow.definition> = { type: TriggerTypes.Shortcut, name: "Send Greeting", description: "Send a greeting to a channel", workflow: `#/workflows/${GreetingWorkflow.definition.callback_id}`, inputs: { // Map trigger context to workflow inputs interactivity: { value: TriggerContextData.Shortcut.interactivity }, channel: { value: TriggerContextData.Shortcut.channel_id }, user: { value: TriggerContextData.Shortcut.user_id }, }, }; export default greetingTrigger; ``` ```typescript // triggers/scheduled_trigger.ts — scheduled trigger import { Trigger } from "deno-slack-api/types.ts"; import { DailyReportWorkflow } from "../workflows/daily_report.ts"; import { TriggerTypes } from "deno-slack-api/mod.ts"; const scheduledTrigger: Trigger<typeof DailyReportWorkflow.definition> = { type: TriggerTypes.Scheduled, name: "Daily Report", workflow: `#/workflows/${DailyReportWorkflow.definition.callback_id}`, inputs: {}, schedule: { // Run every weekday at 9 AM UTC start_time: "2024-01-01T09:00:00Z", frequency: { type: "weekly", on_days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] }, }, }; export default scheduledTrigger; ``` ```typescript // triggers/webhook_trigger.ts — incoming webhook trigger import { Trigger } from "deno-slack-api/types.ts"; import { IngestWorkflow } from "../workflows/ingest.ts"; import { TriggerTypes } from "deno-slack-api/mod.ts"; const webhookTrigger: Trigger<typeof IngestWorkflow.definition> = { type: TriggerTypes.Webhook, name: "External Webhook", workflow: `#/workflows/${IngestWorkflow.definition.callback_id}`, inputs: { // Webhook body fields mapped to workflow inputs payload: { value: "{{data.payload}}" }, }, }; export default webhookTrigger; ``` ### Pattern 4: Manifest validation and function distribution ```bash # Validate manifest before deploying slack manifest validate # Output: ✅ Manifest is valid # View the remote (deployed) manifest slack manifest info --app A0123456789 # Distribute a custom function to the workspace slack function distribute --name my_custom_function # List distributed functions slack function distribute --list ``` ## pitfalls - **Deploying without creating triggers** — Workflows exist on the platform but are unreachable. Always `slack trigger create` after `slack deploy`. - **Wrong trigger type for use case** — Shortcut triggers need user interaction. Event triggers need specific event subscriptions. Scheduled triggers need valid cron/schedule. Webhook triggers expose public URLs. - **Forgetting to update triggers after workflow changes** — If workflow inputs change, existing triggers may break. Update trigger definitions to match. - **Missing `outgoingDomains` in manifest** — External HTTP calls fail silently without the domain in the allowlist. Add every external API domain. - **Missing `botScopes`** — The app can't perform actions without the right scopes. `chat:write` for messaging, `datastore:read`/`datastore:write` for datastores, etc. - **Trigger access too restrictive** — By default, trigger access may be limited. Use `slack trigger access --everyone` for workspace-wide shortcuts. - **Deleting a trigger doesn't delete the workflow** — Triggers are entry points. Removing a trigger just removes the invocation path. The workflow and its functions remain deployed. - **Editing remote manifest directly** — Changes to the remote manifest are overwritten on the next `slack deploy`. Always edit the local `manifest.ts` / `slack.json`. ## references - [App manifest reference](https://tools.slack.dev/cli/guides/creating-an-app-manifest/) - [Trigger types](https://tools.slack.dev/cli/guides/triggers/) - [slack trigger reference](https://tools.slack.dev/cli/reference/slack_trigger/) - [slack manifest validate](https://tools.slack.dev/cli/reference/slack_manifest_validate/) - [Custom functions](https://tools.slack.dev/cli/guides/creating-custom-functions/) - [Function distribution](https://tools.slack.dev/cli/reference/slack_function/) ## instructions Do a web search for: - "Slack CLI manifest.ts validate trigger create shortcut event scheduled 2025" - "Slack CLI trigger types definition files workflow inputs" - "Slack CLI function distribute custom functions Workflow Builder" Pair with: - `cli.getting-started.md` — project scaffolding creates the manifest - `cli.local-dev-deploy.md` — triggers must be created after deploy - `cli.datastore-env.md` — datastores declared in manifest - `runtime.bolt-foundations-ts.md` — Bolt SDK patterns for function implementations ## research Deep Research prompt: "Write a micro expert on Slack CLI manifest management, triggers, and function distribution. Cover manifest structure (manifest.ts for Deno, slack.json for Node/Python, functions/workflows/datastores/botScopes/outgoingDomains), slack manifest validate/info commands, trigger CRUD (create/list/update/delete/info/access), trigger types (shortcut, event, scheduled, webhook), trigger definition file format, trigger input mapping from context data, function distribution to Workflow Builder. Include canonical patterns for: manifest.ts anatomy, trigger CRUD workflow, trigger definition files for each type, manifest validation." -
index.md 8.9 KB
# slack-router ## purpose Route Slack app tasks to the minimal set of micro-expert files. Read only the clusters that match the user's request. ## task clusters ### Bolt Foundations When: setting up a Slack Bolt app, `App()` constructor, listeners, middleware, event subscriptions Read: - `runtime.bolt-foundations-ts.md` - `runtime.ack-rules-ts.md` (ack rules are integral to every Bolt handler) ### Ack Rules When: `ack()` patterns, response timing, acknowledgement requirements, 3-second rule Read: - `runtime.ack-rules-ts.md` Depends on: `runtime.bolt-foundations-ts.md` (ack applies within Bolt handler types) ### Slash Commands When: slash commands, `/command`, command registration, command response Read: - `runtime.slash-commands-ts.md` - `runtime.ack-rules-ts.md` (commands require ack within 3 seconds) - `ui.block-kit-ts.md` (only if command opens a modal or sends Block Kit message) ### Block Kit UI When: Block Kit, blocks, surfaces, modals, home tab, interactive components Read: - `ui.block-kit-ts.md` - `runtime.ack-rules-ts.md` (interactive elements require ack in action/view handlers) Depends on: `runtime.bolt-foundations-ts.md` (action/view handlers registered on the App) ### Events API When: `app.event()`, event subscriptions, event types, `app_mention`, `reaction_added`, `team_join`, `app_home_opened`, `member_joined_channel`, retry handling, `context.retryNum`, `ignoreSelf`, `directMention` Read: - `bolt-events-ts.md` - `runtime.bolt-foundations-ts.md` (handler registration context) ### Assistant Container When: Slack Assistant, assistant panel, `threadStarted`, `userMessage`, `threadContextChanged`, `setStatus`, `setSuggestedPrompts`, `setTitle`, `getThreadContext`, `AssistantThreadContextStore`, `app.assistant()` Read: - `bolt-assistant-ts.md` - `runtime.bolt-foundations-ts.md` (App setup for assistant registration) ### OAuth & Distribution When: OAuth, multi-workspace, `InstallProvider`, `InstallationStore`, `authorize`, `clientId`, `clientSecret`, token storage, app distribution, `stateSecret`, Enterprise Grid, `tokens_revoked`, `app_uninstalled` Read: - `bolt-oauth-distribution-ts.md` - `runtime.bolt-foundations-ts.md` (App constructor OAuth options) ### Socket Mode When: Socket Mode, `socketMode`, `appToken`, `xapp-`, WebSocket, local development, no public URL, `SocketModeReceiver`, `@slack/socket-mode`, connection lifecycle, reconnect, `connections:write` Read: - `runtime.socket-mode-ts.md` - `runtime.bolt-foundations-ts.md` (App constructor setup) Depends on: `runtime.bolt-foundations-ts.md` (App constructor options) ### Web API & Proactive Messaging When: `client.chat.postMessage`, `chat.update`, `chat.delete`, proactive messages, `chat.postEphemeral`, ephemeral, scheduled messages, `chat.scheduleMessage`, `users.info`, `users.lookupByEmail`, `conversations.list`, `conversations.history`, `filesUploadV2`, file upload, say vs respond vs client, pagination, cursor, rate limits Read: - `web-api-proactive-ts.md` - `runtime.bolt-foundations-ts.md` (App setup and client initialization) Depends on: `runtime.bolt-foundations-ts.md` (client property and token management) ### Shortcuts When: shortcuts, global shortcut, message shortcut, `app.shortcut()`, `message_action`, compose menu, message context menu, `callback_id`, shortcut payload Read: - `runtime.shortcuts-ts.md` - `runtime.ack-rules-ts.md` (shortcuts require ack within 3 seconds) - `ui.modals-lifecycle-ts.md` (shortcuts typically open modals via trigger_id) Depends on: `runtime.bolt-foundations-ts.md` (handler registration context) ### Modal Lifecycle When: modals, `views.open`, `views.update`, `views.push`, `view_submission`, `view_closed`, `app.view()`, `response_action`, `private_metadata`, multi-step modal, modal validation, modal stack, `notify_on_close`, `trigger_id` Read: - `ui.modals-lifecycle-ts.md` - `runtime.ack-rules-ts.md` (view submission ack patterns) - `ui.block-kit-ts.md` (block layout for modal content) Depends on: `runtime.bolt-foundations-ts.md` (App setup for view handler registration) ### Bolt for Python When: Python, `slack_bolt`, `AsyncApp`, Flask adapter, FastAPI adapter, Django adapter, `SocketModeHandler`, Python Slack SDK, `@app.message`, `@app.command`, `@app.action`, `@app.event`, `@app.view`, `@app.shortcut`, `client.chat_postMessage`, `client.views_open`, argument injection, decorator listeners Read: - `bolt-python.md` Note: All TS experts provide architectural patterns. This expert provides Python API mappings. Load the relevant TS expert for concepts, then this expert for Python translation. ### Bolt for Java When: Java, `slack-bolt-java`, `com.slack.api.bolt`, Spring Boot, `SlackAppServlet`, `AppConfig.builder()`, `MethodsClient`, `ctx.client()`, `ctx.ack()`, `ctx.say()`, request configurator lambdas, `app.command`, `app.event`, `app.blockAction`, `app.viewSubmission`, `app.globalShortcut`, `SocketModeApp` Read: - `bolt-java.md` Note: Java has SDK support for Slack only (Tier 3). For the Teams side, route to `../bridge/rest-only-integration-ts.md`. ### CLI: Getting Started When: Slack CLI, `slack` command, install CLI, `slack auth login`, `slack auth list`, `slack create`, `slack project create`, `slack project init`, `slack project samples`, `slack doctor`, `.slack/` config, `project.json`, `cli-config.json`, hooks, `slack upgrade`, `slack version`, CLI setup Read: - `cli.getting-started.md` ### CLI: Local Dev & Deploy When: `slack run`, `slack deploy`, `slack activity`, local development, deploy to Slack, hosted platform, dev server, activity logs, hot reload, file watching, Socket Mode dev, `--cleanup`, `--activity-level`, hooks system Read: - `cli.local-dev-deploy.md` - `cli.getting-started.md` (only if project not yet set up) Depends on: `cli.getting-started.md` (project must exist before run/deploy) ### CLI: Manifest & Triggers When: `slack manifest`, `slack manifest validate`, `slack manifest info`, `manifest.ts`, `slack.json`, `slack trigger`, trigger create, trigger list, trigger update, trigger delete, trigger access, trigger types, shortcut trigger, event trigger, scheduled trigger, webhook trigger, `slack function`, function distribute, workflow trigger, trigger definition file Read: - `cli.manifest-triggers.md` Depends on: `cli.getting-started.md` (project must exist before manifest/trigger ops) ### CLI: Datastore & Environment When: `slack datastore`, datastore put, datastore get, datastore delete, datastore query, datastore count, bulk-put, bulk-get, bulk-delete, datastore update, `slack env`, `slack env add`, environment variable, `slack external-auth`, external OAuth provider, DefineDatastore Read: - `cli.datastore-env.md` - `cli.manifest-triggers.md` (datastores must be declared in manifest) Depends on: `cli.local-dev-deploy.md` (app must be deployed before datastore/env ops) ### Slack Automations Platform When: Slack next-gen platform, DefineFunction, DefineWorkflow, DefineDatastore, Slack triggers, Slack hosted functions, Deno, Workflow Builder, custom functions, slack automation, competitive analysis Read: - `workflow.slack-automations-ts.md` - `cli.manifest-triggers.md` (trigger definitions) - `cli.datastore-env.md` (datastore operations) ### CLI: App Management When: `slack app install`, `slack app uninstall`, `slack app delete`, `slack app link`, `slack app unlink`, `slack app list`, `slack app settings`, `slack collaborator`, collaborator add, collaborator remove, collaborator list, multi-workspace, workspace management Read: - `cli.app-management.md` Depends on: `cli.local-dev-deploy.md` (app must be deployed before install/collaborator ops) ## cross-platform bridging If the developer wants to **add Teams support** to an existing Slack app, route to `../bridge/index.md` for cross-platform bridging experts. The bridge domain covers Slack↔Teams feature mapping, UI conversion, identity bridging, and infrastructure migration. ## combining rule If a request spans multiple clusters (e.g., "add a slash command that opens a Block Kit modal"), read files from **every** matching cluster. Avoid duplicates. ## file inventory `bolt-assistant-ts.md` | `bolt-events-ts.md` | `bolt-java.md` | `bolt-oauth-distribution-ts.md` | `bolt-python.md` | `cli.app-management.md` | `cli.datastore-env.md` | `cli.getting-started.md` | `cli.local-dev-deploy.md` | `cli.manifest-triggers.md` | `runtime.ack-rules-ts.md` | `runtime.bolt-foundations-ts.md` | `runtime.shortcuts-ts.md` | `runtime.slash-commands-ts.md` | `runtime.socket-mode-ts.md` | `ui.block-kit-ts.md` | `ui.modals-lifecycle-ts.md` | `web-api-proactive-ts.md` | `workflow.slack-automations-ts.md` <!-- Updated 2026-02-27: Added bolt-assistant-ts (Assistant container), bolt-events-ts (Events API), bolt-oauth-distribution-ts (OAuth/multi-workspace) experts based on @slack/bolt v4.6.0 source --> <!-- Updated 2026-03-05: Added workflow.slack-automations-ts for Slack next-gen platform (functions, workflows, triggers, datastores) --> <!-- Updated 2026-03-01: Added 5 Slack CLI experts (getting-started, local-dev-deploy, manifest-triggers, datastore-env, app-management) based on slack-cli Go source --> -
runtime.ack-rules-ts.md 9.8 KB
# runtime.ack-rules-ts ## purpose Acknowledgement (ack) semantics, timing constraints, and async patterns for Slack interactions in Bolt TypeScript apps. ## rules 1. Slack requires every interactive request (commands, actions, view submissions, shortcuts, options) to be acknowledged within **3 seconds**. Failure to `ack()` in time causes the user to see a "dispatch_failed" error or a timeout spinner. [api.slack.com/interactivity/handling#acknowledgment_response](https://api.slack.com/interactivity/handling#acknowledgment_response) 2. **Commands** (`app.command`): must call `await ack()`. Optionally pass a string or blocks payload to `ack()` to send an immediate ephemeral response. Calling `ack()` with no argument sends a 200 OK with no visible reply. [slack.dev/bolt-js/concepts/commands](https://slack.dev/bolt-js/concepts/commands) 3. **Actions** (`app.action`): must call `await ack()`. The `ack()` function takes no arguments for actions -- it simply acknowledges receipt. Any response goes through `respond()` or `client` API calls after ack. [slack.dev/bolt-js/concepts/actions](https://slack.dev/bolt-js/concepts/actions) 4. **View submissions** (`app.view`): must call `await ack()`. Optionally pass a `response_action` object to control modal behavior: `{ response_action: "errors", errors: { block_id: "msg" } }` to show validation errors, `{ response_action: "update", view: {...} }` to replace the modal, `{ response_action: "push", view: {...} }` to push a new view onto the stack, or `{ response_action: "clear" }` to close all views in the stack. [api.slack.com/surfaces/modals#response_actions](https://api.slack.com/surfaces/modals#response_actions) 5. **Shortcuts** (`app.shortcut`): must call `await ack()` with no arguments. Then use `trigger_id` from the payload to open a modal via `client.views.open()`. [slack.dev/bolt-js/concepts/shortcuts](https://slack.dev/bolt-js/concepts/shortcuts) 6. **Options** (`app.options`): must call `await ack()` with an options payload containing the dynamic choices to display in the select menu. [slack.dev/bolt-js/concepts/options](https://slack.dev/bolt-js/concepts/options) 7. **Messages** (`app.message`) and **events** (`app.event`): do NOT require `ack()`. These are fire-and-forget from Slack's perspective. The `ack` property is not present in their context objects. [slack.dev/bolt-js/concepts/message-listening](https://slack.dev/bolt-js/concepts/message-listening) 8. Always call `ack()` **before** any async work (database calls, API requests, LLM inference). Perform long-running operations after acknowledgement to stay within the 3-second window. [api.slack.com/interactivity/handling#acknowledgment_response](https://api.slack.com/interactivity/handling#acknowledgment_response) 9. For commands that need a visible immediate reply, pass the response to `ack(text)` or `ack({ text, blocks })`. For a delayed or richer response, `ack()` with no arguments first, then use `respond()` or `say()` for the follow-up message. [slack.dev/bolt-js/concepts/commands](https://slack.dev/bolt-js/concepts/commands) 10. Duplicate ack calls are harmless but wasteful -- Slack ignores the second acknowledgement. However, calling `ack()` after the 3-second window has passed still results in a user-visible error regardless of the call. [api.slack.com/interactivity/handling](https://api.slack.com/interactivity/handling) ## patterns ### Command handler: ack immediately, then do async work ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/deploy", async ({ ack, command, respond }) => { // Acknowledge immediately with an ephemeral message await ack(`Deploying \`${command.text || "latest"}\`... please wait.`); // Now safe to do slow async work -- ack already sent const result = await runDeployment(command.text); // Follow up via response_url (visible only to the user by default) await respond({ response_type: "in_channel", // make visible to everyone text: `Deployment complete: ${result.status}`, }); }); async function runDeployment(target: string) { // simulate slow operation await new Promise((r) => setTimeout(r, 5000)); return { status: "success" }; } ``` ### Action handler: ack with no payload, then update the message ```typescript import { App, type BlockAction, type ButtonAction } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.action<BlockAction>("approve_request", async ({ ack, action, body, client }) => { // Step 1: ack immediately -- no arguments for actions await ack(); // Step 2: do async work after ack const requestId = (action as ButtonAction).value; await approveInDatabase(requestId!); // Step 3: update the original message to reflect the new state await client.chat.update({ channel: body.channel!.id, ts: body.message!.ts, text: `Request ${requestId} approved by <@${body.user.id}>`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `:white_check_mark: Request \`${requestId}\` approved by <@${body.user.id}>`, }, }, ], }); }); async function approveInDatabase(id: string) { await new Promise((r) => setTimeout(r, 1000)); } ``` ### View submission: ack with response_action for validation or update ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.view("create_ticket_modal", async ({ ack, view, client }) => { const vals = view.state.values; const title = vals.title_block.title_input.value!; const priority = vals.priority_block.priority_select.selected_option!.value; // Validation: return errors to keep the modal open if (title.length < 5) { await ack({ response_action: "errors", errors: { title_block: "Title must be at least 5 characters." }, }); return; } // Option A: close the modal (default ack behavior) await ack(); // Option B (alternative): update the modal with a confirmation view // await ack({ // response_action: "update", // view: { // type: "modal", // title: { type: "plain_text", text: "Ticket Created" }, // blocks: [ // { type: "section", text: { type: "mrkdwn", text: `:ticket: *${title}* created.` } }, // ], // }, // }); // Post-ack async work: create the ticket and notify the channel await client.chat.postMessage({ channel: "#tickets", text: `New ticket: ${title} (${priority})`, }); }); ``` ## pitfalls - **Doing async work before `ack()`**: Database queries, API calls, or LLM inference before `ack()` risks exceeding the 3-second deadline. Always ack first, then process. - **Passing arguments to `ack()` in action handlers**: Unlike commands, `app.action()` handlers do not accept a payload in `ack()`. Passing text to `ack("done")` in an action handler is silently ignored or throws depending on the Bolt version. - **`response_action: "errors"` keys must be `block_id` values**: In view submissions, the `errors` object keys must match `block_id` strings, not `action_id`. Mismatched keys cause the modal to close without showing errors. - **Missing `ack()` entirely**: If a handler throws before reaching `ack()`, the user sees a timeout error. Wrap handler logic in try/catch and ensure `ack()` is called in both success and error paths. - **Late ack after exactly 3 seconds**: The 3-second limit is strict. Network latency between your server and Slack counts. In practice, aim to ack within the first 100ms of handler execution. - **Confusing `ack()` with `respond()`**: `ack()` is the HTTP response to Slack's request. `respond()` uses the `response_url` and is a separate HTTP call. They serve different purposes and both may be needed in a single handler. - **Calling `ack()` in message/event handlers**: The `ack` function does not exist in `app.message()` or `app.event()` context objects. Attempting to destructure `{ ack }` from these handlers results in `undefined`. ## references - https://api.slack.com/interactivity/handling#acknowledgment_response - https://api.slack.com/surfaces/modals#response_actions - https://slack.dev/bolt-js/concepts/commands - https://slack.dev/bolt-js/concepts/actions - https://slack.dev/bolt-js/concepts/view-submissions - https://slack.dev/bolt-js/concepts/shortcuts - https://slack.dev/bolt-js/concepts/options - https://slack.dev/bolt-js/concepts/acknowledge - https://api.slack.com/interactivity/slash-commands#responding_to_commands - https://github.com/slackapi/bolt-js ## instructions This expert covers Slack Bolt acknowledgement (ack) semantics in TypeScript. Use it when you need to understand: which handler types require ack() and which do not; the 3-second deadline and how to structure handlers to meet it; ack() with response payloads for commands (text/blocks) and view submissions (response_action for errors, update, push, clear); the correct pattern of ack-first-then-async-work; and common mistakes that cause timeout errors or silent failures. This is critical knowledge for any Slack bot that handles commands, button clicks, modal submissions, shortcuts, or dynamic select menus. Pair with `runtime.bolt-foundations-ts.md` for the handler types where ack rules apply, and `ui.block-kit-ts.md` for Block Kit modal submission patterns. ## research Deep Research prompt: "Write a micro expert on Slack Bolt ack() rules in TypeScript. Cover which handler types require ack (commands, actions, views, shortcuts, options) vs those that do not (messages, events), the 3-second deadline, ack() with response payloads for commands and view submissions (response_action: errors/update/push/clear), async-after-ack patterns, and common mistakes that cause timeouts. Provide 2-3 canonical TypeScript examples." -
runtime.bolt-foundations-ts.md 10.7 KB
# runtime.bolt-foundations-ts ## purpose Core Slack Bolt app structure: App constructor, middleware, event loop, and handler registration patterns in TypeScript. ## rules 1. Initialize the Bolt `App` with at minimum `token` (bot OAuth token) and `signingSecret` (request verification); for socket mode add `socketMode: true` and `appToken`. Never hard-code secrets -- pull from `process.env`. [slack.dev/bolt-js/getting-started](https://slack.dev/bolt-js/getting-started) 2. Call `await app.start(port)` to launch the HTTP receiver (default Express). In socket mode the port argument is ignored and a WebSocket connection is established instead. [slack.dev/bolt-js/concepts/socket-mode](https://slack.dev/bolt-js/concepts/socket-mode) 3. Register handlers by type: `app.message()` for messages, `app.command()` for slash commands, `app.action()` for Block Kit interactions, `app.view()` for modal submissions, `app.event()` for Events API, `app.shortcut()` for global/message shortcuts, and `app.options()` for dynamic select menus. Each handler type receives a different payload shape. [slack.dev/bolt-js/reference](https://slack.dev/bolt-js/reference) 4. Every handler receives a context object with named properties. Common properties include `say` (post to the conversation), `respond` (hit the response_url), `client` (Slack WebClient), `body` (full payload), `ack` (acknowledge the request), `next` (pass to next middleware), and `context` (app-level metadata like botUserId). [slack.dev/bolt-js/concepts/listener-middleware](https://slack.dev/bolt-js/concepts/listener-middleware) 5. `app.message()` accepts a string, RegExp, or no argument (catch-all). String matching is substring-based. For exact matching use a RegExp with anchors (e.g., `/^hello$/i`). [slack.dev/bolt-js/concepts/message-listening](https://slack.dev/bolt-js/concepts/message-listening) 6. Use `app.use()` to register global middleware that runs before all route handlers. Middleware must call `await next()` to continue to the next middleware or handler; omitting `next()` silently swallows the event. [slack.dev/bolt-js/concepts/global-middleware](https://slack.dev/bolt-js/concepts/global-middleware) 7. Register `app.error(async (error) => { ... })` for global error handling. Unhandled errors in listeners bubble to this handler. Without it, errors are logged to stderr and the process continues. [slack.dev/bolt-js/concepts/error-handling](https://slack.dev/bolt-js/concepts/error-handling) 8. The `client` property in handler context is a pre-authenticated `WebClient` bound to the bot token. Use it for Slack API calls like `client.chat.postMessage()`, `client.views.open()`, `client.users.info()`, etc. For workspace-level calls needing a user token, instantiate a separate `WebClient`. [slack.dev/bolt-js/concepts/web-api](https://slack.dev/bolt-js/concepts/web-api) 9. Handler registration order matters: Bolt evaluates listeners in registration order and stops at the first match for `app.message()`. Place more specific patterns before catch-all handlers. [slack.dev/bolt-js/concepts/message-listening](https://slack.dev/bolt-js/concepts/message-listening) 10. The `say()` function posts a message to the same channel where the event occurred. It accepts a string or a full message payload with `blocks`, `text`, `thread_ts`, and `attachments`. For posting to a different channel, use `client.chat.postMessage()` with an explicit `channel` parameter. [slack.dev/bolt-js/concepts/message-sending](https://slack.dev/bolt-js/concepts/message-sending) ## patterns ### Initializing a Bolt app with socket mode and registering middleware ```typescript import { App, LogLevel } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, socketMode: true, appToken: process.env.SLACK_APP_TOKEN!, logLevel: LogLevel.INFO, }); // Global middleware -- runs before every handler app.use(async ({ next, context, logger }) => { logger.info(`Event from user ${context.userId ?? "unknown"}`); await next(); }); // Global error handler app.error(async (error) => { console.error("Unhandled error:", error); }); (async () => { await app.start(process.env.PORT || 3000); console.log("Bolt app is running"); })(); ``` ### Registering message handlers with pattern matching ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Exact keyword match via RegExp app.message(/^hello$/i, async ({ message, say }) => { if (message.subtype) return; // skip message_changed, etc. await say(`Hey there <@${(message as any).user}>!`); }); // Substring match -- triggers on any message containing "help" app.message("help", async ({ say }) => { await say("Here are things I can help with..."); }); // Catch-all: fires for every message not matched above app.message(async ({ message, say, client }) => { if (message.subtype) return; const userId = (message as any).user; const userInfo = await client.users.info({ user: userId }); await say(`Got it, ${userInfo.user?.real_name ?? "friend"}.`); }); ``` ### Registering multiple handler types on a single app ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Slash command app.command("/status", async ({ ack, say, command }) => { await ack(); await say(`Status requested by <@${command.user_id}>`); }); // Block Kit button action app.action("approve_btn", async ({ ack, respond }) => { await ack(); await respond({ replace_original: true, text: "Approved!" }); }); // Events API -- channel member joined app.event("member_joined_channel", async ({ event, say }) => { await say(`Welcome to the channel, <@${event.user}>!`); }); // Modal view submission app.view("feedback_modal", async ({ ack, view, client }) => { const vals = view.state.values; const comment = vals.comment_block.comment_input.value!; await ack(); await client.chat.postMessage({ channel: "#feedback", text: `New feedback: ${comment}`, }); }); // Global shortcut app.shortcut("open_ticket", async ({ ack, shortcut, client }) => { await ack(); await client.views.open({ trigger_id: shortcut.trigger_id, view: { type: "modal", callback_id: "ticket_modal", title: { type: "plain_text", text: "New Ticket" }, submit: { type: "plain_text", text: "Create" }, blocks: [ { type: "input", block_id: "title_block", label: { type: "plain_text", text: "Title" }, element: { type: "plain_text_input", action_id: "title_input" }, }, ], }, }); }); // Dynamic options for external select menus app.options("user_search_options", async ({ ack, options }) => { const query = options.value; const matches = await searchUsers(query); await ack({ options: matches.map((u) => ({ text: { type: "plain_text" as const, text: u.name }, value: u.id, })), }); }); async function searchUsers(query: string) { return [{ name: "Alice", id: "U001" }]; } (async () => { await app.start(3000); console.log("App running on port 3000"); })(); ``` ## pitfalls - **Forgetting `await next()` in middleware**: Global middleware registered with `app.use()` must call `await next()` or downstream handlers never execute. The event is silently dropped with no error. - **Not checking `message.subtype`**: Bot messages, message edits (`message_changed`), and deletions (`message_deleted`) all trigger `app.message()`. Filter on `subtype` to avoid infinite loops or duplicate processing. - **Registering catch-all before specific handlers**: `app.message()` with no pattern matches everything. If registered first, more specific `app.message("keyword")` handlers are never reached. - **Confusing `say` and `respond`**: `say()` posts a new visible message to the channel. `respond()` uses the `response_url` (only available in command/action payloads) and can post ephemerally or replace the original message. Using `respond()` in an event handler where no `response_url` exists throws an error. - **Missing `ack()` in interactive handlers**: Commands, actions, views, shortcuts, and options handlers must call `ack()` within 3 seconds. Message and event handlers do not require `ack()`. Calling `ack()` where it does not exist throws a runtime error. - **Using `say()` in non-conversational contexts**: Global shortcuts and some events do not have a channel context. Calling `say()` fails with "channel not found". Use `client.chat.postMessage()` with an explicit channel instead. - **Socket mode with HTTP receiver**: Setting `socketMode: true` without providing `appToken` throws immediately. Conversely, providing `appToken` without `socketMode: true` ignores it and uses the HTTP receiver. ## references - https://slack.dev/bolt-js/getting-started - https://slack.dev/bolt-js/concepts/basic - https://slack.dev/bolt-js/concepts/message-listening - https://slack.dev/bolt-js/concepts/actions - https://slack.dev/bolt-js/concepts/commands - https://slack.dev/bolt-js/concepts/events - https://slack.dev/bolt-js/concepts/view-submissions - https://slack.dev/bolt-js/concepts/global-middleware - https://slack.dev/bolt-js/concepts/error-handling - https://slack.dev/bolt-js/concepts/socket-mode - https://slack.dev/bolt-js/reference - https://api.slack.com/methods - https://github.com/slackapi/bolt-js ## instructions This expert covers the foundational structure of a Slack Bolt application in TypeScript. Use it when you need to: set up an App instance with the correct constructor options (token, signingSecret, socketMode, appToken); register handlers for messages, commands, actions, views, events, shortcuts, and options menus; wire up global middleware with app.use(); understand the context object properties available in each handler type (say, respond, client, body, ack, next); implement global error handling with app.error(); and understand handler registration order and event routing. This is the starting point for any Slack Bolt project and the foundation for all other Slack expert files. Pair with `runtime.ack-rules-ts.md` for acknowledgement timing rules that apply to all interactive handlers. ## research Deep Research prompt: "Write a micro expert on Slack Bolt for JavaScript/TypeScript app foundations: App constructor options (token, signingSecret, socketMode, appToken, logLevel), app.start(), all handler registration methods (app.message, app.command, app.action, app.view, app.event, app.shortcut, app.options), global middleware with app.use() and next(), the context object shape for each handler type, and app.error() global error handling. Provide 2-3 canonical TypeScript examples and common pitfalls." -
runtime.shortcuts-ts.md 11 KB
# runtime.shortcuts-ts ## purpose Global shortcut and message shortcut handling in Slack Bolt TypeScript apps — registration, payload differences, and response patterns. ## rules 1. Register shortcut handlers with `app.shortcut('callback_id', handler)`. The `callback_id` must match the shortcut configured in the Slack app dashboard under **Interactivity & Shortcuts**. Supports string or RegExp matching. [slack.dev/bolt-js/concepts/shortcuts](https://slack.dev/bolt-js/concepts/shortcuts) 2. Slack has two shortcut types: **global shortcuts** (`type: 'shortcut'`) launched from the compose menu or search bar, and **message shortcuts** (`type: 'message_action'`) launched from a message's context menu (three-dot "More actions"). Both require the `ack()` + response pattern. [api.slack.com/interactivity/shortcuts](https://api.slack.com/interactivity/shortcuts) 3. Always call `await ack()` within 3 seconds. Shortcuts provide a `trigger_id` — use it to open a modal with `client.views.open()` immediately after ack. The trigger ID expires quickly, so do not perform slow work between ack and views.open. [api.slack.com/interactivity/shortcuts/using](https://api.slack.com/interactivity/shortcuts/using) 4. Filter by shortcut type using constraints: `app.shortcut({ type: 'message_action', callback_id: 'my_action' }, handler)`. Without a `type` constraint, the handler fires for both global and message shortcuts with that `callback_id`. [bolt-js source: App.ts](https://github.com/slackapi/bolt-js/blob/main/src/App.ts) 5. Global shortcuts provide `shortcut.trigger_id`, `shortcut.user`, and `shortcut.team` but **no channel or message context**. The only response pattern is opening a modal — `say()` is not available. [api.slack.com/interactivity/shortcuts/using#global_shortcuts](https://api.slack.com/interactivity/shortcuts/using#global_shortcuts) 6. Message shortcuts provide `shortcut.message` (the target message with `ts`, `text`, `user`), `shortcut.channel` (channel ID and name), `shortcut.response_url`, and `shortcut.message_ts`. Both `say()` and `respond()` are available. [api.slack.com/interactivity/shortcuts/using#message_shortcuts](https://api.slack.com/interactivity/shortcuts/using#message_shortcuts) 7. Use `shortcut.message.text` and `shortcut.message.ts` from message shortcuts to access the message content the user right-clicked on. The `message` object may not have a `user` field for bot messages. Always handle that as optional. [bolt-js source: types/shortcuts/message-shortcut.ts](https://github.com/slackapi/bolt-js/blob/main/src/types/shortcuts/message-shortcut.ts) 8. Store context for modal follow-up using `private_metadata` on the view. For message shortcuts, serialize the channel ID, message timestamp, and any relevant data into `private_metadata` so the `view_submission` handler can access it. [api.slack.com/surfaces/modals#private_metadata](https://api.slack.com/surfaces/modals#private_metadata) 9. Register shortcuts in the Slack app dashboard: **global shortcuts** under Interactivity → Shortcuts → "Global" tab; **message shortcuts** under Interactivity → Shortcuts → "On messages" tab. Each shortcut needs a name, description, and `callback_id`. [api.slack.com/interactivity/shortcuts#create](https://api.slack.com/interactivity/shortcuts#create) 10. Shortcut payloads include `enterprise` and `is_enterprise_install` fields for Enterprise Grid compatibility. Check `is_enterprise_install` when routing to workspace-specific resources in multi-org deployments. [bolt-js source: types/shortcuts/global-shortcut.ts](https://github.com/slackapi/bolt-js/blob/main/src/types/shortcuts/global-shortcut.ts) ## patterns ### Global shortcut that opens a modal ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Global shortcut — no channel context, must open a modal app.shortcut("create_task", async ({ ack, shortcut, client }) => { await ack(); await client.views.open({ trigger_id: shortcut.trigger_id, view: { type: "modal", callback_id: "task_modal", title: { type: "plain_text", text: "Create Task" }, submit: { type: "plain_text", text: "Create" }, blocks: [ { type: "input", block_id: "task_title", label: { type: "plain_text", text: "Task Title" }, element: { type: "plain_text_input", action_id: "title_input", }, }, { type: "input", block_id: "task_assignee", label: { type: "plain_text", text: "Assign To" }, element: { type: "users_select", action_id: "assignee_select", }, }, ], }, }); }); ``` ### Message shortcut that forwards a message ```typescript import { App, MessageShortcut } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Message shortcut — has channel and message context app.shortcut<MessageShortcut>( { type: "message_action", callback_id: "save_message" }, async ({ ack, shortcut, client }) => { await ack(); const messageText = shortcut.message.text || "(no text content)"; const author = shortcut.message.user ? `<@${shortcut.message.user}>` : "a bot"; // Open a modal with the message content pre-filled await client.views.open({ trigger_id: shortcut.trigger_id, view: { type: "modal", callback_id: "save_message_modal", title: { type: "plain_text", text: "Save Message" }, submit: { type: "plain_text", text: "Save" }, private_metadata: JSON.stringify({ channel: shortcut.channel.id, messageTs: shortcut.message_ts, }), blocks: [ { type: "section", text: { type: "mrkdwn", text: `*Message from ${author}:*\n>${messageText}`, }, }, { type: "input", block_id: "note_block", label: { type: "plain_text", text: "Add a note (optional)" }, optional: true, element: { type: "plain_text_input", action_id: "note_input", multiline: true, }, }, { type: "input", block_id: "dest_block", label: { type: "plain_text", text: "Save to channel" }, element: { type: "conversations_select", action_id: "dest_channel", default_to_current_conversation: true, }, }, ], }, }); } ); ``` ### Handling both shortcut types with one handler ```typescript import { App, GlobalShortcut, MessageShortcut } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Matches both global and message shortcuts with callback_id "quick_note" app.shortcut("quick_note", async ({ ack, shortcut, client }) => { await ack(); // Determine context based on shortcut type const isMessage = shortcut.type === "message_action"; const metadata = isMessage ? JSON.stringify({ channel: (shortcut as MessageShortcut).channel.id, messageTs: (shortcut as MessageShortcut).message_ts, }) : "{}"; await client.views.open({ trigger_id: shortcut.trigger_id, view: { type: "modal", callback_id: "quick_note_modal", title: { type: "plain_text", text: "Quick Note" }, submit: { type: "plain_text", text: "Save" }, private_metadata: metadata, blocks: [ { type: "input", block_id: "note_block", label: { type: "plain_text", text: "Note" }, element: { type: "plain_text_input", action_id: "note_input", multiline: true, }, }, ], }, }); }); ``` ## pitfalls - **Expecting `say()` on global shortcuts**: Global shortcuts have no channel context. Attempting to use `say()` will fail. The only response path is opening a modal via `client.views.open()` using the `trigger_id`. - **Treating `shortcut.message.user` as always present**: Bot-posted messages may not include `user` in the message payload. Always handle it as optional when processing message shortcuts. - **Not using `private_metadata` for modal follow-up**: After a shortcut opens a modal, the `view_submission` handler has no reference to the original shortcut context. Serialize channel ID, message timestamp, and other needed data into `private_metadata`. - **Forgetting dashboard registration**: Code-side `app.shortcut('my_shortcut')` does nothing if the shortcut is not configured in the Slack app dashboard. Both global and message shortcuts need explicit registration with matching `callback_id`. - **Confusing shortcuts with slash commands**: Shortcuts are UI-triggered (compose menu, message context menu) and always provide a `trigger_id`. Slash commands are text-triggered and provide both `trigger_id` and `response_url`. The handler signatures and available utilities differ. - **RegExp matching without type constraint**: Using `app.shortcut(/task_.*/)` matches both global and message shortcuts. If your handler assumes message context (like `shortcut.channel`), add `{ type: 'message_action', callback_id: /task_.*/ }` to avoid runtime errors on global shortcut invocations. ## references - https://api.slack.com/interactivity/shortcuts - https://api.slack.com/interactivity/shortcuts/using - https://api.slack.com/surfaces/modals - https://slack.dev/bolt-js/concepts/shortcuts - https://github.com/slackapi/bolt-js/blob/main/src/types/shortcuts/global-shortcut.ts - https://github.com/slackapi/bolt-js/blob/main/src/types/shortcuts/message-shortcut.ts ## instructions This expert covers Slack global shortcut and message shortcut handling in Bolt TypeScript. Use it when: registering shortcut handlers with `app.shortcut()`; distinguishing between global shortcuts and message shortcuts; accessing message content from message shortcuts; opening modals from shortcuts using `trigger_id`; passing shortcut context to modal submissions via `private_metadata`; or configuring shortcuts in the Slack app dashboard. Pair with: `runtime.ack-rules-ts.md` for ack timing rules. `ui.modals-lifecycle-ts.md` for modal open/push/update/submit patterns after opening from a shortcut. `runtime.bolt-foundations-ts.md` for App setup. ## research Deep Research prompt: "Write a micro expert on Slack shortcuts (global and message) in Bolt TypeScript. Cover app.shortcut() registration with string/RegExp/constraints, GlobalShortcut vs MessageShortcut payload types and their differences (channel context, message data, say() availability), type-based filtering with constraints, trigger_id usage for opening modals, private_metadata for passing context to view_submission, respond() on message shortcuts, Enterprise Grid fields, and dashboard registration requirements. Source from @slack/bolt App.ts shortcut method, types/shortcuts/ directory, and Slack API shortcut docs." -
runtime.slash-commands-ts.md 11.6 KB
# runtime.slash-commands-ts ## purpose Slack slash command registration, payload handling, and response patterns in Bolt TypeScript apps. ## rules 1. Register slash command handlers with `app.command('/command-name', handler)`. The command string must include the leading `/` and match the command configured in the Slack app dashboard. [slack.dev/bolt-js/concepts/commands](https://slack.dev/bolt-js/concepts/commands) 2. The handler context includes `command` (the full payload), `ack`, `say`, `respond`, and `client`. The `command` object contains: `text` (everything after the command), `trigger_id`, `response_url`, `user_id`, `user_name`, `channel_id`, `channel_name`, `team_id`, `team_domain`, and `enterprise_id`. [api.slack.com/interactivity/slash-commands](https://api.slack.com/interactivity/slash-commands) 3. Always call `await ack()` within 3 seconds. Optionally pass a string or `{ text, blocks }` to `ack()` for an immediate ephemeral response visible only to the invoking user. [api.slack.com/interactivity/slash-commands#responding_to_commands](https://api.slack.com/interactivity/slash-commands#responding_to_commands) 4. The default response type for `ack(text)` and `respond()` is **ephemeral** (visible only to the user). To make a response visible to the entire channel, set `response_type: "in_channel"` in the `respond()` payload. [api.slack.com/interactivity/slash-commands#responding_to_commands](https://api.slack.com/interactivity/slash-commands#responding_to_commands) 5. Use `respond()` (backed by `response_url`) for follow-up messages after `ack()`. The `response_url` is valid for 30 minutes and supports up to 5 responses. Each call can set `replace_original`, `delete_original`, or `response_type`. [api.slack.com/interactivity/responding](https://api.slack.com/interactivity/responding) 6. Use `trigger_id` from the command payload to open modals with `client.views.open()`. The trigger ID expires in 3 seconds, so call `ack()` and `client.views.open()` early in the handler. [api.slack.com/surfaces/modals#opening](https://api.slack.com/surfaces/modals#opening) 7. Use `say()` to post a visible message to the channel where the command was invoked. Unlike `respond()`, messages from `say()` are always visible to everyone and appear as normal bot messages (not ephemeral). [slack.dev/bolt-js/concepts/commands](https://slack.dev/bolt-js/concepts/commands) 8. Parse the `command.text` string yourself for sub-commands or arguments. Bolt does not provide built-in argument parsing. Use `text.split(/\s+/)` or a command-parsing library for complex argument structures. [api.slack.com/interactivity/slash-commands](https://api.slack.com/interactivity/slash-commands) 9. Slash commands require the `commands` scope in the bot's OAuth configuration. Each command must be registered in the Slack app dashboard under "Slash Commands" with a request URL pointing to your app's endpoint. [api.slack.com/interactivity/slash-commands#creating_commands](https://api.slack.com/interactivity/slash-commands#creating_commands) 10. Commands invoked in DMs with the bot have `channel_id` set to the DM channel and `channel_name` set to `"directmessage"`. Always handle both channel and DM contexts in your command logic. [api.slack.com/interactivity/slash-commands](https://api.slack.com/interactivity/slash-commands) ## patterns ### Basic command with ephemeral ack and in-channel follow-up ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/status", async ({ ack, command, respond }) => { // Immediate ephemeral acknowledgement await ack("Checking status..."); // Simulate async lookup const status = await getSystemStatus(); // Follow-up visible to entire channel await respond({ response_type: "in_channel", text: `System status: ${status.summary}`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `*System Status* (requested by <@${command.user_id}>)`, }, }, { type: "divider" }, { type: "section", fields: [ { type: "mrkdwn", text: `*API:* ${status.api}` }, { type: "mrkdwn", text: `*Database:* ${status.db}` }, { type: "mrkdwn", text: `*Queue:* ${status.queue}` }, { type: "mrkdwn", text: `*Uptime:* ${status.uptime}` }, ], }, ], }); }); async function getSystemStatus() { return { summary: "All systems operational", api: ":white_check_mark: Healthy", db: ":white_check_mark: Healthy", queue: ":warning: Degraded", uptime: "14d 6h", }; } ``` ### Command that opens a modal using trigger_id ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/ticket", async ({ ack, command, client }) => { // ack with no visible response -- modal provides the UI await ack(); // Open a modal using the trigger_id (must happen within 3 seconds) await client.views.open({ trigger_id: command.trigger_id, view: { type: "modal", callback_id: "ticket_create_modal", title: { type: "plain_text", text: "Create Ticket" }, submit: { type: "plain_text", text: "Create" }, close: { type: "plain_text", text: "Cancel" }, // Pre-fill with command text if provided private_metadata: JSON.stringify({ channel: command.channel_id }), blocks: [ { type: "input", block_id: "title_block", label: { type: "plain_text", text: "Title" }, element: { type: "plain_text_input", action_id: "title_input", initial_value: command.text || "", placeholder: { type: "plain_text", text: "Describe the issue" }, }, }, { type: "input", block_id: "priority_block", label: { type: "plain_text", text: "Priority" }, element: { type: "static_select", action_id: "priority_select", options: [ { text: { type: "plain_text", text: "High" }, value: "high" }, { text: { type: "plain_text", text: "Medium" }, value: "medium" }, { text: { type: "plain_text", text: "Low" }, value: "low" }, ], }, }, ], }, }); }); // Handle the modal submission app.view("ticket_create_modal", async ({ ack, view, client }) => { const vals = view.state.values; const title = vals.title_block.title_input.value!; const priority = vals.priority_block.priority_select.selected_option!.value; const meta = JSON.parse(view.private_metadata || "{}"); if (title.length < 3) { await ack({ response_action: "errors", errors: { title_block: "Title must be at least 3 characters." }, }); return; } await ack(); await client.chat.postMessage({ channel: meta.channel || "#tickets", text: `New ticket: ${title} [${priority}]`, }); }); ``` ### Command with sub-command argument parsing ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/config", async ({ ack, command, respond }) => { await ack(); const args = command.text.trim().split(/\s+/); const subCommand = args[0]?.toLowerCase(); switch (subCommand) { case "get": { const key = args[1]; if (!key) { await respond("Usage: `/config get <key>`"); return; } const value = await getConfigValue(key); await respond(`\`${key}\` = \`${value ?? "not set"}\``); break; } case "set": { const key = args[1]; const value = args.slice(2).join(" "); if (!key || !value) { await respond("Usage: `/config set <key> <value>`"); return; } await setConfigValue(key, value); await respond({ response_type: "in_channel", text: `Configuration updated: \`${key}\` = \`${value}\``, }); break; } case "list": { const all = await listConfigValues(); const formatted = all.map((c) => `\`${c.key}\` = \`${c.value}\``).join("\n"); await respond(formatted || "No configuration values set."); break; } default: await respond("Unknown sub-command. Available: `get`, `set`, `list`"); } }); async function getConfigValue(key: string): Promise<string | null> { return "example-value"; } async function setConfigValue(key: string, value: string): Promise<void> {} async function listConfigValues(): Promise<{ key: string; value: string }[]> { return [{ key: "region", value: "us-east-1" }]; } ``` ## pitfalls - **Forgetting `await ack()`**: Every command handler must acknowledge. Missing it causes a "This command didn't work" error for the user after 3 seconds. - **Doing slow work before `ack()`**: API calls, database queries, or any I/O before `ack()` risks the 3-second timeout. Always ack first. - **Assuming `command.text` is non-empty**: Users can invoke `/command` with no arguments. Always handle the empty string case for `command.text`. - **Ephemeral vs in-channel confusion**: The default `ack(text)` response and `respond()` responses are ephemeral. Users often expect channel-visible responses. Explicitly set `response_type: "in_channel"` when the response should be public. - **`response_url` expiry**: The URL from `command.response_url` expires after 30 minutes and supports at most 5 responses. For longer-lived interactions, switch to `client.chat.postMessage()`. - **Stale `trigger_id`**: If you ack, then do 2+ seconds of work, then try `client.views.open()`, the trigger_id may have expired. Open modals immediately after ack. - **Command not registered in dashboard**: `app.command('/foo')` in code does nothing if `/foo` is not configured in the Slack app's "Slash Commands" settings. Both code and dashboard must agree. - **Missing `commands` scope**: The bot must have the `commands` OAuth scope or slash command registration fails silently. ## references - https://api.slack.com/interactivity/slash-commands - https://api.slack.com/interactivity/slash-commands#creating_commands - https://api.slack.com/interactivity/slash-commands#responding_to_commands - https://api.slack.com/interactivity/responding - https://api.slack.com/surfaces/modals#opening - https://slack.dev/bolt-js/concepts/commands - https://slack.dev/bolt-js/concepts/acknowledge - https://github.com/slackapi/bolt-js ## instructions This expert covers Slack slash command implementation in Bolt TypeScript. Use it when you need to: register command handlers with app.command(); understand the command payload properties (text, trigger_id, response_url, user_id, channel_id); implement ack() with immediate ephemeral responses; choose between ephemeral and in-channel response types; use respond() for follow-up messages via response_url; open modals from commands using trigger_id and client.views.open(); parse command arguments and implement sub-commands; and understand the dashboard configuration requirements for slash commands. Pair with `runtime.ack-rules-ts.md` for command ack timing, and `ui.block-kit-ts.md` when commands open modals or send Block Kit messages. ## research Deep Research prompt: "Write a micro expert on Slack slash commands in Bolt TypeScript. Cover app.command() registration, the command payload shape (text, trigger_id, response_url, user_id, channel_id), ack() with text/blocks, ephemeral vs in_channel response_type, respond() usage and limitations, opening modals from commands, sub-command argument parsing, and dashboard configuration requirements. Provide 2-3 canonical TypeScript examples." -
runtime.socket-mode-ts.md 9.5 KB
# runtime.socket-mode-ts ## purpose Socket Mode setup, connection lifecycle, and production patterns for Slack Bolt TypeScript apps using `@slack/socket-mode`. ## rules 1. Enable Socket Mode by setting `socketMode: true` and providing `appToken` in the `App` constructor. The app token must be an **app-level token** (prefix `xapp-`) with the `connections:write` scope, not a bot token. [api.slack.com/apis/connections/socket](https://api.slack.com/apis/connections/socket) 2. Socket Mode uses WebSocket connections instead of HTTP endpoints. No public URL, no `signingSecret`, and no request signature verification are needed. This makes it ideal for local development and firewall-restricted environments. [slack.dev/bolt-js/concepts/socket-mode](https://slack.dev/bolt-js/concepts/socket-mode) 3. Install `@slack/socket-mode` as a dependency alongside `@slack/bolt`. Bolt's `SocketModeReceiver` wraps the `SocketModeClient` from this package. [github.com/slackapi/node-slack-sdk](https://github.com/slackapi/node-slack-sdk) 4. Call `await app.start()` to open the WebSocket connection. Unlike HTTP mode, `start()` returns an `AppsConnectionsOpenResponse` object, not an HTTP server. [slack.dev/bolt-js/concepts/socket-mode](https://slack.dev/bolt-js/concepts/socket-mode) 5. Auto-reconnect is enabled by default. The `SocketModeClient` handles connection drops, ping/pong heartbeats, and reconnection automatically. Override with `autoReconnectEnabled: false` only for testing. [github.com/slackapi/node-slack-sdk/tree/main/packages/socket-mode](https://github.com/slackapi/node-slack-sdk/tree/main/packages/socket-mode) 6. To add OAuth install routes or custom HTTP endpoints alongside Socket Mode, pass `customRoutes` to the `SocketModeReceiver`. This spins up an HTTP server on port 3000 (default) in addition to the WebSocket connection. [slack.dev/bolt-js/concepts/custom-routes](https://slack.dev/bolt-js/concepts/custom-routes) 7. For multi-workspace apps using Socket Mode with OAuth, provide `clientId`, `clientSecret`, `stateSecret`, and `installationStore` in the receiver options. The receiver creates an HTTP server for OAuth flows while using WebSocket for events. [slack.dev/bolt-js/concepts/authenticating-oauth](https://slack.dev/bolt-js/concepts/authenticating-oauth) 8. Use `processEventErrorHandler` on the receiver to control retry behavior. Return `true` to acknowledge the event (stops Slack retries). Return `false` to let Slack retry. `AuthorizationError` returns `true` by default (retrying won't fix bad tokens). [bolt-js source: SocketModeReceiver.ts](https://github.com/slackapi/bolt-js/blob/main/src/receivers/SocketModeReceiver.ts) 9. Access the underlying `SocketModeClient` via `receiver.client` to listen for low-level events like `connected`, `connecting`, `disconnected`, and `unable_to_socket_mode_start`. [github.com/slackapi/node-slack-sdk/tree/main/packages/socket-mode](https://github.com/slackapi/node-slack-sdk/tree/main/packages/socket-mode) 10. Generate the app-level token in the Slack app dashboard under **Settings → Basic Information → App-Level Tokens**. Create a token with the `connections:write` scope. Store it as `SLACK_APP_TOKEN` in your environment. [api.slack.com/apis/connections/socket#token](https://api.slack.com/apis/connections/socket#token) 11. Socket Mode supports all Bolt listener types — `app.message()`, `app.command()`, `app.action()`, `app.shortcut()`, `app.view()`, `app.event()`, and `app.options()` — with no code changes versus HTTP mode. The transport is transparent to handlers. [slack.dev/bolt-js/concepts/socket-mode](https://slack.dev/bolt-js/concepts/socket-mode) 12. Do not set `signingSecret` when using Socket Mode with `socketMode: true`. Bolt will throw if both are set with conflicting receiver configurations. If you need to switch between Socket Mode (dev) and HTTP (prod), use environment variables to toggle the `App` constructor options. [bolt-js source: App.ts](https://github.com/slackapi/bolt-js/blob/main/src/App.ts) ## patterns ### Minimal Socket Mode setup ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, appToken: process.env.SLACK_APP_TOKEN!, socketMode: true, }); app.message("hello", async ({ message, say }) => { await say(`Hey there <@${message.user}>!`); }); (async () => { await app.start(); console.log("⚡️ Bolt app is running in Socket Mode"); })(); ``` ### Environment-based transport switching (dev vs prod) ```typescript import { App } from "@slack/bolt"; const useSocketMode = process.env.SOCKET_MODE === "true"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, // Socket Mode options — only when enabled ...(useSocketMode && { socketMode: true, appToken: process.env.SLACK_APP_TOKEN!, }), // HTTP options — only when Socket Mode is disabled ...(!useSocketMode && { signingSecret: process.env.SLACK_SIGNING_SECRET!, }), }); (async () => { const port = useSocketMode ? undefined : Number(process.env.PORT || 3000); await app.start(port!); console.log( `⚡️ Bolt app running in ${useSocketMode ? "Socket" : "HTTP"} mode` ); })(); ``` ### Socket Mode with OAuth and custom routes ```typescript import { App } from "@slack/bolt"; import { FileInstallationStore } from "@slack/oauth"; const app = new App({ socketMode: true, appToken: process.env.SLACK_APP_TOKEN!, clientId: process.env.SLACK_CLIENT_ID!, clientSecret: process.env.SLACK_CLIENT_SECRET!, stateSecret: process.env.SLACK_STATE_SECRET!, installationStore: new FileInstallationStore(), scopes: ["chat:write", "commands", "app_mentions:read"], customRoutes: [ { path: "/health", method: "GET", handler: (_req, res) => { res.writeHead(200); res.end("OK"); }, }, ], }); (async () => { await app.start(3000); // WebSocket for events + HTTP on :3000 for OAuth and /health console.log("⚡️ App running: Socket Mode + OAuth on port 3000"); })(); ``` ### Monitoring connection state ```typescript import { App, SocketModeReceiver } from "@slack/bolt"; const receiver = new SocketModeReceiver({ appToken: process.env.SLACK_APP_TOKEN!, clientPingTimeout: 30_000, serverPingTimeout: 30_000, pingPongLoggingEnabled: true, }); const app = new App({ token: process.env.SLACK_BOT_TOKEN!, receiver, }); // Access the underlying SocketModeClient for lifecycle events receiver.client.on("connected", () => { console.log("Socket Mode connected"); }); receiver.client.on("disconnected", () => { console.warn("Socket Mode disconnected — auto-reconnect will retry"); }); receiver.client.on("unable_to_socket_mode_start", (error) => { console.error("Socket Mode failed to start:", error); }); (async () => { await app.start(); })(); ``` ## pitfalls - **Using `signingSecret` with Socket Mode**: Setting both `socketMode: true` and `signingSecret` creates conflicting receiver configurations. Socket Mode does not use HTTP request verification. Remove `signingSecret` when using Socket Mode. - **Wrong token type for `appToken`**: The `appToken` must be an **app-level token** (`xapp-` prefix) with `connections:write` scope, not a bot token (`xoxb-`) or user token (`xoxp-`). Using the wrong token gives a cryptic connection error. - **Assuming HTTP endpoints exist**: In pure Socket Mode (no `customRoutes`, no OAuth), there is no HTTP server. Health check endpoints, webhook receivers, and OAuth callback URLs will not work unless you explicitly configure `customRoutes` or OAuth options. - **Port conflicts with OAuth**: When Socket Mode is used with OAuth, the receiver starts an HTTP server on port 3000 by default. If another service uses that port, pass a different port to `app.start(port)`. - **Missing `@slack/socket-mode` dependency**: `@slack/bolt` does not bundle `@slack/socket-mode`. You must install it separately: `npm install @slack/socket-mode`. Bolt will throw at startup if the package is missing. ## references - https://api.slack.com/apis/connections/socket - https://slack.dev/bolt-js/concepts/socket-mode - https://slack.dev/bolt-js/concepts/custom-routes - https://github.com/slackapi/bolt-js/blob/main/src/receivers/SocketModeReceiver.ts - https://github.com/slackapi/node-slack-sdk/tree/main/packages/socket-mode ## instructions This expert covers Socket Mode transport for Slack Bolt TypeScript apps. Use it when: setting up local development without a public URL; configuring `socketMode: true` and `appToken`; switching between Socket Mode (dev) and HTTP (prod); adding OAuth or custom HTTP routes alongside Socket Mode; monitoring WebSocket connection lifecycle events; or troubleshooting Socket Mode connection issues. Pair with: `runtime.bolt-foundations-ts.md` for App constructor basics. `bolt-oauth-distribution-ts.md` for multi-workspace OAuth configuration alongside Socket Mode. ## research Deep Research prompt: "Write a micro expert on Slack Bolt Socket Mode in TypeScript. Cover SocketModeReceiver configuration (appToken, socketMode flag, auto-reconnect, ping/pong), app-level token generation (connections:write scope, xapp- prefix), connection lifecycle events (connected, disconnected, unable_to_socket_mode_start), environment-based transport switching (Socket Mode for dev vs HTTP for prod), combining Socket Mode with OAuth install flows and custom HTTP routes, processEventErrorHandler for retry control, and common pitfalls (signingSecret conflicts, missing @slack/socket-mode package, wrong token type). Source from @slack/bolt SocketModeReceiver.ts, @slack/socket-mode SocketModeClient, and Slack API docs." -
ui.block-kit-ts.md 11.5 KB
# ui.block-kit-ts ## purpose Composing Block Kit messages, interactive elements, modals, and view submissions in Slack Bolt (TypeScript). ## rules 1. Always include a top-level `text` string alongside `blocks[]` -- it serves as the notification preview, screen-reader fallback, and is displayed when blocks cannot render (api.slack.com/reference/messaging/compositions#text). 2. Every interactive element **must** have a unique `action_id` string. Bolt routes interaction payloads by matching `action_id` in `app.action()` listeners (slack.dev/bolt-js/concepts/actions). 3. Call `await ack()` **before** any async work in every `app.action`, `app.view`, `app.options`, and `app.shortcut` handler. Slack requires acknowledgement within 3 seconds or the user sees an error (api.slack.com/interactivity/handling#acknowledgment_response). 4. Use `trigger_id` from the interaction payload to open modals via `client.views.open()`. Trigger IDs expire after 3 seconds (api.slack.com/surfaces/modals#opening). 5. In `view_submission` handlers, form values live at `view.state.values[block_id][action_id].value` (or `.selected_option`, `.selected_date`, etc. depending on element type). Always access via both `block_id` and `action_id` keys (api.slack.com/reference/interaction-payloads/views#view_submission). 6. Blocks array maximum is 50 blocks per message and 100 blocks per modal/home-tab view (api.slack.com/reference/block-kit/blocks). 7. To update an existing message after an interaction, use `respond()` (which hits the `response_url`) for ephemeral/in-channel replacement, or `client.chat.update()` with `channel` + `ts` for precise message targeting (api.slack.com/methods/chat.update). 8. Use `input` blocks (not `section` accessory elements) inside modals when you need form-style data collection; only `input` blocks contribute to `view.state.values` on submission (api.slack.com/surfaces/modals#gathering_input). 9. Set `dispatch_action: true` on an `input` block to receive real-time `block_actions` payloads while the modal is open; without it, the value is only available on submission (api.slack.com/reference/block-kit/blocks#input). 10. Namespace `action_id` values with a domain prefix (e.g., `ticket_create_priority`, `approval_approve_btn`) so regex-based listeners can match groups and handlers stay discoverable. ## patterns ### Composing a message with blocks and a text fallback ```typescript import { App, type KnownBlock } from "@slack/bolt"; function buildTicketBlocks(title: string, assignee: string): KnownBlock[] { return [ { type: "header", text: { type: "plain_text", text: title } }, { type: "section", fields: [ { type: "mrkdwn", text: `*Assignee:*\n${assignee}` }, { type: "mrkdwn", text: `*Status:*\nOpen` }, ], }, { type: "divider" }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "Approve" }, style: "primary", action_id: "ticket_approve_btn", value: "approved", }, { type: "button", text: { type: "plain_text", text: "Reject" }, style: "danger", action_id: "ticket_reject_btn", value: "rejected", }, { type: "static_select", placeholder: { type: "plain_text", text: "Change priority" }, action_id: "ticket_priority_select", options: [ { text: { type: "plain_text", text: "High" }, value: "high" }, { text: { type: "plain_text", text: "Medium" }, value: "med" }, { text: { type: "plain_text", text: "Low" }, value: "low" }, ], }, ], }, ]; } const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.command("/ticket", async ({ ack, say }) => { await ack(); await say({ text: "New ticket: Server outage", // fallback for notifications blocks: buildTicketBlocks("Server outage", "<@U12345>"), }); }); ``` ### Opening a modal and handling view submission ```typescript import type { App, BlockAction, ViewSubmitAction } from "@slack/bolt"; export function registerTicketModal(app: App): void { // Step 1: Button click opens the modal app.action("ticket_create_btn", async ({ ack, body, client }) => { await ack(); const triggerBody = body as BlockAction; await client.views.open({ trigger_id: triggerBody.trigger_id!, view: { type: "modal", callback_id: "ticket_create_modal", title: { type: "plain_text", text: "Create Ticket" }, submit: { type: "plain_text", text: "Submit" }, close: { type: "plain_text", text: "Cancel" }, blocks: [ { type: "input", block_id: "title_block", label: { type: "plain_text", text: "Title" }, element: { type: "plain_text_input", action_id: "title_input", placeholder: { type: "plain_text", text: "Describe the issue" }, }, }, { type: "input", block_id: "priority_block", label: { type: "plain_text", text: "Priority" }, element: { type: "static_select", action_id: "priority_select", options: [ { text: { type: "plain_text", text: "High" }, value: "high" }, { text: { type: "plain_text", text: "Medium" }, value: "med" }, { text: { type: "plain_text", text: "Low" }, value: "low" }, ], }, }, { type: "input", block_id: "due_block", label: { type: "plain_text", text: "Due Date" }, element: { type: "datepicker", action_id: "due_date" }, optional: true, }, ], }, }); }); // Step 2: Handle the submission app.view("ticket_create_modal", async ({ ack, view, client }) => { const vals = view.state.values; const title = vals.title_block.title_input.value!; const priority = vals.priority_block.priority_select.selected_option!.value; const dueDate = vals.due_block.due_date.selected_date; // string | null // Validate -- return errors to keep modal open if (title.length < 5) { await ack({ response_action: "errors", errors: { title_block: "Title must be at least 5 characters." }, }); return; } await ack(); // closes the modal await client.chat.postMessage({ channel: "#tickets", text: `New ticket: ${title}`, blocks: [ { type: "header", text: { type: "plain_text", text: title } }, { type: "section", fields: [ { type: "mrkdwn", text: `*Priority:* ${priority}` }, { type: "mrkdwn", text: `*Due:* ${dueDate ?? "None"}` }, ], }, ], }); }); } ``` ### Handling actions with regex and updating the original message ```typescript import type { App, BlockAction, ButtonAction } from "@slack/bolt"; export function registerTicketActions(app: App): void { // Regex matches both ticket_approve_btn and ticket_reject_btn app.action<BlockAction>( /^ticket_(approve|reject)_btn$/, async ({ ack, action, body, respond }) => { await ack(); const btnAction = action as ButtonAction; const decision = btnAction.value; // "approved" | "rejected" const user = body.user.id; // respond() uses the response_url -- replaces the original message await respond({ replace_original: true, text: `Ticket ${decision} by <@${user}>`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `Ticket *${decision}* by <@${user}>`, }, }, ], }); } ); // Static select handler app.action("ticket_priority_select", async ({ ack, action, respond }) => { await ack(); const selected = (action as { selected_option: { value: string } }) .selected_option.value; await respond({ replace_original: false, text: `Priority changed to *${selected}*`, }); }); } ``` ## pitfalls - **Forgetting `text` fallback**: If you only send `blocks` without a top-level `text` field, push notifications and screen readers show an empty message. Always set `text`. - **`section` accessory vs `input` block in modals**: Interactive elements placed as `section` accessories inside a modal do NOT populate `view.state.values` on submission. Only `input` blocks contribute form data. - **Stale `trigger_id`**: Trigger IDs expire ~3 seconds after the interaction. If you do async work (DB lookup, API call) before `client.views.open()`, call `ack()` immediately and open the view promptly. Do heavy work after the modal is open or on submission. - **Duplicate `action_id` values**: If two elements share the same `action_id` in a single view, Slack silently drops the second. The API returns an error for messages. Always make `action_id` unique per surface. - **`response_url` expiry**: The `response_url` from an interaction payload is valid for 30 minutes and supports up to 5 responses. After that, use `client.chat.update()` with `channel` and `ts`. - **`view_submission` ack with errors**: The `errors` object keys must match `block_id` values, not `action_id`. Mismatched keys silently fail and the modal closes without showing errors. - **Block limits**: Messages allow max 50 blocks; modals and home tabs allow 100. Exceeding these returns `invalid_blocks` error. - **`private_metadata` size limit**: Modal `private_metadata` is capped at 3000 characters. For larger payloads, store data server-side and pass a lookup key. - **`chat.update` requires the original `ts`**: When updating a bot's own message, store the `ts` from the `chat.postMessage` response. The `ts` acts as the message ID. ## references - https://api.slack.com/reference/block-kit/blocks - https://api.slack.com/reference/block-kit/block-elements - https://api.slack.com/reference/block-kit/composition-objects - https://api.slack.com/surfaces/modals - https://api.slack.com/reference/interaction-payloads/views - https://api.slack.com/tools/block-kit-builder - https://api.slack.com/methods/chat.postMessage - https://api.slack.com/methods/chat.update - https://api.slack.com/methods/views.open - https://slack.dev/bolt-js/concepts/actions - https://slack.dev/bolt-js/concepts/view-submissions - https://github.com/slackapi/bolt-js ## instructions This expert covers Slack Block Kit for bot development using @slack/bolt in TypeScript. Use it when you need to: compose messages with blocks (section, actions, header, divider, context, input, image); wire up interactive elements (buttons, selects, datepickers, checkboxes, overflow menus); open and manage modals with trigger_id and client.views.open(); handle view_submission payloads and extract form values from view.state.values; update or replace messages via respond() or client.chat.update(); and follow action_id naming conventions for maintainable routing. The patterns section provides three canonical examples: a message with blocks and interactive buttons, a modal form with submission handling and validation, and regex-based action routing with message updates. ## research Deep Research prompt: "Write a micro expert on Slack Block Kit for bots: composing blocks, interactive elements, action_id patterns, opening modals, handling view submissions, and payload shapes. Provide 2-3 canonical examples and tips for maintainable block templates." -
ui.modals-lifecycle-ts.md 13.3 KB
# ui.modals-lifecycle-ts ## purpose Modal (view) lifecycle management in Slack Bolt TypeScript — opening, updating, pushing, submitting, closing, and input validation. ## rules 1. Open a modal with `client.views.open({ trigger_id, view })`. A `trigger_id` is required and comes from commands, shortcuts, or interactive actions. It expires in ~3 seconds, so call `views.open` immediately after `ack()`. [api.slack.com/surfaces/modals#opening](https://api.slack.com/surfaces/modals#opening) 2. The `view` object requires `type: 'modal'`, `title` (plain_text, max 24 chars), and `blocks`. Optionally include `submit` (button label), `close` (button label), `callback_id` (for submission handling), and `private_metadata` (max 3000 chars of serialized context). [api.slack.com/reference/surfaces/views](https://api.slack.com/reference/surfaces/views) 3. Register submission handlers with `app.view('callback_id', handler)`. The default event type is `view_submission`. The handler receives `view` (the full view state), `body` (the submission event), and `ack`. [slack.dev/bolt-js/concepts/view-submissions](https://slack.dev/bolt-js/concepts/view-submissions) 4. Access input values through `view.state.values[blockId][actionId]`. Each input element's value depends on its type: `.value` for text inputs, `.selected_option` for selects, `.selected_date` for date pickers, `.selected_users` for multi-user selects, `.selected_conversations` for conversation selects, `.files` for file inputs. [api.slack.com/reference/interaction-payloads/views#view_submission_fields](https://api.slack.com/reference/interaction-payloads/views#view_submission_fields) 5. Validate inputs in the submission handler by returning errors from `ack()`: `await ack({ response_action: 'errors', errors: { block_id: 'Error message' } })`. This keeps the modal open and displays inline errors under the specified blocks. [api.slack.com/surfaces/modals#validation](https://api.slack.com/surfaces/modals#validation) 6. Four response actions are available in `ack()` for `view_submission`: `update` (replace current view), `push` (add new view to stack), `clear` (close all views in stack), and `errors` (show validation errors). Calling `ack()` with no arguments simply closes the current modal. [api.slack.com/surfaces/modals#response_actions](https://api.slack.com/surfaces/modals#response_actions) 7. Update a modal mid-interaction with `client.views.update({ view_id, view })`. Use `body.view.id` (from an action inside the modal) as the `view_id`. Optionally pass `hash` (from `body.view.hash`) to prevent race conditions — the update fails if the view changed since you read it. [api.slack.com/methods/views.update](https://api.slack.com/methods/views.update) 8. Push a new modal onto the stack with `client.views.push({ trigger_id, view })`. Up to 3 modals can be stacked. The `trigger_id` must come from an interaction inside the current modal (e.g., a button action). [api.slack.com/methods/views.push](https://api.slack.com/methods/views.push) 9. Handle `view_closed` events by registering `app.view({ callback_id: 'id', type: 'view_closed' }, handler)`. The payload includes `is_cleared` (true if the user clicked "X", false if the modal was programmatically cleared). You must set `notify_on_close: true` in the view definition to receive this event. [api.slack.com/reference/interaction-payloads/views#view_closed](https://api.slack.com/reference/interaction-payloads/views#view_closed) 10. Pass context between the trigger (command/shortcut/action) and the submission handler using `private_metadata`. Serialize the channel ID, user context, or any data needed by the submission handler as a JSON string. [api.slack.com/surfaces/modals#private_metadata](https://api.slack.com/surfaces/modals#private_metadata) 11. Use `view.response_urls` in the submission handler to post messages back to channels when the view contains inputs with `response_url_enabled: true` (only available on `conversations_select` and `channels_select` inputs). [api.slack.com/surfaces/modals#response_url](https://api.slack.com/surfaces/modals#response_url) 12. Modal `title` is limited to 24 characters, `submit` and `close` labels to 24 characters, and `private_metadata` to 3000 characters. Exceeding these limits causes the `views.open` or `views.update` call to fail silently or return an error. [api.slack.com/reference/surfaces/views](https://api.slack.com/reference/surfaces/views) ## patterns ### Multi-step modal with push and update ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Step 1: Open initial modal from a command app.command("/survey", async ({ ack, command, client }) => { await ack(); await client.views.open({ trigger_id: command.trigger_id, view: { type: "modal", callback_id: "survey_step1", title: { type: "plain_text", text: "Survey (1/2)" }, submit: { type: "plain_text", text: "Next" }, close: { type: "plain_text", text: "Cancel" }, private_metadata: JSON.stringify({ channel: command.channel_id }), blocks: [ { type: "input", block_id: "name_block", label: { type: "plain_text", text: "Your Name" }, element: { type: "plain_text_input", action_id: "name_input", }, }, ], }, }); }); // Step 2: Push second modal on submit app.view("survey_step1", async ({ ack, view }) => { const name = view.state.values.name_block.name_input.value!; // Push step 2 onto the modal stack await ack({ response_action: "push", view: { type: "modal", callback_id: "survey_step2", title: { type: "plain_text", text: "Survey (2/2)" }, submit: { type: "plain_text", text: "Submit" }, private_metadata: JSON.stringify({ ...JSON.parse(view.private_metadata || "{}"), name, }), blocks: [ { type: "section", text: { type: "mrkdwn", text: `Thanks, *${name}*! One more question:`, }, }, { type: "input", block_id: "rating_block", label: { type: "plain_text", text: "Rating (1-5)" }, element: { type: "static_select", action_id: "rating_select", options: [1, 2, 3, 4, 5].map((n) => ({ text: { type: "plain_text" as const, text: String(n) }, value: String(n), })), }, }, ], }, }); }); // Final submission: validate and process app.view("survey_step2", async ({ ack, view, client }) => { const meta = JSON.parse(view.private_metadata || "{}"); const rating = view.state.values.rating_block.rating_select.selected_option!.value; // Clear entire modal stack await ack({ response_action: "clear" }); // Post results to original channel await client.chat.postMessage({ channel: meta.channel, text: `Survey from *${meta.name}*: rated ${rating}/5`, }); }); ``` ### Validation with inline errors ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.view("registration_modal", async ({ ack, view, client }) => { const vals = view.state.values; const email = vals.email_block.email_input.value || ""; const age = vals.age_block.age_input.value || ""; const errors: Record<string, string> = {}; if (!email.includes("@") || !email.includes(".")) { errors.email_block = "Please enter a valid email address."; } const ageNum = parseInt(age, 10); if (isNaN(ageNum) || ageNum < 13 || ageNum > 120) { errors.age_block = "Age must be a number between 13 and 120."; } if (Object.keys(errors).length > 0) { // Return errors — modal stays open with inline error messages await ack({ response_action: "errors", errors }); return; } // Valid — close modal await ack(); const meta = JSON.parse(view.private_metadata || "{}"); await client.chat.postMessage({ channel: meta.channel, text: `Registration: ${email}, age ${age}`, }); }); ``` ### Dynamic modal update from button action inside modal ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Handle button click inside a modal — update the view app.action("add_item", async ({ ack, body, client }) => { await ack(); if (!body.view) return; // Parse current items from private_metadata const meta = JSON.parse(body.view.private_metadata || '{"items":[]}'); meta.items.push(`Item ${meta.items.length + 1}`); // Rebuild blocks with updated item list const itemBlocks = meta.items.map((item: string) => ({ type: "section" as const, text: { type: "mrkdwn" as const, text: `• ${item}` }, })); await client.views.update({ view_id: body.view.id, hash: body.view.hash, // Prevent race conditions view: { type: "modal", callback_id: "item_list_modal", title: { type: "plain_text", text: "Item List" }, submit: { type: "plain_text", text: "Done" }, private_metadata: JSON.stringify(meta), blocks: [ ...itemBlocks, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "Add Item" }, action_id: "add_item", }, ], }, ], }, }); }); ``` ## pitfalls - **Calling `views.open` after the trigger_id expires**: The trigger ID from commands, shortcuts, and actions is valid for ~3 seconds. Do any slow work (API calls, database queries) **after** opening the modal, not before. Use `views.update` to populate the modal with data later. - **Missing `callback_id` on the view**: Without `callback_id`, `app.view()` cannot match the submission. The modal will submit but no handler will fire, leaving the user with a spinning submit button. - **Wrong block IDs in error responses**: `response_action: 'errors'` keys must match `block_id` values from `input` blocks in the view. A typo in the block ID silently ignores the error and closes the modal. - **Exceeding `private_metadata` limit**: The 3000-character limit is easy to hit when serializing large objects. Store minimal identifiers (IDs, keys) and look up full data in your submission handler. - **Not setting `notify_on_close: true`**: The `view_closed` event is not sent by default. If you need cleanup logic when users dismiss a modal, set `notify_on_close: true` in the view definition. - **Stacking more than 3 modals**: Slack limits the modal stack to 3 views. A `views.push` call beyond this limit returns an error. Design multi-step flows to use `update` instead of `push` for 4+ steps. - **Using `views.update` on a closed view**: If the user closes the modal before your `views.update` call arrives, the call fails. Wrap in a try/catch when updates happen asynchronously. - **Accessing `view.state.values` on non-input blocks**: Only `input`-type blocks contribute to `view.state.values`. Section blocks with accessories, action blocks, and context blocks do not appear in the values object. ## references - https://api.slack.com/surfaces/modals - https://api.slack.com/surfaces/modals#opening - https://api.slack.com/surfaces/modals#updating - https://api.slack.com/surfaces/modals#response_actions - https://api.slack.com/surfaces/modals#validation - https://api.slack.com/methods/views.open - https://api.slack.com/methods/views.update - https://api.slack.com/methods/views.push - https://api.slack.com/reference/surfaces/views - https://slack.dev/bolt-js/concepts/view-submissions - https://github.com/slackapi/bolt-js/blob/main/src/types/view/index.ts ## instructions This expert covers the full modal (view) lifecycle in Slack Bolt TypeScript. Use it when: opening modals from commands, shortcuts, or button actions; building multi-step modal flows with push and update; handling `view_submission` with input validation and error responses; dynamically updating modals in response to in-modal interactions; using `private_metadata` to pass context from trigger to submission; handling `view_closed` for cleanup; or working with `response_url` in modal submissions. Pair with: `runtime.ack-rules-ts.md` for ack timing on view submissions. `ui.block-kit-ts.md` for constructing modal block layouts and input elements. `runtime.shortcuts-ts.md` when modals are opened from shortcuts. `runtime.slash-commands-ts.md` when modals are opened from commands. ## research Deep Research prompt: "Write a micro expert on Slack modal (view) lifecycle management in Bolt TypeScript. Cover views.open (trigger_id, view structure with callback_id/title/blocks/submit/close/private_metadata), views.update (view_id, hash for race conditions), views.push (modal stacking, 3-view limit), app.view() submission handlers (view.state.values access patterns for different input types), response_action variants (update, push, clear, errors), input validation with inline errors, view_closed events (notify_on_close, is_cleared), private_metadata for context passing, response_url_enabled for channel-targeted responses, and character limits (title 24, private_metadata 3000). Source from @slack/bolt types/view/index.ts, Slack API modal docs, and bolt-js view handler implementation." -
web-api-proactive-ts.md 11 KB
# web-api-proactive-ts ## purpose Slack Web API client usage and proactive messaging patterns in Bolt TypeScript apps — sending messages outside event handlers, user lookups, conversation management. ## rules 1. Access the Web API client via `client` in any listener context or via `app.client` for proactive messaging outside of handlers. The `client` in listeners is pre-configured with the correct token for the workspace; `app.client` uses the default token from the `App` constructor. [slack.dev/bolt-js/concepts/web-api](https://slack.dev/bolt-js/concepts/web-api) 2. Use `client.chat.postMessage({ channel, text })` to send messages to any channel or DM. The `channel` parameter accepts a channel ID, DM channel ID, or user ID (to open/reuse a DM). Always include `text` as a fallback even when using `blocks`. [api.slack.com/methods/chat.postMessage](https://api.slack.com/methods/chat.postMessage) 3. Use `client.chat.update({ channel, ts, text })` to edit an existing message. Both `channel` and `ts` (the message timestamp) are required to identify the message. Only messages posted by the bot can be updated. [api.slack.com/methods/chat.update](https://api.slack.com/methods/chat.update) 4. Use `client.chat.delete({ channel, ts })` to delete a bot-posted message. For user messages, use `chat.delete` with a user token that has `chat:write` scope. [api.slack.com/methods/chat.delete](https://api.slack.com/methods/chat.delete) 5. Post threaded replies by setting `thread_ts` to the parent message's `ts`. Set `reply_broadcast: true` to also post the reply to the channel as a "replied to a thread" message. [api.slack.com/methods/chat.postMessage](https://api.slack.com/methods/chat.postMessage) 6. Send ephemeral messages (visible only to one user) with `client.chat.postEphemeral({ channel, user, text })`. Ephemeral messages cannot be updated or deleted — they disappear when the user reloads. [api.slack.com/methods/chat.postEphemeral](https://api.slack.com/methods/chat.postEphemeral) 7. Schedule future messages with `client.chat.scheduleMessage({ channel, text, post_at })`. The `post_at` parameter is a Unix timestamp. Scheduled messages can be cancelled with `client.chat.deleteScheduledMessage()` before they post. [api.slack.com/methods/chat.scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) 8. Look up users with `client.users.info({ user })` or list workspace members with `client.users.list()`. For email-to-user mapping, use `client.users.lookupByEmail({ email })`. These require the `users:read` and `users:read.email` scopes respectively. [api.slack.com/methods/users.info](https://api.slack.com/methods/users.info) 9. Manage conversations with `client.conversations.list()`, `client.conversations.info({ channel })`, `client.conversations.members({ channel })`, and `client.conversations.history({ channel })`. Use cursor-based pagination for large result sets — check `response_metadata.next_cursor`. [api.slack.com/methods/conversations.list](https://api.slack.com/methods/conversations.list) 10. Upload files with `client.filesUploadV2({ channel_id, file, filename })`. The v2 method is required — the original `files.upload` is deprecated. For multiple files, pass an array to `file_uploads`. Requires `files:write` scope. [api.slack.com/methods/files.uploadV2](https://api.slack.com/methods/files.uploadV2) 11. For proactive messaging (no incoming event), store the target `channel` ID and bot `token` during installation or a prior interaction. Use `app.client` with an explicit `token` parameter since there is no listener context to infer the workspace. [slack.dev/bolt-js/concepts/web-api](https://slack.dev/bolt-js/concepts/web-api) 12. Handle rate limiting by catching errors with `code === 'slack_webapi_platform_error'` and checking for `retry_after` in the response headers. The `@slack/web-api` client has built-in retry logic with configurable `retryConfig`. [api.slack.com/docs/rate-limits](https://api.slack.com/docs/rate-limits) 13. Distinguish `say()`, `respond()`, and `client` for the right messaging pattern: `say()` posts to the event's channel (requires channel context); `respond()` uses the `response_url` (commands, actions, shortcuts — ephemeral by default, expires in 30 min); `client.chat.postMessage()` works anywhere with explicit channel and token. [slack.dev/bolt-js/concepts/commands](https://slack.dev/bolt-js/concepts/commands) ## patterns ### Proactive message from a cron job or external trigger ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Send a daily standup reminder — no incoming Slack event needed async function sendStandupReminder(channelId: string) { await app.client.chat.postMessage({ token: process.env.SLACK_BOT_TOKEN!, channel: channelId, text: "Time for standup! What did you work on yesterday?", blocks: [ { type: "section", text: { type: "mrkdwn", text: ":sunrise: *Daily Standup*\nPlease share your update in this thread.", }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "Post Update" }, action_id: "standup_post", style: "primary", }, ], }, ], }); } ``` ### Update and delete messages ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.action("approve_request", async ({ ack, body, client }) => { await ack(); // Update the original message to reflect approval if (body.channel && body.message) { await client.chat.update({ channel: body.channel.id, ts: body.message.ts, text: `Request approved by <@${body.user.id}>`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `:white_check_mark: *Approved* by <@${body.user.id}>`, }, }, ], }); } }); app.action("delete_message", async ({ ack, body, client }) => { await ack(); if (body.channel && body.message) { await client.chat.delete({ channel: body.channel.id, ts: body.message.ts, }); } }); ``` ### Threaded replies and ephemeral messages ```typescript import { App } from "@slack/bolt"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); app.message("help", async ({ message, client }) => { // Reply in a thread await client.chat.postMessage({ channel: message.channel, thread_ts: message.ts, text: "Here's what I can do...", }); // Ephemeral hint visible only to the user if (message.subtype === undefined) { await client.chat.postEphemeral({ channel: message.channel, user: message.user, text: "I replied with help in a thread above.", }); } }); ``` ### Cursor-based pagination for user list ```typescript import { App } from "@slack/bolt"; import type { Member } from "@slack/web-api/dist/types/response/UsersListResponse"; const app = new App({ token: process.env.SLACK_BOT_TOKEN!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); async function getAllMembers(): Promise<Member[]> { const members: Member[] = []; let cursor: string | undefined; do { const result = await app.client.users.list({ token: process.env.SLACK_BOT_TOKEN!, limit: 200, cursor, }); if (result.members) { members.push(...result.members); } cursor = result.response_metadata?.next_cursor || undefined; } while (cursor); return members; } ``` ## pitfalls - **Using `app.client` without an explicit `token`**: Outside of listener contexts, `app.client` has no workspace-specific token. Always pass `token` explicitly for proactive messages in multi-workspace apps. - **Sending `blocks` without `text` fallback**: Slack requires `text` as a fallback for notifications and accessibility. Messages with only `blocks` and no `text` show a blank notification on mobile. - **Confusing channel ID with channel name**: All Web API methods require the **channel ID** (e.g., `C01234ABCDE`), not the channel name (e.g., `#general`). Use `conversations.list` to resolve names to IDs. - **Deprecated `files.upload`**: The original `files.upload` method is deprecated. Use `client.filesUploadV2()` which handles the multi-step upload process automatically. - **Ephemeral messages are fire-and-forget**: You cannot update or delete ephemeral messages. They vanish on reload. Do not use them for persistent information. - **Pagination truncation**: Methods like `users.list`, `conversations.list`, and `conversations.history` return at most 100–200 results per call. Always paginate with `cursor` to avoid silently missing data. - **Rate limits on bulk sends**: Sending messages to many channels in a loop can trigger Slack's rate limits (roughly 1 message per second per channel, 50+ messages per minute burst). Add delays or use `chat.scheduleMessage` to spread load. ## references - https://api.slack.com/methods/chat.postMessage - https://api.slack.com/methods/chat.update - https://api.slack.com/methods/chat.delete - https://api.slack.com/methods/chat.postEphemeral - https://api.slack.com/methods/chat.scheduleMessage - https://api.slack.com/methods/users.info - https://api.slack.com/methods/users.lookupByEmail - https://api.slack.com/methods/conversations.list - https://api.slack.com/methods/files.uploadV2 - https://api.slack.com/docs/rate-limits - https://slack.dev/bolt-js/concepts/web-api ## instructions This expert covers Slack Web API client usage and proactive messaging in Bolt TypeScript. Use it when: sending messages outside of event handlers (cron jobs, webhooks, external triggers); updating or deleting existing messages; posting threaded replies or ephemeral messages; looking up users by ID or email; listing and paginating conversations or members; uploading files; scheduling messages for future delivery; or choosing between `say()`, `respond()`, and `client.chat.postMessage()`. Pair with: `runtime.bolt-foundations-ts.md` for App setup and client initialization. `runtime.ack-rules-ts.md` when combining proactive messages with listener handlers. `ui.block-kit-ts.md` for constructing rich message payloads. ## research Deep Research prompt: "Write a micro expert on Slack Web API client usage and proactive messaging in Bolt TypeScript. Cover app.client vs listener context client, chat.postMessage/update/delete, threaded replies (thread_ts, reply_broadcast), ephemeral messages (chat.postEphemeral), scheduled messages (chat.scheduleMessage), user lookups (users.info, users.lookupByEmail, users.list with pagination), conversation management (conversations.list/info/members/history with cursor pagination), file uploads (filesUploadV2), proactive messaging patterns (cron jobs, external triggers, stored channel IDs), rate limiting and retry behavior, and the say() vs respond() vs client distinction. Source from @slack/bolt App.ts, @slack/web-api WebClient, and Slack API method docs." -
workflow.slack-automations-ts.md 10.2 KB
# workflow.slack-automations-ts ## purpose Cover the Slack next-gen automation platform (Workflow Builder, custom functions, triggers, datastores) for understanding the competitive baseline and supporting cross-platform workflow design. ## rules 1. **Slack's next-gen platform is function-based.** Workflows are composed of steps, and each step is a function. Functions can be built-in (send message, create channel) or custom (developer-defined). Custom functions are defined in the app manifest and implemented as event handlers. [api.slack.com -- Functions](https://api.slack.com/automation/functions) 2. **Workflows are defined declaratively in `manifest.ts`.** Use `DefineWorkflow` to compose steps from functions. Each step specifies inputs (from trigger outputs, previous step outputs, or literals) and produces outputs for downstream steps. [api.slack.com -- Workflows](https://api.slack.com/automation/workflows) 3. **Triggers start workflows.** Four trigger types: (a) **Link triggers** — URL click, (b) **Shortcut triggers** — from channel compose menu, (c) **Event triggers** — fire on Slack events (message posted, reaction added, member joined), (d) **Scheduled triggers** — cron-like recurring execution. [api.slack.com -- Triggers](https://api.slack.com/automation/triggers) 4. **Custom functions run on Slack's hosted infrastructure (Deno).** The next-gen platform runs functions on Slack's infrastructure using Deno. No external hosting needed. Functions receive `inputs` and return `outputs` defined by their schema. `slack deploy` pushes code to Slack's runtime. 5. **Datastores provide built-in persistence.** `DefineDatastore` creates a schematized key-value store on Slack's platform. Functions can CRUD datastore records. No external database needed for simple workflows. Limited to 50,000 records per datastore. [api.slack.com -- Datastores](https://api.slack.com/automation/datastores) 6. **Forms collect structured input in-channel.** The `OpenForm` built-in function opens a modal form in the channel context. Form fields map to workflow inputs. This is Slack's equivalent of Teams' task module / message extension action. 7. **Workflow Builder provides no-code authoring.** Non-technical users can create workflows visually in Slack's Workflow Builder UI — selecting triggers, adding steps, mapping variables between steps. This is the key UX advantage over Teams' Power Automate. 8. **Slack workflows are channel-scoped, not cross-app.** Each workflow runs within the app that defines it. There's no cross-app orchestration or marketplace of reusable steps. This limits ecosystem extensibility compared to Power Automate's connector model. 9. **No operational integrations (presence, shifts, call queues).** Slack lacks APIs for presence-driven triggers, shift management, or call queue operations. Workflow triggers are limited to messaging events, schedules, and webhooks. This is Teams' primary competitive advantage for frontline workflows. 10. **Interactivity through Block Kit, not Universal Actions.** Slack workflow steps can send Block Kit messages with interactive elements (buttons, selects, overflow menus). Interactions route back to the workflow, but there's no card-refresh-in-place pattern — interactions typically open modals or send new messages. 11. **`workflow_step_execute` is legacy.** The older `workflow_step_execute` event pattern (Bolt v3) is being replaced by the function-based model. New development should use `DefineFunction` + `DefineWorkflow` on the next-gen platform. ## patterns ### Define a custom function ```typescript import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; export const CreatePtoRequestFn = DefineFunction({ callback_id: "create_pto_request", title: "Create PTO Request", source_file: "functions/create_pto_request.ts", input_parameters: { properties: { requester: { type: Schema.slack.types.user_id }, start_date: { type: "string" }, end_date: { type: "string" }, reason: { type: "string" }, }, required: ["requester", "start_date", "end_date"], }, output_parameters: { properties: { request_id: { type: "string" }, status: { type: "string" }, }, required: ["request_id", "status"], }, }); export default SlackFunction(CreatePtoRequestFn, async ({ inputs, client }) => { // Store in datastore const result = await client.apps.datastore.put({ datastore: "pto_requests", item: { id: crypto.randomUUID(), requester: inputs.requester, start_date: inputs.start_date, end_date: inputs.end_date, reason: inputs.reason || "", status: "pending", created_at: new Date().toISOString(), }, }); return { outputs: { request_id: result.item.id, status: "pending", }, }; }); ``` ### Define a workflow with triggers ```typescript import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; import { CreatePtoRequestFn } from "../functions/create_pto_request.ts"; export const PtoWorkflow = DefineWorkflow({ callback_id: "pto_workflow", title: "Request Time Off", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity }, channel: { type: Schema.slack.types.channel_id }, }, required: ["interactivity"], }, }); // Step 1: Collect input via form const formStep = PtoWorkflow.addStep(Schema.slack.functions.OpenForm, { title: "Request Time Off", interactivity: PtoWorkflow.inputs.interactivity, submit_label: "Submit Request", fields: { elements: [ { name: "start_date", title: "Start Date", type: Schema.types.string }, { name: "end_date", title: "End Date", type: Schema.types.string }, { name: "reason", title: "Reason (optional)", type: Schema.types.string, long: true }, ], required: ["start_date", "end_date"], }, }); // Step 2: Create the PTO record const createStep = PtoWorkflow.addStep(CreatePtoRequestFn, { requester: PtoWorkflow.inputs.interactivity.interactor.id, start_date: formStep.outputs.fields.start_date, end_date: formStep.outputs.fields.end_date, reason: formStep.outputs.fields.reason, }); // Step 3: Post confirmation to channel PtoWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: PtoWorkflow.inputs.channel, message: `PTO request submitted by <@${PtoWorkflow.inputs.interactivity.interactor.id}>: ${formStep.outputs.fields.start_date} to ${formStep.outputs.fields.end_date} (Status: ${createStep.outputs.status})`, }); ``` ### Define a datastore ```typescript import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts"; export const PtoDatastore = DefineDatastore({ name: "pto_requests", primary_key: "id", attributes: { id: { type: Schema.types.string }, requester: { type: Schema.slack.types.user_id }, start_date: { type: Schema.types.string }, end_date: { type: Schema.types.string }, reason: { type: Schema.types.string }, status: { type: Schema.types.string }, created_at: { type: Schema.types.string }, }, }); ``` ### Competitive comparison matrix | Capability | Slack Next-Gen Platform | Teams Message-Native Vision | |---|---|---| | No-code authoring | Workflow Builder GUI | Power Automate (external) | | In-channel initiation | Shortcut triggers, link triggers | Bot commands, message extensions | | Structured input | OpenForm built-in function | Task modules / Adaptive Card forms | | State persistence | Datastores (50K record limit) | SharePoint Lists (30M record limit) | | Operational triggers | Messaging events only | Presence, Shifts, call queues, Graph | | Card interactivity | Block Kit (new message on action) | Adaptive Cards (in-place refresh) | | NL querying | Not built-in | AI function calling over structured state | | Execution runtime | Slack-hosted Deno | Bot hosting (any cloud) or Power Automate | | Ecosystem | Single-app scoped | Power Platform connectors, Graph API | | Frontline integration | None | Shifts, presence, call queues | ## pitfalls - **Deno runtime is Slack-only.** Code written for the next-gen platform doesn't run outside Slack's infrastructure. No local hosting, no Azure/AWS deployment. This limits portability. - **50,000 record datastore limit.** For high-volume workflows, Slack datastores hit their limit quickly. No built-in archival or pagination beyond simple queries. - **No card refresh pattern.** Slack has no equivalent to Teams' `Action.Execute` → card replacement. Interactive elements send new messages or open modals. This creates message sprawl for multi-step workflows. - **Workflow Builder workflows are not version-controlled.** Workflows created in the GUI exist only in Slack's cloud. No git, no code review, no rollback. Code-defined workflows (manifest.ts) don't have this problem. - **Limited event trigger types.** Event triggers cover message events and membership changes, but not presence, file events, or external system state. Webhook triggers partially fill this gap but require external orchestration. ## references - https://api.slack.com/automation/functions - https://api.slack.com/automation/workflows - https://api.slack.com/automation/triggers - https://api.slack.com/automation/datastores - https://api.slack.com/automation/functions/custom ## instructions Use this expert for understanding the Slack next-gen automation platform when doing competitive analysis or cross-platform workflow design. Covers custom functions, declarative workflows, trigger types, datastores, Workflow Builder, and the competitive gap analysis against Teams' message-native vision. Pair with `../bridge/workflow.composable-platform-ts.md` for the Teams architectural response, and `../bridge/workflows-automation-ts.md` for migration patterns between platforms. ## research Deep Research prompt: "Write a micro expert on the Slack next-gen automation platform (TypeScript/Deno). Cover: DefineFunction for custom functions, DefineWorkflow for declarative step composition, trigger types (link, shortcut, event, scheduled), DefineDatastore for built-in persistence, OpenForm for structured input collection, Workflow Builder no-code authoring, and limitations vs Teams (no presence/Shifts triggers, no card refresh, 50K datastore limit). Include a competitive comparison matrix against Teams message-native workflow capabilities."
-
-
teams
-
a2a.client-basics-ts.md 9.7 KB
# a2a.client-basics-ts ## purpose Calling remote A2A agents from a Teams bot using A2AClientPlugin as a ChatPrompt plugin with automatic delegation. ## rules 1. Create an `A2AClientPlugin` instance and pass it as a ChatPrompt plugin in the second argument array: `new ChatPrompt({ ... }, [new A2AClientPlugin()])`. The plugin registers itself under the name `'a2a'`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Connect to remote A2A agents using `.usePlugin('a2a', { key, cardUrl })` chained on the ChatPrompt. The `key` is a unique identifier for the agent and `cardUrl` is the URL to its agent card JSON. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Connect to multiple agents by chaining multiple `.usePlugin('a2a', { key, cardUrl })` calls. Each agent's skills are registered as callable functions, and the LLM decides which agent to delegate to based on skill descriptions. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 4. The `key` parameter must be unique across all connected agents. It is used internally to identify the agent when the LLM invokes delegation functions. Use short, descriptive keys (e.g., `'weather'`, `'calendar'`). [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. The `cardUrl` must point to the agent's well-known agent card endpoint (e.g., `http://localhost:4000/a2a/.well-known/agent-card.json`). The plugin fetches the card at connection time to read the agent's skills and capabilities. [google.github.io/A2A -- Discovery](https://google.github.io/A2A/) 6. The LLM automatically delegates to connected agents as if they were function calls. No explicit delegation code is needed -- the agent's skills appear as functions the LLM can invoke during `prompt.send()`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. Install `@microsoft/teams.a2a` as a dependency. The A2A client and server plugins are both in this package. Also install `@microsoft/teams.ai` and `@microsoft/teams.openai` for the ChatPrompt and model. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. A2AClientPlugin is a ChatPrompt plugin, not an App plugin. Pass it to `new ChatPrompt()`, not to `new App({ plugins: [...] })`. Adding it to the App has no effect. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Agent cards are fetched once at connection time. If a remote agent's skills change, the client bot must be restarted or the plugin must be re-configured to pick up the new skills. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Handle `prompt.send()` failures gracefully. If a remote agent is unreachable or returns an error, the LLM receives an error result for that function call. Wrap `prompt.send()` in try/catch and inform the user. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## patterns ### Basic A2A client calling one agent ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { A2AClientPlugin } from '@microsoft/teams.a2a'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt( { model, instructions: 'You are an orchestrator. Delegate weather questions to the weather agent.', }, [new A2AClientPlugin()], // Pass as ChatPrompt plugin ) // Connect to a remote weather agent .usePlugin('a2a', { key: 'weather', cardUrl: 'http://localhost:4000/a2a/.well-known/agent-card.json', }); const app = new App({ logger: new ConsoleLogger('a2a-client', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); // The LLM can now call the weather agent as a function app.on('message', async ({ send, activity }) => { await send({ type: 'typing' }); const result = await prompt.send(activity.text); if (result.content) { await send(result.content); } }); app.start(3978); ``` ### Multiple agents for different domains ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { A2AClientPlugin } from '@microsoft/teams.a2a'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt( { model, instructions: `You are a helpful office assistant. You can delegate to specialized agents: - Weather agent: for weather questions - Calendar agent: for scheduling and calendar queries - IT agent: for IT support and ticket management Route user requests to the appropriate agent. If a request does not match any agent, answer it yourself.`, }, [new A2AClientPlugin()], ) .usePlugin('a2a', { key: 'weather', cardUrl: 'http://weather-agent:4000/a2a/.well-known/agent-card.json', }) .usePlugin('a2a', { key: 'calendar', cardUrl: 'http://calendar-agent:4001/a2a/.well-known/agent-card.json', }) .usePlugin('a2a', { key: 'it-support', cardUrl: 'http://it-agent:4002/a2a/.well-known/agent-card.json', }); const app = new App({ logger: new ConsoleLogger('multi-agent-client'), plugins: [new DevtoolsPlugin()], }); app.on('message', async ({ send, activity }) => { await send({ type: 'typing' }); try { const result = await prompt.send(activity.text); if (result.content) { await send(result.content); } } catch (err: any) { await send('Sorry, I encountered an error processing your request.'); } }); app.start(3978); ``` ### Combining A2A agents with local functions ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { A2AClientPlugin } from '@microsoft/teams.a2a'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt( { model, instructions: 'You are a helpful assistant with access to both local tools and remote agents.', }, [new A2AClientPlugin()], ) // Remote agent for weather .usePlugin('a2a', { key: 'weather', cardUrl: 'http://localhost:4000/a2a/.well-known/agent-card.json', }) // Local function for time .function('getTime', 'Get the current date and time', () => { return new Date().toISOString(); }) // Local function for calculations .function( 'calculate', 'Evaluate a math expression', { type: 'object', properties: { expression: { type: 'string', description: 'Math expression' }, }, required: ['expression'], }, ({ expression }: { expression: string }) => { return String(eval(expression)); // Use safe parser in production } ); // The LLM sees both remote A2A agents and local functions ``` ## pitfalls - **Wrong `usePlugin` name**: The first argument must be the string `'a2a'` exactly. A typo means the plugin is never activated and no agents are connected. - **Duplicate agent keys**: Using the same `key` for two agents causes one to overwrite the other. Each key must be unique. - **Unreachable agent card URL**: If the `cardUrl` is wrong or the remote agent is not running, the card fetch fails and the agent's skills are not registered. The LLM will not know the agent exists. - **Passing A2AClientPlugin to App**: `A2AClientPlugin` is a ChatPrompt plugin, not an App plugin. Adding it to `new App({ plugins: [...] })` has no effect and the LLM will not see any agents. - **No error handling**: If a remote agent fails during delegation, `prompt.send()` may throw. Always wrap in try/catch and provide a fallback response to the user. - **Vague orchestrator instructions**: The LLM needs clear instructions about which agents handle which types of requests. Without guidance, delegation may be inconsistent or incorrect. - **Agent card caching**: Agent cards are fetched once. If the remote agent updates its skills, the client bot must restart to see the changes. ## references - [A2A Protocol Specification](https://google.github.io/A2A/) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.a2a npm](https://www.npmjs.com/package/@microsoft/teams.a2a) - [A2A Agent Card documentation](https://google.github.io/A2A/#/documentation?id=agent-card) ## instructions This expert covers calling remote A2A agents from a Teams bot using `A2AClientPlugin` from `@microsoft/teams.a2a` in TypeScript. Use it when you need to: - Create an `A2AClientPlugin` and pass it as a ChatPrompt plugin - Connect to one or more remote A2A agents via `.usePlugin('a2a', { key, cardUrl })` - Understand how the LLM automatically delegates to agents as function calls - Combine A2A agent delegation with locally defined `.function()` tools - Handle delegation errors and fallback responses Pair with `a2a.server-basics-ts.md` for building the agent being called, and `a2a.orchestrator-patterns-ts.md` for advanced multi-agent coordination patterns. Pair with `ai.chatprompt-basics-ts.md` for ChatPrompt constructor where A2AClientPlugin is passed, and `a2a.orchestrator-patterns-ts.md` for multi-agent coordination. ## research Deep Research prompt: "Write a micro expert on using A2AClientPlugin in a Teams bot (TypeScript). Cover creating the plugin, passing it to ChatPrompt, .usePlugin('a2a', { key, cardUrl }) for connecting to agents, how the LLM automatically delegates, connecting to multiple agents, combining with local functions, error handling, and common pitfalls. Include 2-3 TypeScript code examples." -
a2a.orchestrator-patterns-ts.md 12.7 KB
# a2a.orchestrator-patterns-ts ## purpose Multi-agent orchestration patterns: routing, delegation, custom behavior, and coordination between A2A agents. ## rules 1. The orchestrator pattern uses one primary bot with `A2AClientPlugin` that delegates to multiple specialized A2A agents. The orchestrator's instructions guide the LLM on when and how to delegate. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Customize delegation behavior by passing options to the `A2AClientPlugin` constructor: `buildFunctionMetadata`, `buildMessageForAgent`, and `buildMessageFromAgentResponse`. These hooks control how agents appear as functions and how messages are formatted. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Use `buildFunctionMetadata(card)` to customize how each agent appears to the LLM. Return `{ name, description }` to control the function name and description the LLM sees. This allows consistent naming conventions across agents. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 4. Use `buildMessageForAgent(card, input)` to transform the outgoing message before it reaches the remote agent. This lets you add context, reformat the request, or inject routing metadata. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. Use `buildMessageFromAgentResponse(card, response)` to transform the remote agent's response before returning it to the LLM. This lets you format, summarize, or annotate responses. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. Write explicit orchestrator instructions that list available agents by name and describe their capabilities. The LLM uses these instructions plus the agent function descriptions to decide delegation. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. Combine A2A agents with MCP tools in the same ChatPrompt. Use `A2AClientPlugin` for agent delegation and `McpClientPlugin` for tool invocation. The LLM sees both as callable functions. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. Prevent infinite delegation loops by designing clear skill boundaries between agents. An orchestrator should not delegate to an agent that delegates back to the orchestrator. Keep delegation unidirectional. [google.github.io/A2A -- Best practices](https://google.github.io/A2A/) 9. Add fallback handling in orchestrator instructions: if no agent matches the user's request, the orchestrator should answer directly or inform the user that the request cannot be handled. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Test multi-agent flows end-to-end. Start all agent processes, verify agent card URLs are reachable, and test delegation with representative user queries for each agent's skill set. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## patterns ### Orchestrator with custom delegation behavior ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { A2AClientPlugin } from '@microsoft/teams.a2a'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt( { model, instructions: `You are the main office assistant. You coordinate with specialized agents: - askWeatherAgent: handles weather and forecast questions - askCalendarAgent: handles scheduling and calendar queries - askITAgent: handles IT support, password resets, and tickets Delegate to the appropriate agent based on the user's request. If no agent matches, answer the question yourself. Always present the agent's response clearly to the user.`, }, [new A2AClientPlugin({ // Customize how agent functions are named buildFunctionMetadata: (card) => ({ name: `ask${card.name.replace(/\s+/g, '')}`, description: `Ask ${card.name}: ${card.description}`, }), // Customize outgoing messages to agents buildMessageForAgent: (card, input) => { return `[Request from orchestrator to ${card.name}]: ${input}`; }, // Customize how agent responses are returned to the LLM buildMessageFromAgentResponse: (card, response) => { if (response.kind === 'message') { const text = response.parts .filter((p: any) => p.kind === 'text') .map((p: any) => p.text) .join(' '); return `${card.name} responded: ${text}`; } return `${card.name} sent a non-text response.`; }, })], ) .usePlugin('a2a', { key: 'weather', cardUrl: 'http://weather-agent:4000/a2a/.well-known/agent-card.json', }) .usePlugin('a2a', { key: 'calendar', cardUrl: 'http://calendar-agent:4001/a2a/.well-known/agent-card.json', }) .usePlugin('a2a', { key: 'it-support', cardUrl: 'http://it-agent:4002/a2a/.well-known/agent-card.json', }); const app = new App({ logger: new ConsoleLogger('orchestrator', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); app.on('message', async ({ send, stream, activity }) => { await send({ type: 'typing' }); try { const result = await prompt.send(activity.text); if (result.content) { await send(result.content); } } catch (err: any) { await send('Sorry, one of my specialized agents is unavailable. Please try again later.'); } }); app.start(3978); ``` ### Combining A2A agents with MCP tools ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { A2AClientPlugin } from '@microsoft/teams.a2a'; import { McpClientPlugin } from '@microsoft/teams.mcpclient'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const logger = new ConsoleLogger('hybrid-orchestrator'); const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt( { model, instructions: `You are a powerful assistant with access to: - Remote agents for complex tasks (weather, IT support) - MCP tools for data lookup (search, documents) - Local functions for simple operations (time, math) Choose the best tool or agent for each request.`, }, [ new A2AClientPlugin(), new McpClientPlugin({ logger }), ], ) // A2A agents for complex, conversational tasks .usePlugin('a2a', { key: 'weather', cardUrl: 'http://weather-agent:4000/a2a/.well-known/agent-card.json', }) .usePlugin('a2a', { key: 'it-support', cardUrl: 'http://it-agent:4002/a2a/.well-known/agent-card.json', }) // MCP tools for data access .usePlugin('mcpClient', { url: 'http://localhost:5000/mcp', }) // Local function .function('getTime', 'Get the current date and time', () => { return new Date().toISOString(); }); const app = new App({ logger, plugins: [new DevtoolsPlugin()], }); app.on('message', async ({ send, activity }) => { await send({ type: 'typing' }); const result = await prompt.send(activity.text); if (result.content) { await send(result.content); } }); app.start(3978); ``` ### Orchestrator that is also an A2A server ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { A2APlugin, A2AClientPlugin } from '@microsoft/teams.a2a'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); // This bot is both an A2A server AND an A2A client // It can be called by other agents AND delegate to downstream agents const prompt = new ChatPrompt( { model, instructions: `You are a coordinator agent. You handle general queries and delegate specialized tasks to sub-agents.`, }, [new A2AClientPlugin()], ) .usePlugin('a2a', { key: 'weather', cardUrl: 'http://weather-agent:4000/a2a/.well-known/agent-card.json', }); // Agent card so other agents can call this orchestrator const agentCard = { name: 'Coordinator Agent', description: 'A coordinator that routes requests to specialized agents', url: 'http://localhost:3978/a2a', version: '1.0.0', protocolVersion: '0.3.0', capabilities: {}, skills: [ { id: 'coordinate', name: 'Coordinate Request', description: 'Route a request to the best available specialist agent', tags: ['coordination', 'routing'], examples: ['What is the weather in Paris?', 'Help me with a general question'], }, ], }; const app = new App({ logger: new ConsoleLogger('coordinator'), plugins: [ new DevtoolsPlugin(), new A2APlugin({ agentCard }), // Server: accept A2A messages ], }); // Handle A2A messages from other agents app.event('a2a:message', async ({ respond, requestContext }) => { const textInput = requestContext.userMessage.parts .filter((p: any) => p.kind === 'text') .at(0)?.text; if (!textInput) { await respond('Please send a text message.'); return; } const result = await prompt.send(textInput); await respond(result.content || 'Unable to process request.'); }); // Handle direct Teams messages app.on('message', async ({ send, activity }) => { const result = await prompt.send(activity.text); if (result.content) await send(result.content); }); app.start(3978); ``` ## pitfalls - **Infinite delegation loops**: Agent A delegates to Agent B which delegates back to Agent A. Design clear, unidirectional delegation hierarchies. Do not connect an orchestrator to an agent that connects back to the orchestrator. - **Inconsistent function naming**: Without `buildFunctionMetadata`, agent function names are auto-generated from the agent card. This can produce confusing names for the LLM. Use the hook to standardize naming. - **Overloading the orchestrator prompt**: Too many agents with overlapping skill descriptions confuse the LLM about where to delegate. Keep agent responsibilities distinct and non-overlapping. - **Not handling partial failures**: In a multi-agent system, one agent may be down while others are available. The orchestrator should gracefully handle individual agent failures without crashing the entire flow. - **Missing fallback behavior**: If no agent matches the user's request, the orchestrator needs instructions to answer directly. Without fallback instructions, the LLM may force a bad delegation. - **Forgetting A2AClientPlugin is a ChatPrompt plugin**: It must be passed to `new ChatPrompt()`, not to `new App()`. This is a common mistake when combining A2A with App-level plugins like `A2APlugin`. - **Stale agent cards**: If a downstream agent changes its skills, the orchestrator will not see the updates until it is restarted. Plan for periodic restarts or manual refresh in dynamic environments. ## references - [A2A Protocol Specification](https://google.github.io/A2A/) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.a2a npm](https://www.npmjs.com/package/@microsoft/teams.a2a) - [Model Context Protocol -- Introduction](https://modelcontextprotocol.io/introduction) ## instructions This expert covers multi-agent orchestration patterns for Teams bots using A2A and MCP in the Teams AI Library v2 (`@microsoft/teams.ts`). Use it when you need to: - Build an orchestrator bot that delegates to multiple specialized A2A agents - Customize delegation behavior with `buildFunctionMetadata`, `buildMessageForAgent`, and `buildMessageFromAgentResponse` - Combine A2A agents with MCP tools and local functions in a single ChatPrompt - Build a bot that is both an A2A server and client (bidirectional agent) - Design delegation hierarchies that avoid infinite loops - Handle partial failures in multi-agent systems Pair with `a2a.client-basics-ts.md` for basic client setup, `a2a.server-basics-ts.md` for building the agents being called, and `mcp.client-basics-ts.md` for combining MCP tools. Pair with `a2a.server-basics-ts.md` and `a2a.client-basics-ts.md` for foundational A2A setup, and `mcp.client-basics-ts.md` when combining A2A with MCP tool consumption. ## research Deep Research prompt: "Write a micro expert on multi-agent orchestration patterns for Teams bots (TypeScript). Cover orchestrator design with A2AClientPlugin custom behavior (buildFunctionMetadata, buildMessageForAgent, buildMessageFromAgentResponse), combining A2A with MCP, building a bot that is both A2A server and client, delegation hierarchy design, preventing infinite loops, partial failure handling, and naming conventions. Include 2-3 TypeScript code examples." -
a2a.server-basics-ts.md 11.4 KB
# a2a.server-basics-ts ## purpose Exposing a Teams bot as an A2A agent with AgentCard definition, A2APlugin setup, and a2a:message event handling. ## rules 1. Create an `A2APlugin` with `new A2APlugin({ agentCard })` where `agentCard` is an `AgentCard` object describing your agent's identity and capabilities. The plugin registers the A2A HTTP endpoint automatically. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. The `AgentCard` must include these required fields: `name`, `description`, `url`, `version`, `protocolVersion`, `capabilities`, and `skills`. The `url` must match the A2A endpoint (e.g., `http://localhost:3978/a2a`). [google.github.io/A2A -- Agent Card](https://google.github.io/A2A/#/documentation?id=agent-card) 3. Set `protocolVersion` to `'0.3.0'` to match the current A2A protocol version. Client agents use this to verify compatibility before sending messages. [google.github.io/A2A -- Protocol](https://google.github.io/A2A/) 4. Define `skills` as an array of objects with `id`, `name`, `description`, and optionally `tags` and `examples`. Skills describe what your agent can do. Client agents and LLMs use skill descriptions to decide when to delegate to your agent. [google.github.io/A2A -- Skills](https://google.github.io/A2A/#/documentation?id=agent-card) 5. Handle incoming A2A messages with `app.event('a2a:message', handler)`. The handler receives `{ respond, requestContext }` where `requestContext.userMessage.parts` contains the message parts sent by the calling agent. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. Extract text input from message parts by filtering for `kind === 'text'`: `requestContext.userMessage.parts.filter((p) => p.kind === 'text').at(0)?.text`. Always check for the text part before processing. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. Send responses using the `respond(text)` helper function. This sends a text response back to the calling agent through the A2A protocol. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. Add the `A2APlugin` to the App's `plugins` array. The plugin registers two HTTP routes: the A2A message endpoint at `/a2a` and the agent card at `/a2a/.well-known/agent-card.json`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Install `@microsoft/teams.a2a` as a dependency. If the agent uses AI to process messages, also install `@microsoft/teams.ai` and `@microsoft/teams.openai`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Keep skill descriptions specific and actionable. Vague descriptions like "general assistant" cause LLM orchestrators to delegate inappropriately. Include example queries in the `examples` array to guide delegation decisions. [google.github.io/A2A -- Best practices](https://google.github.io/A2A/) ## patterns ### Basic A2A server with AI processing ```typescript import { App } from '@microsoft/teams.apps'; import { A2APlugin } from '@microsoft/teams.a2a'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt({ model, instructions: 'You are a weather expert. Provide weather information for requested locations.', }); const agentCard = { name: 'Weather Agent', description: 'An agent that provides weather information', url: 'http://localhost:3978/a2a', version: '0.0.1', protocolVersion: '0.3.0', capabilities: {}, skills: [ { id: 'get_weather', name: 'Get Weather', description: 'Get current weather conditions for a location', tags: ['weather', 'forecast'], examples: ['What is the weather in London?', 'Temperature in Tokyo'], }, ], }; const app = new App({ logger: new ConsoleLogger('weather-agent', { level: 'debug' }), plugins: [new DevtoolsPlugin(), new A2APlugin({ agentCard })], }); // Handle incoming A2A messages from other agents app.event('a2a:message', async ({ respond, requestContext }) => { const textInput = requestContext.userMessage.parts .filter((p: any) => p.kind === 'text') .at(0)?.text; if (!textInput) { await respond('I only support text input.'); return; } // Process with AI and respond const result = await prompt.send(textInput); await respond(result.content || 'No response available.'); }); // Also handle direct Teams messages app.on('message', async ({ send, activity }) => { const result = await prompt.send(activity.text); if (result.content) await send(result.content); }); app.start(3978); // A2A endpoint: http://localhost:3978/a2a // Agent card: http://localhost:3978/a2a/.well-known/agent-card.json ``` ### Multi-skill agent card ```typescript import { A2APlugin } from '@microsoft/teams.a2a'; const agentCard = { name: 'IT Help Desk Agent', description: 'An agent that handles IT support requests, password resets, and ticket creation', url: 'http://localhost:3978/a2a', version: '1.0.0', protocolVersion: '0.3.0', capabilities: {}, skills: [ { id: 'password_reset', name: 'Password Reset', description: 'Initiate a password reset for a user account', tags: ['password', 'account', 'reset'], examples: [ 'Reset password for user john@company.com', 'I forgot my password', ], }, { id: 'create_ticket', name: 'Create Support Ticket', description: 'Create a new IT support ticket with priority and description', tags: ['ticket', 'support', 'helpdesk'], examples: [ 'Create a high priority ticket for laptop replacement', 'Submit a ticket about VPN issues', ], }, { id: 'check_status', name: 'Check Ticket Status', description: 'Check the status of an existing support ticket by ID', tags: ['ticket', 'status'], examples: [ 'What is the status of ticket IT-1234?', 'Check my open tickets', ], }, ], }; const a2aPlugin = new A2APlugin({ agentCard }); ``` ### A2A message handler with routing ```typescript import { App } from '@microsoft/teams.apps'; import { A2APlugin } from '@microsoft/teams.a2a'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt({ model, instructions: `You are an IT help desk agent. You can: - Reset passwords (ask for the user email) - Create support tickets (ask for priority and description) - Check ticket status (ask for ticket ID) Be concise and helpful.`, }) .function('resetPassword', 'Reset a user password', { type: 'object', properties: { email: { type: 'string', description: 'User email address' }, }, required: ['email'], }, async ({ email }: { email: string }) => { return { success: true, message: `Password reset initiated for ${email}` }; }) .function('createTicket', 'Create a support ticket', { type: 'object', properties: { priority: { type: 'string', description: 'high, medium, or low' }, description: { type: 'string', description: 'Issue description' }, }, required: ['priority', 'description'], }, async ({ priority, description }: { priority: string; description: string }) => { const ticketId = `IT-${Date.now()}`; return { ticketId, priority, description, status: 'open' }; }); const agentCard = { name: 'IT Help Desk Agent', description: 'Handles IT support requests', url: 'http://localhost:3978/a2a', version: '1.0.0', protocolVersion: '0.3.0', capabilities: {}, skills: [ { id: 'it_support', name: 'IT Support', description: 'Password resets, ticket creation, and status checks', tags: ['it', 'support'], examples: ['Reset my password', 'Create a ticket for VPN issues'], }, ], }; const app = new App({ plugins: [new A2APlugin({ agentCard })], }); app.event('a2a:message', async ({ respond, requestContext }) => { const textInput = requestContext.userMessage.parts .filter((p: any) => p.kind === 'text') .at(0)?.text; if (!textInput) { await respond('Please send a text message describing your IT issue.'); return; } const result = await prompt.send(textInput); await respond(result.content || 'I was unable to process your request.'); }); app.start(3978); ``` ## pitfalls - **Missing required AgentCard fields**: Omitting `name`, `url`, `version`, or `protocolVersion` causes client agents to reject the agent card during discovery. Include all required fields. - **URL mismatch in AgentCard**: The `url` field must match the actual A2A endpoint URL. If your bot runs on port 3978, the URL is `http://localhost:3978/a2a`. A mismatch causes clients to connect to the wrong endpoint. - **Not handling non-text message parts**: A2A messages can contain parts of different kinds (text, file, data). Always filter for the expected kind and handle unexpected types gracefully. - **Forgetting to add A2APlugin to App plugins**: Creating the plugin without adding it to `new App({ plugins: [...] })` means the A2A endpoints are never registered. - **Vague skill descriptions**: Skills with descriptions like "general helper" provide no guidance to orchestrator agents. Be specific about what the agent can do and include example queries. - **No error handling in message handler**: If `prompt.send()` throws (e.g., model error), the A2A client receives no response. Wrap processing in try/catch and use `respond()` to send error messages. - **Missing `protocolVersion`**: Without this field, client agents cannot verify protocol compatibility. Always set it to the current version (`'0.3.0'`). ## references - [A2A Protocol Specification](https://google.github.io/A2A/) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.a2a npm](https://www.npmjs.com/package/@microsoft/teams.a2a) - [A2A Agent Card documentation](https://google.github.io/A2A/#/documentation?id=agent-card) ## instructions This expert covers exposing a Teams bot as an A2A (Agent-to-Agent) server using `A2APlugin` from `@microsoft/teams.a2a` in TypeScript. Use it when you need to: - Define an `AgentCard` with name, description, URL, version, capabilities, and skills - Create an `A2APlugin` and add it to the App's plugins array - Handle incoming A2A messages via `app.event('a2a:message', handler)` - Extract text from `requestContext.userMessage.parts` - Send responses using the `respond()` helper - Understand the A2A endpoint URL (`/a2a`) and agent card URL (`/a2a/.well-known/agent-card.json`) Pair with `a2a.client-basics-ts.md` for calling other A2A agents and `a2a.orchestrator-patterns-ts.md` for multi-agent coordination. Pair with `runtime.app-init-ts.md` for adding A2APlugin to the App, and `ai.chatprompt-basics-ts.md` for processing A2A messages with AI. ## research Deep Research prompt: "Write a micro expert on implementing an A2A server in a Teams bot using @microsoft/teams.a2a (TypeScript). Cover AgentCard structure (name, description, url, version, protocolVersion, capabilities, skills), A2APlugin constructor, handling a2a:message events, extracting text from requestContext.userMessage.parts, respond() helper, endpoint URLs, and common pitfalls. Include 2-3 canonical TypeScript code examples." -
ai.chatprompt-basics-ts.md 8.1 KB
# ai.chatprompt-basics-ts ## purpose ChatPrompt construction, system instructions, sending messages, and response handling in Teams AI v2. ## rules 1. Always import `ChatPrompt` from `@microsoft/teams.ai` and pass a configured `IChatModel` instance (typically `OpenAIChatModel`) as the `model` option. The `model` field is the only required option. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Set `instructions` to define the system prompt. This accepts a `string`, `string[]` (joined with newlines), or an `ITemplate` for dynamic instructions. The `role` option controls whether instructions are sent as `'system'` (default) or `'user'` role. [github.com/microsoft/teams.ts -- ChatPrompt](https://github.com/microsoft/teams.ts) 3. Pass a `LocalMemory` instance as `messages` for automatic conversation history management with configurable limits and auto-summarization. Alternatively, pass a raw `Message[]` array for manual control. [github.com/microsoft/teams.ts -- LocalMemory](https://github.com/microsoft/teams.ts) 4. Call `prompt.send(input)` to send a user message and get a `ModelMessage` response. The input can be a `string` or a `ContentPart[]` array for multimodal input (text + images). [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. Always check `response.content` before sending -- it may be `undefined` if the model returned only function calls. When `autoFunctionCalling` is `true` (the default), function results are automatically fed back and the final response will have content. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. Use `prompt.send(input, { request: { temperature, max_tokens } })` to override model parameters per-request. These merge with the model's `requestOptions` defaults. [OpenAI -- Chat Completions](https://platform.openai.com/docs/api-reference/chat/create) 7. Use `prompt.send(input, { messages: extraMessages })` to inject additional context messages for a single request without persisting them to memory. This is useful for RAG-injected context. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. Use `.use(otherPrompt)` to compose sub-prompts and inherit their function definitions. This enables modular function organization across multiple ChatPrompt instances. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Set `name` and `description` on the prompt for debugging and identification. These appear in logs when a `logger` is provided and are used by ChatPrompt plugins for metadata. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Pass ChatPrompt plugins as the second constructor argument: `new ChatPrompt(options, [plugin1, plugin2])`. Plugins hook into the send lifecycle (before/after send, before/after function calls). [github.com/microsoft/teams.ts -- ChatPromptPlugin](https://github.com/microsoft/teams.ts) ## patterns ### Basic ChatPrompt with system instructions ```typescript import { ChatPrompt, LocalMemory } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt({ name: 'my-agent', description: 'A helpful assistant', model: model, instructions: 'You are a helpful assistant that answers questions concisely.', messages: new LocalMemory({ max: 50 }), }); // In a message handler app.on('message', async ({ send, activity }) => { const response = await prompt.send(activity.text); if (response.content) { await send(response.content); } }); ``` ### Sending with per-request options and multimodal input ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; const prompt = new ChatPrompt({ model, instructions: 'You are a vision-capable assistant. Describe images in detail.', }); // Text-only with request overrides const textResponse = await prompt.send('Summarize quantum computing', { request: { temperature: 0.3, max_tokens: 500 }, }); // Multimodal: text + image const visionResponse = await prompt.send([ { type: 'text', text: 'What is in this image?' }, { type: 'image_url', image_url: 'https://example.com/photo.jpg' }, ]); if (visionResponse.content) { await send(visionResponse.content); } ``` ### Composing prompts with .use() ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; // Sub-prompt with specialized functions const weatherPrompt = new ChatPrompt({ model, instructions: 'Weather helper', }) .function('getWeather', 'Get weather for a city', { type: 'object', properties: { city: { type: 'string', description: 'City name' }, }, required: ['city'], }, async ({ city }: { city: string }) => { const res = await fetch(`https://api.weather.example.com/${city}`); return await res.json(); }); // Main prompt inherits weather functions via .use() const mainPrompt = new ChatPrompt({ model, instructions: 'You are a general-purpose assistant with weather capabilities.', messages: new LocalMemory({ max: 100 }), }) .use(weatherPrompt); app.on('message', async ({ send, activity }) => { const result = await mainPrompt.send(activity.text); if (result.content) { await send(result.content); } }); ``` ## pitfalls - **Forgetting to check `response.content`**: When the model returns only function calls (and `autoFunctionCalling` is `false`), `content` is `undefined`. Sending `undefined` to Teams produces an error. - **Sharing a single prompt across conversations**: A `ChatPrompt` with a `LocalMemory` or `Message[]` accumulates history. If shared across conversations, users see each other's messages. Create a new prompt (or separate memory) per conversation. - **Instructions too long**: Very long system prompts consume tokens from every request. Keep instructions focused and use function descriptions to offload behavioral guidance. - **Missing `model` option**: The `model` field is required. Omitting it throws at construction time, not at `send()` time. - **Using `.use()` after `.send()`**: While not strictly an error, composing prompts with `.use()` should be done during setup, not mid-conversation. Function registrations happen at composition time. - **Ignoring the `function_calls` field**: When `autoFunctionCalling` is `false`, the response may contain `function_calls` that need manual handling. Always check both `content` and `function_calls` on `ModelMessage`. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.ai -- npm](https://www.npmjs.com/package/@microsoft/teams.ai) - [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat/create) - [OpenAI Vision Guide](https://platform.openai.com/docs/guides/vision) - [Teams AI v2 Examples](https://github.com/microsoft/teams.ts/tree/main/examples) ## instructions This expert covers creating and using `ChatPrompt` from `@microsoft/teams.ai` in Teams AI v2. Use it when you need to: - Construct a ChatPrompt with system instructions, name, description, and memory - Send text or multimodal (text + image) input to the LLM via `prompt.send()` - Handle `ModelMessage` responses (content, function_calls, context/citations) - Override request parameters (temperature, max_tokens) per-send - Compose prompts with `.use()` for modular function organization - Pass ChatPrompt plugins for lifecycle hooks Pair with `ai.model-setup-ts.md` for model configuration, `ai.function-calling-design-ts.md` and `ai.function-calling-implementation-ts.md` for adding functions, and `ai.memory-localmemory-ts.md` for conversation history management. ## research Deep Research prompt: "Write a micro expert on ChatPrompt in the Teams AI Library v2 (TypeScript). Cover the ChatPrompt constructor options (model, name, description, instructions, role, messages, logger), the ChatPromptOptions reference table, prompt.send() with all options (onChunk, autoFunctionCalling, messages, request overrides), ModelMessage response shape (content, function_calls, audio, context), multimodal input (text + images via ContentPart[]), composing prompts with .use(), and ChatPrompt plugin integration." -
ai.citations-feedback-ts.md 6.9 KB
# ai.citations-feedback-ts ## purpose AI-generated message markers, citation annotations, and user feedback collection. ## rules 1. Import `MessageActivity` from `@microsoft/teams.api`. This is the builder class for constructing rich AI messages with markers, citations, and feedback buttons. 2. Call `.addAiGenerated()` on a `MessageActivity` to mark the message as AI-generated. Teams renders a visual indicator so users know the content was produced by an AI model. Always add this marker to LLM-generated responses. 3. Call `.addFeedback()` to attach thumbs-up/thumbs-down feedback buttons to the message. This enables users to rate the AI response quality directly in the chat. 4. Call `.addCitation(index, { name, abstract })` to annotate the message with a numbered source citation. The `index` is the citation number (1-based), `name` is the source title, and `abstract` is a brief description. Add citations for every source the LLM references in its response. 5. Handle user feedback by registering `app.on('message.submit.feedback', handler)`. The feedback payload contains `activity.value.actionValue.reaction` (`'like'` or `'dislike'`) and optionally `activity.value.actionValue.feedback` (free-text user comment). 6. The feedback handler receives `activity.replyToId` (or `activity.id`) which identifies the original AI message the user rated. Use this to correlate feedback with specific responses for analytics. 7. Always return `{ status: 200 }` from the feedback handler to acknowledge receipt. Failing to return a status causes a retry loop in the Teams client. 8. Chain all `MessageActivity` methods fluently: `new MessageActivity(text).addAiGenerated().addFeedback().addCitation(1, {...})`. The builder pattern returns `this` for each method. 9. When combining with streaming, construct a `new MessageActivity(chunk)` inside the `onChunk` callback and call `.addFeedback()` on it. The stream accumulates content and the final message retains the feedback buttons. 10. Store feedback data (reaction, text, message ID, user ID, timestamp) in a database for quality monitoring dashboards. Track like/dislike ratios per prompt template or function to identify areas for improvement. ## patterns ### MessageActivity with AI markers, feedback, and citations ```typescript import { MessageActivity } from '@microsoft/teams.api'; app.on('message', async ({ send, activity }) => { const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant.' }); const response = await prompt.send(activity.text); if (response.content) { const msg = new MessageActivity(response.content) .addAiGenerated() // Marks the message as AI-generated .addFeedback() // Adds thumbs up/down feedback buttons .addCitation(1, { name: 'Getting Started Guide', abstract: 'Setup and installation instructions' }) .addCitation(2, { name: 'API Reference', abstract: 'Complete API documentation' }); await send(msg); } }); ``` ### Handling feedback events ```typescript app.on('message.submit.feedback', async ({ activity, log }) => { const feedback = { messageId: activity.replyToId || activity.id, reaction: activity.value.actionValue.reaction, // 'like' or 'dislike' feedback: activity.value.actionValue.feedback, // Optional text from user }; log.info('Feedback received:', feedback); // Store feedback for analytics await feedbackStore.save({ ...feedback, userId: activity.from.id, timestamp: new Date().toISOString(), }); return { status: 200 }; }); ``` ### Streaming with feedback buttons ```typescript import { MessageActivity } from '@microsoft/teams.api'; app.on('message', async ({ stream, activity }) => { const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant.' }); const response = await prompt.send(activity.text, { onChunk: (chunk: string) => { // Each streamed chunk includes feedback buttons stream.emit( new MessageActivity(chunk) .addAiGenerated() .addFeedback() ); }, }); // Final message retains AI marker and feedback buttons }); ``` ## pitfalls - **Forgetting `.addAiGenerated()`**: Without the AI marker, Teams renders the message as if it came from a human agent. Users may be confused about the source of the response. Always add it for LLM-generated content. - **Not returning `{ status: 200 }` from the feedback handler**: The Teams client retries the feedback submission if it does not receive an acknowledgment, causing duplicate feedback entries and UI flicker. - **Citation index starting at 0**: Citation indices are 1-based to match how they appear in the message text (e.g., `[1]`, `[2]`). Starting at 0 causes a mismatch between the rendered citation number and the annotation. - **Ignoring the feedback text field**: Users can optionally provide free-text feedback alongside their thumbs-up/thumbs-down. Capture `activity.value.actionValue.feedback` -- it often contains actionable improvement suggestions. - **Adding citations without instructing the LLM to cite**: The LLM must be prompted to reference sources by number (e.g., `"Always cite sources as [1], [2]"` in instructions). Otherwise, citation annotations exist but the response text has no matching references. - **Using `send()` instead of `MessageActivity`**: Calling `await send(response.content)` sends a plain string with no markers, citations, or feedback buttons. Always wrap LLM responses in `MessageActivity` for production bots. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [AI Message Markers -- Microsoft Learn](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bot-messages-ai-generated-content) - [Citations in Teams Messages -- Microsoft Learn](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bot-messages-ai-generated-content#citations) - [@microsoft/teams.api -- npm](https://www.npmjs.com/package/@microsoft/teams.api) ## instructions This expert covers AI-generated message markers, citation annotations, and user feedback collection in Teams AI v2. Use it when you need to: - Mark messages as AI-generated with `.addAiGenerated()` - Attach thumbs-up/thumbs-down feedback buttons with `.addFeedback()` - Annotate responses with numbered source citations using `.addCitation()` - Handle `message.submit.feedback` events and store feedback for analytics - Combine feedback buttons with streaming responses - Build quality monitoring dashboards from collected feedback data Pair with `ai.streaming-ts.md` for streaming with feedback, `ai.rag-retrieval-ts.md` for annotating RAG results with citations, and `ai.chatprompt-basics-ts.md` for prompt responses. ## research Deep Research prompt: "Write a micro expert on adding AI markers, feedback, and citations in Teams SDK v2 (TypeScript). Cover MessageActivity.addAiGenerated(), addFeedback(), addCitation(), and handling app.on('message.submit.feedback'). Include payload examples, storage patterns for analytics, and UX considerations." -
ai.conversational-query-ts.md 14.2 KB
# ai.conversational-query-ts ## purpose Enable natural language retrieval over structured workflow state — translating user questions like "Who is on break?" or "Show PTO for March" into list/datastore queries and rendering results as interactive message-backed cards. ## rules 1. **NL queries go through AI function calling, not regex parsing.** Define tool/function schemas that accept structured parameters (status filter, date range, person). The LLM translates the user's natural language into function calls with the right parameters. This handles the infinite variation of how users phrase queries. 2. **Define focused query functions, not a generic "search" function.** Create specific functions: `queryPtoRequests(status?, dateRange?, requester?)`, `queryBreakStatus(teamId?)`, `queryEquipmentBookings(item?, dateRange?)`. Specific schemas give the LLM better guidance than a single catch-all. [learn.microsoft.com -- Function calling](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/function-calling) 3. **Map function parameters to OData `$filter` expressions.** Each function parameter translates to a filter clause: `status: "pending"` → `fields/Status eq 'Pending'`, `dateRange: { start, end }` → `fields/StartDate ge '2024-03-01' and fields/StartDate le '2024-03-31'`. Build filters programmatically from the AI's structured output. 4. **Return structured data to the LLM, not raw JSON.** Format query results as a readable table or summary before passing back to the model. The LLM then generates a natural language response that can include counts, trends, and highlights. Don't dump raw Graph API responses into the prompt. 5. **Render query results as Adaptive Cards, not plain text.** After the LLM generates a summary, also render the actual records as interactive cards. The text answer provides context; the cards provide actionable records. Users get both "You have 3 pending PTO requests" and the cards to act on them. 6. **Support aggregate queries with server-side computation.** For "average break duration" or "how many PTO days used this quarter," compute aggregates in the function implementation (sum, count, average over fetched records). Return the computed result to the LLM for natural language rendering. 7. **Use `$top` and `$skip` for pagination.** When queries may return many results, default to `$top=10`. If the user asks "show all," paginate and summarize: "Showing 10 of 47 results. Say 'show more' to see the next page." Track pagination state per conversation. 8. **Include a `queryWorkflowRecords` function for cross-workflow queries.** In addition to workflow-specific functions, provide a general function that queries across all workflow lists. The LLM uses this when the user asks something like "what's overdue?" without specifying a workflow type. 9. **Ground AI responses in actual data.** Always include the source record count and date range in the response. "Based on 12 PTO requests from March 1-31..." prevents hallucination about records that don't exist. 10. **Cache frequent queries for low-latency responses.** Queries like "who is on break right now?" are likely repeated frequently. Cache results for 30-60 seconds to avoid hitting Graph API limits on every message. ## patterns ### Define query functions for ChatPrompt ```typescript import { ChatPrompt } from "@anthropic-ai/sdk"; // or teams-ai equivalent const queryFunctions = [ { name: "queryPtoRequests", description: "Query PTO/time-off requests. Use when the user asks about PTO, time off, vacation, leave, or days off.", parameters: { type: "object", properties: { status: { type: "string", enum: ["Pending", "Approved", "Rejected", "All"], description: "Filter by request status. Default: All", }, requester: { type: "string", description: "Filter by requester name (partial match). Omit for all requesters.", }, month: { type: "string", description: "Filter by month, e.g. '2024-03' for March 2024. Omit for all dates.", }, }, }, }, { name: "queryBreakStatus", description: "Query who is currently on break or break history. Use when the user asks about breaks, availability, or who is away.", parameters: { type: "object", properties: { currentOnly: { type: "boolean", description: "True to show only active breaks. False for break history.", }, dateRange: { type: "object", properties: { start: { type: "string", description: "ISO date" }, end: { type: "string", description: "ISO date" }, }, }, }, }, }, { name: "queryEquipmentBookings", description: "Query equipment reservations and availability. Use when the user asks about bookings, reservations, equipment, or availability.", parameters: { type: "object", properties: { item: { type: "string", description: "Equipment name or type to filter" }, status: { type: "string", enum: ["Active", "Returned", "Overdue", "All"] }, dateRange: { type: "object", properties: { start: { type: "string" }, end: { type: "string" }, }, }, }, }, }, ]; ``` ### Implement query function with OData filter building ```typescript async function queryPtoRequests( graphClient: Client, siteId: string, listId: string, params: { status?: string; requester?: string; month?: string } ): Promise<{ records: any[]; summary: string }> { const filters: string[] = []; if (params.status && params.status !== "All") { filters.push(`fields/Status eq '${params.status}'`); } if (params.month) { const start = `${params.month}-01`; const endDate = new Date( parseInt(params.month.split("-")[0]), parseInt(params.month.split("-")[1]), 0 ); const end = endDate.toISOString().split("T")[0]; filters.push(`fields/StartDate ge '${start}' and fields/StartDate le '${end}'`); } let query = graphClient .api(`/sites/${siteId}/lists/${listId}/items`) .expand("fields") .top(20) .orderby("fields/StartDate desc"); if (filters.length > 0) { query = query.filter(filters.join(" and ")); } const response = await query.get(); const records = response.value.map((item: any) => ({ id: item.id, requester: item.fields.Title, startDate: item.fields.StartDate, endDate: item.fields.EndDate, status: item.fields.Status, hoursRequested: item.fields.HoursRequested, })); // Filter requester client-side (OData doesn't support contains on all field types) const filtered = params.requester ? records.filter((r: any) => r.requester.toLowerCase().includes(params.requester!.toLowerCase()) ) : records; // Build summary for LLM const summary = [ `Found ${filtered.length} PTO request(s).`, params.status && params.status !== "All" ? `Status: ${params.status}.` : "", params.month ? `Month: ${params.month}.` : "", `Total hours: ${filtered.reduce((sum: number, r: any) => sum + (r.hoursRequested || 0), 0)}.`, ] .filter(Boolean) .join(" "); return { records: filtered, summary }; } ``` ### Wire functions into the message handler ```typescript app.message(async (ctx) => { const userMessage = ctx.activity.text ?? ""; // Send to LLM with function definitions const response = await openai.chat.completions.create({ model: "gpt-4o", messages: [ { role: "system", content: "You are a workflow assistant. Answer questions about PTO, breaks, equipment, and other workflow records. " + "Always use the provided functions to query real data. Never make up record counts or details.", }, { role: "user", content: userMessage }, ], tools: queryFunctions.map((f) => ({ type: "function", function: f })), }); const choice = response.choices[0]; if (choice.message.tool_calls?.length) { const toolCall = choice.message.tool_calls[0]; const args = JSON.parse(toolCall.function.arguments); // Execute the query let result: { records: any[]; summary: string }; switch (toolCall.function.name) { case "queryPtoRequests": result = await queryPtoRequests(graphClient, siteId, ptoListId, args); break; case "queryBreakStatus": result = await queryBreakStatus(graphClient, siteId, breakListId, args); break; case "queryEquipmentBookings": result = await queryEquipmentBookings(graphClient, siteId, equipListId, args); break; default: result = { records: [], summary: "Unknown query type." }; } // Send result back to LLM for natural language response const followUp = await openai.chat.completions.create({ model: "gpt-4o", messages: [ { role: "system", content: "Summarize the query results naturally. Include counts and key details." }, { role: "user", content: userMessage }, choice.message, { role: "tool", tool_call_id: toolCall.id, content: result.summary, }, ], }); const textResponse = followUp.choices[0].message.content ?? ""; // Send text summary + record cards await ctx.send(textResponse); if (result.records.length > 0 && result.records.length <= 5) { // Inline cards for small result sets for (const record of result.records) { await ctx.send({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: buildRecordCard(record), }], }); } } else if (result.records.length > 5) { // Summary card for large result sets await ctx.send({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: buildRecordListCard(result.records.slice(0, 10)), }], }); } } else { // No function call — direct response await ctx.send(choice.message.content ?? "I couldn't find relevant records for that query."); } }); ``` ### Aggregate query example ```typescript async function queryBreakStatus( graphClient: Client, siteId: string, listId: string, params: { currentOnly?: boolean; dateRange?: { start: string; end: string } } ): Promise<{ records: any[]; summary: string }> { const filters: string[] = []; if (params.currentOnly) { filters.push("fields/Status eq 'Active'"); } if (params.dateRange) { filters.push( `fields/StartTime ge '${params.dateRange.start}' and fields/StartTime le '${params.dateRange.end}'` ); } let query = graphClient .api(`/sites/${siteId}/lists/${listId}/items`) .expand("fields") .top(50); if (filters.length) query = query.filter(filters.join(" and ")); const response = await query.get(); const records = response.value.map((item: any) => item.fields); // Compute aggregates const activeBreaks = records.filter((r: any) => r.Status === "Active"); const completedBreaks = records.filter((r: any) => r.Status === "Ended"); const avgDuration = completedBreaks.length > 0 ? completedBreaks.reduce((sum: number, r: any) => sum + (r.DurationMinutes || 0), 0) / completedBreaks.length : 0; const summary = [ `Currently on break: ${activeBreaks.length} people.`, activeBreaks.map((r: any) => r.EmployeeName).join(", ") || "None.", completedBreaks.length > 0 ? `Average break duration today: ${avgDuration.toFixed(1)} minutes.` : "", ] .filter(Boolean) .join(" "); return { records, summary }; } ``` ## pitfalls - **LLM may call the wrong function.** Provide clear, non-overlapping descriptions. "Who is available?" could match breaks or equipment. Use `description` fields to disambiguate and add examples in the system prompt. - **OData `$filter` doesn't support `contains()` on all column types.** SharePoint Lists OData implementation is limited compared to full OData. `contains()` works on text columns but not choice or person columns. Fall back to client-side filtering when needed. - **Don't pass raw Graph API responses to the LLM.** They contain metadata, `@odata` annotations, and nested objects that waste tokens and confuse the model. Extract only the fields you need into a clean summary string. - **Pagination state must be conversation-scoped.** If user A asks "show more" in a channel, it should continue user A's query, not user B's. Key pagination state on `(conversationId, userId)`. - **Token limits on large result sets.** If a query returns 50 records, the summary string passed to the LLM might be too long. Summarize with counts and top-N details rather than listing every record. - **Cache invalidation matters for "right now" queries.** "Who is on break?" expects real-time accuracy. Cache TTL should be short (30s max) for current-state queries. Historical queries can cache longer. ## references - https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/function-calling - https://learn.microsoft.com/en-us/graph/api/listitem-list - https://learn.microsoft.com/en-us/graph/query-parameters - https://platform.openai.com/docs/guides/function-calling ## instructions Use this expert when implementing natural language retrieval over structured workflow data. Covers AI function calling design for query translation, OData filter building from function parameters, aggregate computation, result rendering as cards, and pagination. Pair with `workflow.sharepoint-lists-ts.md` for the underlying data store, `ai.function-calling-design-ts.md` for function schema best practices, `ai.function-calling-implementation-ts.md` for execution patterns, and `workflow.message-native-records-ts.md` for rendering results as interactive record cards. ## research Deep Research prompt: "Write a micro expert on natural language querying over structured workflow state in Microsoft Teams (TypeScript). Cover: AI function calling to translate NL to SharePoint List OData queries, function schema design for PTO/break/equipment queries, OData filter building, aggregate computation (averages, counts, trends), result rendering as Adaptive Cards, pagination, caching, and grounding AI responses in actual data. Include complete patterns from user message through LLM function call through query execution through card rendering." -
ai.function-calling-design-ts.md 9 KB
# ai.function-calling-design-ts ## purpose Designing AI functions for Teams AI v2: naming conventions, parameter schemas, descriptions, and composition patterns. ## rules 1. Name functions with `camelCase` verbs that clearly describe the action: `getWeather`, `searchDocuments`, `createTicket`. The LLM uses the function name as a primary signal for when to call it. Avoid generic names like `doAction` or `process`. [OpenAI -- Function Calling](https://platform.openai.com/docs/guides/function-calling) 2. Write function descriptions from the LLM's perspective -- explain what the function does and when to use it. Good: `'Search the knowledge base for documents matching a query'`. Bad: `'Calls the search API'`. The description is part of the system prompt the model sees. [OpenAI -- Function Calling](https://platform.openai.com/docs/guides/function-calling) 3. Define parameter schemas using JSON Schema with `type: 'object'` at the root. Supported property types are `string`, `number`, `integer`, `boolean`, `object`, `array`, and `null`. Always include `description` on every property. [json-schema.org](https://json-schema.org/) 4. Mark parameters as required in the `required` array. Only include parameters that the function truly cannot operate without. Optional parameters give the LLM flexibility to omit them. [OpenAI -- Function Calling](https://platform.openai.com/docs/guides/function-calling) 5. Use `enum` constraints on string parameters when there is a fixed set of valid values (e.g., `{ type: 'string', enum: ['celsius', 'fahrenheit'] }`). This prevents the LLM from hallucinating invalid values. [json-schema.org -- enum](https://json-schema.org/understanding-json-schema/reference/generic.html) 6. Keep the total number of functions per prompt under 20. Each function definition consumes tokens in every request. Too many functions slow down inference and increase cost. Group related operations or use sub-prompts via `.use()`. [OpenAI -- Function Calling Best Practices](https://platform.openai.com/docs/guides/function-calling) 7. Use `.use(otherPrompt)` to compose function sets from separate ChatPrompt instances. This enables modular organization -- e.g., a `weatherPrompt` and a `calendarPrompt` composed into a `mainPrompt`. The main prompt inherits all functions from sub-prompts. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. For functions with no parameters, omit the schema argument entirely. The `.function(name, description, handler)` three-argument overload registers a zero-parameter function. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Return structured data (objects, arrays) from function handlers -- the SDK serializes them to JSON for the LLM. Return human-readable strings for simple status messages. Never return `undefined`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Decide between auto and manual function calling at design time. Use auto (default) for straightforward tool use where the LLM should handle the full loop. Use manual (`autoFunctionCalling: false`) when you need to validate, log, or gate function calls before execution. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## patterns ### Well-designed function schema with enum and optional fields ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; const prompt = new ChatPrompt({ model, instructions: 'You help users find weather information.' }) .function( 'getWeather', 'Get the current weather for a city. Use celsius unless the user specifies fahrenheit.', { type: 'object', properties: { city: { type: 'string', description: 'The city name, e.g. "London" or "New York"', }, units: { type: 'string', description: 'Temperature units', enum: ['celsius', 'fahrenheit'], }, }, required: ['city'], }, async ({ city, units }: { city: string; units?: string }) => { const u = units || 'celsius'; const res = await fetch(`https://api.weather.example.com/${city}?units=${u}`); return await res.json(); } ); ``` ### Composing function sets with .use() ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; // Separate prompt for weather functions const weatherPrompt = new ChatPrompt({ model, instructions: 'Weather tools' }) .function('getWeather', 'Get weather for a city', { type: 'object', properties: { city: { type: 'string', description: 'City name' } }, required: ['city'], }, async ({ city }: { city: string }) => { return { temp: 22, condition: 'sunny', city }; }); // Separate prompt for calendar functions const calendarPrompt = new ChatPrompt({ model, instructions: 'Calendar tools' }) .function('listEvents', 'List upcoming calendar events', { type: 'object', properties: { days: { type: 'integer', description: 'Number of days ahead to check (1-30)' }, }, required: ['days'], }, async ({ days }: { days: number }) => { return [{ title: 'Team standup', date: '2025-01-15', time: '09:00' }]; }); // Main prompt composes both function sets const mainPrompt = new ChatPrompt({ model, instructions: 'You are a personal assistant with weather and calendar capabilities.', }) .use(weatherPrompt) .use(calendarPrompt); ``` ### Zero-parameter function and state-modifying function ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; let lightIsOn = false; const prompt = new ChatPrompt({ model, instructions: 'You control smart home lights. Report status when asked.', }) // Zero-parameter function: no schema argument .function('getLightStatus', 'Get the current light on/off status', () => { return { isOn: lightIsOn }; }) // State-modifying function .function( 'setLight', 'Turn the light on or off', { type: 'object', properties: { state: { type: 'string', description: 'Desired light state', enum: ['on', 'off'], }, }, required: ['state'], }, ({ state }: { state: 'on' | 'off' }) => { lightIsOn = state === 'on'; return `Light turned ${state}`; } ); ``` ## pitfalls - **Vague function names**: Names like `handleRequest` or `getData` give the LLM no semantic signal. Use specific verb-noun pairs: `searchProducts`, `sendEmail`, `getOrderStatus`. - **Missing `description` on schema properties**: Without property descriptions, the LLM guesses what values to pass. Always describe expected format, range, and examples. - **Too many functions**: Registering 30+ functions bloats every request with function definitions. Split into sub-prompts with `.use()` or create specialized prompts for different conversation flows. - **Returning undefined from handlers**: If a function handler returns `undefined`, the LLM receives no result and may retry or hallucinate. Always return a value, even if it is an empty string or `{ success: true }`. - **Not using `required` array**: Omitting the `required` array means all parameters are optional. The LLM may skip parameters you assumed would always be provided. - **Deeply nested schemas**: Complex nested `object` schemas with multiple levels are harder for the LLM to fill correctly. Flatten when possible or break into multiple simpler functions. - **Enum values not matching descriptions**: If the enum is `['c', 'f']` but the description says "celsius or fahrenheit", the LLM may hallucinate `'celsius'` instead of `'c'`. Keep enum values readable. ## references - [OpenAI -- Function Calling Guide](https://platform.openai.com/docs/guides/function-calling) - [JSON Schema Reference](https://json-schema.org/understanding-json-schema/) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.ai -- npm](https://www.npmjs.com/package/@microsoft/teams.ai) - [Teams AI v2 Lights Example](https://github.com/microsoft/teams.ts/tree/main/examples/lights) ## instructions This expert covers the design aspects of AI function calling in Teams AI v2. Use it when you need to: - Design function names, descriptions, and parameter schemas for LLM tool use - Choose JSON Schema types and constraints (enums, required fields, descriptions) - Organize functions into modular sub-prompts with `.use()` composition - Decide between auto and manual function calling modes - Understand how function definitions affect token consumption and LLM behavior Pair with `ai.function-calling-implementation-ts.md` for the actual `.function()` handler registration and execution patterns, and `ai.chatprompt-basics-ts.md` for ChatPrompt construction. ## research Deep Research prompt: "Write a micro expert on designing AI functions for the Teams AI Library v2 (TypeScript). Cover function naming conventions, JSON Schema parameter definitions (supported types: string, number, integer, boolean, object, array, null), writing effective descriptions for LLM comprehension, enum constraints, required vs optional parameters, composing sub-prompts with .use(), function count limits, auto vs manual function calling trade-offs, and return value best practices." -
ai.function-calling-implementation-ts.md 7.7 KB
# ai.function-calling-implementation-ts ## purpose Implementing .function() handlers: registration, typed parameters, return values, and error handling. ## rules 1. Register functions on a `ChatPrompt` instance using the `.function(name, description, schema, handler)` chain API. Each call returns the prompt so you can chain multiple `.function()` calls fluently. Import `ChatPrompt` from `@microsoft/teams.ai`. 2. For simple functions with no parameters, omit the schema argument entirely and pass only `(name, description, handler)`. The handler receives no arguments and returns a value directly. 3. For typed parameter functions, provide a JSON Schema object as the third argument. Supported schema types are `string`, `number`, `integer`, `boolean`, `object`, `array`, and `null`. Always include `required` for mandatory fields. 4. Handlers can be synchronous or `async`. Async handlers must return a `Promise`. The return value is serialized and sent back to the LLM as the function result. Return structured objects when the LLM needs rich context; return simple strings for status confirmations. 5. Auto function calling is enabled by default. When `prompt.send()` is called, the SDK automatically executes matched functions and feeds results back to the LLM in a loop until the LLM produces a final text response. 6. To disable auto execution, pass `{ autoFunctionCalling: false }` as the second argument to `prompt.send()`. The response object will then contain a `function_calls` array you can inspect and execute manually. 7. Wrap handler bodies in try/catch and return descriptive error strings (e.g., `'Error: Pokemon not found'`) rather than throwing. Thrown exceptions break the auto-calling loop and surface as unhandled errors. 8. Use `.use(otherPrompt)` to compose sub-prompts. The parent prompt inherits all functions registered on the child prompt, enabling modular function libraries. 9. Keep function names short, camelCase, and descriptive. The LLM uses the `name` and `description` to decide when to call a function, so a clear description is critical for reliable tool selection. 10. Never register functions with side effects (database writes, API mutations) without confirming intent in the description. The LLM may call functions speculatively during auto function calling. ## patterns ### Simple function with no parameters ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); let lightIsOn = false; const prompt = new ChatPrompt({ model, instructions: 'You control smart home lights.' }) .function('getLightStatus', 'Get the current light status', () => { return lightIsOn; }) .function('turnOnLights', 'Turn the lights on', () => { lightIsOn = true; return 'Lights turned on'; }); // Auto function calling (default): LLM calls functions and incorporates results const response = await prompt.send('Are the lights on?'); console.log(response.content); // "The lights are currently off." ``` ### Typed parameter function with async handler ```typescript const prompt = new ChatPrompt({ model, instructions: 'You are a Pokemon expert.' }) .function( 'searchPokemon', 'Search for a Pokemon by name', { type: 'object', properties: { name: { type: 'string', description: 'Pokemon name' }, }, required: ['name'], }, async ({ name }: { name: string }) => { try { const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`); if (!res.ok) return `Error: Pokemon "${name}" not found`; return await res.json(); } catch (err) { return `Error: Failed to search for "${name}"`; } } ); const response = await prompt.send('What Pokemon is #25?'); ``` ### Manual function calling and sub-prompt composition ```typescript // Manual: inspect function_calls without auto-executing const response = await prompt.send('What Pokemon is #25?', { autoFunctionCalling: false, }); if (response.function_calls) { for (const call of response.function_calls) { console.log(call.name, call.arguments); // call.name = 'searchPokemon', call.arguments = { name: 'pikachu' } } } // Sub-prompt composition: modular function libraries const weatherPrompt = new ChatPrompt({ model, instructions: 'Weather helper' }) .function( 'getWeather', 'Get weather for a city', { type: 'object', properties: { city: { type: 'string', description: 'City name' }, units: { type: 'string', description: 'Temperature units: celsius or fahrenheit' }, }, required: ['city'], }, async ({ city }: { city: string }) => { return { city, temp: 72, condition: 'sunny' }; } ); const mainPrompt = new ChatPrompt({ model, instructions: 'Main assistant' }) .use(weatherPrompt); // Inherits weather functions ``` ## pitfalls - **Throwing exceptions in handlers**: Unhandled throws break the auto-calling loop. Always catch errors inside the handler and return a descriptive error string so the LLM can report the failure gracefully. - **Missing `required` in schema**: If you omit the `required` array, the LLM may call the function without mandatory parameters, producing undefined values in your handler. - **Overly generic function names**: Names like `getData` or `run` give the LLM insufficient signal. Use specific names like `searchPokemon` or `getWeather` so the LLM selects the right tool. - **Side effects during auto calling**: The LLM may call a function multiple times in a single turn. Functions that mutate state (write to DB, send email) should be idempotent or guarded against duplicate execution. - **Forgetting `autoFunctionCalling: false`**: If you intend to inspect `function_calls` manually but forget to disable auto calling, the SDK will execute the functions automatically and you will only see the final text response. - **Returning non-serializable values**: Handler return values are serialized to JSON. Returning class instances, circular references, or `undefined` can produce unexpected results. Return plain objects or strings. - **Schema type mismatches**: Using `type: 'int'` instead of `type: 'integer'` silently fails validation. Stick to the seven supported JSON Schema types. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [OpenAI Function Calling Guide](https://platform.openai.com/docs/guides/function-calling) - [JSON Schema Specification](https://json-schema.org/understanding-json-schema/) - [@microsoft/teams.ai -- npm](https://www.npmjs.com/package/@microsoft/teams.ai) ## instructions This expert covers implementing function calling on `ChatPrompt` in Teams AI v2. Use it when you need to: - Register functions with `.function(name, description, schema, handler)` on a ChatPrompt - Define typed parameter schemas using JSON Schema - Handle async operations (API calls, database queries) inside function handlers - Choose between auto function calling (default) and manual inspection of `function_calls` - Compose modular function libraries using `.use()` sub-prompts - Handle errors gracefully inside function handlers Pair with `ai.function-calling-design-ts.md` for schema design principles, `ai.chatprompt-basics-ts.md` for prompt.send() options, and `mcp.expose-chatprompt-tools-ts.md` for bridging functions to MCP tools. ## research Deep Research prompt: "Write a micro expert on implementing function calling in Teams ChatPrompt (TypeScript). Cover chaining .function calls, handler signatures, async handlers, autoFunctionCalling defaults, manual inspection of function_calls, and error handling. Include canonical patterns for: read-only tools, state-mutating tools, and tools that call external APIs." -
ai.memory-localmemory-ts.md 7.3 KB
# ai.memory-localmemory-ts ## purpose Conversation history management with LocalMemory, message limits, and auto-summarization. ## rules 1. Import `LocalMemory` from `@microsoft/teams.ai`. This is the built-in memory class that implements the `IMemory` interface for managing conversation history with automatic overflow handling. 2. Pass a `max` value to the `LocalMemory` constructor to cap the number of messages retained. When the limit is reached, the collapse strategy is triggered automatically. Choose a value that balances context quality with token budget (e.g., 20-50 messages for typical chat bots). 3. Set `collapse.strategy` to `'half'` (default) to summarize and discard the oldest half of messages when the limit is hit, or `'full'` to summarize all messages into a single summary message. The `'half'` strategy preserves recent context while the `'full'` strategy maximizes compression. 4. Provide a `collapse.model` -- an `OpenAIChatModel` instance used to generate the summary when collapse is triggered. This can be the same model used for chat or a cheaper/faster model dedicated to summarization. 5. Pass the `LocalMemory` instance as the `messages` property of the `ChatPrompt` constructor. The prompt reads from and writes to this memory automatically on each `prompt.send()` call. 6. For multi-turn bots, maintain a `Map<string, LocalMemory>` keyed by conversation ID. Create a new `LocalMemory` per conversation to prevent history leaking across users or channels. 7. Use the `IMemory` interface methods (`push`, `pop`, `get`, `set`, `delete`, `values`, `length`, `where`, `collapse`) for programmatic access to conversation history. Call `memory.where(predicate)` to filter messages by role or content. 8. Seed initial context by passing a `messages` array to the `LocalMemory` constructor. Use this for few-shot examples or system-level context that should always be present at the start of a conversation. 9. Call `memory.collapse()` manually when you need to free token budget mid-conversation (e.g., before a large function call result). The method returns the summary message or `undefined` if collapse was not needed. 10. For production deployments that must survive restarts, serialize `memory.values()` to persistent storage (database, blob) and rehydrate by passing the stored messages array to a new `LocalMemory` constructor. ## patterns ### Basic LocalMemory with collapse ```typescript import { LocalMemory, ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const summaryModel = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o-mini', }); const memory = new LocalMemory({ max: 50, // Keep up to 50 messages messages: [], // Optional initial messages collapse: { strategy: 'half', // Summarize oldest half when full model: summaryModel, // Model used for summarization }, }); const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant.', messages: memory, }); const result = await prompt.send('Hello!'); ``` ### Per-conversation memory with Map ```typescript import { LocalMemory, ChatPrompt, Message } from '@microsoft/teams.ai'; const conversationMemories = new Map<string, LocalMemory>(); app.on('message', async ({ send, activity }) => { const convId = activity.conversation.id; // Get or create per-conversation memory if (!conversationMemories.has(convId)) { conversationMemories.set(convId, new LocalMemory({ max: 30, collapse: { strategy: 'half', model: summaryModel, }, })); } const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant.', messages: conversationMemories.get(convId)!, }); const result = await prompt.send(activity.text); if (result.content) { await send(result.content); } }); ``` ### IMemory interface methods ```typescript // Push a message manually memory.push({ role: 'user', content: 'Hello' }); // Get message count const count = memory.length(); // Retrieve all messages const allMessages = memory.values(); // Filter messages by role const userMessages = memory.where((msg) => msg.role === 'user'); // Get a specific message by index const first = memory.get(0); // Replace a message at index memory.set(0, { role: 'system', content: 'Updated context' }); // Remove the last message memory.pop(); // Delete message at index memory.delete(2); // Manually trigger collapse/summarization const summary = await memory.collapse(); ``` ## pitfalls - **Sharing a single LocalMemory across conversations**: All users see each other's history. Always key memory instances by conversation ID (or user ID for 1:1 bots). - **Setting `max` too low**: A max of 5-10 causes frequent collapse, losing important context. Start with 20-50 and tune based on your token budget and average conversation length. - **Setting `max` too high**: Exceeding the model's context window causes truncation errors or degraded response quality. Keep `max * average_message_tokens` well under the model's context limit. - **Forgetting `collapse.model`**: If you set a collapse strategy but omit the model, summarization will fail silently and old messages will simply be dropped instead of summarized. - **Memory lost on restart**: `LocalMemory` is in-memory only. Bot process restarts lose all conversation history. For production, serialize `memory.values()` to a database and rehydrate on startup. - **Passing a raw `Message[]` instead of `LocalMemory`**: Passing a plain array as `messages` works for simple cases but you lose collapse, max limits, and the `IMemory` interface. Use `LocalMemory` for anything beyond trivial demos. - **Not cleaning up stale conversations**: The `Map` grows indefinitely. Implement a TTL or LRU eviction policy to remove inactive conversation memories. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.ai -- npm](https://www.npmjs.com/package/@microsoft/teams.ai) - [OpenAI Context Window Limits](https://platform.openai.com/docs/models) - [Conversation History Best Practices -- Microsoft Learn](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/) ## instructions This expert covers conversation history management with `LocalMemory` in Teams AI v2. Use it when you need to: - Configure `LocalMemory` with max message limits and collapse strategies - Choose between `'half'` and `'full'` collapse strategies for summarization - Implement per-conversation or per-user memory isolation using a `Map` - Use the `IMemory` interface methods for programmatic history access - Seed conversations with initial context messages - Persist and rehydrate conversation history across bot restarts Pair with `ai.chatprompt-basics-ts.md` for passing memory to ChatPrompt constructor, and `state.storage-patterns-ts.md` for persisting conversation history across restarts. ## research Deep Research prompt: "Write a micro expert on memory in Teams AI (TypeScript). Cover LocalMemory configuration, max messages, collapse strategies (half/full), supplying a summarization model, and state scoping (per-user vs per-conversation). Include practical code patterns and warnings about memory leakage across conversations." -
ai.model-setup-ts.md 6.6 KB
# ai.model-setup-ts ## purpose Configuring OpenAI and Azure OpenAI chat models for Teams AI using OpenAIChatModel and its full options surface. ## rules 1. Always import `OpenAIChatModel` from `@microsoft/teams.openai` -- this is the only model class in the Teams AI v2 SDK. It handles both OpenAI and Azure OpenAI backends. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. For plain OpenAI, provide `apiKey` and `model` (e.g., `'gpt-4o'`). Do not set `endpoint` or `apiVersion` -- those trigger Azure mode. [OpenAI API reference](https://platform.openai.com/docs/api-reference/chat) 3. For Azure OpenAI, provide `apiKey`, `endpoint`, `apiVersion`, and `model` (the deployment name, not the base model name). Setting `endpoint` is what switches the client into Azure mode. [learn.microsoft.com -- Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) 4. For Azure Managed Identity authentication, omit `apiKey` and provide `azureADTokenProvider: () => Promise<string>` instead. This function is called before each request to obtain a fresh token. [learn.microsoft.com -- Managed Identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) 5. Store all secrets (`apiKey`, `endpoint`, `apiVersion`, deployment name) in environment variables and load them via `process.env`. Never hard-code API keys in source files. Use `dotenv` for local development. [dotenv on npm](https://www.npmjs.com/package/dotenv) 6. Use the `requestOptions` field to set default chat completion parameters (`temperature`, `max_tokens`, `top_p`, etc.). These apply to every `prompt.send()` call unless overridden per-request via `prompt.send(input, { request: { ... } })`. [OpenAI -- Chat Completions](https://platform.openai.com/docs/api-reference/chat/create) 7. Use `logger` on the model constructor to get request/response debug logging. Pass a `ConsoleLogger` child for scoped output (e.g., `logger.child('openai')`). [github.com/microsoft/teams.ts -- ConsoleLogger](https://github.com/microsoft/teams.ts) 8. Set `timeout` (in milliseconds) to prevent hanging requests. A reasonable default is 30000-60000ms for chat completions. The SDK does not set a default timeout. [OpenAI SDK -- timeout](https://platform.openai.com/docs/api-reference) 9. Use `headers` for custom HTTP headers required by proxies or API gateways. This is a `Record<string, string>` merged into every outgoing request. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Use `organization` and `project` only when your OpenAI account requires org/project scoping. Azure OpenAI ignores these fields. [OpenAI -- Organization](https://platform.openai.com/docs/api-reference/organization-optional) ## patterns ### OpenAI configuration ```typescript import { OpenAIChatModel } from '@microsoft/teams.openai'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); ``` ### Azure OpenAI configuration ```typescript import { OpenAIChatModel } from '@microsoft/teams.openai'; const model = new OpenAIChatModel({ apiKey: process.env.AZURE_OPENAI_API_KEY, endpoint: process.env.AZURE_OPENAI_ENDPOINT, apiVersion: process.env.AZURE_OPENAI_API_VERSION, model: process.env.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME, }); ``` ### Azure OpenAI with Managed Identity and request defaults ```typescript import { OpenAIChatModel } from '@microsoft/teams.openai'; import { ConsoleLogger } from '@microsoft/teams.common'; const logger = new ConsoleLogger('my-bot', { level: 'debug' }); const model = new OpenAIChatModel({ endpoint: process.env.AZURE_OPENAI_ENDPOINT, apiVersion: process.env.AZURE_OPENAI_API_VERSION, model: process.env.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME, azureADTokenProvider: () => getAzureADToken(), timeout: 30000, logger: logger.child('openai'), requestOptions: { temperature: 0.7, max_tokens: 1000, }, }); ``` ## pitfalls - **Setting `endpoint` with an OpenAI key**: If you provide `endpoint`, the SDK switches to Azure mode and your plain OpenAI key will fail authentication. Only set `endpoint` for Azure OpenAI. - **Using the base model name for Azure**: Azure OpenAI `model` must be the deployment name (e.g., `'my-gpt4o-deployment'`), not the base model name (`'gpt-4o'`). Mismatches produce 404 errors. - **Forgetting `apiVersion` for Azure**: Azure OpenAI requires `apiVersion`. Omitting it results in a request path error. Use a known stable version like `'2024-02-01'`. - **No timeout set**: Without `timeout`, a stalled Azure endpoint can hang your bot indefinitely. Always set an explicit timeout for production deployments. - **`azureADTokenProvider` returning stale tokens**: The provider function is called per-request. Make sure it handles token caching and refresh internally (e.g., via `@azure/identity` `DefaultAzureCredential`). - **Committing `.env` files**: API keys in `.env` should be in `.gitignore`. Never commit secrets to version control. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [OpenAI API Reference -- Chat Completions](https://platform.openai.com/docs/api-reference/chat/create) - [Azure OpenAI Service REST API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) - [Azure Managed Identity Overview](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) - [@microsoft/teams.openai -- npm](https://www.npmjs.com/package/@microsoft/teams.openai) ## instructions This expert covers configuring the `OpenAIChatModel` class from `@microsoft/teams.openai` for use with ChatPrompt in Teams AI v2. Use it when you need to: - Create a new model instance for OpenAI or Azure OpenAI - Configure Azure Managed Identity token providers for keyless authentication - Set default request parameters (temperature, max_tokens) at the model level - Add custom headers, timeouts, or logging to the model client - Understand the full `OpenAIChatModelOptions` reference table and which fields trigger Azure mode Pair with `ai.chatprompt-basics-ts.md` for passing the model to ChatPrompt, `ai.streaming-ts.md` for streaming configuration, and `runtime.app-init-ts.md` for the App context where models are used. ## research Deep Research prompt: "Write a micro expert on configuring OpenAIChatModel in the Teams AI Library v2 (TypeScript). Cover the OpenAIChatModel constructor, OpenAI vs Azure OpenAI configuration differences, all OpenAIChatModelOptions fields (apiKey, endpoint, apiVersion, model, azureADTokenProvider, baseUrl, organization, project, headers, timeout, requestOptions, logger), Azure Managed Identity patterns, model selection guidance, and environment variable best practices." -
ai.rag-retrieval-ts.md 7.4 KB
# ai.rag-retrieval-ts ## purpose Retrieval-augmented generation using function calling with search backends. ## rules 1. Implement RAG in Teams AI v2 by registering a search function on `ChatPrompt` via `.function()`. The LLM decides when to call the search tool based on user queries, retrieves relevant documents, and incorporates them into its response. This is the canonical RAG pattern in the SDK. 2. Define a search function with a `query` parameter of type `string`. The function body performs the search against your index and returns an array of result objects containing at minimum `title` and `content` fields. 3. Use `fuse.js` for lightweight in-memory full-text search. Initialize a `Fuse` instance with your document array and configure `keys` (fields to search) and `threshold` (0.0 = exact match, 1.0 = match anything; 0.3-0.4 is a good default). 4. Format search results as structured objects the LLM can reason about. Return `{ title, content }` pairs so the model can cite sources by name. Limit results to 3-5 documents to avoid overwhelming the context window. 5. Set the system instructions to tell the LLM to always cite sources. For example: `'Answer questions using the search tool. Always cite sources as [1], [2], etc.'` Without explicit citation instructions, the LLM will not reference sources consistently. 6. After receiving the LLM response, annotate it with `.addCitation(index, { name, abstract })` for each source referenced. Map citation indices to the search results that were actually used in the response. 7. Always pair citations with `.addAiGenerated()` so the Teams client renders both the AI marker and the citation annotations correctly. 8. For production, replace `fuse.js` with a scalable search backend (Azure AI Search, Elasticsearch, Pinecone). The function handler signature stays the same -- only the search implementation inside changes. 9. Enable auto function calling (the default) for RAG so the LLM can seamlessly call the search function, receive results, and generate a cited response in a single `prompt.send()` call. 10. Index your documents with meaningful titles and chunked content. Large documents should be split into sections of 500-1000 tokens each. Include metadata (title, section heading, URL) so the LLM can produce useful citations. ## patterns ### RAG with fuse.js in-memory search ```typescript import Fuse from 'fuse.js'; import { ChatPrompt } from '@microsoft/teams.ai'; import { MessageActivity } from '@microsoft/teams.api'; // Build a searchable index const docs = [ { title: 'Getting Started', content: 'Install the SDK with npm install @microsoft/teams.ai...' }, { title: 'Authentication Guide', content: 'Configure OAuth with clientId and clientSecret...' }, { title: 'Adaptive Cards', content: 'Use CardFactory to create rich card layouts...' }, ]; const fuse = new Fuse(docs, { keys: ['title', 'content'], threshold: 0.4, }); const prompt = new ChatPrompt({ model, instructions: 'Answer questions using the search tool. Always cite sources as [1], [2], etc.', }) .function( 'search', 'Search documentation for relevant information', { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, }, required: ['query'], }, async ({ query }: { query: string }) => { const results = fuse.search(query); return results.map((r) => ({ title: r.item.title, content: r.item.content, })); } ); ``` ### Sending RAG response with citations ```typescript app.on('message', async ({ send, activity }) => { const result = await prompt.send(activity.text); if (result.content) { const msg = new MessageActivity(result.content) .addAiGenerated() .addCitation(1, { name: 'Getting Started', abstract: 'Installation and setup guide' }) .addCitation(2, { name: 'Authentication Guide', abstract: 'OAuth configuration reference' }); await send(msg); } }); ``` ### RAG with streaming and feedback ```typescript app.on('message', async ({ stream, activity }) => { stream.update('Searching documentation...'); const result = await prompt.send(activity.text, { onChunk: (chunk: string) => { stream.emit( new MessageActivity(chunk) .addAiGenerated() .addFeedback() ); }, }); // Citations are added to the final streamed message automatically // For dynamic citations based on actual search results, track them in the handler }); ``` ## pitfalls - **Not instructing the LLM to cite sources**: Without explicit instructions like `"Always cite sources as [1], [2]"`, the LLM will use search results but not reference them by number, making citation annotations meaningless. - **Returning too many search results**: Flooding the context with 20+ documents wastes tokens and confuses the model. Limit to 3-5 top results. Use relevance scoring (fuse.js `score`) to filter. - **Hardcoding citation indices**: If you always add `addCitation(1, ...)` and `addCitation(2, ...)` regardless of which documents the LLM actually cited, users see irrelevant citations. Track which sources the search function returned and map them dynamically. - **Using fuse.js for large document sets**: `fuse.js` loads all documents into memory and performs linear search. For more than a few hundred documents, switch to an external search service (Azure AI Search, Elasticsearch). - **Not chunking large documents**: Passing a 10,000-token document as a single search result consumes most of the context window. Split documents into 500-1000 token chunks with overlapping context. - **Forgetting `.addAiGenerated()` with citations**: Citations without the AI-generated marker may not render correctly in the Teams client. Always chain both methods. - **Search function returning raw HTML or markdown**: Strip formatting from indexed content. The LLM handles raw text better than markup, and markup tokens waste context budget. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [Fuse.js -- Lightweight Fuzzy Search](https://www.fusejs.io/) - [RAG Pattern -- Microsoft Learn](https://learn.microsoft.com/en-us/azure/search/retrieval-augmented-generation-overview) - [Azure AI Search -- Vector Search](https://learn.microsoft.com/en-us/azure/search/vector-search-overview) - [@microsoft/teams.ai -- npm](https://www.npmjs.com/package/@microsoft/teams.ai) ## instructions This expert covers implementing retrieval-augmented generation (RAG) in Teams AI v2 using function calling with search backends. Use it when you need to: - Implement the RAG pattern by registering a search function on `ChatPrompt` - Build a lightweight in-memory search index with `fuse.js` - Format search results for LLM consumption with title and content fields - Annotate LLM responses with source citations using `MessageActivity.addCitation()` - Instruct the LLM to cite sources in its responses - Combine RAG with streaming and feedback buttons Pair with `ai.function-calling-implementation-ts.md` for search functions, `ai.citations-feedback-ts.md` for citations, and `ai.rag-vectorstores-ts.md` for vector search backends. ## research Deep Research prompt: "Write a micro expert on RAG for Teams AI (TypeScript). Provide an architecture pattern where the model calls a search tool (function calling) and returns answers with citations. Cover indexing choices (simple in-memory like Fuse.js vs external vector DB), chunking, citation mapping, and guardrails. Include a minimal working example with a local search index." -
ai.rag-vectorstores-ts.md 8.5 KB
# ai.rag-vectorstores-ts ## purpose Vector store integration patterns for semantic search in RAG pipelines. ## rules 1. Use vector stores when keyword search (fuse.js, Elasticsearch BM25) is insufficient. Vector search finds semantically similar documents even when the user's query uses different words than the source text. This is the recommended approach for production RAG systems. 2. Choose a vector store based on your infrastructure: Azure AI Search (managed, integrated with Azure ecosystem), Pinecone (managed, purpose-built for vectors), pgvector (self-hosted, PostgreSQL extension), or Weaviate (self-hosted or managed, hybrid search). All integrate with the same ChatPrompt function calling pattern. 3. Generate embeddings using OpenAI's embedding models (e.g., `text-embedding-3-small` or `text-embedding-3-large`) or Azure OpenAI embedding deployments. Send document chunks to the embedding API during indexing and query text at search time. 4. Define a retrieval interface with a single `search(query: string): Promise<SearchResult[]>` method. This abstraction lets you swap vector backends without changing the ChatPrompt function registration. 5. Register the vector search as a ChatPrompt `.function()` with a `query` parameter. The handler calls your retrieval interface, formats results, and returns them to the LLM. This is identical to the keyword search pattern -- only the search implementation differs. 6. Chunk documents into 500-1000 token segments with 50-100 token overlap between chunks. Store metadata (document title, section heading, page number, URL) alongside each chunk for citation mapping. 7. Normalize search scores to a 0-1 range and filter results below a relevance threshold (e.g., 0.7). Return only the top 3-5 results to stay within the LLM context budget. 8. Cache embeddings for frequently repeated queries. Embedding API calls add latency and cost. Use a simple in-memory cache or Redis for production deployments. 9. Re-index documents on a schedule or trigger. Stale vector indices produce irrelevant search results. Automate re-indexing when source documents change. 10. Test retrieval quality independently of the LLM. Build a test suite with known queries and expected documents. Measure recall and precision before integrating with ChatPrompt. ## patterns ### Retrieval interface abstraction ```typescript interface SearchResult { title: string; content: string; score: number; metadata?: Record<string, string>; } interface IRetriever { search(query: string): Promise<SearchResult[]>; } ``` ### Azure AI Search vector store implementation ```typescript import { SearchClient, AzureKeyCredential } from '@azure/search-documents'; class AzureAISearchRetriever implements IRetriever { private client: SearchClient<{ title: string; content: string; embedding: number[] }>; constructor() { this.client = new SearchClient( process.env.AZURE_SEARCH_ENDPOINT!, process.env.AZURE_SEARCH_INDEX!, new AzureKeyCredential(process.env.AZURE_SEARCH_KEY!) ); } async search(query: string): Promise<SearchResult[]> { // Generate embedding for the query const queryEmbedding = await this.getEmbedding(query); const results = await this.client.search(query, { vectorSearchOptions: { queries: [{ kind: 'vector', vector: queryEmbedding, kNearestNeighborsCount: 5, fields: ['embedding'], }], }, top: 5, }); const docs: SearchResult[] = []; for await (const result of results.results) { docs.push({ title: result.document.title, content: result.document.content, score: result.score ?? 0, }); } return docs.filter((d) => d.score > 0.7); } private async getEmbedding(text: string): Promise<number[]> { const response = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'text-embedding-3-small', input: text, }), }); const data = await response.json(); return data.data[0].embedding; } } ``` ### Exposing vector search as a ChatPrompt function ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; import { MessageActivity } from '@microsoft/teams.api'; const retriever: IRetriever = new AzureAISearchRetriever(); const prompt = new ChatPrompt({ model, instructions: 'Answer questions using the search tool. Always cite sources as [1], [2], etc.', }) .function( 'search', 'Search the knowledge base for relevant information', { type: 'object', properties: { query: { type: 'string', description: 'Semantic search query' }, }, required: ['query'], }, async ({ query }: { query: string }) => { const results = await retriever.search(query); return results.map((r, i) => ({ index: i + 1, title: r.title, content: r.content, })); } ); app.on('message', async ({ send, activity }) => { const result = await prompt.send(activity.text); if (result.content) { const msg = new MessageActivity(result.content) .addAiGenerated() .addCitation(1, { name: 'Document A', abstract: 'Primary source' }) .addCitation(2, { name: 'Document B', abstract: 'Supporting reference' }); await send(msg); } }); ``` ## pitfalls - **Using the wrong embedding model at query time vs index time**: The same embedding model must be used for both indexing and querying. Mismatched models produce incompatible vector spaces and garbage results. - **Not chunking documents before embedding**: Embedding a 10-page document as a single vector loses granularity. The embedding represents the average meaning, missing specific details. Chunk into 500-1000 token segments. - **Skipping the relevance threshold**: Without filtering low-score results (e.g., `score > 0.7`), the LLM receives irrelevant documents and may hallucinate answers based on unrelated content. - **Embedding API rate limits**: Batch embedding calls during indexing. At query time, a single embedding call per search is typical, but high-traffic bots should cache embeddings for repeated queries. - **Hardcoding vector dimensions**: Different embedding models produce different dimensions (e.g., `text-embedding-3-small` = 1536). Configure your vector index to match the model's output dimension. - **Not testing retrieval independently**: If the LLM gives bad answers, the problem may be retrieval (wrong documents returned) or generation (LLM misinterpreting results). Test the search function in isolation first. - **Stale indices**: Documents change but the vector index is never updated. Implement a re-indexing pipeline triggered by document updates or on a regular schedule. - **Ignoring hybrid search**: Pure vector search can miss exact keyword matches. Many vector stores (Azure AI Search, Weaviate) support hybrid search combining BM25 keyword scoring with vector similarity. Use hybrid mode for best results. ## references - [Azure AI Search -- Vector Search](https://learn.microsoft.com/en-us/azure/search/vector-search-overview) - [OpenAI Embeddings API](https://platform.openai.com/docs/guides/embeddings) - [Pinecone Documentation](https://docs.pinecone.io/) - [pgvector -- PostgreSQL Extension](https://github.com/pgvector/pgvector) - [Weaviate Documentation](https://weaviate.io/developers/weaviate) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) ## instructions This expert covers integrating vector stores for semantic search in RAG pipelines with Teams AI v2. Use it when you need to: - Choose a vector store backend (Azure AI Search, Pinecone, pgvector, Weaviate) - Generate embeddings using OpenAI or Azure OpenAI embedding models - Design a retrieval interface abstraction for swappable backends - Implement a vector search function and expose it as a ChatPrompt `.function()` - Chunk documents, manage metadata, and configure relevance thresholds - Understand hybrid search (keyword + vector) for improved retrieval quality Pair with `ai.rag-retrieval-ts.md` for the overall RAG pattern, and `ai.function-calling-implementation-ts.md` for exposing vector search as a function. ## research Deep Research prompt: "Write a micro expert on integrating vector search backends for RAG in TypeScript bots. Cover common options (Azure AI Search vector, pgvector, Pinecone, Weaviate), how to design a retrieval interface, and how to expose retrieval as a ChatPrompt tool. Keep it vendor-neutral with small pseudo-code and a checklist." -
ai.streaming-ts.md 6.9 KB
# ai.streaming-ts ## purpose Real-time streaming of AI responses with typing indicators and progressive rendering. ## rules 1. Use the `onChunk` callback in `prompt.send()` options to receive text chunks as they arrive from the LLM. Each chunk is a `string` fragment of the ongoing response. 2. Inside `onChunk`, call `stream.emit(chunk)` to send the accumulated text to the user with a typing indicator. The `stream` object is available on the handler context (`ctx.stream`). 3. `stream.emit()` accepts either a plain `string` or a `MessageActivity` instance. Use `MessageActivity` when you need to attach feedback buttons, AI-generated markers, or citations to the streaming message. 4. Call `stream.update(text)` to send a status update (e.g., `"Thinking..."`, `"Searching documents..."`). Status updates are separate from the accumulated content and display as informative indicators. 5. `stream.close()` is called automatically when the message handler returns. It sends the final message containing all accumulated content, attachments, and entities. You do not need to call it manually in typical usage. 6. If you need to finalize the stream early (e.g., after an error), call `stream.close()` explicitly. After close, further `emit()` calls are ignored. 7. Streaming works internally by batching: content is queued and flushed in batches of up to 10 items every 500ms. Text accumulates across chunks so the final message contains the complete response. 8. Listen to stream events with `stream.events.on('chunk', handler)` for each sent chunk and `stream.events.once('close', handler)` for the final message. Use these for logging, analytics, or post-processing. 9. When combining streaming with `MessageActivity` features (feedback, citations), construct a new `MessageActivity` in each `onChunk` call. The stream accumulates content across emissions automatically. 10. Do not call `await send()` for the final message when streaming -- `stream.close()` handles it. Calling both `send()` and allowing the auto-close results in duplicate messages. ## patterns ### Basic text streaming with onChunk ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; app.on('message', async ({ send, stream, activity }) => { const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant.' }); // Stream chunks as they arrive const response = await prompt.send(activity.text, { onChunk: (chunk: string) => { stream.emit(chunk); // Sends typing indicators with accumulated text }, }); // stream.close() is called automatically after the handler returns, // sending the final message with all accumulated content }); ``` ### Streaming with feedback buttons and AI markers ```typescript import { MessageActivity } from '@microsoft/teams.api'; app.on('message', async ({ stream, activity }) => { const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant.' }); const response = await prompt.send(activity.text, { onChunk: (chunk: string) => { // Emit a MessageActivity with feedback buttons on each chunk stream.emit(new MessageActivity(chunk).addFeedback()); }, }); // Final message automatically includes feedback buttons }); ``` ### Stream API with status updates and event listeners ```typescript app.on('message', async ({ stream, activity }) => { // Show a status while the LLM is thinking stream.update('Searching documents...'); const prompt = new ChatPrompt({ model, instructions: 'You are a research assistant.' }); // Listen for stream events stream.events.on('chunk', (sentActivity) => { console.log('Chunk sent to user'); }); stream.events.once('close', (sentActivity) => { console.log('Final message delivered:', sentActivity.id); }); const response = await prompt.send(activity.text, { onChunk: (chunk: string) => { stream.emit(chunk); }, }); // stream.close() sends the final message automatically }); ``` ## pitfalls - **Calling `send()` after streaming**: If you call `await send(response.content)` after streaming, the user receives a duplicate final message. The auto-close on `stream.close()` already sends the complete response. - **Forgetting `stream.emit()` inside `onChunk`**: Defining `onChunk` without calling `stream.emit()` means the user sees nothing until the final message. The `onChunk` callback alone does not send anything to the client. - **Calling `stream.close()` too early**: Explicitly closing the stream before `prompt.send()` resolves discards remaining chunks. Only call `close()` manually for error bailout scenarios. - **Heavy computation in `onChunk`**: The callback fires on every token. Expensive operations (API calls, database writes) inside `onChunk` create backpressure and degrade streaming performance. Log or buffer instead. - **Not handling errors during streaming**: If the LLM request fails mid-stream, the user sees partial text with no indication of failure. Wrap `prompt.send()` in try/catch and call `stream.emit('An error occurred.')` followed by `stream.close()` in the catch block. - **Assuming chunk boundaries are semantic**: Chunks are raw token fragments, not words or sentences. Do not parse or process individual chunks as complete text units. - **Ignoring batching behavior**: The SDK batches up to 10 items every 500ms. Very rapid `emit()` calls do not produce 1:1 client updates. This is normal and expected. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [Teams Streaming Protocol -- Microsoft Learn](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/streaming) - [@microsoft/teams.ai -- npm](https://www.npmjs.com/package/@microsoft/teams.ai) - [OpenAI Streaming -- API Reference](https://platform.openai.com/docs/api-reference/chat/create#chat-create-stream) ## instructions This expert covers real-time streaming of AI responses in Teams AI v2. Use it when you need to: - Stream LLM responses to the user with typing indicators using `onChunk` and `stream.emit()` - Display status updates during long-running operations with `stream.update()` - Combine streaming with `MessageActivity` for feedback buttons and AI-generated markers - Understand the internal batching mechanism (10 items / 500ms) and its effect on UX - Handle errors gracefully during streaming - Use stream events (`chunk`, `close`) for logging and analytics Pair with `ai.chatprompt-basics-ts.md` for prompt.send() with onChunk, `ai.citations-feedback-ts.md` for combining streaming with feedback buttons, and `runtime.routing-handlers-ts.md` for ctx.stream. ## research Deep Research prompt: "Write a micro expert on streaming AI responses in Teams SDK v2 (TypeScript). Explain how ctx.stream works, how onChunk accumulates text, how to emit MessageActivity vs strings, and how to combine streaming with typing indicators, final messages, and error handling. Include at least two patterns: (1) plain text streaming, (2) streaming with addAiGenerated/addFeedback." -
auth.oauth-sso-ts.md 10.1 KB
# auth.oauth-sso-ts ## purpose OAuth/SSO sign-in flows, token management, and connection configuration in Teams bots using the Teams AI Library v2. ## rules 1. Always configure OAuth by passing `oauth: { defaultConnectionName: 'graph' }` to the `App` constructor alongside `clientId`, `clientSecret`, and `tenantId`. Without all four properties the sign-in flow will fail silently. [learn.microsoft.com -- Bot SSO](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/bot-sso-overview) 2. Guard every message handler with an `isSignedIn` check before accessing `userGraph` or `userToken`. If the user is not signed in, call `await signin()` and return immediately -- the handler will re-fire after the sign-in completes. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Pass `oauthCardText` and `signInButtonText` options to `signin()` to customize the sign-in card displayed to the user. These are the only two customization points for the OAuth card. [learn.microsoft.com -- Auth flow](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/auth-flow-bot) 4. Handle the post-sign-in event with `app.event('signin', handler)` to greet the user or execute first-time logic. The handler receives `{ send, userGraph, token }` and fires after the token exchange completes. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. Implement sign-out with a dedicated command (e.g., `app.message('/signout', ...)`) that checks `isSignedIn`, calls `await signout()`, and confirms to the user. Forgetting the `isSignedIn` guard on signout causes confusing errors. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. The `userToken` property on the handler context is the raw OAuth access token string. Use it only for direct REST calls outside of the Graph client; prefer `userGraph` for Microsoft Graph calls as it handles token injection automatically. [learn.microsoft.com -- Get token](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/auth-flow-bot) 7. For app-level (service-to-service) calls, use `appGraph` which authenticates with client credentials and does not require user sign-in. Do not confuse `appGraph` (application permissions) with `userGraph` (delegated permissions). [learn.microsoft.com -- Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-overview) 8. Choose the appropriate credential method: `clientId` + `clientSecret` for standard deployments, `managedIdentityClientId: 'system'` for system-assigned managed identity, a specific identity string for user-assigned managed identity, or `token` for a custom token factory. [learn.microsoft.com -- Managed identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) 9. The OAuth connection name (e.g., `'graph'`) must exactly match the OAuth connection setting configured in the Azure Bot resource. A mismatch results in a generic "sign-in failed" error with no helpful diagnostics. [learn.microsoft.com -- Add authentication](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/add-authentication) 10. Never store or log raw tokens. The `userToken` is a bearer credential that grants access to the user's data. If you need to persist auth state, store a flag or user ID, not the token itself. [learn.microsoft.com -- Security best practices](https://learn.microsoft.com/en-us/azure/active-directory/develop/security-best-practices-for-app-registration) ## patterns ### Basic sign-in guard with Graph call ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, oauth: { defaultConnectionName: 'graph' }, logger: new ConsoleLogger('auth-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); app.on('message', async ({ isSignedIn, signin, userGraph, send }) => { // Always check isSignedIn before using userGraph if (!isSignedIn) { await signin({ oauthCardText: 'Please sign in to continue', signInButtonText: 'Sign In', }); return; } // User is authenticated -- safe to call Graph with delegated token const me = await userGraph.call(endpoints.me.get); await send(`Hello, ${me.displayName}!`); }); app.start(3978); ``` ### Post-sign-in event and sign-out command ```typescript import { App } from '@microsoft/teams.apps'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, oauth: { defaultConnectionName: 'graph' }, }); // Fires after successful token exchange app.event('signin', async ({ send, userGraph }) => { const me = await userGraph.call(endpoints.me.get); await send(`Welcome, ${me.displayName}! You are now signed in.`); }); // Dedicated sign-out command app.message('/signout', async ({ isSignedIn, signout, send }) => { if (!isSignedIn) { await send('You are not signed in.'); return; } await signout(); await send('You have been signed out.'); }); app.start(3978); ``` ### Token types configuration ```typescript import { App } from '@microsoft/teams.apps'; // Option 1: Client credentials (most common) const appWithSecret = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, oauth: { defaultConnectionName: 'graph' }, }); // Option 2: System-assigned managed identity (Azure deployment) const appWithSystemMI = new App({ clientId: process.env.CLIENT_ID, tenantId: process.env.TENANT_ID, managedIdentityClientId: 'system', oauth: { defaultConnectionName: 'graph' }, }); // Option 3: User-assigned managed identity const appWithUserMI = new App({ clientId: process.env.CLIENT_ID, tenantId: process.env.TENANT_ID, managedIdentityClientId: process.env.MANAGED_IDENTITY_CLIENT_ID, oauth: { defaultConnectionName: 'graph' }, }); // Option 4: Custom token factory const appWithFactory = new App({ clientId: process.env.CLIENT_ID, tenantId: process.env.TENANT_ID, token: async () => { // Return a token string from your custom provider return await getTokenFromVault(); }, oauth: { defaultConnectionName: 'graph' }, }); ``` ## pitfalls - **Missing `oauth` in App options**: Setting `clientId`/`clientSecret`/`tenantId` without `oauth: { defaultConnectionName: 'graph' }` means `isSignedIn` is always `false` and `signin()` does nothing. All four must be present. - **Calling `userGraph` before sign-in check**: Accessing `userGraph.call()` when `isSignedIn` is `false` throws an error because there is no delegated token. Always gate behind `if (!isSignedIn)`. - **Connection name mismatch**: The `defaultConnectionName` value must exactly match the OAuth connection setting name in the Azure Bot resource. A typo silently breaks the sign-in flow. - **Confusing `appGraph` and `userGraph`**: `appGraph` uses application permissions (no user context). `userGraph` uses delegated permissions (user's identity). Using the wrong one leads to permission-denied errors or data leaks. - **Not returning after `signin()`**: After calling `await signin()`, you must `return` from the handler. Code after `signin()` executes with no user context and will fail. - **Storing raw tokens**: The `userToken` string is a bearer credential. Logging or persisting it creates a security vulnerability. Store only non-sensitive identifiers. - **Forgetting the sign-in event handler**: Without `app.event('signin', ...)`, there is no feedback to the user after they complete the OAuth flow. They see the sign-in card but no confirmation. - **Using `signout()` without `isSignedIn` guard**: Calling `signout()` when the user is not signed in may throw or produce confusing behavior. Always check first. ## references - [Teams Bot SSO overview](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/bot-sso-overview) - [Add authentication to a Teams bot](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/add-authentication) - [Bot authentication flow](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/auth-flow-bot) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [Microsoft Graph permissions overview](https://learn.microsoft.com/en-us/graph/permissions-overview) - [Azure Managed Identities](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) ## instructions This expert covers OAuth/SSO authentication flows in Microsoft Teams bots built with the Teams AI Library v2 (`@microsoft/teams.ts`) in TypeScript. Use it when you need to: - Configure OAuth settings on the App constructor with `clientId`, `clientSecret`, `tenantId`, and `oauth.defaultConnectionName` - Implement the sign-in guard pattern (`isSignedIn` check, `signin()` call, early return) - Handle the post-sign-in event with `app.event('signin', ...)` - Implement a sign-out command - Choose between credential types (client secret, managed identity, custom token factory) - Understand the difference between `userGraph` (delegated) and `appGraph` (app-level) authentication contexts Pair with `graph.usergraph-appgraph-ts.md` for Graph API call patterns after authentication, and `state.storage-patterns-ts.md` for persisting user session data. Pair with `graph.usergraph-appgraph-ts.md` for calling Graph API after sign-in, and `runtime.app-init-ts.md` for oauth configuration in the App constructor. ## research Deep Research prompt: "Write a micro expert on OAuth/SSO authentication in Microsoft Teams bots using the Teams AI Library v2 (TypeScript). Cover App oauth configuration, the isSignedIn/signin/signout flow, the signin event handler, token types (client credentials, managed identity, custom factory), sign-in guard patterns in message handlers, and common authentication pitfalls. Include 2-3 canonical TypeScript code examples." -
compat.botbuilder-interop-ts.md 14.6 KB
# compat.botbuilder-interop-ts ## purpose Backward compatibility with legacy BotBuilder/Bot Framework bots using `@microsoft/teams.botbuilder`, migration patterns, and interop decisions. ## rules 1. The `@microsoft/teams.botbuilder` package provides a backward-compatibility layer between legacy BotBuilder bots (using `TeamsActivityHandler` from `botbuilder`) and the Teams SDK v2 (`@microsoft/teams.apps`). Use it only when you have an existing BotBuilder codebase that cannot be fully rewritten immediately. [github.com/microsoft/teams.ts -- botbuilder](https://github.com/microsoft/teams.ts/tree/main/packages/botbuilder) 2. For new projects, always use `@microsoft/teams.apps` directly. The compat layer adds overhead and limits access to newer SDK v2 features (plugin system, streaming, DevTools, MCP, A2A). Only use `@microsoft/teams.botbuilder` for incremental migration of existing bots. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Legacy BotBuilder bots use `TeamsActivityHandler` with method overrides (`onMessage()`, `onMembersAdded()`, `onTeamsChannelCreated()`, etc.) and the `TurnContext` object. Teams SDK v2 uses `App` with `app.on()` route handlers and a destructured activity context. The compat layer bridges these two models. [github.com/microsoft/teams.ts -- botbuilder](https://github.com/microsoft/teams.ts/tree/main/packages/botbuilder) 4. The migration path from BotBuilder to SDK v2 follows: (a) install `@microsoft/teams.botbuilder` alongside existing `botbuilder` packages, (b) wrap the existing handler with the compat layer, (c) incrementally move handlers from `TeamsActivityHandler` overrides to `app.on()` routes, (d) once all handlers are migrated, remove the compat layer and `botbuilder` dependencies entirely. [github.com/microsoft/teams.ts -- botbuilder](https://github.com/microsoft/teams.ts/tree/main/packages/botbuilder) 5. Key API differences between BotBuilder and SDK v2: BotBuilder uses `TurnContext.sendActivity()` while SDK v2 uses `ctx.send()` / `ctx.reply()`; BotBuilder uses `CardFactory.adaptiveCard()` while SDK v2 sends raw attachment objects; BotBuilder uses `ActivityHandler.run()` in an Express middleware while SDK v2 uses `app.start(port)`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. BotBuilder's state management (`ConversationState`, `UserState`, `MemoryStorage`) does not carry over. SDK v2 uses `IStorage` from `@microsoft/teams.common` and `LocalMemory` from `@microsoft/teams.ai`. Migrate state stores as part of the transition. [github.com/microsoft/teams.ts -- common](https://github.com/microsoft/teams.ts/tree/main/packages/common) 7. BotBuilder's dialog system (`ComponentDialog`, `WaterfallDialog`) is not available in SDK v2. Replace with SDK v2 `dialog.open` / `dialog.submit` invoke routes and Adaptive Card-based task modules, or use AI-driven conversation flows with `ChatPrompt`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. Authentication patterns differ: BotBuilder uses `OAuthPrompt` and `TokenResponseEventHandler`, while SDK v2 uses `oauth` in `AppOptions` with `ctx.isSignedIn`, `ctx.signin()`, and `ctx.userGraph`. Migrate OAuth configuration to the App constructor. [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) 9. The `@examples/botbuilder` example in the Teams SDK v2 repository demonstrates the compat layer usage. Reference it for concrete interop patterns. [github.com/microsoft/teams.ts -- examples/botbuilder](https://github.com/microsoft/teams.ts/tree/main/examples/botbuilder) 10. Before starting migration, audit the existing bot's feature set: message handlers, card actions, dialogs, proactive messaging, authentication, and any Bot Framework middleware. Map each feature to its SDK v2 equivalent. Features without direct equivalents (BotBuilder dialogs, custom middleware adapters) require the most rework. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## interview ### Q1 — Migration Strategy ``` question: "Your project has an existing BotBuilder bot. Do you want to use the compatibility layer for incremental migration, or do a full rewrite to SDK v2?" header: "Strategy" options: - label: "Full rewrite (Recommended)" description: "Rewrite all handlers directly in Teams SDK v2. Cleaner result, full access to streaming/plugins/MCP/A2A. Best for bots with <15 routes or when starting fresh." - label: "Compat layer (incremental)" description: "Use @microsoft/teams.botbuilder to run old and new handlers side by side. Migrate one handler at a time. Best for large bots (15+ routes) or tight timelines." - label: "You Decide Everything" description: "Accept recommended defaults for all decisions and skip remaining questions." multiSelect: false ``` ### Q2 — Dialog Migration ``` question: "Does your existing bot use BotBuilder dialogs (WaterfallDialog, ComponentDialog)?" header: "Dialogs" options: - label: "No dialogs" description: "Bot uses simple message handlers only. No dialog migration needed." - label: "Yes — replace with Adaptive Cards (Recommended)" description: "Replace dialog flows with Adaptive Card forms + dialog.open/submit routes. Modern Teams pattern." - label: "Yes — replace with AI conversation" description: "Replace dialog flows with ChatPrompt-driven AI conversation. Best for open-ended inputs." multiSelect: false ``` ### Q3 — State Migration ``` question: "How is your existing bot managing state (ConversationState, UserState)?" header: "State" options: - label: "MemoryStorage (dev only)" description: "In-memory storage — no migration needed, just switch to SDK v2 IStorage." - label: "Azure Blob/Cosmos (Recommended)" description: "Persistent storage — migrate connection config to SDK v2 IStorage adapter. Data format is compatible." - label: "Custom storage provider" description: "Custom IStorage implementation — will need to be adapted to SDK v2's IStorage interface." multiSelect: false ``` ### defaults table | Question | Default | |---|---| | Q1 | Full rewrite | | Q2 | No dialogs | | Q3 | MemoryStorage | ## patterns ### BotBuilder handler vs SDK v2 equivalent ```typescript // --- BEFORE: Legacy BotBuilder pattern --- // Uses TeamsActivityHandler, TurnContext, and method overrides import { TeamsActivityHandler, TurnContext, MessageFactory } from 'botbuilder'; class LegacyBot extends TeamsActivityHandler { async onMessage(context: TurnContext): Promise<void> { const text = context.activity.text?.trim(); if (text === '/help') { await context.sendActivity(MessageFactory.text('Here is how I can help...')); } else { await context.sendActivity(MessageFactory.text(`You said: "${text}"`)); } } async onMembersAdded(context: TurnContext): Promise<void> { for (const member of context.activity.membersAdded || []) { if (member.id !== context.activity.recipient.id) { await context.sendActivity('Welcome!'); } } } } // --- AFTER: Teams SDK v2 pattern --- // Uses App, app.on(), app.message(), destructured context import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ logger: new ConsoleLogger('migrated-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); app.message('/help', async ({ send }) => { await send('Here is how I can help...'); }); app.on('message', async ({ send, activity }) => { await send(`You said: "${activity.text}"`); }); app.on('install.add', async ({ send }) => { await send('Welcome!'); }); app.start(3978); ``` ### Migration decision checklist ```typescript // When to use the compat layer (@microsoft/teams.botbuilder): // // 1. Existing BotBuilder bot with many handlers that cannot be rewritten at once // 2. Dependencies on BotBuilder-specific libraries or middleware // 3. Need to run old and new handlers side by side during transition // 4. Legacy dialog flows (WaterfallDialog) that need time to redesign // // When to do a full rewrite (skip the compat layer): // // 1. Small bot with few handlers (< 10 routes) // 2. Starting a new project (always use @microsoft/teams.apps directly) // 3. Want access to SDK v2-only features: // - DevtoolsPlugin for debugging // - Plugin system (MCP, A2A, custom plugins) // - Streaming responses with stream.emit() // - Built-in OAuth with ctx.isSignedIn / ctx.signin() // - app.send() for proactive messaging // - ChatPrompt for AI integration // 4. The existing bot has no complex dialog flows // // Feature mapping: // BotBuilder TeamsActivityHandler.onMessage() -> app.on('message') // BotBuilder TeamsActivityHandler.onMembersAdded() -> app.on('install.add') // BotBuilder TurnContext.sendActivity() -> ctx.send() / ctx.reply() // BotBuilder CardFactory.adaptiveCard() -> raw attachment object // BotBuilder OAuthPrompt -> oauth in AppOptions + ctx.signin() // BotBuilder ConversationState / UserState -> IStorage + LocalStorage // BotBuilder WaterfallDialog -> dialog.open/submit + Adaptive Cards // BotBuilder ActivityHandler.run() + Express -> app.start(port) // BotBuilder proactiveMessage via ConversationRef -> app.send(conversationId, message) ``` ### Incremental migration with compat layer ```typescript // Step 1: Install the compat package alongside existing botbuilder // npm install @microsoft/teams.botbuilder @microsoft/teams.apps // Step 2: Wrap existing handler with compat layer // (Specific API depends on @microsoft/teams.botbuilder version -- // refer to the package README for exact usage) // Step 3: Incrementally move handlers to SDK v2 patterns // Move one handler at a time from the legacy class to app.on() / app.message() // Test each migration step independently // Step 4: Remove compat layer when all handlers are migrated // Remove @microsoft/teams.botbuilder and botbuilder from package.json // Your final code should look like a standard SDK v2 app: import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger('fully-migrated-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); // All handlers now use SDK v2 patterns app.on('message', async ({ send, activity }) => { await send(`Echo: ${activity.text}`); }); app.on('install.add', async ({ send }) => { await send('Welcome! Bot has been installed.'); }); app.on('card.action', async ({ activity, send }) => { const data = activity.value; await send(`Action received: ${JSON.stringify(data)}`); }); app.start(process.env.PORT || 3978).catch(console.error); ``` ## pitfalls - **Using the compat layer for new projects**: The compat layer exists solely for migration. New projects should always use `@microsoft/teams.apps` directly for full SDK v2 feature access. - **Trying to use BotBuilder dialogs in SDK v2**: `WaterfallDialog`, `ComponentDialog`, and the BotBuilder dialog stack do not exist in SDK v2. Replace them with `dialog.open`/`dialog.submit` invoke routes and Adaptive Card forms. - **Mixing `TurnContext` and SDK v2 context**: During migration, do not pass BotBuilder's `TurnContext` into SDK v2 handlers or vice versa. They are incompatible objects with different method signatures. - **Forgetting to migrate state stores**: BotBuilder's `ConversationState`/`UserState` backed by `MemoryStorage` does not work in SDK v2. Migrate to `IStorage`/`LocalStorage` from `@microsoft/teams.common`. - **Keeping `botbuilder` dependency after full migration**: Once all handlers are moved to SDK v2 patterns, remove `botbuilder`, `botbuilder-dialogs`, and `@microsoft/teams.botbuilder` from `package.json` to reduce bundle size. - **Expecting identical behavior**: SDK v2 handles some activities differently than BotBuilder (e.g., `@mention` text stripping, conversation update events). Test each migrated handler against real Teams clients. - **Not referencing the botbuilder example**: The `@examples/botbuilder` directory in the teams.ts repository contains a working compat layer demo. Always check it before starting migration. ## references - [Teams SDK v2 -- @microsoft/teams.botbuilder](https://github.com/microsoft/teams.ts/tree/main/packages/botbuilder) - [Teams SDK v2 -- botbuilder example](https://github.com/microsoft/teams.ts/tree/main/examples/botbuilder) - [Teams SDK v2 GitHub repository](https://github.com/microsoft/teams.ts) - [Bot Framework SDK for JavaScript](https://github.com/microsoft/botframework-sdk) - [BotBuilder documentation](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-basics) - [Teams: Migrate from BotBuilder](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/what-are-bots) ## instructions This expert covers backward compatibility and migration from legacy BotBuilder/Bot Framework bots to Teams SDK v2 using `@microsoft/teams.botbuilder`. Use it when you need to: - Decide whether to use the compat layer or do a full rewrite - Map BotBuilder concepts to SDK v2 equivalents (TeamsActivityHandler -> App, TurnContext -> ctx, OAuthPrompt -> oauth config, WaterfallDialog -> dialog.open/submit) - Plan an incremental migration strategy (install compat, migrate handlers one by one, remove compat) - Understand which BotBuilder features have no direct SDK v2 equivalent - Migrate state management from ConversationState/UserState to IStorage - Migrate authentication from OAuthPrompt to App oauth config Pair with `runtime.app-init-ts.md` for SDK v2 App initialization and `runtime.routing-handlers-ts.md` for the SDK v2 route handler equivalents of BotBuilder method overrides. Pair with `runtime.app-init-ts.md` for the target SDK v2 App patterns, and `runtime.routing-handlers-ts.md` for mapping TeamsActivityHandler methods to SDK v2 routes. ## research Deep Research prompt: "Write a micro expert on interoperability and migration between legacy BotBuilder (botbuilder npm package, TeamsActivityHandler) and Teams SDK v2 (@microsoft/teams.apps) in TypeScript. Cover the @microsoft/teams.botbuilder compat layer, when to use it vs full rewrite, feature mapping (onMessage -> app.on('message'), onMembersAdded -> install.add, TurnContext.sendActivity -> ctx.send, CardFactory -> raw attachments, OAuthPrompt -> oauth config, ConversationState -> IStorage, WaterfallDialog -> dialog.open/submit), incremental migration steps, the @examples/botbuilder reference, and common pitfalls. Include a side-by-side BotBuilder vs SDK v2 code comparison and a migration decision checklist." -
dev.debug-test-ts.md 14.1 KB
# dev.debug-test-ts ## purpose Developer tools, local debugging, DevTools plugin, sideloading, tunneling, ConsoleLogger configuration, and build verification for Teams SDK v2. ## rules 1. Always include `DevtoolsPlugin` from `@microsoft/teams.dev` in the `plugins` array during local development. It provides a web-based DevTools UI, WebSocket-based real-time activity inspection, and message replay capabilities. [github.com/microsoft/teams.ts -- dev](https://github.com/microsoft/teams.ts/tree/main/packages/dev) 2. The DevTools UI runs at `http://localhost:{PORT+1}/devtools` (default `http://localhost:3979/devtools`). It starts automatically when the app starts with `DevtoolsPlugin` registered. Open it in a browser to inspect inbound and outbound activities. [github.com/microsoft/teams.ts -- dev](https://github.com/microsoft/teams.ts/tree/main/packages/dev) 3. The bot endpoint is `http://localhost:{PORT}/api/messages` (default `http://localhost:3978/api/messages`). This is the URL that Teams (or the Bot Framework Emulator) sends activities to. For Azure deployment, update the messaging endpoint to `https://your-domain/api/messages`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 4. Run locally with `npm run dev` which executes `tsx watch -r dotenv/config src/index.ts`. This provides TypeScript execution with automatic file watching -- changes to source files trigger an instant restart without a build step. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. Configure logging with `ConsoleLogger` at the appropriate level: `'trace'` for deep SDK internals, `'debug'` for development, `'info'` for staging, `'warn'` or `'error'` for production. Use `pattern: '-azure/msal-node'` to suppress noisy MSAL authentication logs. Child loggers are created with `logger.child('name')`. [github.com/microsoft/teams.ts -- common](https://github.com/microsoft/teams.ts/tree/main/packages/common) 6. Run `npx tsc --noEmit` as a build verification gate before testing or deploying. This type-checks all TypeScript source without producing output files. The project must compile cleanly -- type errors caught here prevent runtime failures. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. For testing with real Teams clients locally, use a tunneling solution (ngrok, dev tunnels, Cloudflare Tunnel) to expose your local `localhost:3978` endpoint over HTTPS. Update the Azure Bot messaging endpoint to the tunnel URL (e.g., `https://abc123.ngrok.io/api/messages`). [learn.microsoft.com -- Dev tunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview) 8. Sideload the app by opening the Teams sideloading URL after provisioning: `https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TENANT_ID}}&login_hint=${{USER_EMAIL}}` Use `TEAMS_APP_ID` and `USER_EMAIL` from `env/.env.local`; use `TENANT_ID` from `.localConfigs` (generated by `atk deploy --env local`, mapped from `TEAMS_APP_TENANT_ID` in `env/.env.local`). Alternatively, zip the `appPackage/` directory (manifest.json + icons) and upload in Teams via Apps > Manage your apps > Upload a custom app. Both paths require admin-enabled custom app upload or a developer tenant. [learn.microsoft.com -- Sideload apps](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) 9. Use `skipAuth: true` in `AppOptions` for purely local development against DevTools without Azure Bot credentials. This disables JWT validation. Never use it in production or when testing against real Teams clients. [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) 10. Use Agents Toolkit (ATK) as the recommended provisioning path for local development. First start a devtunnel (`devtunnel host -p 3978 --allow-anonymous`) and set `BOT_ENDPOINT` in `env/.env.local` to the tunnel URL. Then run `atk provision --env local -i false` to create the Entra ID app registration, Bot Framework registration, and Teams app. Then run `atk deploy --env local -i false` to generate `.localConfigs` with CLIENT_ID, CLIENT_SECRET, TENANT_ID, and PORT. Verify `.localConfigs` has TENANT_ID — if missing, copy the value of `TEAMS_APP_TENANT_ID` from `env/.env.local` into `TENANT_ID` in `.localConfigs`. See → `toolkit.lifecycle-cli.md` for the full m365agents.yml reference. [learn.microsoft.com -- Agents Toolkit](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli) 11. For manual bot registration without ATK, create the Entra ID app registration and Azure Bot resource through the Azure Portal or Azure CLI, then copy the credentials into `.env`. This path is useful for understanding what ATK automates or when ATK is not available. See → `azure-bot-deploy-ts.md` for the full manual workflow. ## patterns ### Local development setup with DevTools ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ // Use skipAuth for local DevTools-only testing (no Azure credentials needed) // Remove skipAuth when testing against real Teams clients skipAuth: true, logger: new ConsoleLogger('dev-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); app.on('message', async ({ reply, activity }) => { await reply(`Echo: ${activity.text}`); }); // Bot endpoint: http://localhost:3978/api/messages // DevTools UI: http://localhost:3979/devtools app.start(3978); ``` ### Production-ready logging configuration ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; // Development logger: verbose, noisy auth logs suppressed const devLogger = new ConsoleLogger('my-bot', { level: 'debug', pattern: '-azure/msal-node', }); // Production logger: only warnings and errors const prodLogger = new ConsoleLogger('my-bot', { level: 'warn', }); // Choose based on environment const isProduction = process.env.NODE_ENV === 'production'; const logger = isProduction ? prodLogger : devLogger; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger, // Only include DevtoolsPlugin in non-production plugins: isProduction ? [] : [new DevtoolsPlugin()], }); // Child loggers for scoped output app.on('message', async (ctx) => { const handlerLog = ctx.log; // Scoped to this activity handlerLog.info(`Message from ${ctx.activity.from.name}`); // Output: [my-bot] Message from John Doe await ctx.send('Hello!'); }); app.start(process.env.PORT || 3978).catch(console.error); ``` ### Build verification and debugging workflow ```typescript // === Option A: ATK-provisioned workflow (recommended) === // 1. Install dependencies // npm install // 2. Start a dev tunnel (must be running BEFORE provisioning) // devtunnel host -p 3978 --allow-anonymous // Copy the tunnel URL and set BOT_ENDPOINT in env/.env.local // 3. Provision bot resources via ATK (creates Entra ID app + Bot Framework + Teams app) // atk provision --env local -i false // 4. Generate .localConfigs with credentials // atk deploy --env local -i false // Verify .localConfigs contains CLIENT_ID, CLIENT_SECRET, TENANT_ID, PORT. // If TENANT_ID is missing, copy the value of TEAMS_APP_TENANT_ID from env/.env.local into TENANT_ID in .localConfigs. // 5. Type-check the project (build gate) // npx tsc --noEmit // 6. Start dev server with file watching // npm run dev // 7. Open DevTools in browser // http://localhost:3979/devtools // 7. For real Teams testing, start a dev tunnel and provision: // devtunnel host -p 3978 --allow-anonymous // Set BOT_ENDPOINT in env/.env.local to the tunnel URL // atk provision --env local -i false // atk deploy --env local -i false // Open Teams sideload URL (TEAMS_APP_ID from env/.env.local): // https://teams.microsoft.com/l/app/$TEAMS_APP_ID?installAppPackage=true&webjoin=true&appTenantId=$TENANT_ID // (TEAMS_APP_ID from env/.env.local; TENANT_ID from .localConfigs, mapped from TEAMS_APP_TENANT_ID in env/.env.local) // === Option B: Manual workflow (no ATK) === // 1. npm install // 2. Create .env: CLIENT_ID, CLIENT_SECRET, TENANT_ID, PORT=3978 // (Register bot manually -- see azure-bot-deploy-ts.md) // 3. npx tsc --noEmit // 4. npm run dev // 5. devtunnel host -p 3978 --allow-anonymous → update Azure Bot messaging endpoint // 6. Sideload: zip appPackage/ → upload in Teams // === Build for production (both options) === // npm run build # Compiles to dist/ via tsup // npm run start # Runs compiled JS: node -r dotenv/config . // Common troubleshooting: // - Bot not responding in Teams? // Check: tunnel running, messaging endpoint updated, manifest scopes correct // - DevTools blank? // Check: DevtoolsPlugin in plugins array, port+1 not blocked // - Type errors on import? // Check: tsconfig module is "NodeNext", not "commonjs" // - .env not loaded? // Check: dotenv in devDependencies, -r dotenv/config in scripts // - 401 Unauthorized after atk deploy? // Check: .localConfigs has TENANT_ID; if missing, copy the value of TEAMS_APP_TENANT_ID from env/.env.local into TENANT_ID in .localConfigs ``` ## pitfalls - **Forgetting to start the tunnel**: Without ngrok or dev tunnels, the Azure Bot Framework cannot reach your local endpoint. Teams messages never arrive. Always verify the tunnel is running and the messaging endpoint is updated. - **DevTools port conflict**: DevTools runs on `PORT + 1`. If port 3979 is already in use, DevTools fails silently. Check for port conflicts or change the bot's `PORT`. - **Using `skipAuth` with real Teams clients**: `skipAuth: true` disables JWT validation. Real Teams activities require proper authentication. Use `skipAuth` only with DevTools for rapid iteration. - **Not running `npx tsc --noEmit`**: Skipping the type-check means errors surface only at runtime. Always run this gate after changes, especially before committing or deploying. - **Stale tunnel URL in Azure Bot config**: Ngrok generates a new URL each time it restarts (unless on a paid plan). Forgetting to update the Azure Bot messaging endpoint after restarting ngrok means the bot stops receiving messages. - **Missing sideload permissions**: Sideloading requires either admin-enabled custom app upload or a Microsoft 365 developer tenant. Without it, the "Upload a custom app" option does not appear in Teams. - **Wrong log level in production**: Running with `'debug'` or `'trace'` in production floods logs and can impact performance. Switch to `'info'` or `'warn'` for deployed environments. - **Testing only in DevTools**: DevTools simulates a Teams client but does not replicate all Teams behaviors (e.g., @mention stripping, SSO token exchange, card rendering differences). Always test in a real Teams client before shipping. - **`.localConfigs` missing TENANT_ID**: After `atk deploy --env local`, the generated `.localConfigs` may omit `TENANT_ID`. Without it, MSAL defaults to the wrong token authority, causing 401 errors. Copy the value of `TEAMS_APP_TENANT_ID` from `env/.env.local` into `TENANT_ID` in `.localConfigs`. - **Dev tunnel URL blacklisted or expired**: Dev tunnel URLs can be flagged by corporate proxies or expire after inactivity. If the bot suddenly stops receiving messages with a working tunnel, create a fresh tunnel and update the messaging endpoint. ## references - [Teams SDK v2 -- @microsoft/teams.dev (DevtoolsPlugin)](https://github.com/microsoft/teams.ts/tree/main/packages/dev) - [Teams SDK v2 GitHub repository](https://github.com/microsoft/teams.ts) - [Teams: Sideload apps](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) - [Azure Dev Tunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview) - [ngrok documentation](https://ngrok.com/docs) - [M365 Agents Toolkit](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/toolkit-v4/teams-toolkit-fundamentals-vs) - [Teams: Test and debug](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/debug) ## instructions This expert covers local development, debugging, and testing workflows for Teams SDK v2 bots. Use it when you need to: - Set up `DevtoolsPlugin` and access the DevTools UI at `localhost:3979/devtools` - Run the bot locally with `npm run dev` (tsx watch with hot reload) - Configure `ConsoleLogger` levels and noise filtering for different environments - Use `skipAuth: true` for credential-free local testing - Provision bot credentials locally with `atk provision --env local` and `atk deploy --env local` - Set up dev tunnels or ngrok for testing with real Teams clients - Sideload the bot via the Teams sideloading URL or by packaging and uploading `appPackage/` as a zip - Run `npx tsc --noEmit` as a build verification gate - Troubleshoot common issues (bot not responding, DevTools blank, import errors) - Understand the difference between DevTools testing and real Teams testing Pair with `runtime.app-init-ts.md` for App constructor setup and `project.scaffold-files-ts.md` for npm scripts and project file structure. Pair with `project.scaffold-files-ts.md` for npm scripts and build verification, and `runtime.app-init-ts.md` for DevtoolsPlugin configuration. ## research Deep Research prompt: "Write a micro expert on developing, debugging, and testing Teams SDK v2 bots in TypeScript. Cover DevtoolsPlugin setup from @microsoft/teams.dev, DevTools UI at localhost:3979/devtools with WebSocket activity inspection and message replay, local development with npm run dev (tsx watch), bot endpoint at localhost:3978/api/messages, ConsoleLogger configuration (levels: error/warn/info/debug/trace, pattern filtering, child loggers), skipAuth for local testing, ngrok and dev tunnels for HTTPS exposure, sideloading via zip upload, npx tsc --noEmit build gate, M365 Agents Toolkit VS Code extension, and a troubleshooting flowchart for common issues (bot not responding, port conflicts, stale tunnel URLs, missing sideload permissions). Include a step-by-step local workflow and production logging patterns." -
graph.usergraph-appgraph-ts.md 9 KB
# graph.usergraph-appgraph-ts ## purpose Microsoft Graph API access via userGraph (delegated) and appGraph (app-level) clients with typed endpoint imports. ## rules 1. Import Graph endpoints from `@microsoft/teams.graph-endpoints` for v1.0 APIs and `@microsoft/teams.graph-endpoints-beta` for beta APIs. These are auto-generated typed functions, not raw URL strings. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Use `userGraph` for operations that act on behalf of the signed-in user (delegated permissions). This requires the user to have completed the OAuth sign-in flow (`isSignedIn === true`). [learn.microsoft.com -- Delegated permissions](https://learn.microsoft.com/en-us/graph/permissions-overview#delegated-permissions) 3. Use `appGraph` for operations that run under the application's own identity (application permissions). This does not require user sign-in but requires admin consent for the target tenant. [learn.microsoft.com -- Application permissions](https://learn.microsoft.com/en-us/graph/permissions-overview#application-permissions) 4. Call endpoints with `graph.call(endpoints.{resource}.{action}, params)` where `params` is an object containing path parameters, query parameters, and the request body. Path parameters use kebab-case keys matching the Graph URL template (e.g., `'chat-id'`). [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. Use OData query parameters (`$top`, `$filter`, `$select`, `$orderby`, `$expand`) as top-level keys in the params object to control response shape and size. Always set `$top` on list endpoints to avoid unbounded result sets. [learn.microsoft.com -- OData query params](https://learn.microsoft.com/en-us/graph/query-parameters) 6. Endpoint names follow a consistent pattern: `endpoints.{resource}.get` for single-item GET, `endpoints.{resource}.list` for collection GET, `endpoints.{resource}.create` for POST, `endpoints.{resource}.update` for PATCH, `endpoints.{resource}.delete` for DELETE. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. Always wrap Graph calls in try/catch. Failed calls throw errors with HTTP status codes and Graph error details. Check for 401 (token expired), 403 (insufficient permissions), and 429 (throttled). [learn.microsoft.com -- Error responses](https://learn.microsoft.com/en-us/graph/errors) 8. For nested resources, endpoints chain with dot notation: `endpoints.chats.messages.list`, `endpoints.chats.messages.create`. Pass the parent resource ID as a path parameter (e.g., `'chat-id': chatId`). [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Prefer `$select` to retrieve only the fields you need. This reduces payload size and avoids retrieving sensitive data. For example, `$select: 'displayName,mail'` on a user query. [learn.microsoft.com -- Select parameter](https://learn.microsoft.com/en-us/graph/query-parameters#select-parameter) 10. Never call `userGraph` without first verifying `isSignedIn`. Calling `userGraph.call()` without a valid delegated token throws an authentication error. Gate all `userGraph` usage behind the sign-in guard pattern. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## patterns ### Delegated user profile lookup ```typescript import { App } from '@microsoft/teams.apps'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, oauth: { defaultConnectionName: 'graph' }, }); app.on('message', async ({ isSignedIn, signin, userGraph, send }) => { if (!isSignedIn) { await signin({ signInButtonText: 'Sign In' }); return; } // GET /me -- delegated call using the signed-in user's token const me = await userGraph.call(endpoints.me.get); await send(`Hello ${me.displayName}! Your email is ${me.mail}.`); }); app.start(3978); ``` ### App-level user listing with query parameters ```typescript import { App } from '@microsoft/teams.apps'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, }); app.on('message', async ({ appGraph, send, activity }) => { // GET /users -- app-level call, no user sign-in required // Requires Application permission: User.Read.All with admin consent const users = await appGraph.call(endpoints.users.list, { $top: 10, $filter: "department eq 'Engineering'", $select: 'displayName,mail,department', }); const names = users.value.map((u: any) => u.displayName).join(', '); await send(`Engineering team: ${names}`); }); app.start(3978); ``` ### Sending a chat message via Graph ```typescript import { App } from '@microsoft/teams.apps'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, }); app.on('message', async ({ appGraph, send, activity }) => { const chatId = activity.conversation.id; try { // POST /chats/{chat-id}/messages -- send a message to a chat await appGraph.call(endpoints.chats.messages.create, { 'chat-id': chatId, body: { content: 'Hello from the bot via Graph!' }, }); await send('Message sent via Graph API.'); } catch (err: any) { if (err.status === 403) { await send('Insufficient permissions to send chat messages.'); } else if (err.status === 429) { await send('Throttled by Graph API. Please try again later.'); } else { throw err; } } }); app.start(3978); ``` ## pitfalls - **Calling `userGraph` without sign-in guard**: `userGraph.call()` throws if `isSignedIn` is `false`. Always check `isSignedIn` first and call `signin()` if needed. - **Missing admin consent for app permissions**: `appGraph` calls with application permissions (e.g., `User.Read.All`) require an Azure AD admin to grant consent. Without it, calls return 403. - **Unbounded list queries**: Calling `endpoints.users.list` without `$top` returns a default page size but may trigger pagination. Always set `$top` to control result size. - **Wrong path parameter key names**: Graph endpoint path parameters use kebab-case (e.g., `'chat-id'`, `'user-id'`), not camelCase. A wrong key silently omits the parameter, producing a malformed URL. - **Confusing v1.0 and beta endpoints**: Importing from `@microsoft/teams.graph-endpoints` gives v1.0 stable APIs. Beta endpoints from `@microsoft/teams.graph-endpoints-beta` may change without notice and should not be used in production. - **Not handling throttling (429)**: Graph API enforces rate limits. A 429 response includes a `Retry-After` header. Ignoring it causes cascading failures. - **Over-fetching data**: Not using `$select` retrieves all properties, including potentially sensitive fields. Always scope queries to needed fields. - **Using `appGraph` for user-specific data**: `appGraph` has no user context. Calling `endpoints.me.get` with `appGraph` fails because `/me` requires delegated permissions. ## references - [Microsoft Graph API overview](https://learn.microsoft.com/en-us/graph/overview) - [Graph permissions overview](https://learn.microsoft.com/en-us/graph/permissions-overview) - [Graph OData query parameters](https://learn.microsoft.com/en-us/graph/query-parameters) - [Graph error responses](https://learn.microsoft.com/en-us/graph/errors) - [Graph API rate limiting](https://learn.microsoft.com/en-us/graph/throttling) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.graph-endpoints npm](https://www.npmjs.com/package/@microsoft/teams.graph-endpoints) ## instructions This expert covers Microsoft Graph API access in Teams bots built with the Teams AI Library v2 (`@microsoft/teams.ts`) in TypeScript. Use it when you need to: - Call Graph endpoints using `userGraph` (delegated, on behalf of a signed-in user) or `appGraph` (application-level, service-to-service) - Import and use typed endpoints from `@microsoft/teams.graph-endpoints` or `@microsoft/teams.graph-endpoints-beta` - Understand the endpoint naming pattern (`endpoints.{resource}.{action}`) - Pass path parameters, query parameters (`$top`, `$filter`, `$select`), and request bodies - Handle Graph API errors (401, 403, 429) gracefully Pair with `auth.oauth-sso-ts.md` for sign-in flow setup before using `userGraph`, and `runtime.app-init-ts.md` for App constructor configuration. Pair with `auth.oauth-sso-ts.md` for the sign-in flow that enables userGraph, and `runtime.app-init-ts.md` for App credential configuration. ## research Deep Research prompt: "Write a micro expert on Microsoft Graph usage in Teams SDK v2 (TypeScript). Explain appGraph vs userGraph, required permissions/consent, calling generated endpoints from @microsoft/teams.graph-endpoints, OData query parameters, common endpoints (me, users, chats, messages), error handling patterns, and beta endpoint usage. Include 2-3 TypeScript code examples." -
index.md 9.8 KB
# teams-router ## purpose Route Teams bot/agent tasks to the minimal set of micro-expert files. Read only the clusters that match the user's request. For multi-area requests, combine files from each relevant cluster. ## task clusters ### Runtime: App Initialisation When: creating a new bot, setting up `app.ts`, configuring Teams AI library, bot entry point Read: - `runtime.app-init-ts.md` - `project.scaffold-files-ts.md` (only if scaffolding a new project) - `dev.debug-test-ts.md` (only if setting up local dev environment) ### Runtime: Routing & Handlers When: adding activity handlers, message routing, turn handling, middleware Read: - `runtime.routing-handlers-ts.md` Depends on: `runtime.app-init-ts.md` (App must be initialized before registering handlers) ### Runtime: Manifest When: Teams app manifest, `manifest.json`, app registration, scopes, permissions Read: - `runtime.manifest-ts.md` - `project.scaffold-files-ts.md` (for appPackage directory structure) ### Runtime: Proactive Messaging When: sending messages outside a conversation turn, proactive notifications, scheduled messages Read: - `runtime.proactive-messaging-ts.md` - `state.storage-patterns-ts.md` (for persisting conversation IDs across restarts) Depends on: `runtime.app-init-ts.md` (requires App credentials for proactive sends) ### UI: Adaptive Cards When: building cards, card actions, card templates, card rendering Read: - `ui.adaptive-cards-ts.md` - `runtime.routing-handlers-ts.md` (for `card.action` handler registration) - `ui.dialogs-task-modules-ts.md` (only if cards open dialogs/task modules) ### UI: Dialogs & Task Modules When: dialogs, task modules, modal popups, multi-step forms Read: - `ui.dialogs-task-modules-ts.md` - `ui.adaptive-cards-ts.md` (task modules render Adaptive Cards) - `runtime.routing-handlers-ts.md` (for `dialog.open`/`dialog.submit` routes) ### UI: Message Extensions When: message extensions, search commands, action commands, link unfurling Read: - `ui.message-extensions-ts.md` - `runtime.manifest-ts.md` (composeExtensions must be declared in manifest) - `ui.adaptive-cards-ts.md` (extensions return card attachments) ### Auth & Graph When: SSO, OAuth, authentication, Graph API, user profile, app-only token Read: - `auth.oauth-sso-ts.md` - `graph.usergraph-appgraph-ts.md` Depends on: `runtime.app-init-ts.md` (oauth config set in App constructor) ### State & Storage When: conversation state, user state, storage, persistence, memory patterns Read: - `state.storage-patterns-ts.md` - `ai.memory-localmemory-ts.md` (only if combining state with AI conversation history) Depends on: `runtime.app-init-ts.md` (storage passed to App constructor) ### AI: ChatPrompt & Model When: ChatPrompt setup, prompt templates, model configuration, OpenAI/Azure OpenAI Read: - `ai.chatprompt-basics-ts.md` - `ai.model-setup-ts.md` Depends on: `runtime.app-init-ts.md` (ChatPrompt used inside message handlers) ### AI: Function Calling When: defining tools/functions for the LLM, JSON schema, function design, function implementation Read: - `ai.function-calling-design-ts.md` - `ai.function-calling-implementation-ts.md` Depends on: `ai.chatprompt-basics-ts.md` (functions chain off ChatPrompt), `ai.model-setup-ts.md` ### AI: RAG & Retrieval When: retrieval-augmented generation, embeddings, vector stores, knowledge base Read: - `ai.rag-retrieval-ts.md` - `ai.rag-vectorstores-ts.md` - `ai.citations-feedback-ts.md` (for annotating RAG responses with source citations) Depends on: `ai.function-calling-implementation-ts.md` (RAG uses search as a function), `ai.chatprompt-basics-ts.md` ### AI: Streaming & Citations When: streaming responses, SSE, citation rendering, feedback loops, thumbs up/down Read: - `ai.streaming-ts.md` - `ai.citations-feedback-ts.md` Depends on: `ai.chatprompt-basics-ts.md` (streaming wraps prompt.send()), `runtime.routing-handlers-ts.md` (for ctx.stream) ### AI: Memory When: LocalMemory, conversation memory, chat history, context window management Read: - `ai.memory-localmemory-ts.md` - `state.storage-patterns-ts.md` (only if persisting memory across restarts) Depends on: `ai.chatprompt-basics-ts.md` (memory passed to ChatPrompt constructor) ### MCP: Model Context Protocol When: MCP server, MCP client, exposing tools via MCP, MCP security Read: - `mcp.server-basics-ts.md` - `mcp.client-basics-ts.md` - `mcp.expose-chatprompt-tools-ts.md` - `mcp.security-ts.md` (only if security/auth questions) Depends on: `runtime.app-init-ts.md` (McpPlugin added to App plugins). MCP client also depends on `ai.chatprompt-basics-ts.md` (McpClientPlugin is a ChatPrompt plugin). `mcp.expose-chatprompt-tools-ts.md` depends on `ai.function-calling-implementation-ts.md` (bridges prompt functions to MCP tools). ### A2A: Agent-to-Agent When: A2A protocol, agent orchestration, multi-agent, agent discovery Read: - `a2a.server-basics-ts.md` - `a2a.client-basics-ts.md` - `a2a.orchestrator-patterns-ts.md` (only if orchestrating multiple agents) Depends on: `runtime.app-init-ts.md` (A2APlugin added to App plugins). A2A client also depends on `ai.chatprompt-basics-ts.md` (A2AClientPlugin is a ChatPrompt plugin). ### Compatibility: BotBuilder Interop When: mixing BotBuilder SDK with Teams AI, legacy bot code, adapter patterns Read: - `compat.botbuilder-interop-ts.md` - `runtime.app-init-ts.md` (for understanding the target SDK v2 patterns) - `runtime.routing-handlers-ts.md` (for mapping TeamsActivityHandler to SDK v2 routes) ### Dev: Debug & Test When: debugging, testing, Agents Toolkit, local tunnel, dev tools, unit tests Read: - `dev.debug-test-ts.md` - `project.scaffold-files-ts.md` (for npm scripts and build verification) ### Scaffolding When: new project, file structure, folder layout, boilerplate, starter template Read: - `project.scaffold-files-ts.md` - `runtime.app-init-ts.md` - `runtime.manifest-ts.md` (for appPackage/manifest.json setup) ### Teams SDK for Python When: Python, `microsoft_teams`, `microsoft_teams.apps`, `microsoft_teams.ai`, `ActivityContext`, `@app.on_message`, `@app.on_message_pattern`, `ChatPrompt`, `OpenAICompletionsAIModel`, Pydantic, FastAPI, Python Teams bot Read: - `teams-python.md` Note: All TS experts provide architectural patterns. This expert provides Python API mappings. Load the relevant TS expert for concepts, then this expert for Python translation. ### Teams SDK for .NET (C#) When: C#, .NET, `Microsoft.Teams.Apps`, `Microsoft.Teams.AI`, `AddTeams()`, `UseTeams()`, `IContext<TActivity>`, `OnMessage`, `OnAdaptiveCardAction`, `OpenAIChatPrompt`, `[Prompt]`, `[Function]`, ASP.NET Core, NuGet Read: - `teams-dotnet.md` Note: C# has SDK support for Teams only (Tier 3). For the Slack side, route to `../bridge/rest-only-integration-ts.md`. ### Toolkit: Lifecycle & CLI When: `m365agents.yml`, `atk` CLI, `atk provision`, `atk deploy`, `atk publish`, `atk new`, lifecycle hooks, CI/CD pipeline, built-in actions, `uses:`, `runs:`, `arm/deploy`, `azureAppService/deploy`, `teamsApp/create`, `writeToEnvironmentFile` Read: - `toolkit.lifecycle-cli.md` Depends on: `project.scaffold-files-ts.md` (project must exist before lifecycle commands apply) ### Toolkit: Environments When: env files, environment variables, `${{VAR}}`, `SECRET_` prefix, multi-environment, staging, production env, `.env.dev`, `.env.staging`, `.env.*.user`, `environmentFolderPath`, `TEAMS_APP_ID`, `BOT_ID`, cross-platform env vars, Slack + Teams env coexistence Read: - `toolkit.environments.md` Depends on: `toolkit.lifecycle-cli.md` (environments are consumed by lifecycle hooks) ### Toolkit: Agents Playground When: Agents Playground, local testing, `atk preview`, test harness, mock activity, playground config, `.m365agentsplayground.yml`, browser-based testing Read: - `toolkit.playground-ts.md` - `dev.debug-test-ts.md` (broader debug patterns) ### Toolkit: Publishing When: publish to org, Teams Store, Partner Center, admin approval, `atk publish`, `atk validate`, `atk package`, `atk update`, app validation, sideload to org, org catalog, version bump Read: - `toolkit.publish.md` - `runtime.manifest-ts.md` (manifest must be valid before publishing) ## cross-platform bridging If the developer wants to **add Slack support** to an existing Teams bot, route to `../bridge/index.md` for cross-platform bridging experts. The bridge domain covers Teams↔Slack feature mapping, UI conversion, identity bridging, and infrastructure migration. The Toolkit experts (`toolkit.environments.md`, `toolkit.lifecycle-cli.md`) also cover dual-platform patterns — cross-platform env var layout and projects that skip `m365agents.yml`. ## combining rule If a request spans multiple clusters (e.g., "add a function-calling tool that returns an Adaptive Card"), read files from **every** matching cluster. Avoid duplicates. ## file inventory `a2a.client-basics-ts.md` | `a2a.orchestrator-patterns-ts.md` | `a2a.server-basics-ts.md` | `ai.chatprompt-basics-ts.md` | `ai.citations-feedback-ts.md` | `ai.function-calling-design-ts.md` | `ai.function-calling-implementation-ts.md` | `ai.memory-localmemory-ts.md` | `ai.model-setup-ts.md` | `ai.rag-retrieval-ts.md` | `ai.rag-vectorstores-ts.md` | `ai.streaming-ts.md` | `auth.oauth-sso-ts.md` | `compat.botbuilder-interop-ts.md` | `dev.debug-test-ts.md` | `graph.usergraph-appgraph-ts.md` | `mcp.client-basics-ts.md` | `mcp.expose-chatprompt-tools-ts.md` | `mcp.security-ts.md` | `mcp.server-basics-ts.md` | `project.scaffold-files-ts.md` | `runtime.app-init-ts.md` | `runtime.manifest-ts.md` | `runtime.proactive-messaging-ts.md` | `runtime.routing-handlers-ts.md` | `state.storage-patterns-ts.md` | `teams-dotnet.md` | `teams-python.md` | `toolkit.environments.md` | `toolkit.lifecycle-cli.md` | `toolkit.playground-ts.md` | `toolkit.publish.md` | `ui.adaptive-cards-ts.md` | `ui.dialogs-task-modules-ts.md` | `ui.message-extensions-ts.md` <!-- Updated 2026-03-01: Added 4 Agents Toolkit experts (lifecycle-cli, environments, playground, publish) and 5 Slack CLI experts to slack/ domain --> -
mcp.client-basics-ts.md 9.5 KB
# mcp.client-basics-ts ## purpose Consuming external MCP servers as AI tools using McpClientPlugin with ChatPrompt integration. ## rules 1. Create an `McpClientPlugin` instance and pass it as a ChatPrompt plugin in the second argument array: `new ChatPrompt({ ... }, [new McpClientPlugin({ logger })])`. The plugin registers itself under the name `'mcpClient'`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Connect to MCP servers using `.usePlugin('mcpClient', { url })` chained on the ChatPrompt. Each call adds one server connection. The URL must point to the server's MCP endpoint (e.g., `http://localhost:3978/mcp`). [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Connect to multiple MCP servers by chaining multiple `.usePlugin('mcpClient', { url })` calls. Each server's tools are merged and made available to the LLM as callable functions. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 4. Set transport options via `params.transport`: `'sse'` (default) for Server-Sent Events or `'streamable-http'` for HTTP-based streaming. Match the transport to what the remote MCP server supports. [spec.modelcontextprotocol.io -- Transports](https://spec.modelcontextprotocol.io/specification/basic/transports/) 5. Use `params.refetchTimeoutMs` to control how often tools are re-fetched from the server. Set this when MCP servers may add or remove tools dynamically. Default behavior fetches tools once at connection. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. Pass custom headers for authentication via `params.headers`. For Azure Functions-hosted MCP servers, include `'x-functions-key'` with the function key. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. Install the required packages: `@microsoft/teams.mcpclient`, `@modelcontextprotocol/sdk`, plus `@microsoft/teams.ai` and `@microsoft/teams.openai` for the ChatPrompt and model. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. Tools from MCP servers are automatically available to the LLM during `prompt.send()`. The LLM sees them as callable functions alongside any locally defined `.function()` tools. No extra configuration is needed. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. MCP client connections are established lazily on the first `prompt.send()` call, not at construction time. Ensure the MCP servers are running before the bot processes its first message. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Handle connection failures gracefully. If an MCP server is unreachable, the tool list for that server will be empty and the LLM will not be able to invoke those tools. Log connection errors for debugging. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## patterns ### Basic MCP client consuming one server ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { McpClientPlugin } from '@microsoft/teams.mcpclient'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const logger = new ConsoleLogger('mcp-client-bot', { level: 'debug' }); const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt( { model, instructions: 'You are a helpful assistant. Use available tools to answer questions.', }, [new McpClientPlugin({ logger })], // Pass as ChatPrompt plugin ) // Connect to an MCP server -- tools are auto-discovered .usePlugin('mcpClient', { url: 'http://localhost:3978/mcp', }); const app = new App({ logger, plugins: [new DevtoolsPlugin()], }); // Tools from the MCP server are automatically available to the LLM app.on('message', async ({ send, activity }) => { const result = await prompt.send(activity.text); if (result.content) { await send(result.content); } }); app.start(4000); ``` ### Multiple MCP servers with different transports ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { McpClientPlugin } from '@microsoft/teams.mcpclient'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const logger = new ConsoleLogger('multi-mcp-bot'); const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt( { model, instructions: 'Use the available tools to help the user. You have access to weather, tasks, and search tools.', }, [new McpClientPlugin({ logger })], ) // Local MCP server (default SSE transport) .usePlugin('mcpClient', { url: 'http://localhost:3978/mcp', }) // Remote Azure Functions MCP server with auth header .usePlugin('mcpClient', { url: 'https://my-mcp-server.azurewebsites.net/mcp/sse', params: { headers: { 'x-functions-key': process.env.FUNCTION_KEY! }, transport: 'sse', refetchTimeoutMs: 60_000, // Re-fetch tools every 60 seconds }, }) // Another server using streamable-http transport .usePlugin('mcpClient', { url: 'https://search-mcp.example.com/mcp', params: { transport: 'streamable-http', }, }); const app = new App({ logger, plugins: [new DevtoolsPlugin()], }); app.on('message', async ({ send, activity }) => { const result = await prompt.send(activity.text); if (result.content) { await send(result.content); } }); app.start(4000); ``` ### Combining MCP client with local functions ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { McpClientPlugin } from '@microsoft/teams.mcpclient'; import { ConsoleLogger } from '@microsoft/teams.common'; const logger = new ConsoleLogger('hybrid-bot'); const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt( { model, instructions: 'You are a helpful assistant with both local and remote tools.', }, [new McpClientPlugin({ logger })], ) // Remote tools from MCP server .usePlugin('mcpClient', { url: 'http://localhost:3978/mcp', }) // Local function defined directly on the prompt .function( 'getTime', 'Get the current date and time', () => new Date().toISOString() ); // The LLM sees both MCP tools and local functions ``` ## pitfalls - **Wrong `usePlugin` name**: The first argument must be the string `'mcpClient'` exactly. A typo silently fails to connect and no tools are discovered. - **MCP server not running**: If the server at the specified URL is not running when `prompt.send()` is called, tools from that server are unavailable. The LLM will not know about them. - **Missing `@modelcontextprotocol/sdk`**: The McpClientPlugin depends on the MCP SDK package. Forgetting to install it causes a runtime import error. - **Transport mismatch**: Specifying `transport: 'streamable-http'` against a server that only supports SSE (or vice versa) causes connection failures. Verify the server's supported transport. - **No error handling on `prompt.send()`**: If an MCP tool call fails server-side, the error propagates through `prompt.send()`. Wrap it in try/catch and inform the user. - **Stale tool list**: Without `refetchTimeoutMs`, tools are fetched once. If a server adds new tools after the bot connects, the LLM will not see them. Set `refetchTimeoutMs` for dynamic tool discovery. - **Passing McpClientPlugin to App instead of ChatPrompt**: `McpClientPlugin` is a ChatPrompt plugin (second argument to `new ChatPrompt()`), not an App plugin. Adding it to `new App({ plugins: [...] })` has no effect. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.mcpclient npm](https://www.npmjs.com/package/@microsoft/teams.mcpclient) - [MCP Protocol Specification -- Transports](https://spec.modelcontextprotocol.io/specification/basic/transports/) - [Model Context Protocol -- Introduction](https://modelcontextprotocol.io/introduction) ## instructions This expert covers consuming external MCP servers from Teams bots using `McpClientPlugin` from `@microsoft/teams.mcpclient` in TypeScript. Use it when you need to: - Create an `McpClientPlugin` and pass it as a ChatPrompt plugin - Connect to one or more MCP servers via `.usePlugin('mcpClient', { url })` - Configure transport options (`sse`, `streamable-http`), refresh intervals, and authentication headers - Understand how MCP tools are automatically discovered and made available to the LLM - Combine MCP remote tools with locally defined `.function()` tools Pair with `mcp.server-basics-ts.md` to understand the server side, and `ai.chatprompt-basics-ts.md` for ChatPrompt fundamentals. Pair with `ai.chatprompt-basics-ts.md` for ChatPrompt constructor where McpClientPlugin is passed, and `mcp.security-ts.md` for authenticating to remote MCP servers. ## research Deep Research prompt: "Write a micro expert on consuming external MCP servers from a Teams bot using McpClientPlugin (TypeScript). Cover McpClientPlugin construction, ChatPrompt integration, .usePlugin('mcpClient', { url, params }) for connecting to servers, transport options (sse, streamable-http), refetchTimeoutMs, custom headers, multiple server connections, and error handling. Include 2-3 TypeScript code examples." -
mcp.expose-chatprompt-tools-ts.md 9.5 KB
# mcp.expose-chatprompt-tools-ts ## purpose Bridging ChatPrompt functions to MCP tools via mcpPlugin.use(prompt) for external discoverability. ## rules 1. Call `mcpPlugin.use(prompt)` to expose all functions defined on a `ChatPrompt` instance as MCP tools. Each `.function()` on the prompt becomes a discoverable, callable MCP tool. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. The `mcpPlugin.use(prompt)` call must happen after all `.function()` definitions on the prompt. Functions added after `.use()` are not automatically exposed. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Function names on the ChatPrompt become MCP tool names. Function descriptions become MCP tool descriptions. JSON Schema parameter definitions are translated to the MCP tool schema. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 4. You can combine `mcpPlugin.use(prompt)` with direct `.tool()` definitions on the same McpPlugin. Both sets of tools are exposed at the `/mcp` endpoint. Ensure names are unique across both. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. Use `mcpPlugin.use(prompt)` when you want external systems to call the same tools your LLM uses internally. Use direct `.tool()` when you need MCP-specific features like `authInfo` or custom return formats that differ from function calling. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. Functions exposed via `.use(prompt)` do not have MCP tool hints (`readOnlyHint`, `idempotentHint`). If you need hints, define the tool directly with `.tool()` instead of relying on the bridge. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. The prompt's function handler receives only the typed parameters object, not the MCP `authInfo` context. If you need caller validation, define the tool directly with `.tool()` which provides `authInfo`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. Install the same packages required for both MCP server and AI: `@microsoft/teams.mcp`, `@modelcontextprotocol/sdk`, `zod`, `@microsoft/teams.ai`, and `@microsoft/teams.openai`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## patterns ### Exposing all ChatPrompt functions as MCP tools ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { McpPlugin } from '@microsoft/teams.mcp'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant with weather and time tools.', }) .function('getTime', 'Get the current date and time', () => { return new Date().toISOString(); }) .function( 'getWeather', 'Get weather for a location', { type: 'object', properties: { city: { type: 'string', description: 'City name' }, }, required: ['city'], }, async ({ city }: { city: string }) => { // Simulated weather lookup return { city, temperature: '72F', condition: 'sunny' }; } ); const mcpPlugin = new McpPlugin({ name: 'weather-mcp', description: 'Weather and time tools', }); // Bridge: expose all prompt functions as MCP tools mcpPlugin.use(prompt); const app = new App({ logger: new ConsoleLogger('bridge-bot'), plugins: [new DevtoolsPlugin(), mcpPlugin], }); app.on('message', async ({ send, activity }) => { const result = await prompt.send(activity.text); if (result.content) await send(result.content); }); app.start(3978); // MCP tools "getTime" and "getWeather" available at http://localhost:3978/mcp ``` ### Combining bridged functions with direct MCP tools ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { McpPlugin } from '@microsoft/teams.mcp'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; import { z } from 'zod'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); // Functions used by the LLM internally const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant.', }) .function('searchDocs', 'Search documentation', { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, }, required: ['query'], }, async ({ query }: { query: string }) => { return [{ title: 'Getting Started', snippet: 'How to set up...' }]; }); const mcpPlugin = new McpPlugin({ name: 'hybrid-server', description: 'Documentation search and admin tools', }); // Expose LLM functions as MCP tools (searchDocs) mcpPlugin.use(prompt); // Add MCP-only tools with authInfo and hints mcpPlugin.tool( 'adminReset', 'Reset a user session (admin only)', { userId: z.string().describe('User ID to reset'), }, // No readOnlyHint -- this is a mutating operation async ({ userId }, { authInfo }) => { if (!authInfo) { return { content: [{ type: 'text', text: 'Unauthorized' }] }; } // Perform reset... return { content: [{ type: 'text', text: `Session reset for ${userId}` }], }; } ); const app = new App({ plugins: [new DevtoolsPlugin(), mcpPlugin], }); app.start(3978); // MCP exposes both "searchDocs" (from prompt) and "adminReset" (direct) ``` ### When to use .use(prompt) vs direct .tool() ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; import { McpPlugin } from '@microsoft/teams.mcp'; import { z } from 'zod'; // Use mcpPlugin.use(prompt) when: // - You want external MCP clients to call the same functions your LLM uses // - The functions are stateless and do not need caller identity // - You want to avoid duplicating function definitions // Use direct .tool() when: // - You need authInfo to validate the caller // - You need tool hints (readOnlyHint, idempotentHint) // - The tool is MCP-only and should NOT be available to the LLM // - The return format differs from function calling (MCP content array) const prompt = new ChatPrompt({ model: myModel, instructions: '...' }) .function('safeRead', 'Read data (no auth needed)', async () => 'data'); const mcpPlugin = new McpPlugin({ name: 'example', description: 'Example' }); // Bridge safe functions mcpPlugin.use(prompt); // Define sensitive tools directly with auth mcpPlugin.tool( 'deleteRecord', 'Delete a record (requires auth)', { recordId: z.string().describe('Record ID') }, async ({ recordId }, { authInfo }) => { if (!authInfo) { return { content: [{ type: 'text', text: 'Unauthorized' }] }; } return { content: [{ type: 'text', text: `Deleted ${recordId}` }] }; } ); ``` ## pitfalls - **Calling `.use(prompt)` before defining functions**: Functions added to the prompt after `mcpPlugin.use(prompt)` may not be exposed. Define all `.function()` calls first, then call `.use()`. - **Tool name collisions**: If a prompt function has the same name as a direct `.tool()` definition, behavior is undefined. Ensure unique names across both sources. - **Expecting `authInfo` in bridged functions**: Functions exposed via `.use(prompt)` do not receive MCP's `authInfo`. If you need caller validation, define the tool directly with `.tool()`. - **Missing tool hints on bridged functions**: The `.use(prompt)` bridge does not set `readOnlyHint` or `idempotentHint`. MCP clients will assume the default (potential side effects). Use direct `.tool()` for hinted tools. - **Exposing dangerous functions**: Every function on the prompt becomes externally callable via MCP. Review all prompt functions before calling `.use()` to ensure none should be internal-only. - **Forgetting to add McpPlugin to App**: Even with `.use(prompt)` configured, the MCP endpoint is not registered unless the plugin is in the App's `plugins` array. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.mcp npm](https://www.npmjs.com/package/@microsoft/teams.mcp) - [MCP Protocol Specification -- Tools](https://spec.modelcontextprotocol.io/specification/server/tools/) - [Model Context Protocol -- Introduction](https://modelcontextprotocol.io/introduction) ## instructions This expert covers bridging ChatPrompt function-calling tools to MCP tools using `mcpPlugin.use(prompt)` in the Teams AI Library v2 (`@microsoft/teams.ts`). Use it when you need to: - Expose existing ChatPrompt `.function()` definitions as externally discoverable MCP tools - Understand when to use `mcpPlugin.use(prompt)` vs defining tools directly with `.tool()` - Combine bridged prompt functions with direct MCP tool definitions on the same plugin - Understand the limitations of the bridge (no authInfo, no tool hints) Pair with `mcp.server-basics-ts.md` for direct tool definition patterns and `mcp.security-ts.md` for securing exposed tools. Pair with `mcp.server-basics-ts.md` for McpPlugin setup, and `ai.function-calling-implementation-ts.md` for the ChatPrompt functions being bridged. ## research Deep Research prompt: "Write a micro expert on bridging ChatPrompt functions to MCP tools via mcpPlugin.use(prompt) in Teams SDK v2 (TypeScript). Cover how the bridge works, what gets translated (names, descriptions, schemas), limitations (no authInfo, no hints), when to use .use(prompt) vs direct .tool(), combining both approaches, and security considerations. Include 2-3 TypeScript code examples." -
mcp.security-ts.md 10.4 KB
# mcp.security-ts ## purpose Security considerations for MCP server/client: authentication, authorization, input validation, and endpoint hardening. ## rules 1. Always check the `authInfo` parameter in tool handlers for tools that modify state or access sensitive data. The `authInfo` object contains caller identity information provided by the MCP transport layer. Reject requests where `authInfo` is missing or invalid. [spec.modelcontextprotocol.io -- Security](https://spec.modelcontextprotocol.io/specification/basic/security/) 2. Validate all tool inputs using zod schemas. The `.tool()` API validates parameters before the handler runs, but add additional business-logic validation (e.g., string length limits, allowed values, format checks) inside the handler. [zod.dev](https://zod.dev/) 3. Apply the principle of least privilege to tool permissions. Only expose tools that external callers genuinely need. Internal-only functions should remain on the ChatPrompt without being bridged to MCP via `mcpPlugin.use(prompt)`. [spec.modelcontextprotocol.io -- Security considerations](https://spec.modelcontextprotocol.io/specification/basic/security/) 4. Require HTTPS for production MCP endpoints. SSE and streamable-http transports transmit tool calls and responses in cleartext over HTTP. Use TLS termination (Azure App Service, reverse proxy, or load balancer) to encrypt traffic. [spec.modelcontextprotocol.io -- Transports](https://spec.modelcontextprotocol.io/specification/basic/transports/) 5. For Azure Functions-hosted MCP servers, require a function key via the `x-functions-key` header. MCP clients pass this in `params.headers`. Without it, the endpoint is publicly accessible. [learn.microsoft.com -- Azure Functions auth](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger#authorization-keys) 6. Rate-limit MCP tool invocations to prevent abuse. Track call counts per caller (using `authInfo`) and return an error response when limits are exceeded. The MCP protocol does not enforce rate limits; you must implement them. [spec.modelcontextprotocol.io -- Security](https://spec.modelcontextprotocol.io/specification/basic/security/) 7. Sanitize tool output before returning it. If tool results include user-generated content or database values, escape or validate them to prevent injection attacks in downstream consumers. [spec.modelcontextprotocol.io -- Security](https://spec.modelcontextprotocol.io/specification/basic/security/) 8. Log all tool invocations with caller identity, tool name, parameters (redacting secrets), and result status. Audit logs are critical for detecting misuse and debugging authorization failures. [spec.modelcontextprotocol.io -- Security](https://spec.modelcontextprotocol.io/specification/basic/security/) 9. Do not embed secrets (API keys, connection strings) in tool schemas or descriptions. These are exposed to MCP clients during tool discovery. Keep secrets in environment variables and access them only inside handler implementations. [spec.modelcontextprotocol.io -- Security](https://spec.modelcontextprotocol.io/specification/basic/security/) 10. When consuming external MCP servers as a client, validate the server's TLS certificate and pin to known server URLs. Do not connect to arbitrary MCP server URLs provided by untrusted input. [spec.modelcontextprotocol.io -- Security](https://spec.modelcontextprotocol.io/specification/basic/security/) ## patterns ### Tool handler with authInfo validation ```typescript import { McpPlugin } from '@microsoft/teams.mcp'; import { z } from 'zod'; const ALLOWED_CALLERS = new Set([ 'trusted-client-id-1', 'trusted-client-id-2', ]); const mcpPlugin = new McpPlugin({ name: 'secure-server', description: 'Server with auth-gated tools', }) .tool( 'deleteUser', 'Delete a user account (admin only)', { userId: z.string().min(1).describe('User ID to delete'), }, async ({ userId }, { authInfo }) => { // Reject unauthenticated callers if (!authInfo) { return { content: [{ type: 'text', text: 'Error: Authentication required' }], }; } // Validate caller is in the allowlist if (!ALLOWED_CALLERS.has(authInfo.clientId)) { return { content: [{ type: 'text', text: `Error: Caller ${authInfo.clientId} not authorized` }], }; } // Perform the deletion // await userService.delete(userId); return { content: [{ type: 'text', text: `User ${userId} deleted successfully` }], }; } ); ``` ### Input validation beyond zod schema ```typescript import { McpPlugin } from '@microsoft/teams.mcp'; import { z } from 'zod'; const mcpPlugin = new McpPlugin({ name: 'validated-server', description: 'Server with strict input validation', }) .tool( 'sendEmail', 'Send an email notification', { to: z.string().email().describe('Recipient email address'), subject: z.string().max(200).describe('Email subject (max 200 chars)'), body: z.string().max(5000).describe('Email body (max 5000 chars)'), }, { idempotentHint: false }, async ({ to, subject, body }, { authInfo }) => { // Additional business-logic validation if (!authInfo) { return { content: [{ type: 'text', text: 'Error: Authentication required' }], }; } // Block external email addresses if policy requires it if (!to.endsWith('@company.com')) { return { content: [{ type: 'text', text: 'Error: Can only send to @company.com addresses' }], }; } // Sanitize body content (strip HTML/scripts) const sanitizedBody = body.replace(/<[^>]*>/g, ''); // Send email via your service // await emailService.send({ to, subject, body: sanitizedBody }); return { content: [{ type: 'text', text: `Email sent to ${to}` }], }; } ); ``` ### Secure MCP client with auth headers ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { McpClientPlugin } from '@microsoft/teams.mcpclient'; import { ConsoleLogger } from '@microsoft/teams.common'; const logger = new ConsoleLogger('secure-client'); const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); const prompt = new ChatPrompt( { model, instructions: 'Use tools to help the user.' }, [new McpClientPlugin({ logger })], ) // Connect to secure Azure Functions MCP server .usePlugin('mcpClient', { url: 'https://my-mcp-server.azurewebsites.net/mcp/sse', params: { headers: { 'x-functions-key': process.env.FUNCTION_KEY!, 'Authorization': `Bearer ${process.env.MCP_API_TOKEN}`, }, transport: 'sse', }, }); // Only connect to known, trusted server URLs // NEVER construct MCP server URLs from user input ``` ## pitfalls - **No `authInfo` check on mutating tools**: Tools that create, update, or delete data without checking `authInfo` are callable by any MCP client. Always validate the caller for side-effecting tools. - **Relying only on zod for validation**: Zod validates data types and shapes but not business rules. A valid string can still contain malicious content, a valid email can be an external address, and a valid number can be out of business range. - **Exposing all prompt functions via `.use(prompt)`**: This makes every ChatPrompt function externally callable. Review functions for sensitivity before bridging. Keep internal-only tools off the MCP surface. - **HTTP in production**: Running MCP over unencrypted HTTP in production exposes tool calls, parameters, and responses to network sniffing. Always use HTTPS with proper TLS certificates. - **Hardcoded secrets in tool schemas**: Tool parameter descriptions and names are sent to clients during discovery. Never include API keys, connection strings, or internal URLs in schema metadata. - **No rate limiting**: Without rate limits, a malicious or buggy MCP client can invoke expensive tools thousands of times. Implement per-caller rate limiting in your tool handlers. - **Connecting to untrusted MCP servers**: An MCP client that connects to user-supplied server URLs risks executing malicious tool definitions. Only connect to server URLs from your configuration, never from user input. - **Missing audit logging**: Without logs of tool invocations, you cannot detect misuse, investigate incidents, or comply with security audits. Log every tool call with caller, tool name, and outcome. ## references - [MCP Protocol Specification -- Security Considerations](https://spec.modelcontextprotocol.io/specification/basic/security/) - [MCP Protocol Specification -- Transports](https://spec.modelcontextprotocol.io/specification/basic/transports/) - [Azure Functions HTTP authorization keys](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger#authorization-keys) - [Zod documentation](https://zod.dev/) - [OWASP Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) ## instructions This expert covers security hardening for MCP servers and clients in Teams bots built with the Teams AI Library v2 (`@microsoft/teams.ts`). Use it when you need to: - Validate caller identity using `authInfo` in MCP tool handlers - Implement authorization allowlists for sensitive tools - Add input validation beyond zod schema validation (business rules, sanitization) - Secure MCP endpoints with HTTPS and function keys - Configure authenticated MCP client connections with custom headers - Apply the principle of least privilege to tool exposure - Implement rate limiting and audit logging for tool invocations Pair with `mcp.server-basics-ts.md` for tool definition patterns and `mcp.client-basics-ts.md` for client connection setup. Pair with `mcp.server-basics-ts.md` for McpPlugin tool definitions, and `../security/input-validation-ts.md` for general input validation patterns. ## research Deep Research prompt: "Write a micro expert on MCP security for Teams bot tool exposure and consumption (TypeScript). Cover authInfo validation in tool handlers, authorization allowlists, zod schema validation plus business-logic validation, HTTPS requirements, Azure Functions key headers, rate limiting, audit logging, principle of least privilege for tool exposure, and securing client connections. Include 2-3 TypeScript code examples." -
mcp.server-basics-ts.md 9.9 KB
# mcp.server-basics-ts ## purpose Exposing bot capabilities as MCP tools using McpPlugin with zod schemas, tool hints, and SSE transport. ## rules 1. Create an `McpPlugin` with `new McpPlugin({ name, description })`. The `name` is used as the MCP server identifier and the `description` appears in tool discovery responses. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Define tools using the fluent `.tool(name, description, schema, hints, handler)` chain API. The schema uses zod objects where each key becomes a tool parameter. All parameters are validated before the handler runs. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Set tool hints to communicate tool behavior to clients: `readOnlyHint: true` signals no side effects, `idempotentHint: true` signals safe retries. Omitting hints defaults to assuming the tool may have side effects. [spec.modelcontextprotocol.io -- Tool annotations](https://spec.modelcontextprotocol.io/specification/server/tools/#annotations) 4. Tool handlers must return `{ content: [{ type: 'text', text: string }] }` format. The `content` array can contain multiple content items. Always return at least one content item. [spec.modelcontextprotocol.io -- Tool results](https://spec.modelcontextprotocol.io/specification/server/tools/) 5. Access caller identity via the `authInfo` parameter in tool handlers: `async (params, { authInfo }) => { ... }`. Use this to verify who is calling the tool and enforce authorization. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. Add the `McpPlugin` instance to the `App` constructor's `plugins` array. The plugin registers the `/mcp` HTTP endpoint automatically during app initialization. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. The MCP server endpoint is available at `http://localhost:{PORT}/mcp` using SSE transport by default. Clients connect to this URL to discover and invoke tools. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. Use descriptive tool names and parameter descriptions. MCP clients (including LLMs) rely on names and descriptions to decide when and how to invoke tools. Vague names lead to misuse. [spec.modelcontextprotocol.io -- Tool naming](https://spec.modelcontextprotocol.io/specification/server/tools/) 9. Install the required packages: `@microsoft/teams.mcp`, `@modelcontextprotocol/sdk`, and `zod`. All three are needed -- the plugin depends on the SDK and uses zod for schema validation. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Tools are stateless by default. If a tool needs access to bot state or external services, close over them in the handler or pass the `App` instance. Do not store mutable state inside tool definitions. [spec.modelcontextprotocol.io -- Server design](https://spec.modelcontextprotocol.io/specification/server/) ## patterns ### Basic MCP server with two tools ```typescript import { App } from '@microsoft/teams.apps'; import { McpPlugin } from '@microsoft/teams.mcp'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; import { z } from 'zod'; const mcpPlugin = new McpPlugin({ name: 'my-mcp-server', description: 'Exposes greeting and echo tools', }) .tool( 'greet', 'Greet a user by name', { name: z.string().describe('Name to greet') }, { readOnlyHint: true, idempotentHint: true }, async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name}!` }], }) ) .tool( 'echo', 'Echoes back the input text', { input: z.string().describe('The text to echo') }, { readOnlyHint: true, idempotentHint: true }, async ({ input }) => ({ content: [{ type: 'text', text: `You said: "${input}"` }], }) ); const app = new App({ logger: new ConsoleLogger('mcp-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin(), mcpPlugin], }); app.on('message', async ({ reply, activity }) => { await reply(`Echo: ${activity.text}`); }); app.start(3978); // MCP endpoint: http://localhost:3978/mcp ``` ### Tool with authInfo and side effects ```typescript import { App } from '@microsoft/teams.apps'; import { McpPlugin } from '@microsoft/teams.mcp'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; import { z } from 'zod'; // Shared state the tool will modify const userConversationMap = new Map<string, string>(); const mcpPlugin = new McpPlugin({ name: 'notification-server', description: 'Send notifications to Teams users', }) .tool( 'notifyUser', 'Send a notification to a user', { message: z.string().describe('Notification text'), userId: z.string().describe('User AAD Object ID'), }, // No readOnlyHint -- this tool has side effects async ({ message, userId }, { authInfo }) => { // Validate caller identity if (!authInfo) { return { content: [{ type: 'text', text: 'Unauthorized: no auth info' }], }; } const convId = userConversationMap.get(userId); if (!convId) { return { content: [{ type: 'text', text: `No conversation found for user ${userId}` }], }; } await app.send(convId, `Notification: ${message}`); return { content: [{ type: 'text', text: 'User notified successfully' }], }; } ); const app = new App({ plugins: [new DevtoolsPlugin(), mcpPlugin], }); // Track conversations for proactive messaging app.on('install.add', async ({ activity }) => { userConversationMap.set( activity.from.aadObjectId!, activity.conversation.id ); }); app.start(3978); ``` ### Multiple tool schemas with complex parameters ```typescript import { McpPlugin } from '@microsoft/teams.mcp'; import { z } from 'zod'; const mcpPlugin = new McpPlugin({ name: 'task-manager', description: 'Manage tasks and assignments', }) .tool( 'createTask', 'Create a new task with title, description, and optional assignee', { title: z.string().describe('Task title'), description: z.string().describe('Task description'), assignee: z.string().optional().describe('User ID to assign the task to'), priority: z.enum(['low', 'medium', 'high']).describe('Task priority level'), dueDate: z.string().optional().describe('Due date in ISO 8601 format'), }, async ({ title, description, assignee, priority, dueDate }) => { // Create task in your backend const taskId = `task-${Date.now()}`; return { content: [{ type: 'text', text: JSON.stringify({ taskId, title, priority, assignee, dueDate }), }], }; } ) .tool( 'listTasks', 'List all tasks, optionally filtered by status', { status: z.enum(['open', 'in-progress', 'done']).optional().describe('Filter by status'), }, { readOnlyHint: true }, async ({ status }) => { // Query your backend const tasks = [{ id: 'task-1', title: 'Example', status: 'open' }]; return { content: [{ type: 'text', text: JSON.stringify(tasks) }], }; } ); ``` ## pitfalls - **Missing zod import**: The `.tool()` schema requires `z` from `zod`. Forgetting to install or import `zod` produces a runtime error. - **Wrong return shape**: Tool handlers must return `{ content: [{ type: 'text', text: '...' }] }`. Returning a plain string or object causes the MCP client to reject the response. - **Forgetting to add McpPlugin to plugins array**: Creating the plugin and defining tools without adding it to `new App({ plugins: [...] })` means the `/mcp` endpoint is never registered. - **Not installing all three packages**: `@microsoft/teams.mcp`, `@modelcontextprotocol/sdk`, and `zod` are all required. Missing any one produces import or runtime errors. - **Exposing dangerous tools without auth checks**: Tools that modify data or send messages should check `authInfo` to verify the caller. Without validation, any MCP client can invoke destructive operations. - **Tool name collisions**: If you combine `mcpPlugin.use(prompt)` with direct `.tool()` definitions, ensure tool names are unique. Duplicate names cause undefined behavior. - **Overly broad tool descriptions**: Vague descriptions cause LLM clients to invoke tools incorrectly. Be specific about what the tool does, what it returns, and when to use it. ## references - [MCP Protocol Specification -- Tools](https://spec.modelcontextprotocol.io/specification/server/tools/) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [@microsoft/teams.mcp npm](https://www.npmjs.com/package/@microsoft/teams.mcp) - [Model Context Protocol -- Introduction](https://modelcontextprotocol.io/introduction) - [Zod documentation](https://zod.dev/) ## instructions This expert covers building MCP servers in Teams bots using the `McpPlugin` from `@microsoft/teams.mcp` in TypeScript. Use it when you need to: - Create an `McpPlugin` with name and description - Define tools using the `.tool()` chain API with zod schemas - Set tool hints (`readOnlyHint`, `idempotentHint`) to communicate behavior - Access `authInfo` in tool handlers for caller validation - Add the plugin to the App's `plugins` array - Understand the MCP endpoint URL and SSE transport Pair with `mcp.expose-chatprompt-tools-ts.md` for bridging existing ChatPrompt functions to MCP tools, and `mcp.security-ts.md` for hardening MCP endpoints. Pair with `mcp.security-ts.md` for securing MCP endpoints, `mcp.expose-chatprompt-tools-ts.md` for bridging ChatPrompt functions to MCP tools, and `runtime.app-init-ts.md` for adding McpPlugin to the App. ## research Deep Research prompt: "Write a micro expert on building an MCP server in a Teams bot using @microsoft/teams.mcp (TypeScript). Cover McpPlugin constructor, defining tools with zod schemas and the .tool() chain API, tool hints (readOnlyHint, idempotentHint), authInfo in handlers, adding to App plugins, the /mcp SSE endpoint, and common pitfalls. Include 2-3 canonical TypeScript code examples." -
project.scaffold-files-ts.md 12.2 KB
# project.scaffold-files-ts ## purpose Project file structure, package.json dependencies, tsconfig, .env setup, npm scripts, appPackage directory, and CLI scaffolding for Teams SDK v2. ## rules 1. Every Teams SDK v2 project requires these base dependencies: `@microsoft/teams.api`, `@microsoft/teams.apps`, `@microsoft/teams.cards`, `@microsoft/teams.common`, `@microsoft/teams.dev`. These are always present regardless of features. Dev dependencies are: `@types/node` (^22.5.4), `dotenv` (^16.4.5), `rimraf` (^6.0.1), `tsx` (^4.20.6), `tsup` (^8.4.0), `typescript` (^5.4.5). [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Feature-specific dependencies must be added based on the selected capabilities: AI/LLM requires `@microsoft/teams.ai` + `@microsoft/teams.openai`; Authentication/Graph requires `@microsoft/teams.graph` + `@microsoft/teams.graph-endpoints`; Graph beta API requires `@microsoft/teams.graph-endpoints-beta`; MCP Server requires `@microsoft/teams.mcp` + `@modelcontextprotocol/sdk` + `zod`; MCP Client requires `@microsoft/teams.mcpclient` + `@microsoft/teams.ai` + `@microsoft/teams.openai` + `@modelcontextprotocol/sdk`; A2A requires `@microsoft/teams.a2a` + `@microsoft/teams.ai` + `@microsoft/teams.openai`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Standard npm scripts are: `"clean": "npx rimraf ./dist"`, `"build": "npx tsup"`, `"start": "node -r dotenv/config ."`, `"dev": "tsx watch -r dotenv/config src/index.ts"`. The `dev` script uses `tsx` for TypeScript execution with file watching. The `start` script runs the compiled output from `dist/`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 4. The `tsconfig.json` must use `"module": "NodeNext"`, `"target": "ESNext"`, `"moduleResolution": "NodeNext"`, `"strict": true`, `"outDir": "dist"`, `"rootDir": "src"`, and `"types": ["node"]`. The `include` array targets `"src/**/*.ts"`. These settings align with the Teams SDK v2 package expectations. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. The `.env` file always includes `CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`, and `PORT` (default 3978). For AI features, add `OPENAI_API_KEY` or the Azure OpenAI set (`AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_API_VERSION`, `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME`). [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. The `appPackage/` directory must contain `manifest.json`, `color.png` (192x192), and `outline.png` (32x32). This directory is zipped for sideloading. It is not part of the compiled `dist/` output. [learn.microsoft.com -- App package](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/apps-package) 7. The CLI scaffolding command is `npx @microsoft/teams.cli@latest new typescript <name> --template <template>` where templates include `echo`, `ai`, `lights`, `auth`, etc. However, for full control over output, create files directly rather than using the CLI. [github.com/microsoft/teams.ts -- cli](https://github.com/microsoft/teams.ts/tree/main/packages/cli) 8. The recommended project structure places the entry point at `src/index.ts` and organizes larger projects into `src/handlers/`, `src/prompts/`, `src/functions/`, `src/cards/`, and `src/services/`. Keep simple bots in a single `src/index.ts`. Only create subdirectories when the project warrants it. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Set `"main": "dist/index"` and `"types": "dist/index"` in `package.json` so the `start` script resolves to the compiled entry point. The `"files": ["dist"]` field restricts published content to the build output. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Run `npx tsc --noEmit` as a build verification gate after creating or modifying source files. This type-checks without producing output. The project must compile cleanly before testing or deploying. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## patterns ### Complete package.json with feature dependency table ```typescript // package.json -- base configuration (always required) const packageJson = { "name": "my-teams-bot", "version": "0.0.1", "private": true, "license": "MIT", "main": "dist/index", "types": "dist/index", "files": ["dist"], "scripts": { "clean": "npx rimraf ./dist", "build": "npx tsup", "start": "node -r dotenv/config .", "dev": "tsx watch -r dotenv/config src/index.ts" }, "dependencies": { // --- Always required --- "@microsoft/teams.api": "latest", "@microsoft/teams.apps": "latest", "@microsoft/teams.cards": "latest", "@microsoft/teams.common": "latest", "@microsoft/teams.dev": "latest", // --- Add per feature --- // AI / LLM: // "@microsoft/teams.ai": "latest", // "@microsoft/teams.openai": "latest", // Authentication / Graph: // "@microsoft/teams.graph": "latest", // "@microsoft/teams.graph-endpoints": "latest", // Graph beta API: // "@microsoft/teams.graph-endpoints-beta": "latest", // MCP Server: // "@microsoft/teams.mcp": "latest", // "@modelcontextprotocol/sdk": "latest", // "zod": "latest", // MCP Client: // "@microsoft/teams.mcpclient": "latest", // "@microsoft/teams.ai": "latest", // "@microsoft/teams.openai": "latest", // "@modelcontextprotocol/sdk": "latest", // A2A (Server or Client): // "@microsoft/teams.a2a": "latest", // "@microsoft/teams.ai": "latest", // "@microsoft/teams.openai": "latest", }, "devDependencies": { "@types/node": "^22.5.4", "dotenv": "^16.4.5", "rimraf": "^6.0.1", "tsx": "^4.20.6", "tsup": "^8.4.0", "typescript": "^5.4.5" } }; ``` ### tsconfig.json and .env templates ```typescript // tsconfig.json -- standard configuration const tsconfig = { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "module": "NodeNext", "target": "ESNext", "moduleResolution": "NodeNext", "strict": true, "noImplicitAny": true, "declaration": true, "inlineSourceMap": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": false, "resolveJsonModule": true, "noUnusedLocals": true, "noUnusedParameters": true, "pretty": true, "outDir": "dist", "rootDir": "src", "types": ["node"] }, "include": ["src/**/*.ts"] }; // .env -- base variables (always required) // CLIENT_ID= // CLIENT_SECRET= // TENANT_ID= // PORT=3978 // // For AI with OpenAI: // OPENAI_API_KEY= // // For AI with Azure OpenAI: // AZURE_OPENAI_API_KEY= // AZURE_OPENAI_ENDPOINT= // AZURE_OPENAI_API_VERSION=2024-02-01 // AZURE_OPENAI_MODEL_DEPLOYMENT_NAME= ``` ### Recommended project structure ```typescript // Minimal project (simple bot) // my-teams-bot/ // ├── appPackage/ // │ ├── manifest.json # Teams app manifest // │ ├── color.png # 192x192 app icon // │ └── outline.png # 32x32 outline icon // ├── src/ // │ └── index.ts # App entry point (all logic here) // ├── .env # Environment variables // ├── package.json // └── tsconfig.json // Expanded project (complex agent) // my-teams-bot/ // ├── appPackage/ // │ ├── manifest.json // │ ├── color.png // │ └── outline.png // ├── src/ // │ ├── index.ts # App entry point, App init, start // │ ├── handlers/ # Message and invoke handlers // │ │ ├── messages.ts // │ │ └── cardActions.ts // │ ├── prompts/ # AI prompt configurations // │ │ └── mainPrompt.ts // │ ├── functions/ # AI function definitions // │ │ ├── weather.ts // │ │ └── search.ts // │ ├── cards/ # Adaptive Card templates // │ │ ├── welcomeCard.ts // │ │ └── feedbackCard.ts // │ └── services/ # API clients, business logic // │ └── apiClient.ts // ├── .env // ├── package.json // └── tsconfig.json // CLI scaffolding (alternative to manual creation): // npx @microsoft/teams.cli@latest new typescript my-teams-bot --template echo // cd my-teams-bot // npm install ``` ## pitfalls - **Missing base dependencies**: Omitting any of the five core packages (`teams.api`, `teams.apps`, `teams.cards`, `teams.common`, `teams.dev`) causes import errors. Always include all five. - **Wrong `main` field**: Setting `"main": "src/index"` instead of `"main": "dist/index"` causes the `start` script to fail because it runs compiled JS. The `dev` script uses `tsx` and runs TypeScript directly from `src/`. - **Missing `dotenv` in dev script**: The `-r dotenv/config` flag in both `start` and `dev` scripts requires `dotenv` as a devDependency. Without it, environment variables are not loaded and credentials fail silently. - **`tsconfig` module mismatch**: Using `"module": "commonjs"` instead of `"NodeNext"` causes runtime import errors with the Teams SDK packages which use ESM-compatible patterns. - **Forgetting `appPackage/` icons**: The manifest references `color.png` and `outline.png`. Missing or wrong-sized icons cause Teams to reject the app package on upload. - **Not running `npx tsc --noEmit`**: Skipping the type-check gate means type errors surface only at runtime or in production. Always verify before testing. - **Using `npm start` during development**: The `start` script runs compiled JS from `dist/`. Use `npm run dev` during development for live TypeScript reloading with `tsx watch`. - **Installing feature packages without code**: Adding `@microsoft/teams.ai` to `package.json` but not importing or using it adds unnecessary weight. Only add dependencies you actually use in code. ## references - [Teams SDK v2 GitHub repository](https://github.com/microsoft/teams.ts) - [Teams SDK v2 -- @microsoft/teams.cli](https://github.com/microsoft/teams.ts/tree/main/packages/cli) - [Teams SDK v2 -- Package catalog](https://github.com/microsoft/teams.ts#packages) - [Teams: App package structure](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/apps-package) - [tsup documentation](https://tsup.egoist.dev/) - [tsx documentation](https://github.com/privatenumber/tsx) ## instructions This expert covers the canonical project scaffold for a Teams SDK v2 TypeScript bot. Use it when you need to: - Create a new project from scratch with the correct file structure - Set up `package.json` with base and feature-specific dependencies - Configure `tsconfig.json` for Teams SDK v2 compatibility - Create a `.env` file with the correct variables per feature set - Understand the recommended directory layout for simple and complex projects - Use the CLI (`npx @microsoft/teams.cli`) for quick scaffolding - Configure npm scripts for clean, build, start, and dev workflows - Set up the `appPackage/` directory with manifest and icons - Run build verification with `npx tsc --noEmit` Pair with `runtime.app-init-ts.md` for the `src/index.ts` entry point code and `runtime.manifest-ts.md` for the `appPackage/manifest.json` structure. Pair with `runtime.app-init-ts.md` for the src/index.ts entry point, and `runtime.manifest-ts.md` for appPackage/manifest.json details. ## research Deep Research prompt: "Write a micro expert defining the canonical file scaffold for a Teams SDK v2 TypeScript bot project. Cover package.json with all base dependencies (@microsoft/teams.api, teams.apps, teams.cards, teams.common, teams.dev) and the complete feature dependency table (AI, Auth/Graph, Graph beta, MCP Server, MCP Client, A2A, RAG), devDependencies (@types/node, dotenv, rimraf, tsx, tsup, typescript), npm scripts (clean/build/start/dev), tsconfig.json with NodeNext module and ESNext target, .env template with base and feature-specific variables, appPackage/ directory with manifest.json and icon requirements, recommended directory structure (minimal vs expanded), CLI scaffolding with npx @microsoft/teams.cli, and build verification with npx tsc --noEmit. Include the full package.json template, tsconfig.json, and directory tree." -
runtime.app-init-ts.md 10.9 KB
# runtime.app-init-ts ## purpose Teams SDK v2 App initialization, constructor options, plugins, logger setup, storage config, OAuth, activity context, and startup lifecycle. ## rules 1. Always import `App` from `@microsoft/teams.apps`, `ConsoleLogger` from `@microsoft/teams.common`, and `DevtoolsPlugin` from `@microsoft/teams.dev` as the minimum bootstrap triple. These three packages are always present in `dependencies`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Pass `clientId`, `clientSecret`, and `tenantId` to the `App` constructor when the bot requires Azure Bot registration credentials. All three come from environment variables (`CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`). Omit them only for local-only development with `skipAuth: true`. [learn.microsoft.com -- Bot registration](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/bot-sso-register-aad) 3. Configure logging with `new ConsoleLogger(name, { level })` where `level` is one of `'error' | 'warn' | 'info' | 'debug' | 'trace'`. Use `pattern: '-azure/msal-node'` to suppress noisy child loggers. Child loggers inherit settings via `logger.child('sub-name')` and prefix output as `[parent/child]`. [github.com/microsoft/teams.ts -- common](https://github.com/microsoft/teams.ts/tree/main/packages/common) 4. Register plugins via the `plugins` array in the constructor or dynamically with `app.plugin(instance)`. Every development project should include `DevtoolsPlugin`. Plugin lifecycle follows: register -> `onInit()` -> `onStart({ port })` -> activity loop (`onActivity()` / `onActivitySent()`) -> `onStop()`. [github.com/microsoft/teams.ts -- dev](https://github.com/microsoft/teams.ts/tree/main/packages/dev) 5. Configure OAuth by adding `oauth: { defaultConnectionName: 'graph' }` to `AppOptions`. This enables `ctx.isSignedIn`, `ctx.signin()`, `ctx.signout()`, and `ctx.userGraph` on every handler context. Requires `clientId`, `clientSecret`, and `tenantId` to also be set. [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) 6. Storage defaults to in-memory. Pass a custom `IStorage` implementation to the `storage` option for persistence across restarts. Use `LocalStorage` from `@microsoft/teams.common` for development with optional LRU eviction via `{ max: N }`. [github.com/microsoft/teams.ts -- common](https://github.com/microsoft/teams.ts/tree/main/packages/common) 7. Call `app.start(port)` (default `3978`) as the final step. It returns a `Promise` -- always attach `.catch(console.error)` or use `await`. The bot endpoint is `http://localhost:{port}/api/messages` and DevTools UI runs on `{port + 1}`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. Set `skipAuth: true` only during local development without Azure credentials. This disables JWT validation on inbound activities. Never use this in production. [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) 9. The full `AppOptions` reference includes: `clientId`, `clientSecret`, `tenantId`, `token` (custom token factory), `managedIdentityClientId` (`'system'` or string), `client` (custom HTTP client), `logger` (`ILogger`), `storage` (`IStorage`), `plugins` (`IPlugin[]`), `oauth` (`OAuthSettings`), `manifest` (`Partial<Manifest>`), `skipAuth` (boolean), and `activity.mentions.stripText` (boolean). [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) 10. Use `app.event('start', ...)` for post-listen setup, `app.event('error', ...)` for global error handling, `app.event('signin', ...)` for post-authentication logic, and `app.event('activity', ...)`/`app.event('activity.sent', ...)` for observing all inbound/outbound activities. These are lifecycle events, not activity route handlers. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## patterns ### Minimal App with DevTools and logger ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ logger: new ConsoleLogger('echo-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); app.on('message', async ({ reply, activity }) => { await reply({ type: 'typing' }); await reply(`You said: "${activity.text}"`); }); app.start(3978); ``` ### Full production App with credentials, OAuth, and storage ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger, LocalStorage } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; interface AppState { conversationIds: string[]; } const app = new App({ // Azure Bot registration credentials clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, // Custom logger with noise filtering logger: new ConsoleLogger('prod-bot', { level: 'info', pattern: '-azure/msal-node', }), // Persistent storage (in-memory with LRU for dev) storage: new LocalStorage<AppState>({}, { max: 1000 }), // OAuth for Microsoft Graph oauth: { defaultConnectionName: 'graph' }, // Plugins plugins: [new DevtoolsPlugin()], }); // Lifecycle events app.event('start', (logger) => { logger.info('Bot is running'); }); app.event('error', ({ error, log }) => { log.error('Unhandled error:', error); }); app.event('signin', async ({ send, userGraph }) => { // Fired after successful OAuth sign-in await send('You are now signed in.'); }); app.event('activity', ({ activity }) => { // Fired for every inbound activity }); app.event('activity.sent', ({ activity }) => { // Fired after every outbound activity }); app.start(process.env.PORT || 3978).catch(console.error); ``` ### Activity context usage in a handler ```typescript app.on('message', async (ctx) => { // --- Properties --- ctx.appId; // Bot app ID ctx.activity; // The inbound Activity object ctx.ref; // ConversationReference for proactive messaging ctx.log; // Scoped logger ctx.api; // Teams API client ctx.appGraph; // Graph client (app credentials) ctx.userGraph; // Graph client (user credentials, after signin) ctx.storage; // Persistent storage ctx.stream; // Streaming response helper ctx.isSignedIn; // Whether user has authenticated ctx.userToken; // User's OAuth access token ctx.connectionName; // OAuth connection name // --- Methods --- await ctx.send('Hello!'); // Send a new message await ctx.reply('Reply to this'); // Reply to the current message await ctx.signin(); // Trigger OAuth sign-in flow await ctx.signout(); // Sign the user out ctx.next(); // Pass to next middleware/handler }); ``` ## pitfalls - **Missing `.catch()` on `app.start()`**: The method returns a Promise. Unhandled rejections crash the process in Node 20+. Always add `.catch(console.error)` or wrap in an async IIFE with try/catch. - **Using `skipAuth: true` in production**: This disables JWT validation entirely. Any HTTP client can send fake activities to your bot endpoint. Only use it for local DevTools testing. - **Forgetting `DevtoolsPlugin` during development**: Without it, there is no DevTools UI at `localhost:3979/devtools` and no WebSocket-based activity inspection. Always include it in the `plugins` array for local dev. - **Setting OAuth without credentials**: Adding `oauth: { defaultConnectionName: 'graph' }` without `clientId`/`clientSecret`/`tenantId` causes silent auth failures. All four options must be present together. - **Calling `app.start()` before registering handlers**: Handlers registered after `start()` may miss early activities. Register all `app.on()`, `app.message()`, and `app.use()` calls before calling `app.start()`. - **Logger level too verbose in production**: Using `'debug'` or `'trace'` floods logs with internal SDK chatter. Use `'info'` or `'warn'` for deployed bots. - **Hardcoding the port**: Always read from `process.env.PORT` with a fallback (`process.env.PORT || 3978`). Azure App Service and container hosts set `PORT` dynamically. - **Confusing `app.on()` with `app.event()`**: `app.on()` registers activity route handlers (message, card.action, etc.). `app.event()` registers app lifecycle hooks (start, error, signin). Mixing them up causes handlers that never fire. ## references - [Teams SDK v2 GitHub repository](https://github.com/microsoft/teams.ts) - [Teams SDK v2 -- @microsoft/teams.apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) - [Teams SDK v2 -- @microsoft/teams.common](https://github.com/microsoft/teams.ts/tree/main/packages/common) - [Teams SDK v2 -- @microsoft/teams.dev (DevtoolsPlugin)](https://github.com/microsoft/teams.ts/tree/main/packages/dev) - [Azure Bot Service documentation](https://learn.microsoft.com/en-us/azure/bot-service/) - [Teams platform: Build bots](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/what-are-bots) ## instructions This expert covers the foundational `App` class from `@microsoft/teams.apps` -- the entry point for every Teams SDK v2 bot. Use it when you need to: - Initialize a new `App` instance with the correct constructor options - Configure credentials (`clientId`, `clientSecret`, `tenantId`) for Azure Bot registration - Set up logging with `ConsoleLogger`, child loggers, and noise filtering - Register plugins (especially `DevtoolsPlugin`) and understand plugin lifecycle - Configure OAuth for Microsoft Graph access - Set up storage backends for persistent state - Understand the activity context object (`ctx`) and its full set of properties/methods - Handle app lifecycle events (`start`, `error`, `activity`, `activity.sent`, `signin`) - Start the server with `app.start()` and understand the default endpoints Pair with `runtime.routing-handlers-ts.md` for route registration patterns and `project.scaffold-files-ts.md` for full project setup including package.json and tsconfig. Pair with `project.scaffold-files-ts.md` for package.json and project structure, and `dev.debug-test-ts.md` for local development setup. ## research Deep Research prompt: "Write a micro expert on Teams SDK v2 App initialization in TypeScript. Cover the App constructor from @microsoft/teams.apps, all AppOptions fields (clientId, clientSecret, tenantId, logger, storage, plugins, oauth, skipAuth, manifest, token, managedIdentityClientId, client, activity.mentions.stripText), ConsoleLogger configuration with levels and pattern filtering, DevtoolsPlugin setup and lifecycle hooks (onInit, onStart, onActivity, onActivitySent, onStop), activity context properties and methods (send, reply, signin, signout, next, stream, isSignedIn, appGraph, userGraph, ref, api, storage, log), app.start() lifecycle, and app.event() hooks (start, error, signin, activity, activity.sent). Include 2-3 initialization patterns from minimal to production-ready." -
runtime.manifest-ts.md 12.3 KB
# runtime.manifest-ts ## purpose Teams app manifest (manifest.json) structure, schema, bots config, permissions, compose extensions, commands, and deployment packaging. ## rules 1. Use schema version `1.20` with `"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.20/MicrosoftTeams.schema.json"` and `"manifestVersion": "1.20"`. This is the current stable schema for Teams SDK v2 projects. [learn.microsoft.com -- Manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) 2. Required top-level fields are: `$schema`, `version`, `manifestVersion`, `id`, `name` (with `short` max 30 chars and `full` max 100 chars), `description` (with `short` max 80 chars and `full` max 4000 chars), `developer` (with `name`, `websiteUrl`, `privacyUrl`, `termsOfUseUrl`), `icons` (`outline` and `color`), and `accentColor`. Omitting any causes validation failure on upload. [learn.microsoft.com -- Manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) 3. The `bots` array defines bot registrations. Each entry requires `botId` (the Azure Bot ID, typically a placeholder like `${{BOT_ID}}`), `scopes` (array of `"personal"`, `"team"`, `"groupChat"`), and optional flags `isNotificationOnly`, `supportsCalling`, `supportsVideo`, `supportsFiles`. Scopes determine where the bot can receive activities. [learn.microsoft.com -- Bots in manifest](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#bots) 4. Add `composeExtensions` for message extensions. Each entry needs `botId`, `type` (`"query"` or `"action"`), and a `commands` array. Each command has `id`, `type`, `title`, and `parameters` for query commands or `fetchTask: true` for action commands. [learn.microsoft.com -- Compose extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#composeextensions) 5. The `validDomains` array lists domains the bot is allowed to open in web views and task modules. Always include `"*.botframework.com"` and your bot's domain. Omitting a domain causes blank task modules. [learn.microsoft.com -- Valid domains](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#validdomains) 6. The `webApplicationInfo` section provides SSO configuration with `id` (the bot/app ID) and `resource` (the application ID URI, typically `"api://botid-${{BOT_ID}}"`). Required for OAuth/SSO flows. [learn.microsoft.com -- webApplicationInfo](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#webapplicationinfo) 7. Icons must be: `color.png` at exactly 192x192 pixels and `outline.png` at exactly 32x32 pixels with a transparent background. Both are PNG format. Place them in the `appPackage/` directory alongside `manifest.json`. [learn.microsoft.com -- App icons](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/apps-package#app-icons) 8. Package the app as a `.zip` file containing `manifest.json`, `color.png`, and `outline.png` from the `appPackage/` directory. The zip must contain these files at the root level (not nested in subdirectories). Use `atk package` to generate the zip with placeholders resolved, or manually zip and upload via Teams > Apps > Upload a custom app, or through Teams Admin Center. [learn.microsoft.com -- App package](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/apps-package) 9. Use placeholder variables like `${{TEAMS_APP_ID}}`, `${{BOT_ID}}`, and `${{BOT_DOMAIN}}` in the manifest for values that change between environments. The M365 Agents Toolkit resolves these during packaging. For manual deployment, replace them with actual values before zipping. [learn.microsoft.com -- Agents Toolkit](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/toolkit-v4/teams-toolkit-fundamentals-vs) 10. Add `staticTabs` for personal-scope tab experiences. The two default entries (`conversations` and `about`) are recommended for all bots. Add `commands` inside bot entries for slash-command discoverability in the Teams compose box. [learn.microsoft.com -- Static tabs](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#statictabs) ## patterns ### Minimal bot manifest ```typescript // appPackage/manifest.json const manifest = { "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.20/MicrosoftTeams.schema.json", "version": "1.0.0", "manifestVersion": "1.20", "id": "${{TEAMS_APP_ID}}", "name": { "short": "My Bot", "full": "My Teams Bot Application" }, "developer": { "name": "Contoso", "mpnId": "", "websiteUrl": "https://example.com", "privacyUrl": "https://example.com/privacy", "termsOfUseUrl": "https://example.com/terms" }, "description": { "short": "A helpful Teams bot", "full": "A Teams bot built with Teams SDK v2 that helps users with tasks." }, "icons": { "outline": "outline.png", "color": "color.png" }, "accentColor": "#FFFFFF", "staticTabs": [ { "entityId": "conversations", "scopes": ["personal"] }, { "entityId": "about", "scopes": ["personal"] } ], "bots": [ { "botId": "${{BOT_ID}}", "scopes": ["personal", "team", "groupChat"], "isNotificationOnly": false, "supportsCalling": false, "supportsVideo": false, "supportsFiles": false } ], "validDomains": ["${{BOT_DOMAIN}}", "*.botframework.com"], "webApplicationInfo": { "id": "${{BOT_ID}}", "resource": "api://botid-${{BOT_ID}}" } }; ``` ### Manifest with message extensions ```typescript // appPackage/manifest.json -- adding composeExtensions for a search command const manifestWithExtensions = { "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.20/MicrosoftTeams.schema.json", "version": "1.0.0", "manifestVersion": "1.20", "id": "${{TEAMS_APP_ID}}", "name": { "short": "Search Bot", "full": "Search Bot with Message Extension" }, "developer": { "name": "Contoso", "mpnId": "", "websiteUrl": "https://example.com", "privacyUrl": "https://example.com/privacy", "termsOfUseUrl": "https://example.com/terms" }, "description": { "short": "Search and share results in Teams", "full": "A Teams bot with a search-based message extension." }, "icons": { "outline": "outline.png", "color": "color.png" }, "accentColor": "#FFFFFF", "bots": [ { "botId": "${{BOT_ID}}", "scopes": ["personal", "team", "groupChat"], "isNotificationOnly": false } ], "composeExtensions": [ { "botId": "${{BOT_ID}}", "commands": [ { "id": "searchCmd", "type": "query", "title": "Search", "description": "Search for items", "parameters": [ { "name": "query", "title": "Search query", "description": "Enter search terms", "inputType": "text" } ] }, { "id": "createCmd", "type": "action", "title": "Create Item", "description": "Create a new item", "fetchTask": true } ] } ], "validDomains": ["${{BOT_DOMAIN}}", "*.botframework.com"], "webApplicationInfo": { "id": "${{BOT_ID}}", "resource": "api://botid-${{BOT_ID}}" } }; ``` ### Packaging the app for sideloading ```typescript // Build script or manual steps to create the app package // Preferred: atk package --env <environment> (resolves placeholders automatically) // Manual steps below: // 1. Ensure appPackage/ contains: // - manifest.json (with placeholders replaced) // - color.png (192x192 pixels) // - outline.png (32x32 pixels, transparent background) // 2. Replace placeholders before zipping: // ${{TEAMS_APP_ID}} -> your Azure AD app registration ID // ${{BOT_ID}} -> your Azure Bot resource ID // ${{BOT_DOMAIN}} -> your deployment domain (e.g., mybot.azurewebsites.net) // 3. Create zip from the appPackage directory: // cd appPackage && zip -r ../mybot.zip manifest.json color.png outline.png // 4. Upload in Teams: // Teams > Apps > Manage your apps > Upload a custom app > Upload mybot.zip ``` ## pitfalls - **Wrong icon dimensions**: Teams silently rejects or distorts icons that are not exactly 192x192 (color) and 32x32 (outline). Validate dimensions before packaging. - **Nested zip structure**: The zip must contain `manifest.json`, `color.png`, and `outline.png` at the root. If they are in a subdirectory inside the zip (e.g., `appPackage/manifest.json`), Teams cannot read them. - **Missing scopes**: If `bots[0].scopes` does not include `"team"`, the bot cannot be added to channels and never receives channel messages. If `"personal"` is missing, 1:1 chat does not work. Always verify scopes match your intended deployment. - **Unresolved placeholders**: Shipping `${{BOT_ID}}` literally in the manifest causes the bot registration to fail. Either use the M365 Agents Toolkit to auto-resolve or manually replace all `${{...}}` values before zipping. - **`validDomains` missing your domain**: Task modules, web views, and link unfurling that reference domains not listed in `validDomains` show blank content or fail silently. - **Schema version mismatch**: Using features from a newer schema version (e.g., 1.17 features with `manifestVersion: "1.13"`) causes validation errors on upload. Keep `manifestVersion` and `$schema` in sync. - **Forgetting `composeExtensions` for message extensions**: Registering `app.on('message.ext.query', ...)` in code without a matching `composeExtensions` entry in the manifest means the extension never appears in Teams. - **`webApplicationInfo` misconfigured for SSO**: The `resource` field must match the Application ID URI configured in Azure AD. A mismatch causes the SSO token exchange to fail silently. ## references - [Teams manifest schema reference](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) - [Teams app package structure](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/apps-package) - [Teams: Bots in manifest](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#bots) - [Teams: Compose extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#composeextensions) - [Teams: App icons guidelines](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/apps-package#app-icons) - [Teams: Valid domains](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#validdomains) ## instructions This expert covers the Teams app manifest (`manifest.json`) and packaging for deployment. Use it when you need to: - Create or modify `appPackage/manifest.json` for a Teams bot project - Configure the `bots` section with correct `botId` and `scopes` - Add `composeExtensions` for search or action-based message extensions - Set up `validDomains` for task modules and web views - Configure `webApplicationInfo` for OAuth/SSO - Prepare icons (`color.png` 192x192, `outline.png` 32x32) - Package the app as a zip for sideloading or admin deployment - Resolve placeholder variables (`${{BOT_ID}}`, `${{TEAMS_APP_ID}}`, etc.) Pair with `project.scaffold-files-ts.md` for the full project file structure and `runtime.app-init-ts.md` for the corresponding code-side initialization. Pair with `project.scaffold-files-ts.md` for appPackage directory structure, and `ui.message-extensions-ts.md` when adding composeExtensions to the manifest. ## research Deep Research prompt: "Write a micro expert on Microsoft Teams app manifest.json for SDK v2 bots (TypeScript). Cover the v1.20 schema, all required fields (id, name, description, developer, icons, accentColor, manifestVersion), bots section (botId, scopes, isNotificationOnly, supportsCalling, supportsVideo, supportsFiles), composeExtensions for message extensions (query and action types, commands, parameters, fetchTask), staticTabs, validDomains, webApplicationInfo for SSO, icon requirements (192x192 color PNG, 32x32 outline PNG with transparency), placeholder variable patterns (${{BOT_ID}}), zip packaging rules, and common validation errors. Include a complete manifest template and a manifest-with-extensions template." -
runtime.proactive-messaging-ts.md 12.6 KB
# runtime.proactive-messaging-ts ## purpose Sending messages outside of conversation turns using stored conversation references, `app.send()`, and timer/webhook-triggered notifications. ## rules 1. Proactive messaging requires a stored `conversationId`. Capture it during the `install.add` event from `activity.conversation.id` and associate it with a user identifier such as `activity.from.aadObjectId`. Without a stored conversation ID, proactive messaging is impossible. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Send proactive messages with `app.send(conversationId, message)` where `message` is a string or activity object. This method is available on the `App` instance directly -- it does not require an active activity context. [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) 3. Store conversation IDs persistently for production use. In-memory maps (e.g., `new Map<string, string>()`) work for development but are lost on restart. Use the App's `storage` (IStorage) or an external database for production deployments. [github.com/microsoft/teams.ts -- common](https://github.com/microsoft/teams.ts/tree/main/packages/common) 4. The `install.add` event fires when the bot is installed to a personal chat, team channel, or group chat. This is the canonical place to capture conversation IDs. Also consider capturing from any `activity.conversation.id` in message handlers as a fallback. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. For channel-scoped proactive messages, the `conversationId` format differs from personal chat. Channel conversation IDs include the channel thread ID. Store them separately and send to the correct one based on context. [learn.microsoft.com -- Proactive messages](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) 6. Proactive messages can be triggered by timers (`setTimeout`, `setInterval`, cron jobs), webhooks (Express routes), external events (queue messages, database triggers), or scheduled tasks. The trigger mechanism is independent of the Teams SDK. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. The `ctx.ref` property on any handler context contains a `ConversationReference` that can also be stored for proactive messaging. This provides richer context (serviceUrl, channelId, bot info) beyond just the conversation ID. [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) 8. Proactive messages require valid bot credentials (`clientId`, `clientSecret`, `tenantId`) to authenticate with the Bot Framework. The `skipAuth: true` option works only with DevTools, not for proactive messages to real Teams clients. [learn.microsoft.com -- Bot authentication](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication) 9. Rate limits apply to proactive messaging. Teams throttles bots that send too many messages too quickly. Implement exponential backoff and respect HTTP 429 responses. Batch notifications and add delays between sends for large user bases. [learn.microsoft.com -- Rate limiting](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/rate-limit) 10. Handle the `install.remove` event to clean up stored conversation IDs. When a user uninstalls the bot, proactive messages to their conversation ID will fail. Remove stale entries to avoid unnecessary API calls and errors. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) ## patterns ### Basic proactive messaging with install tracking ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger('proactive-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); // In-memory store (use persistent storage in production) const conversationIds = new Map<string, string>(); // Store conversation ID when bot is installed app.on('install.add', async ({ activity, send }) => { conversationIds.set(activity.from.aadObjectId!, activity.conversation.id); await send('Hi! I will send you reminders.'); }); // Clean up when bot is uninstalled app.on('install.remove', async ({ activity, log }) => { conversationIds.delete(activity.from.aadObjectId!); log.info(`Removed conversation for user ${activity.from.aadObjectId}`); }); // Send a proactive message to a specific user async function notifyUser(userId: string, message: string): Promise<void> { const conversationId = conversationIds.get(userId); if (conversationId) { await app.send(conversationId, message); } } // Example: scheduled notification via timer setTimeout(() => { notifyUser('user-aad-id', 'Reminder: your meeting starts in 5 minutes!'); }, 60_000); app.on('message', async ({ send, activity }) => { await send(`Echo: ${activity.text}`); }); app.start(process.env.PORT || 3978).catch(console.error); ``` ### Proactive messaging from a webhook endpoint ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger, LocalStorage } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; import express from 'express'; interface ConversationEntry { conversationId: string; userName: string; } const conversationStore = new LocalStorage<ConversationEntry>(); const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger('webhook-bot', { level: 'debug' }), storage: conversationStore, plugins: [new DevtoolsPlugin()], }); // Capture conversation references from any message app.on('message', async ({ activity, send }) => { const userId = activity.from.aadObjectId!; if (!conversationStore.get(userId)) { conversationStore.set(userId, { conversationId: activity.conversation.id, userName: activity.from.name || 'Unknown', }); } await send(`Echo: ${activity.text}`); }); app.on('install.add', async ({ activity, send }) => { conversationStore.set(activity.from.aadObjectId!, { conversationId: activity.conversation.id, userName: activity.from.name || 'Unknown', }); await send('Installed! You will receive notifications here.'); }); // Notify all stored users (called from external webhook or scheduled job) async function broadcastMessage(message: string): Promise<void> { // Note: In a real app, iterate stored entries from your database // and add delays between sends to respect rate limits } app.start(process.env.PORT || 3978).catch(console.error); ``` ### Proactive messaging with Adaptive Cards ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, logger: new ConsoleLogger('card-notify-bot'), plugins: [new DevtoolsPlugin()], }); const conversationIds = new Map<string, string>(); app.on('install.add', async ({ activity, send }) => { conversationIds.set(activity.from.aadObjectId!, activity.conversation.id); await send('Notifications enabled.'); }); // Send a rich Adaptive Card proactively async function sendAlertCard(userId: string, title: string, body: string): Promise<void> { const conversationId = conversationIds.get(userId); if (!conversationId) return; await app.send(conversationId, { type: 'message', attachments: [ { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: title, weight: 'Bolder', size: 'Medium' }, { type: 'TextBlock', text: body, wrap: true }, ], actions: [ { type: 'Action.Submit', title: 'Acknowledge', data: { verb: 'ackAlert', userId }, }, ], }, }, ], }); } app.on('card.action', async ({ activity, send }) => { const data = activity.value?.action?.data; if (data?.verb === 'ackAlert') { await send('Alert acknowledged.'); } }); app.on('message', async ({ send, activity }) => { await send(`Echo: ${activity.text}`); }); app.start(process.env.PORT || 3978).catch(console.error); ``` ## pitfalls - **No stored conversation ID**: Without capturing the conversation ID during `install.add` or from an activity, `app.send()` has nowhere to send. Always persist conversation IDs as early as possible. - **In-memory storage lost on restart**: Using a `Map` or plain object for conversation IDs means all stored IDs vanish when the process restarts. Use persistent storage (database, Azure Table Storage, etc.) in production. - **Missing bot credentials for proactive sends**: `app.send()` authenticates with the Bot Framework using `clientId`/`clientSecret`/`tenantId`. Without valid credentials, proactive messages fail with 401 errors. - **Rate limiting / throttling**: Teams throttles bots that send too many proactive messages. A burst of notifications to thousands of users triggers HTTP 429 responses. Add delays (e.g., 1-2 seconds between sends) and implement retry logic with exponential backoff. - **Stale conversation IDs**: When a user uninstalls the bot, the conversation ID becomes invalid. Sending to it produces errors. Handle `install.remove` to prune stale entries. - **Channel vs personal conversation IDs**: Channel and personal chat conversation IDs have different formats. A conversation ID captured from a channel install targets that channel's general thread, not individual users. Map them correctly based on your notification requirements. - **Proactive messages in DevTools only**: With `skipAuth: true` and no real credentials, proactive messaging works in DevTools but fails against real Teams clients. Always test with real Azure Bot credentials before deploying. - **Forgetting error handling on `app.send()`**: The `app.send()` call can throw (network errors, invalid conversation ID, rate limits). Always wrap in try/catch or handle the rejected Promise. ## references - [Teams SDK v2 GitHub repository](https://github.com/microsoft/teams.ts) - [Teams: Send proactive messages](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) - [Teams: Bot rate limiting](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/rate-limit) - [Bot Framework: Conversation reference](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference) - [Teams: Conversation events (install)](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/subscribe-to-conversation-events) ## instructions This expert covers proactive messaging in Teams SDK v2 -- sending messages to users outside of a direct conversation turn. Use it when you need to: - Capture and store conversation IDs on bot install (`install.add`) - Send proactive messages using `app.send(conversationId, message)` - Trigger notifications from timers, webhooks, external events, or scheduled jobs - Send proactive Adaptive Cards (not just text) - Handle the difference between personal chat and channel conversation IDs - Implement cleanup on `install.remove` to prune stale references - Understand rate limiting and throttling constraints for bulk notifications Pair with `runtime.app-init-ts.md` for App constructor setup (credentials are required for proactive messaging) and `runtime.routing-handlers-ts.md` for the `install.add` / `install.remove` route registration. Pair with `state.storage-patterns-ts.md` for persisting conversation IDs across restarts, and `runtime.app-init-ts.md` for App credentials required by proactive sends. ## research Deep Research prompt: "Write a micro expert on proactive messaging in Teams SDK v2 (TypeScript). Cover capturing conversation IDs on install.add from activity.conversation.id and activity.from.aadObjectId, storing references persistently, sending with app.send(conversationId, message), ConversationReference from ctx.ref, timer/webhook/cron-triggered sends, channel vs personal chat conversation ID differences, rate limiting and throttling (HTTP 429, backoff), cleanup on install.remove, sending Adaptive Cards proactively, and error handling. Include 2-3 canonical patterns (basic install tracking, webhook-triggered, rich card notifications) and common failure modes." -
runtime.routing-handlers-ts.md 12.3 KB
# runtime.routing-handlers-ts ## purpose Routing patterns: message handlers, event handlers, invoke routes, middleware, pattern matching, and all available route names in Teams SDK v2. ## rules 1. Use `app.on(routeName, handler)` for activity-based routes and invoke routes. The handler receives an activity context (`ctx`) with properties and methods for that activity type. Always destructure only what you need. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. Use `app.message(pattern, handler)` for pattern-matched message handling. The `pattern` argument accepts a `string` (exact match) or `RegExp`. String patterns match the full message text; regex patterns test against `activity.text`. Multiple `app.message()` calls are evaluated in registration order -- first match wins. [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) 3. Register middleware with `app.use(handler)`. Middleware runs before all route handlers for every activity. Call `ctx.next()` inside middleware to pass control to the next middleware or the matched route handler. Omitting `ctx.next()` short-circuits the pipeline. [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) 4. Activity routes include: `message`, `conversationUpdate`, `typing`, `messageUpdate`, `messageDelete`, `event`, `endOfConversation`, `contactRelationUpdate`, `mention`, and `activity` (catch-all fallback). The `activity` route fires only if no more-specific route matched. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 5. Install routes are `install.add` (bot installed to conversation/team) and `install.remove` (bot uninstalled). Use `install.add` to send welcome messages and store conversation IDs for proactive messaging. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. Invoke routes map to Teams invoke names: `dialog.open` (`task/fetch`), `dialog.submit` (`task/submit`), `card.action` (`adaptiveCard/action`), `message.ext.query` (`composeExtension/query`), `message.ext.select-item` (`composeExtension/selectItem`), `message.ext.submit` (`composeExtension/submitAction`), `message.ext.open` (`composeExtension/fetchTask`), `message.ext.query-link` (`composeExtension/queryLink`), `message.ext.setting` (`composeExtension/setting`). Invoke handlers must return a response object with `status` and `body`. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. Additional invoke routes: `config.open` (`config/fetch`), `config.submit` (`config/submit`), `tab.open` (`tab/fetch`), `tab.submit` (`tab/submit`), `signin.token-exchange` (`signin/tokenExchange`), `signin.verify-state` (`signin/verifyState`), `file.consent` (`fileConsent/invoke`), `handoff.action` (`handoff/action`), `message.submit` (`message/submitAction`). [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 8. App-level events (registered with `app.event()`, not `app.on()`) include: `start` (server listening), `signin` (user completed OAuth), `error` (unhandled error), `activity` (every inbound activity), and `activity.sent` (every outbound activity). These are lifecycle observers, not route handlers. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Feedback handling uses `app.on('message.submit.feedback', handler)` where `activity.value.actionValue.reaction` is `'like'` or `'dislike'` and `activity.value.actionValue.feedback` contains optional text. Return `{ status: 200 }` from the handler. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Register all handlers and middleware before calling `app.start()`. The route matching order is: middleware (in registration order) -> `app.message()` pattern matches (first match wins) -> `app.on()` specific routes -> `app.on('activity')` catch-all. [github.com/microsoft/teams.ts -- apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) ## patterns ### Message handlers with pattern matching ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ logger: new ConsoleLogger('router-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); // Exact string match app.message('/help', async ({ send }) => { await send('Here is how I can help...'); }); // RegExp pattern match (case-insensitive) app.message(/^hello/i, async ({ send }) => { await send('Hi there!'); }); // Catch-all message handler (runs if no pattern matched above) app.on('message', async ({ send, activity }) => { await send(`You said "${activity.text}"`); }); app.start(3978); ``` ### Middleware, install handlers, and invoke routes ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ logger: new ConsoleLogger('full-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); // --- Middleware: runs before all route handlers --- app.use(async (ctx) => { ctx.log.info(`Received: ${ctx.activity.type}`); await ctx.next(); }); // --- Install routes --- app.on('install.add', async ({ send }) => { await send('Thanks for installing me!'); }); app.on('install.remove', async ({ activity, log }) => { log.info(`Uninstalled from ${activity.conversation.id}`); }); // --- Dialog invoke routes (return response objects) --- app.on('dialog.open', async ({ send }) => { return { status: 200, body: { task: { type: 'continue', value: { title: 'My Dialog', card: { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [{ type: 'TextBlock', text: 'Enter data below' }], }, }, }, }, }, }; }); app.on('dialog.submit', async ({ activity }) => { const formData = activity.value.data; return { status: 200, body: { task: { type: 'message', value: 'Form submitted successfully!' }, }, }; }); // --- Card action handler --- app.on('card.action', async ({ activity, send }) => { const data = activity.value; await send(`Card action received: ${JSON.stringify(data)}`); }); // --- Feedback handler --- app.on('message.submit.feedback', async ({ activity, log }) => { const feedback = { messageId: activity.replyToId || activity.id, reaction: activity.value.actionValue.reaction, feedback: activity.value.actionValue.feedback, }; log.info('Feedback received:', feedback); return { status: 200 }; }); // --- Catch-all fallback --- app.on('message', async ({ send, activity }) => { await send(`Echo: ${activity.text}`); }); app.start(3978); ``` ### App-level lifecycle events ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, oauth: { defaultConnectionName: 'graph' }, logger: new ConsoleLogger('lifecycle-bot'), plugins: [new DevtoolsPlugin()], }); // Fires when server starts listening app.event('start', (logger) => { logger.info('Bot started'); }); // Fires after successful OAuth sign-in app.event('signin', async ({ send, userGraph }) => { const me = await userGraph.call(endpoints.me.get); await send(`Welcome, ${me.displayName}!`); }); // Fires on unhandled errors app.event('error', ({ error, log }) => { log.error('Unhandled error:', error); }); // Fires for every inbound activity (observer, not a route) app.event('activity', ({ activity }) => { // Logging, telemetry, analytics }); // Fires after every outbound activity app.event('activity.sent', ({ activity }) => { // Track sent messages }); app.start(process.env.PORT || 3978).catch(console.error); ``` ## pitfalls - **Forgetting `ctx.next()` in middleware**: Without it, the pipeline stops and no route handler fires. The request appears to hang or silently succeed without processing. - **Registering `app.on('message')` before `app.message()` patterns**: The generic `message` handler may consume the activity before pattern-matched handlers run. Register `app.message()` calls first, then `app.on('message')` as a catch-all. - **Not returning from invoke handlers**: Dialog, card action, and message extension handlers must return a response object with `status` and `body`. Returning `undefined` causes the Teams client to show an error or hang. - **Confusing `app.on()` with `app.event()`**: `app.on('message', ...)` is an activity route handler. `app.event('start', ...)` is a lifecycle event. Using the wrong method means your handler never fires. - **Missing manifest scopes for routes**: If the manifest `bots.scopes` does not include `"team"`, the bot never receives activities from channels. If `"personal"` is missing, 1:1 chat does not work. Match scopes to your route expectations. - **Using `send()` vs `reply()`**: `send()` creates a new top-level message. `reply()` threads under the current message. In channels, `reply()` is usually what you want to keep conversations organized. - **Duplicate route registration**: Registering the same route name twice does not throw an error -- the second handler replaces or competes with the first. Be explicit about which handler owns each route. - **Forgetting `message.submit.feedback` handler**: If you use `.addFeedback()` on outbound messages but never register the feedback handler, thumbs up/down clicks fail silently in the Teams client. ## references - [Teams SDK v2 GitHub repository](https://github.com/microsoft/teams.ts) - [Teams SDK v2 -- @microsoft/teams.apps](https://github.com/microsoft/teams.ts/tree/main/packages/apps) - [Teams platform: Bot activity handlers](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/bot-basics) - [Teams platform: Task modules and cards](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/what-are-task-modules) - [Teams platform: Message extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions) - [Teams platform: Conversation events](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/subscribe-to-conversation-events) ## instructions This expert covers all routing and event handling in Teams SDK v2. Use it when you need to: - Register message handlers with `app.on('message')` or pattern-matched handlers with `app.message()` - Implement middleware with `app.use()` and understand the `next()` pipeline - Handle invoke routes for dialogs (`dialog.open`, `dialog.submit`), card actions (`card.action`), message extensions (`message.ext.*`), tabs, config, and sign-in - Handle install/uninstall events (`install.add`, `install.remove`) - Register app-level lifecycle events (`start`, `signin`, `error`, `activity`, `activity.sent`) - Handle user feedback from `.addFeedback()` buttons via `message.submit.feedback` - Understand route matching order and fallback behavior Pair with `runtime.app-init-ts.md` for App constructor patterns and `ui.adaptive-cards-ts.md` for card action handler details. Pair with `runtime.app-init-ts.md` for App constructor setup before registering handlers, and `ui.adaptive-cards-ts.md` for card.action handler details. ## research Deep Research prompt: "Write a micro expert for Teams SDK v2 routing and event handling in TypeScript. Cover app.on() vs app.message() vs app.event(), string and RegExp pattern matching, middleware with app.use() and ctx.next(), all activity routes (message, conversationUpdate, typing, messageUpdate, messageDelete, event, endOfConversation, contactRelationUpdate, mention, activity catch-all), install routes (install.add, install.remove), all invoke routes (dialog.open/submit, card.action, message.ext.query/select-item/submit/open/query-link/setting, config.open/submit, tab.open/submit, signin.token-exchange/verify-state, file.consent, handoff.action, message.submit, message.submit.feedback), app-level lifecycle events (start, signin, error, activity, activity.sent), invoke handler return contracts, and route matching order. Include canonical patterns and common mistakes." -
state.storage-patterns-ts.md 9.4 KB
# state.storage-patterns-ts ## purpose State management with IStorage interface, LocalStorage, and per-user/per-conversation state patterns in Teams bots. ## rules 1. Use the `IStorage` interface (`get`/`set`/`delete`) for all state management. It supports both synchronous and async implementations, so custom backends (Redis, Cosmos DB) can return Promises. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 2. `LocalStorage` from `@microsoft/teams.common` is an in-memory store with optional LRU eviction. Pass `{ max: N }` to cap entries and prevent unbounded memory growth in long-running bots. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 3. Pass storage to the `App` constructor via the `storage` option. The storage instance is then available as `ctx.storage` in all route handlers. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 4. For per-user state, key on `activity.from.id` (or `activity.from.aadObjectId` for AAD-stable IDs). For per-conversation state, key on `activity.conversation.id`. Choose the right scope for your data. [learn.microsoft.com -- Bot state](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-context) 5. When combining state with `ChatPrompt`, pass the user's stored `messages` array to the prompt's `messages` option. This restores conversation history across handler invocations. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 6. Initialize state lazily: check if state exists for the key, and if not, create a default state object and store it. This avoids null reference errors on first interaction. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 7. `LocalStorage` is volatile -- data is lost on process restart. For production bots, implement `IStorage` backed by a persistent store (Azure Cosmos DB, Redis, SQL). [learn.microsoft.com -- Bot state management](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-state) 8. Keep state objects small. Store only what is needed (message history, preferences, session flags). Large state objects increase memory pressure and serialization cost for persistent backends. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. The `IStorage` generic signature is `IStorage<TKey, TValue>`. Type both the key and value for compile-time safety. For example, `new LocalStorage<IUserState>()` types the value while using string keys. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 10. Do not rely on in-memory state in multi-instance deployments (e.g., Azure App Service with multiple instances or containers). Each instance has its own `LocalStorage`. Use a shared persistent store instead. [learn.microsoft.com -- Scale out](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-state) ## patterns ### Per-user state with ChatPrompt message history ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt, LocalMemory, Message } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { LocalStorage, ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); interface IUserState { messages: Message[]; preferences: Record<string, any>; } const userStore = new LocalStorage<IUserState>({}, { max: 1000, // LRU eviction after 1000 users }); const app = new App({ logger: new ConsoleLogger('state-bot', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); app.on('message', async ({ activity, send }) => { const userId = activity.from.id; // Lazy initialization: create state if it does not exist let state = userStore.get(userId); if (!state) { state = { messages: [], preferences: {} }; userStore.set(userId, state); } const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant.', messages: state.messages, // Restore conversation history }); const result = await prompt.send(activity.text); if (result.content) { await send(result.content); } }); app.start(3978); ``` ### Per-conversation state with App storage ```typescript import { App } from '@microsoft/teams.apps'; import { ChatPrompt, Message } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; import { LocalStorage, ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const model = new OpenAIChatModel({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', }); interface IConversationState { messages: Message[]; topicCount: number; } // Pass storage to App -- available as ctx.storage in handlers const storage = new LocalStorage<IConversationState>({}, { max: 500 }); const app = new App({ storage, logger: new ConsoleLogger('conv-bot'), plugins: [new DevtoolsPlugin()], }); app.on('message', async ({ activity, send, storage }) => { const convId = activity.conversation.id; let state = storage.get(convId) as IConversationState | undefined; if (!state) { state = { messages: [], topicCount: 0 }; storage.set(convId, state); } const prompt = new ChatPrompt({ model, instructions: 'You are a helpful assistant. Be concise.', messages: state.messages, }); const result = await prompt.send(activity.text); if (result.content) { state.topicCount++; await send(result.content); } }); app.start(3978); ``` ### IStorage interface for custom backends ```typescript import { IStorage } from '@microsoft/teams.common'; // Example: custom Redis-backed storage implementing IStorage class RedisStorage<T> implements IStorage<string, T> { private client: any; // Your Redis client constructor(redisClient: any) { this.client = redisClient; } async get(key: string): Promise<T | undefined> { const raw = await this.client.get(`bot:state:${key}`); return raw ? JSON.parse(raw) : undefined; } async set(key: string, value: T): Promise<void> { await this.client.set(`bot:state:${key}`, JSON.stringify(value)); } async delete(key: string): Promise<void> { await this.client.del(`bot:state:${key}`); } } // Usage with App // const storage = new RedisStorage<IConversationState>(redisClient); // const app = new App({ storage }); ``` ## pitfalls - **Unbounded `LocalStorage`**: Not setting `max` on `LocalStorage` allows the store to grow without limit, eventually exhausting process memory. Always specify a max entry count. - **Data loss on restart**: `LocalStorage` is in-memory only. Restarting the process loses all state. Use a persistent backend (Cosmos DB, Redis) for production bots. - **Wrong state key scope**: Using `activity.from.id` when you want conversation-scoped state (or vice versa) causes data to bleed across contexts. Use `activity.conversation.id` for conversation state and `activity.from.id` for user state. - **Mutating state without re-setting**: If your `IStorage` backend uses serialization (e.g., Redis), mutating the returned object does not persist changes. Call `storage.set(key, state)` after modifications. - **Large message history**: Passing the entire message history to `ChatPrompt` without a `max` limit on `LocalMemory` can exceed token limits. Use `LocalMemory` with `max` and `collapse` for automatic summarization. - **Multi-instance deployments with `LocalStorage`**: Each process instance has its own in-memory store. In scaled deployments, a user may hit different instances, seeing inconsistent state. - **Missing null check on `get()`**: `storage.get(key)` returns `undefined` if the key does not exist. Always check for `undefined` and initialize before accessing properties. ## references - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [Bot state management concepts](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-state) - [Azure Cosmos DB for state storage](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction) - [Teams bot conversation context](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-context) ## instructions This expert covers state management and storage patterns for Teams bots built with the Teams AI Library v2 (`@microsoft/teams.ts`) in TypeScript. Use it when you need to: - Understand the `IStorage` interface (`get`/`set`/`delete`) and implement custom backends - Configure `LocalStorage` with LRU eviction limits - Pass storage to the `App` constructor and access it in handlers via `ctx.storage` - Implement per-user state keyed on `activity.from.id` - Implement per-conversation state keyed on `activity.conversation.id` - Combine stored message history with `ChatPrompt` for multi-turn conversations Pair with `ai.memory-localmemory-ts.md` for `LocalMemory` (AI message memory with summarization) and `auth.oauth-sso-ts.md` for authenticated state patterns. Pair with `ai.memory-localmemory-ts.md` for combining state with AI conversation history, and `runtime.app-init-ts.md` for passing storage to the App constructor. ## research Deep Research prompt: "Write a micro expert on state and storage patterns for Teams SDK v2 bots (TypeScript). Cover IStorage interface, LocalStorage with LRU eviction, per-user and per-conversation state, combining state with ChatPrompt messages, implementing custom persistent backends (Redis, Cosmos DB), and warnings about multi-instance deployments. Include 2-3 TypeScript code examples." -
teams-dotnet.md 8.2 KB
# teams-dotnet ## purpose Microsoft Teams SDK for .NET (C#) patterns — app initialization, activity handling, AI integration, and Adaptive Cards for Tier 3 C# projects. ## rules 1. Add the NuGet packages: `Microsoft.Teams.Apps`, `Microsoft.Teams.AI`, `Microsoft.Teams.AI.Models.OpenAI`, and `Microsoft.Teams.Plugins.AspNetCore`. The SDK targets **.NET 8+** and uses the modern minimal API pattern. [teams.net source: Libraries/] 2. Initialize with ASP.NET Core dependency injection: `builder.AddTeams()` registers core Teams services, then `app.UseTeams()` returns the `App` instance for handler registration. When no ClientId is configured, pass `skipAuth: true` to disable auth validation: `builder.AddTeams(skipAuth: true)`. This replaces the TS `Application.create()` pattern. [teams.net source: HostApplicationBuilder.cs] 3. Register activity handlers using fluent methods: `teams.OnMessage(async (context, ct) => { ... })`. Supports pattern matching: `teams.OnMessage(@"^hi$", async (context, ct) => { ... })`. Handlers are checked in registration order — first match wins. [teams.net source: App.cs] 4. All handlers receive `IContext<TActivity>` and `CancellationToken`. Access the activity via `context.Activity`, the API client via `context.Api`, storage via `context.Storage`, and logger via `context.Log`. This replaces the TS `TurnContext` pattern. [teams.net source: Context.cs] 5. Send messages with `await context.Send("text", ct)` or `await context.Reply("reply", ct)`. The `Send` method accepts strings, `ActivityParams`, or `AdaptiveCard` objects. For typing indicators, use `await context.Typing(cancellationToken: ct)`. [teams.net source: Context.cs] 6. Handle Adaptive Card actions with `teams.OnAdaptiveCardAction(async (context, ct) => { ... })`. Return `ActionResponse.Message("text")` from the handler. Access submitted data via `context.Activity.Value?.Action?.Data`. [teams.net source: App.cs] 7. For AI integration, create an `OpenAIChatModel` (from `Microsoft.Teams.AI.Models.OpenAI`) with Azure OpenAI or OpenAI credentials. Create a prompt with `new OpenAIChatPrompt(model)` and call `await prompt.Send(input, ct)`. [teams.net source: OpenAIChatPrompt.cs] 8. Define AI functions using C# attributes: decorate a class with `[Prompt]` and methods with `[Function]`. Parameters use `[Param]`. Or use the fluent API: `prompt.Function("name", "desc", async (string param) => result)`. This replaces TS function-calling with type-safe C# patterns. [teams.net source: Annotations/] 9. Use `IContext` tuple deconstruction for concise handler code: `var (log, api, activity) = context;`. This is idiomatic C# that has no TS equivalent. [teams.net source: Context.cs] 10. For OAuth/SSO, call `await context.SignIn(new OAuthOptions { ConnectionName = "name" }, ct)`. Check `context.IsSignedIn` and access `context.UserGraphToken` for Microsoft Graph calls. Access Graph via `context.UserGraph` (user) or `context.AppGraph` (app-only). [teams.net source: activity_context.py] 11. The SDK uses a **plugin architecture**. `AspNetCorePlugin` handles HTTP ingestion (registered by `AddTeams()`). Add `DevToolsPlugin` for development. Custom plugins implement `IPlugin` with lifecycle hooks (`OnInit`, `OnStart`, `OnActivity`). [teams.net source: IPlugin.cs] 12. Default endpoint is `POST /api/messages` on port 5000 (ASP.NET default) or the `PORT`/`ASPNETCORE_URLS` environment variable. This replaces the TS Express endpoint pattern. [teams.net source: AspNetCorePlugin] ## patterns ### Basic echo bot with ASP.NET Core ```csharp using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Extensions; using Microsoft.Teams.Plugins.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); builder.AddTeams(skipAuth: true); var app = builder.Build(); var teams = app.UseTeams(); teams.OnMessage(async (context, ct) => { await context.Send($"Echo: {context.Activity.Text}", ct); }); app.Run(); ``` ### AI bot with function calling ```csharp using Azure.AI.OpenAI; using System.ClientModel; using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Extensions; using Microsoft.Teams.Plugins.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); builder.AddTeams(skipAuth: true); var azureClient = new AzureOpenAIClient( new Uri(builder.Configuration["AzureOpenAIEndpoint"]!), new ApiKeyCredential(builder.Configuration["AzureOpenAIKey"]!) ); var model = new OpenAIChatModel("gpt-4", azureClient); var app = builder.Build(); var teams = app.UseTeams(); teams.OnMessage(async (context, ct) => { await context.Typing(cancellationToken: ct); var prompt = new OpenAIChatPrompt(model); prompt.Function("get_weather", "Get weather for a location", async (string location) => $"Sunny, 72F in {location}"); var result = await prompt.Send(context.Activity.Text, ct); if (result.Content != null) { await context.Send(result.Content, ct); } }); app.Run(); ``` ### Attribute-based prompt class ```csharp using Microsoft.Teams.AI.Annotations; [Prompt] [Prompt.Instructions("You are a helpful assistant that can search and summarize.")] public class AssistantPrompt(IContext<IActivity> context) { [Function] [Function.Description("Search the knowledge base")] public async Task<string> Search([Param] string query) { // Call your search API return $"Found 3 results for: {query}"; } [Function] [Function.Description("Get the current user's name")] public string GetUserName() { return context.Activity.From?.Name ?? "Unknown"; } } // Usage in handler: teams.OnMessage(async (context, ct) => { var prompt = OpenAIChatPrompt.From(model, new AssistantPrompt(context)); var result = await prompt.Send(context.Activity.Text, ct); if (result.Content != null) await context.Send(result.Content, ct); }); ``` ## pitfalls - **.NET 8+ required**: The SDK uses modern C# features (primary constructors, collection expressions) that require .NET 8 or later. - **No Slack SDK for C#**: No official Slack SDK exists for .NET. For the Slack side in Tier 3 C# projects, use REST API patterns from `experts/bridge/rest-only-integration-ts.md`. - **CancellationToken everywhere**: All async methods require passing `CancellationToken`. Forgetting it compiles but prevents graceful shutdown. - **AddTeams() before Build()**: The `builder.AddTeams()` call must happen before `builder.Build()`. Calling `UseTeams()` without `AddTeams()` throws at startup. - **Handler registration order matters**: Handlers are checked in order. Put specific patterns (`OnMessage(@"^help$")`) before generic handlers (`OnMessage(...)`) or the generic handler catches everything. - **ActionResponse return type**: `OnAdaptiveCardAction` handlers must return an `ActionResponse`, not just call `Send`. Returning null causes a 500 error. ## references - teams.net source: Libraries/Microsoft.Teams.Apps/App.cs - teams.net source: Libraries/Microsoft.Teams.AI.Models.OpenAI/OpenAIChatPrompt.cs - teams.net source: Samples/Samples.AI/Program.cs - teams.net source: Samples/Samples.Echo/Program.cs ## instructions This expert covers the Microsoft Teams SDK for .NET — the C# equivalent of `@microsoft/teams-ai`. Use it for Tier 3 C# projects that need Teams SDK patterns. C# projects have SDK support for Teams but not Slack. For the Slack side, pair with `experts/bridge/rest-only-integration-ts.md` to implement REST-based Slack integration. Pair with: `bridge/rest-only-integration-ts.md` for REST-based Slack integration (the unsupported side). TS Teams experts for conceptual architecture reference. ## research Deep Research prompt: "Write a micro expert on Microsoft Teams SDK for .NET (C#). Cover ASP.NET Core setup (AddTeams, UseTeams, minimal API), activity handler registration (OnMessage with regex, OnAdaptiveCardAction, OnConversationUpdate), IContext<TActivity> (Send, Reply, Typing, Activity, Api, Storage, Log, SignIn, IsSignedIn), OpenAIChatModel/OpenAIChatPrompt for AI, function calling (attribute-based [Prompt][Function][Param] and fluent API), Adaptive Cards (creating and handling actions), plugin architecture (IPlugin lifecycle), state management (IStorage), and context tuple deconstruction. Source from teams.net Libraries source code and Samples." -
teams-python.md 8.3 KB
# teams-python ## purpose Microsoft Teams SDK for Python patterns — translating TypeScript Teams AI concepts to Python equivalents using `microsoft_teams`. ## rules 1. Install the Teams Python SDK packages: `pip install microsoft-teams-apps microsoft-teams-ai microsoft-teams-openai`. The SDK is modular — `microsoft_teams.apps` for the App framework, `microsoft_teams.ai` for AI/prompts, `microsoft_teams.openai` for OpenAI model integration. [github.com/nicekid1/Microsoft-Teams.ts](https://github.com/nicekid1/Microsoft-Teams.ts) 2. Initialize the App with `App(client_id=..., client_secret=...)` or let it read from `CLIENT_ID` and `CLIENT_SECRET` environment variables. The App class is in `microsoft_teams.apps`. This replaces the TS `Application` class setup. [teams.py source: app.py] 3. Register activity handlers using Python decorators: `@app.on_message`, `@app.on_message_pattern(r"regex")`, `@app.on_conversation_update`, `@app.on_adaptive_card_action`, `@app.on_typing`, `@app.on_message_reaction`. This replaces the TS `app.message()`, `app.activity()` method-call patterns. [teams.py source: activity_handlers.py] 4. All handlers are `async def` and receive an `ActivityContext[T]` parameter, where `T` is the specific activity type (e.g., `MessageActivity`, `ConversationUpdateActivity`). This replaces the TS `TurnContext` pattern. [teams.py source: activity_context.py] 5. Use `await ctx.send(message)` to send a message and `await ctx.reply(message)` to reply in a thread. The `send` method accepts strings, `ActivityParams`, or `AdaptiveCard` objects. This replaces the TS `context.sendActivity()` pattern. [teams.py source: activity_context.py] 6. The App uses **FastAPI** internally (via `HttpPlugin`) with **uvicorn** as the ASGI server. Start with `asyncio.run(app.start(port=3978))`. Default endpoint is `POST /api/messages`. This replaces the TS Express-based setup. [teams.py source: http_plugin.py] 7. For AI integration, create a `ChatPrompt` with an AI model and call `await prompt.send(input, instructions=...)`. The model is typically `OpenAICompletionsAIModel` or `OpenAIResponsesAIModel` from `microsoft_teams.openai`. This replaces the TS `ChatPrompt` / `OpenAIModel` classes. [teams.py source: chat_prompt.py] 8. Define AI functions using `Function[ParamsType]` with **Pydantic models** for parameter schemas. The handler can be sync or async and returns a string. This replaces the TS function-calling pattern with its TypeScript interfaces. [teams.py source: function.py] 9. Access Microsoft Graph via `ctx.user_graph` (user-delegated) or `ctx.app_graph` (app-only). Check `ctx.is_signed_in` before using user Graph. Initiate sign-in with `await ctx.sign_in(SignInOptions(...))`. This replaces the TS `context.graph` patterns. [teams.py source: activity_context.py] 10. Use `ListMemory` from `microsoft_teams.ai` for conversation history in AI prompts. Pass it to `ChatPrompt(model=model, memory=memory)`. Supports `push`, `get_all`, and `set_all` operations. This replaces the TS `MemoryStorage` pattern. [teams.py source: memory.py] 11. Add custom HTTP routes using `@app.http.get("/path")` or `@app.http.post("/path")` — these are FastAPI route decorators exposed through the HttpPlugin. This replaces TS custom Express routes. [teams.py source: http_plugin.py] 12. Use Pydantic `BaseModel` subclasses for typed data throughout (function parameters, API models, card data). This replaces TypeScript interfaces and type definitions. [teams.py patterns] 13. The SDK requires **Python 3.12+** and uses modern Python features (type hints, dataclasses, protocols, `async`/`await`). [teams.py pyproject.toml] ## patterns ### Basic echo bot ```python import asyncio from microsoft_teams.apps import App, ActivityContext from microsoft_teams.api import MessageActivity app = App() @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.reply(f"Echo: {ctx.activity.text}") if __name__ == "__main__": asyncio.run(app.start(port=3978)) ``` ### AI bot with function calling ```python import asyncio from microsoft_teams.apps import App, ActivityContext from microsoft_teams.api import MessageActivity from microsoft_teams.ai import ChatPrompt, Function, ListMemory from microsoft_teams.openai import OpenAICompletionsAIModel from pydantic import BaseModel app = App() model = OpenAICompletionsAIModel(model="gpt-4") memory = ListMemory() class SearchParams(BaseModel): query: str """The search query""" async def search_handler(params: SearchParams) -> str: return f"Results for: {params.query}" @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): prompt = ChatPrompt(model=model, memory=memory) prompt.with_function(Function[SearchParams]( name="search", description="Search for information", parameter_schema=SearchParams, handler=search_handler, )) result = await prompt.send( input=ctx.activity.text, instructions="You are a helpful assistant.", ) if result.response.content: await ctx.send(result.response.content) if __name__ == "__main__": asyncio.run(app.start(port=3978)) ``` ### Adaptive Card handling ```python import asyncio from microsoft_teams.apps import App, ActivityContext from microsoft_teams.api import MessageActivity, MessageSubmitActionInvokeActivity app = App() @app.on_message_pattern(r"^card$") async def send_card(ctx: ActivityContext[MessageActivity]): from microsoft_teams.cards import AdaptiveCard card = AdaptiveCard() card.add_text_block("Feedback Form") card.add_text_input("feedback", placeholder="Your feedback") card.add_submit_action("Submit") await ctx.send(card) @app.on_message_submit_action async def handle_card_action(ctx: ActivityContext[MessageSubmitActionInvokeActivity]): data = ctx.activity.value await ctx.reply(f"Got feedback: {data}") if __name__ == "__main__": asyncio.run(app.start(port=3978)) ``` ## pitfalls - **Python 3.12+ required**: The Teams Python SDK uses modern Python features that require 3.12 or later. Earlier versions will fail at import time. - **All handlers are async**: Unlike Slack Bolt Python which supports sync handlers, Teams Python SDK handlers must all be `async def`. Forgetting `async` or `await` causes runtime errors. - **Package naming**: Import from `microsoft_teams.apps`, `microsoft_teams.ai`, etc. — not `teams_ai` or `botbuilder`. The namespace is `microsoft_teams`. - **Port 3978 vs 3000**: Teams SDK defaults to port 3978 (Teams convention), not 3000 (Slack convention). When running both in one process, configure different ports. - **No sync adapter**: Unlike Slack Bolt which has both sync and async paths, Teams Python SDK is async-only with FastAPI. No Flask adapter exists. ## references - https://github.com/nicekid1/Microsoft-Teams.ts (Python SDK repo) - teams.py source: packages/apps/src/microsoft_teams/apps/ - teams.py source: packages/ai/src/microsoft_teams/ai/ - teams.py source: packages/openai/src/microsoft_teams/openai/ ## instructions This expert covers the Microsoft Teams SDK for Python — the Python equivalent of `@microsoft/teams-ai`. Use it when building Teams bots in Python (Tier 2 or standalone), translating TypeScript Teams AI patterns to Python, or setting up the FastAPI-based Teams bot server. All TS Teams experts provide the architectural patterns — this expert provides the Python API mappings. Pair with: Teams TS experts (conceptual architecture — translate to Python). `bridge/python-cross-platform.md` for unified Python server with both Slack and Teams. `slack/bolt-python.md` for the Slack side of a Python dual-platform bot. ## research Deep Research prompt: "Write a micro expert mapping Microsoft Teams AI TypeScript patterns to Python equivalents using the microsoft_teams SDK. Cover App initialization (client_id, client_secret, env vars), decorator-based activity routing (@app.on_message, @app.on_message_pattern, @app.on_adaptive_card_action), ActivityContext[T] parameter (send, reply, stream, sign_in, is_signed_in, user_graph), ChatPrompt with OpenAICompletionsAIModel, Function[Params] with Pydantic BaseModel schemas, ListMemory for conversation history, FastAPI/uvicorn web framework, Adaptive Cards (AdaptiveCard class), custom HTTP routes via app.http, and key differences from TS (async-only, Pydantic vs interfaces, Python 3.12+). Source from teams.py packages source code." -
ui.adaptive-cards-ts.md 12.9 KB
# ui.adaptive-cards-ts ## purpose Adaptive Cards construction, sending as attachments, handling Action.Submit via card.action handlers, and Teams-specific constraints. ## rules 1. Always set `"type": "AdaptiveCard"` and `"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"` at the card root; Teams requires `"version": "1.5"` or lower (1.6+ features are silently ignored). [adaptivecards.io/designer](https://adaptivecards.io/designer) 2. Wrap every card in a `CardFactory.adaptiveCard(cardJson)` attachment -- never send raw JSON as message text. The attachment `contentType` is `"application/vnd.microsoft.card.adaptive"`. [learn.microsoft.com -- Cards reference](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#adaptive-card) 3. Register card action handlers with `app.adaptiveCards.actionSubmit(actionVerb, handler)` where `actionVerb` matches the `data.verb` (or `data.action`) string you embed in the card's `Action.Submit`. The handler receives `(ctx, state, data)` where `data` is the merged `activity.value` object. [github.com/microsoft/teams-ai](https://github.com/microsoft/teams-ai) 4. Every `Action.Submit` must include a `data` object with a routing identifier (e.g., `{ "verb": "approve", ...inputValues }`). Without it, Teams merges only input field values into `activity.value` and there is no way to distinguish which button was pressed. 5. Input element `id` values become keys in `activity.value`. For example, `Input.Text` with `"id": "comment"` yields `activity.value.comment`. Keep IDs short and unique within a card. 6. To update an existing message (e.g., replacing a card after action), return an updated card from the handler or call `await ctx.updateActivity({ ...activity, attachments: [newCard] })`. To send a new message instead, call `await ctx.sendActivity(MessageFactory.attachment(newCard))`. 7. Teams Adaptive Cards do NOT support `Action.Http`, `Action.ToggleVisibility` (partial -- works for simple show/hide but not nested), `backgroundImage` on mobile, `Media` element playback, or `hostConfig` overrides. Always test on desktop + mobile. [learn.microsoft.com -- Cards reference](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#adaptive-card) 8. Card payload size must be under 28 KB (after JSON serialization). Larger cards are rejected silently. [learn.microsoft.com -- Card size limit](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#card-size-limit) 9. For `Action.Execute` (Universal Actions), the handler is `app.adaptiveCards.actionExecute(verb, handler)` and the return must be an Adaptive Card (used for automatic card refresh / user-specific views). Prefer `Action.Submit` for standard form flows; use `Action.Execute` only when you need per-user card refresh. [learn.microsoft.com -- Universal Actions](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview) 10. Always validate `activity.value` server-side -- clients can tamper with the JSON payload. Use a schema validator (e.g., zod) before trusting input data. ## patterns ### Confirm / Cancel card with action routing ```typescript import { App, TurnState } from "@microsoft/teams-ai"; import { CardFactory, MessageFactory } from "botbuilder"; // -- Card JSON --------------------------------------------------------------- const confirmCard = { type: "AdaptiveCard", $schema: "http://adaptivecards.io/schemas/adaptive-card.json", version: "1.5", body: [ { type: "TextBlock", text: "Delete this item?", weight: "Bolder", size: "Medium", }, { type: "TextBlock", text: "This action cannot be undone.", wrap: true, isSubtle: true, }, ], actions: [ { type: "Action.Submit", title: "Confirm", style: "destructive", data: { verb: "deleteConfirm", itemId: "abc-123" }, }, { type: "Action.Submit", title: "Cancel", data: { verb: "deleteCancel", itemId: "abc-123" }, }, ], }; // -- Handlers ---------------------------------------------------------------- export function registerConfirmHandlers(app: App<TurnState>): void { app.adaptiveCards.actionSubmit("deleteConfirm", async (ctx, _state, data) => { const itemId = (data as Record<string, string>).itemId; // ... perform deletion logic ... // Replace the card with a confirmation message const doneCard = CardFactory.adaptiveCard({ type: "AdaptiveCard", version: "1.5", body: [{ type: "TextBlock", text: `Item ${itemId} deleted.` }], }); await ctx.updateActivity({ type: "message", id: ctx.activity.replyToId, attachments: [doneCard], }); return undefined; }); app.adaptiveCards.actionSubmit("deleteCancel", async (ctx) => { const cancelCard = CardFactory.adaptiveCard({ type: "AdaptiveCard", version: "1.5", body: [{ type: "TextBlock", text: "Deletion cancelled." }], }); await ctx.updateActivity({ type: "message", id: ctx.activity.replyToId, attachments: [cancelCard], }); return undefined; }); } ``` ### Form submission with input extraction ```typescript import { App, TurnState } from "@microsoft/teams-ai"; import { CardFactory, MessageFactory } from "botbuilder"; const feedbackFormCard = { type: "AdaptiveCard", $schema: "http://adaptivecards.io/schemas/adaptive-card.json", version: "1.5", body: [ { type: "TextBlock", text: "Submit Feedback", weight: "Bolder", size: "Large" }, { type: "Input.Text", id: "userName", label: "Your name", isRequired: true, errorMessage: "Name is required", }, { type: "Input.ChoiceSet", id: "rating", label: "Rating", style: "compact", value: "3", choices: [ { title: "1 - Poor", value: "1" }, { title: "2 - Fair", value: "2" }, { title: "3 - Good", value: "3" }, { title: "4 - Great", value: "4" }, { title: "5 - Excellent", value: "5" }, ], }, { type: "Input.Text", id: "comments", label: "Comments", isMultiline: true, placeholder: "Tell us more...", }, { type: "Input.Toggle", id: "followUp", title: "Contact me for follow-up", value: "false", valueOn: "true", valueOff: "false", }, ], actions: [ { type: "Action.Submit", title: "Submit", data: { verb: "submitFeedback" }, }, ], }; interface FeedbackData { verb: string; userName: string; rating: string; comments?: string; followUp: string; } export function registerFeedbackHandlers(app: App<TurnState>): void { app.adaptiveCards.actionSubmit("submitFeedback", async (ctx, _state, data) => { const fd = data as FeedbackData; // Input values are merged into activity.value alongside the data object const msg = `Thanks ${fd.userName}! Rating: ${fd.rating}/5.`; await ctx.sendActivity(MessageFactory.text(msg)); return undefined; }); } // -- Sending the card -------------------------------------------------------- // Inside any handler or proactive flow: // await ctx.sendActivity(MessageFactory.attachment( // CardFactory.adaptiveCard(feedbackFormCard) // )); ``` ### Dynamic choices via Action.Execute refresh ```typescript import { App, TurnState } from "@microsoft/teams-ai"; import { CardFactory } from "botbuilder"; // Card with Action.Execute for per-user refresh (Universal Actions) function buildTicketCard(tickets: { id: string; title: string }[]): object { return { type: "AdaptiveCard", version: "1.4", refresh: { action: { type: "Action.Execute", title: "Refresh", verb: "refreshTickets", }, userIds: [], // empty = refresh for all users }, body: [ { type: "TextBlock", text: "Open Tickets", weight: "Bolder" }, { type: "Input.ChoiceSet", id: "selectedTicket", label: "Pick a ticket", choices: tickets.map((t) => ({ title: t.title, value: t.id })), }, ], actions: [ { type: "Action.Execute", title: "Claim", verb: "claimTicket", data: {}, }, ], }; } export function registerTicketHandlers(app: App<TurnState>): void { // Action.Execute handler -- must return an Adaptive Card app.adaptiveCards.actionExecute("refreshTickets", async (_ctx, _state) => { const tickets = [ { id: "T-1", title: "Login page broken" }, { id: "T-2", title: "Report export fails" }, ]; // replace with real DB call return CardFactory.adaptiveCard(buildTicketCard(tickets)); }); app.adaptiveCards.actionExecute("claimTicket", async (ctx, _state, data) => { const ticketId = (data as Record<string, string>).selectedTicket; // ... assign ticket ... return CardFactory.adaptiveCard({ type: "AdaptiveCard", version: "1.4", body: [{ type: "TextBlock", text: `Ticket ${ticketId} claimed by you.` }], }); }); } ``` ## pitfalls - **Missing `verb` in data**: If `Action.Submit` has no `data` object (or no routing key), the `actionSubmit` handler cannot route by verb. Always include `{ verb: "myAction" }` in the `data` property. - **Input IDs collide with data keys**: If an `Input.Text` has `id: "verb"`, it overwrites the `data.verb` routing key when merged into `activity.value`. Use prefixes (e.g., `input_name`) or avoid reserved keys. - **Updating the wrong activity**: `ctx.activity.replyToId` is the ID of the message containing the card. Use this for `updateActivity`. Using `ctx.activity.id` targets the invoke activity itself, not the card message. - **Card version too high**: Teams desktop/mobile silently drops elements from schema versions above what the client supports. Stick to version `"1.5"` for broadest compatibility. Test version `"1.6"` features explicitly before shipping. - **`Action.Execute` vs `Action.Submit`**: `Action.Execute` requires the handler to return a card (for automatic replacement). `Action.Submit` handlers are fire-and-forget from the card's perspective. Mixing them up causes silent failures or empty card replacements. - **Card not rendering**: Forgetting `CardFactory.adaptiveCard()` and instead passing raw JSON to `attachments` results in a blank message. Always wrap with `CardFactory`. - **ChoiceSet value types**: All `Input.ChoiceSet` values arrive as strings in `activity.value`, even if they look numeric. Parse explicitly with `parseInt()` or a validation library. - **28 KB limit**: Large dynamically generated cards (e.g., long lists) can exceed the Teams payload limit. Paginate or truncate before serializing. ## references - [Adaptive Cards Schema Explorer](https://adaptivecards.io/explorer/) - [Adaptive Cards Designer](https://adaptivecards.io/designer/) - [Teams: Cards and card actions](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-actions) - [Teams: Adaptive Card for bots](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#adaptive-card) - [Teams: Universal Actions for Adaptive Cards](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview) - [Teams AI SDK GitHub -- samples](https://github.com/microsoft/teams-ai/tree/main/js/samples) - [Teams: Format cards](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format) - [Teams: Card size limits](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#card-size-limit) ## instructions This expert covers building, sending, and handling Adaptive Cards in Microsoft Teams bots using the Teams AI SDK v2 (`@microsoft/teams-ai`) in TypeScript. Use it when you need to: - Construct an Adaptive Card JSON payload (body elements, inputs, actions) - Send a card as a bot attachment via `CardFactory.adaptiveCard()` + `MessageFactory.attachment()` - Handle `Action.Submit` button presses with `app.adaptiveCards.actionSubmit(verb, handler)` - Handle `Action.Execute` (Universal Actions) with `app.adaptiveCards.actionExecute(verb, handler)` - Extract user input from `activity.value` (merged input IDs + action data) - Update an existing card message vs. sending a new reply - Avoid Teams-specific limitations (version caps, unsupported elements, size limits) Pair with `ui.dialogs-task-modules-ts.md` for modal/dialog card flows and `runtime.routing-handlers-ts.md` for broader handler registration context. ## research Deep Research prompt: "Write a micro expert on Adaptive Cards in Teams (TypeScript). Cover card anatomy, input elements, Action.Submit payloads, sending attachments, handling app.on('card.action'), extracting action identifiers from activity.value, updating messages vs sending new, and Teams-specific card limitations. Include 2-3 canonical card patterns (confirm/cancel, form submit, dynamic choices)." -
ui.dialogs-task-modules-ts.md 15.1 KB
# ui.dialogs-task-modules-ts ## purpose Dialog/task module flows: opening, submitting, and chaining dialogs in Teams bots using the Teams AI Library v2. ## rules 1. Handle dialog open requests with `app.on('dialog.open', handler)`. This fires when Teams invokes `task/fetch`. The handler must return a response with `task.type: 'continue'` containing a card, or `task.type: 'message'` containing text. [learn.microsoft.com -- Task modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots) 2. Handle dialog submissions with `app.on('dialog.submit', handler)`. This fires when the user submits the form inside the task module (`task/submit`). Form data is available in `activity.value.data`. [learn.microsoft.com -- Task modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots) 3. To open a dialog with an Adaptive Card, return `{ status: 200, body: { task: { type: 'continue', value: { title, card } } } }` where `card` is an object with `contentType: 'application/vnd.microsoft.card.adaptive'` and `content` containing the card JSON. [learn.microsoft.com -- Task modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots) 4. To close a dialog with a text message, return `{ status: 200, body: { task: { type: 'message', value: 'Success message' } } }` from the submit handler. This displays the message and closes the dialog. [learn.microsoft.com -- Task modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots) 5. To chain dialogs (open a new dialog after submission), return a `continue` response from the submit handler with a new card. This replaces the current dialog content without closing it. [learn.microsoft.com -- Task modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots) 6. Control dialog dimensions with `width` and `height` properties in the `value` object. Accepted values are `'small'`, `'medium'`, `'large'`, or pixel values (e.g., `500`). Default is `'medium'`. [learn.microsoft.com -- Task module size](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots#the-taskinfo-object) 7. Trigger a dialog from a message by sending an Adaptive Card with `Action.Submit` containing `{ msteams: { type: 'task/fetch' } }` in its data, or by adding the bot to the manifest with `taskInfo` commands. [learn.microsoft.com -- Invoke task modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots#invoke-a-task-module-from-a-bot) 8. The `dialog.open` route maps to `task/fetch` and `dialog.submit` maps to `task/submit` in the Teams invoke system. Both are invoke routes that require a structured return value, not a simple `send()` call. [github.com/microsoft/teams.ts](https://github.com/microsoft/teams.ts) 9. Form input IDs in the Adaptive Card become keys in `activity.value.data`. For example, `Input.Text` with `id: 'email'` appears as `activity.value.data.email` in the submit handler. [learn.microsoft.com -- Adaptive Card inputs](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-actions) 10. Always validate submitted data server-side. Users can tamper with the JSON payload sent by the dialog. Use schema validation before processing form data. [learn.microsoft.com -- Task modules security](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots) ## patterns ### Opening a dialog with an Adaptive Card form ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ logger: new ConsoleLogger('dialog-bot'), plugins: [new DevtoolsPlugin()], }); // Open a dialog when task/fetch is invoked app.on('dialog.open', async () => { return { status: 200, body: { task: { type: 'continue', value: { title: 'Submit Feedback', width: 'medium', height: 'medium', card: { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: 'Submit your feedback', weight: 'Bolder', size: 'Large', }, { type: 'Input.Text', id: 'userName', label: 'Your name', isRequired: true, errorMessage: 'Name is required', }, { type: 'Input.ChoiceSet', id: 'rating', label: 'Rating', style: 'compact', value: '3', choices: [ { title: '1 - Poor', value: '1' }, { title: '2 - Fair', value: '2' }, { title: '3 - Good', value: '3' }, { title: '4 - Great', value: '4' }, { title: '5 - Excellent', value: '5' }, ], }, { type: 'Input.Text', id: 'comments', label: 'Comments', isMultiline: true, placeholder: 'Tell us more...', }, ], actions: [ { type: 'Action.Submit', title: 'Submit', }, ], }, }, }, }, }, }; }); // Handle the dialog form submission app.on('dialog.submit', async ({ activity }) => { const formData = activity.value.data; const { userName, rating, comments } = formData; // Validate and process console.log(`Feedback from ${userName}: ${rating}/5 - ${comments}`); // Close dialog with a message return { status: 200, body: { task: { type: 'message', value: `Thanks ${userName}! Your feedback (${rating}/5) has been recorded.`, }, }, }; }); app.start(3978); ``` ### Chaining dialogs (multi-step form) ```typescript import { App } from '@microsoft/teams.apps'; const app = new App(); // Step 1: Open initial dialog app.on('dialog.open', async () => { return { status: 200, body: { task: { type: 'continue', value: { title: 'Step 1: Basic Info', width: 'medium', height: 'small', card: { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: 'Step 1 of 2', weight: 'Bolder' }, { type: 'Input.Text', id: 'projectName', label: 'Project name', isRequired: true, }, { type: 'Input.ChoiceSet', id: 'projectType', label: 'Type', choices: [ { title: 'Feature', value: 'feature' }, { title: 'Bug Fix', value: 'bugfix' }, { title: 'Research', value: 'research' }, ], }, ], actions: [ { type: 'Action.Submit', title: 'Next', data: { step: 'step1' }, }, ], }, }, }, }, }, }; }); // Handle submissions -- route by step app.on('dialog.submit', async ({ activity, send }) => { const data = activity.value.data; if (data.step === 'step1') { // Chain to step 2: return a continue response with a new card return { status: 200, body: { task: { type: 'continue', value: { title: 'Step 2: Details', width: 'medium', height: 'medium', card: { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: `Project: ${data.projectName}`, weight: 'Bolder' }, { type: 'TextBlock', text: 'Step 2 of 2' }, { type: 'Input.Text', id: 'description', label: 'Description', isMultiline: true, isRequired: true, }, { type: 'Input.Date', id: 'dueDate', label: 'Due date', }, ], actions: [ { type: 'Action.Submit', title: 'Create', data: { step: 'step2', projectName: data.projectName, projectType: data.projectType, }, }, ], }, }, }, }, }, }; } if (data.step === 'step2') { // Final step: process all data and close await send(`Project "${data.projectName}" (${data.projectType}) created! Due: ${data.dueDate || 'No date set'}`); return { status: 200, body: { task: { type: 'message', value: 'Project created successfully!', }, }, }; } return { status: 200, body: { task: { type: 'message', value: 'Unknown step.' } } }; }); app.start(3978); ``` ### Triggering a dialog from an Adaptive Card ```typescript import { App } from '@microsoft/teams.apps'; const app = new App(); // Send a card with a button that triggers dialog.open app.on('message', async ({ send }) => { await send({ type: 'message', attachments: [{ contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: 'Click the button below to open a form dialog.', wrap: true, }, ], actions: [ { type: 'Action.Submit', title: 'Open Form', data: { msteams: { type: 'task/fetch' }, }, }, ], }, }], }); }); // The dialog.open handler fires when the button is clicked app.on('dialog.open', async () => { return { status: 200, body: { task: { type: 'continue', value: { title: 'My Form', card: { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'Input.Text', id: 'name', label: 'Enter your name' }, ], actions: [ { type: 'Action.Submit', title: 'Submit' }, ], }, }, }, }, }, }; }); app.on('dialog.submit', async ({ activity }) => { return { status: 200, body: { task: { type: 'message', value: `Hello, ${activity.value.data.name}!` }, }, }; }); app.start(3978); ``` ## pitfalls - **Wrong response structure**: `dialog.open` and `dialog.submit` are invoke handlers that must return `{ status: 200, body: { task: { ... } } }`. Using `await send()` instead of returning the response results in an empty dialog. - **Missing card `contentType` wrapper**: The `card` in the task value must include `contentType: 'application/vnd.microsoft.card.adaptive'` and `content`. Passing raw card JSON without the wrapper results in a blank dialog. - **Not routing multi-step dialogs**: When chaining dialogs, the submit handler fires for every step. Without a routing key (e.g., `data.step`), you cannot distinguish which step was submitted. - **Forgetting to pass data between steps**: When chaining, data from step 1 is not automatically available in step 2. Include previous step data in the `Action.Submit` `data` object to carry it forward. - **Dialog not opening**: Ensure the triggering card action includes `data: { msteams: { type: 'task/fetch' } }` for `Action.Submit`, or that the manifest defines a command with `taskInfo`. Without this, Teams does not invoke `dialog.open`. - **Form data not appearing**: Input element `id` values become keys in `activity.value.data`. If `id` is missing on an input, its value is not submitted. - **Size not taking effect**: The `width` and `height` properties must be in the `value` object alongside `title` and `card`. Placing them at the wrong nesting level is silently ignored. - **Returning undefined**: If the handler returns `undefined` or nothing, Teams shows a generic error. Always return a valid task response. ## references - [Task modules in Teams bots](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots) - [Task module invocation](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots#invoke-a-task-module-from-a-bot) - [TaskInfo object](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots#the-taskinfo-object) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) - [Adaptive Cards for task modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference) ## instructions This expert covers dialog/task module flows in Microsoft Teams bots built with the Teams AI Library v2 (`@microsoft/teams.ts`) in TypeScript. Use it when you need to: - Open a dialog with an Adaptive Card form via `app.on('dialog.open', ...)` - Process form submissions via `app.on('dialog.submit', ...)` - Return `continue` (card) or `message` (text) task responses - Chain multiple dialog steps (multi-step wizard) - Trigger dialogs from Adaptive Card buttons using `msteams: { type: 'task/fetch' }` - Control dialog dimensions with `width` and `height` Pair with `ui.adaptive-cards-ts.md` for card construction details and `runtime.routing-handlers-ts.md` for handler registration context. Pair with `ui.adaptive-cards-ts.md` for card construction inside task modules, and `runtime.routing-handlers-ts.md` for dialog.open/dialog.submit route registration. ## research Deep Research prompt: "Write a micro expert on Teams Task Modules (dialogs) using Teams SDK v2 in TypeScript. Cover app.on('dialog.open') return payload structure, app.on('dialog.submit') handling, embedding Adaptive Card forms in dialogs, multi-step dialog patterns with chaining, triggering dialogs from card actions, dialog dimensions, and common failure modes. Include 2-3 canonical TypeScript code examples." -
ui.message-extensions-ts.md 15.6 KB
# ui.message-extensions-ts ## purpose Search-based and action-based message extensions (compose extensions) for Teams bots using the Teams AI Library v2. ## rules 1. Configure message extensions in `appPackage/manifest.json` under the `composeExtensions` array. Each extension has a `botId`, `commands` array, and each command specifies `type` (`query` for search, `action` for task module). [learn.microsoft.com -- Message extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions) 2. Handle search queries with `app.on('message.ext.query', handler)`. The handler receives the query text in `activity.value.parameters[0].value` and must return a response with `composeExtension.type: 'result'` containing an attachments array. [learn.microsoft.com -- Search extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command) 3. Set `attachmentLayout` to `'list'` for vertical result layout or `'grid'` for a tile grid. Use `'list'` for text-heavy results and `'grid'` for image-heavy results. [learn.microsoft.com -- Respond to search](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/respond-to-search) 4. Each attachment in the results array needs both a `content` (the full Adaptive Card inserted into the compose box) and a `preview` (a smaller Thumbnail Card shown in the search results list). The preview uses `contentType: 'application/vnd.microsoft.card.thumbnail'`. [learn.microsoft.com -- Respond to search](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/respond-to-search) 5. Handle action-based extensions with two routes: `app.on('message.ext.open', handler)` for displaying the task module form (`composeExtension/fetchTask`) and `app.on('message.ext.submit', handler)` for processing the submitted data (`composeExtension/submitAction`). [learn.microsoft.com -- Action extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command) 6. The `message.ext.open` handler returns a task module response identical to `dialog.open`: `{ status: 200, body: { task: { type: 'continue', value: { title, card } } } }`. [learn.microsoft.com -- Action extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command) 7. The `message.ext.submit` handler receives form data in `activity.value.data` and can return a card to insert into the compose box or perform a server-side action. [learn.microsoft.com -- Action extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/respond-to-task-module-submit) 8. Manifest command parameters define the search fields displayed in the Teams UI. Each parameter has `name`, `title`, and optionally `description` and `inputType`. The first parameter is the default search field. [learn.microsoft.com -- Define search command](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command) 9. Search result counts should be limited (10-15 items) because Teams truncates long result lists. Always handle empty query strings gracefully by returning popular or recent results. [learn.microsoft.com -- Search extensions](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/respond-to-search) 10. Handle link unfurling with `app.on('message.ext.query-link', handler)`. This fires when a user pastes a URL matching a domain in the manifest's `messageHandlers`. Return a card attachment to preview the link. [learn.microsoft.com -- Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling) ## patterns ### Search-based message extension ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; // Manifest excerpt (appPackage/manifest.json): // { // "composeExtensions": [{ // "botId": "${{BOT_ID}}", // "commands": [{ // "id": "searchCmd", // "type": "query", // "title": "Search Products", // "parameters": [{ "name": "query", "title": "Search query" }] // }] // }] // } interface Product { id: string; title: string; description: string; price: number; imageUrl: string; } async function searchProducts(query: string): Promise<Product[]> { // Replace with your actual search logic const products: Product[] = [ { id: '1', title: 'Widget Pro', description: 'A premium widget', price: 29.99, imageUrl: 'https://example.com/widget.png' }, { id: '2', title: 'Gadget Plus', description: 'An advanced gadget', price: 49.99, imageUrl: 'https://example.com/gadget.png' }, ]; return products.filter(p => p.title.toLowerCase().includes(query.toLowerCase()) ); } const app = new App({ logger: new ConsoleLogger('ext-bot'), plugins: [new DevtoolsPlugin()], }); app.on('message.ext.query', async ({ activity }) => { const query = activity.value.parameters?.[0]?.value || ''; const results = await searchProducts(query); return { status: 200, body: { composeExtension: { type: 'result', attachmentLayout: 'list', attachments: results.map(item => ({ // Full card inserted into compose box when selected contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: item.title, weight: 'Bolder', size: 'Large' }, { type: 'TextBlock', text: item.description, wrap: true }, { type: 'TextBlock', text: `Price: $${item.price}`, weight: 'Bolder' }, ], }, // Preview card shown in search results list preview: { contentType: 'application/vnd.microsoft.card.thumbnail', content: { title: item.title, text: `$${item.price} - ${item.description}`, images: [{ url: item.imageUrl }], }, }, })), }, }, }; }); app.start(3978); ``` ### Action-based message extension with task module ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; // Manifest excerpt (appPackage/manifest.json): // { // "composeExtensions": [{ // "botId": "${{BOT_ID}}", // "commands": [{ // "id": "createItem", // "type": "action", // "title": "Create Item", // "fetchTask": true // }] // }] // } const app = new App({ logger: new ConsoleLogger('action-ext-bot'), plugins: [new DevtoolsPlugin()], }); // Open a task module form when the action is triggered app.on('message.ext.open', async () => { return { status: 200, body: { task: { type: 'continue', value: { title: 'Create New Item', width: 'medium', height: 'medium', card: { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: 'Create a new item', weight: 'Bolder', size: 'Large', }, { type: 'Input.Text', id: 'title', label: 'Title', isRequired: true, errorMessage: 'Title is required', }, { type: 'Input.Text', id: 'description', label: 'Description', isMultiline: true, }, { type: 'Input.ChoiceSet', id: 'priority', label: 'Priority', value: 'medium', choices: [ { title: 'Low', value: 'low' }, { title: 'Medium', value: 'medium' }, { title: 'High', value: 'high' }, ], }, ], actions: [ { type: 'Action.Submit', title: 'Create' }, ], }, }, }, }, }, }; }); // Process the submitted form data app.on('message.ext.submit', async ({ activity, send }) => { const data = activity.value.data; const { title, description, priority } = data; // Create the item in your backend const itemId = `ITEM-${Date.now()}`; await send(`Created item "${title}" (${priority} priority) - ID: ${itemId}`); }); app.start(3978); ``` ### Combined search and action extensions ```typescript import { App } from '@microsoft/teams.apps'; // Manifest excerpt (appPackage/manifest.json): // { // "composeExtensions": [{ // "botId": "${{BOT_ID}}", // "commands": [ // { // "id": "searchItems", // "type": "query", // "title": "Search Items", // "parameters": [{ "name": "query", "title": "Search" }] // }, // { // "id": "createItem", // "type": "action", // "title": "Create Item", // "fetchTask": true // } // ] // }] // } const app = new App(); // Search extension handler app.on('message.ext.query', async ({ activity }) => { const query = activity.value.parameters?.[0]?.value || ''; const commandId = activity.value.commandId; // You can route by commandId if multiple search commands exist const items = [ { id: '1', title: 'Task Alpha', status: 'open' }, { id: '2', title: 'Task Beta', status: 'closed' }, ].filter(i => i.title.toLowerCase().includes(query.toLowerCase())); return { status: 200, body: { composeExtension: { type: 'result', attachmentLayout: 'list', attachments: items.map(item => ({ contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: item.title, weight: 'Bolder' }, { type: 'TextBlock', text: `Status: ${item.status}` }, ], }, preview: { contentType: 'application/vnd.microsoft.card.thumbnail', content: { title: item.title, text: `Status: ${item.status}`, }, }, })), }, }, }; }); // Action extension: open form app.on('message.ext.open', async () => { return { status: 200, body: { task: { type: 'continue', value: { title: 'Create Item', card: { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'Input.Text', id: 'title', label: 'Title', isRequired: true }, { type: 'Input.Text', id: 'notes', label: 'Notes', isMultiline: true }, ], actions: [ { type: 'Action.Submit', title: 'Create' }, ], }, }, }, }, }, }; }); // Action extension: process submission app.on('message.ext.submit', async ({ activity, send }) => { const { title, notes } = activity.value.data; await send(`Created: ${title}${notes ? ` - ${notes}` : ''}`); }); app.start(3978); ``` ## pitfalls - **Missing manifest `composeExtensions`**: Message extensions require the `composeExtensions` array in the manifest. Without it, the extension does not appear in the Teams compose box. Update the manifest and re-sideload after changes. - **Empty query handling**: Users often open the search extension without typing. Handle empty or blank `query` strings by returning popular/recent results instead of an empty list. - **Missing preview card**: Each search result attachment must include a `preview` with `contentType: 'application/vnd.microsoft.card.thumbnail'`. Without it, the result appears blank in the search results list. - **Wrong route handler**: Search uses `message.ext.query` (not `message.ext.open`). Action uses `message.ext.open` + `message.ext.submit`. Mixing them up results in handlers never firing. - **Too many results**: Teams limits the number of displayed results. Return 10-15 items maximum. Longer lists are silently truncated. - **Attachment layout mismatch**: Using `'grid'` layout requires images in the preview. Using `'grid'` with text-only thumbnails produces a poor visual experience. Match layout to content type. - **Not setting `fetchTask: true` for action commands**: In the manifest, action commands must have `"fetchTask": true` for the `message.ext.open` handler to fire. Without it, Teams does not invoke the task module. - **Preview vs content confusion**: The `preview` is what users see in the results dropdown. The `content` is what gets inserted when they select it. A missing or incorrect `content` card means the wrong card (or nothing) is inserted. ## references - [Message extensions overview](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions) - [Define search commands](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command) - [Respond to search commands](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/respond-to-search) - [Define action commands](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command) - [Respond to action commands](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/respond-to-task-module-submit) - [Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling) - [Teams AI Library v2 -- GitHub](https://github.com/microsoft/teams.ts) ## instructions This expert covers search-based and action-based message extensions (compose extensions) in Microsoft Teams bots built with the Teams AI Library v2 (`@microsoft/teams.ts`) in TypeScript. Use it when you need to: - Configure `composeExtensions` in the manifest with search (`query`) or action commands - Handle search queries with `app.on('message.ext.query', ...)` and return result attachments with previews - Handle action extensions with `app.on('message.ext.open', ...)` for task modules and `app.on('message.ext.submit', ...)` for processing - Build thumbnail preview cards for search results - Choose between `'list'` and `'grid'` attachment layouts - Combine search and action commands in a single extension Pair with `ui.adaptive-cards-ts.md` for card construction and `ui.dialogs-task-modules-ts.md` for task module patterns used in action extensions. Pair with `runtime.manifest-ts.md` for composeExtensions manifest configuration, and `ui.adaptive-cards-ts.md` for building card attachments returned by extensions. ## research Deep Research prompt: "Write a micro expert on Message Extensions in Teams (TypeScript). Cover manifest composeExtensions configuration, search-based query flow (message.ext.query handler, parameters, result attachments with preview), action-based flow (message.ext.open for task module, message.ext.submit for processing), attachment layouts (list/grid), thumbnail preview cards, link unfurling, and common pitfalls. Include 2-3 canonical TypeScript code examples." -
workflow.approvals-inline-ts.md 12.4 KB
# workflow.approvals-inline-ts ## purpose Implement in-channel approval workflows that stay embedded in threads as interactive Adaptive Cards with state persistence, routing logic, and escalation — the bot-native alternative to Power Automate Approvals. ## rules 1. **Approvals are a state machine: Pending -> Approved|Rejected|Escalated.** Model every approval as an explicit state machine. Store the current state, assignee, and history on the backing record. Never rely on card UI state alone — the backing store is the source of truth. 2. **Use `Action.Execute` for approve/reject actions.** Each approval card has Approve and Reject buttons as `Action.Execute` with `verb: "approve"` / `verb: "reject"` and the record ID in `data`. The invoke handler updates the backing store and returns a refreshed read-only card. 3. **Support three routing patterns: single, sequential, parallel.** (a) **Single**: one approver, one decision. (b) **Sequential (chain)**: approver 1 must approve before approver 2 sees the request. (c) **Parallel**: all approvers see the request simultaneously; configurable as "any" (first response wins) or "all" (unanimous required). 4. **Use `refresh.userIds` for approver-specific card views.** Only the assigned approver should see action buttons. Other viewers see a read-only status card. Set `refresh.userIds` to the current approver's AAD ID. The refresh invoke returns the appropriate card variant. 5. **Persist approval history as an array on the record.** Store `[{ approver, action, timestamp, comment }]` on the backing row. This provides a complete audit trail rendered in the card's history section. 6. **Send the approval card as a thread reply.** Anchor the approval to the request's originating message via `replyToId`. This keeps the approval decision visible in context, not lost in the channel timeline. 7. **Implement escalation timers.** When an approval has been pending for a configurable duration (e.g., 24 hours), either send a reminder to the current approver or auto-escalate to their manager. Look up the manager via Graph: `GET /users/{userId}/manager`. 8. **Update the card in-place on every state transition.** After approve, reject, escalate, or reassign, call `updateActivity()` with the refreshed card. The thread always shows the current state without duplicate messages. 9. **Support optional comments on approve/reject.** Add an `Input.Text` field that appears alongside approve/reject buttons. The `Action.Execute.data` includes the comment, which is stored in the approval history. 10. **Notify the requester on resolution.** When the approval is finalized (approved or rejected), send a proactive message or @mention the requester in the thread with the outcome. ## patterns ### Approval record type ```typescript interface ApprovalRecord { id: string; title: string; description: string; requesterId: string; requesterName: string; status: "Pending" | "Approved" | "Rejected" | "Escalated"; routingType: "single" | "sequential" | "parallel-any" | "parallel-all"; approvers: ApproverEntry[]; history: ApprovalHistoryEntry[]; conversationId: string; cardActivityId: string; serviceUrl: string; createdAt: string; resolvedAt?: string; } interface ApproverEntry { userId: string; displayName: string; order: number; // For sequential routing decision?: "approved" | "rejected"; decidedAt?: string; } interface ApprovalHistoryEntry { actor: string; action: string; comment?: string; timestamp: string; } ``` ### Build approval card with role-specific actions ```typescript function buildApprovalCard(record: ApprovalRecord, viewerUserId: string): object { const isApprover = record.approvers.some( (a) => a.userId === viewerUserId && !a.decision ); const isPending = record.status === "Pending"; return { type: "AdaptiveCard", version: "1.5", refresh: { action: { type: "Action.Execute", verb: "refreshApproval", data: { recordId: record.id }, }, userIds: record.approvers .filter((a) => !a.decision) .map((a) => a.userId), }, body: [ { type: "TextBlock", text: "Approval Request", weight: "Bolder", size: "Medium" }, { type: "FactSet", facts: [ { title: "From", value: record.requesterName }, { title: "Status", value: record.status }, { title: "Type", value: record.routingType }, { title: "Created", value: new Date(record.createdAt).toLocaleString() }, ], }, { type: "TextBlock", text: record.description, wrap: true }, // Approval history ...(record.history.length > 0 ? [ { type: "TextBlock", text: "History", weight: "Bolder", spacing: "Medium" }, ...record.history.map((h) => ({ type: "TextBlock", text: `${h.actor}: ${h.action}${h.comment ? ` - "${h.comment}"` : ""} (${new Date(h.timestamp).toLocaleString()})`, isSubtle: true, wrap: true, spacing: "None", })), ] : []), // Comment input (only for pending approvers) ...(isApprover && isPending ? [{ type: "Input.Text", id: "comment", placeholder: "Optional comment...", isMultiline: false, }] : []), ], actions: isApprover && isPending ? [ { type: "Action.Execute", title: "Approve", verb: "approve", data: { recordId: record.id }, style: "positive", }, { type: "Action.Execute", title: "Reject", verb: "reject", data: { recordId: record.id }, style: "destructive", }, ] : [], }; } ``` ### Handle approval action with routing logic ```typescript app.on("card.action", async (ctx) => { const { verb, recordId } = ctx.activity.value?.action?.data ?? {}; const comment = ctx.activity.value?.action?.data?.comment; const actorId = ctx.activity.from?.aadObjectId!; const actorName = ctx.activity.from?.name ?? "Unknown"; if (verb !== "approve" && verb !== "reject" && verb !== "refreshApproval") return; const record = await getApprovalRecord(recordId); if (!record) return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.card.adaptive", value: {} } }; if (verb === "refreshApproval") { return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.card.adaptive", value: buildApprovalCard(record, actorId), }, }; } // Record the decision const approver = record.approvers.find((a) => a.userId === actorId && !a.decision); if (!approver) { return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.card.adaptive", value: buildApprovalCard(record, actorId) } }; } approver.decision = verb === "approve" ? "approved" : "rejected"; approver.decidedAt = new Date().toISOString(); record.history.push({ actor: actorName, action: verb === "approve" ? "Approved" : "Rejected", comment, timestamp: new Date().toISOString(), }); // Evaluate routing record.status = evaluateApprovalStatus(record); // If sequential and approved, notify next approver if (record.status === "Pending" && record.routingType === "sequential") { const nextApprover = record.approvers.find((a) => !a.decision); if (nextApprover) { // Proactive notify next approver await notifyApprover(nextApprover, record); } } // Persist await updateApprovalRecord(record); // Notify requester on resolution if (record.status === "Approved" || record.status === "Rejected") { record.resolvedAt = new Date().toISOString(); await notifyRequester(record); } return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.card.adaptive", value: buildApprovalCard(record, actorId), }, }; }); function evaluateApprovalStatus(record: ApprovalRecord): ApprovalRecord["status"] { const decisions = record.approvers.filter((a) => a.decision); switch (record.routingType) { case "single": return decisions[0]?.decision === "approved" ? "Approved" : "Rejected"; case "sequential": if (decisions.some((d) => d.decision === "rejected")) return "Rejected"; if (decisions.length === record.approvers.length) return "Approved"; return "Pending"; case "parallel-any": if (decisions.some((d) => d.decision === "approved")) return "Approved"; if (decisions.length === record.approvers.length) return "Rejected"; return "Pending"; case "parallel-all": if (decisions.some((d) => d.decision === "rejected")) return "Rejected"; if (decisions.length === record.approvers.length) return "Approved"; return "Pending"; default: return "Pending"; } } ``` ### Escalation timer ```typescript async function startEscalationTimer(record: ApprovalRecord, timeoutMs: number = 24 * 60 * 60 * 1000) { setTimeout(async () => { const current = await getApprovalRecord(record.id); if (current?.status !== "Pending") return; // Already resolved // Look up manager const pendingApprover = current.approvers.find((a) => !a.decision); if (!pendingApprover) return; const manager = await graphClient .api(`/users/${pendingApprover.userId}/manager`) .get(); current.status = "Escalated"; current.history.push({ actor: "System", action: `Escalated to ${manager.displayName} (timeout after ${timeoutMs / 3600000}h)`, timestamp: new Date().toISOString(), }); // Replace approver with manager pendingApprover.userId = manager.id; pendingApprover.displayName = manager.displayName; current.status = "Pending"; // Reset to pending for new approver await updateApprovalRecord(current); await updateRecordCardInThread(adapter, current); }, timeoutMs); } ``` ## pitfalls - **`refresh.userIds` max 60 users.** Parallel approvals with more than 60 approvers won't auto-refresh. For large groups, send individual proactive messages instead of relying on card refresh. - **Race condition on parallel approvals.** Two approvers clicking simultaneously can both read "Pending" and both write. Use optimistic concurrency (etag on the list item) or a queue to serialize decision processing. - **Comment input value location varies.** In `Action.Execute`, input values may be in `ctx.activity.value.action.data` (merged with action data) or `ctx.activity.value.data` depending on the Teams client version. Check both locations. - **Escalation timers don't survive restarts.** For production, persist escalation deadlines to the backing store and use a polling reconciliation loop or Azure Durable Functions. - **Sequential chain can stall.** If an approver in a sequential chain is unavailable, the entire workflow blocks. Implement auto-escalation timeouts for each step, not just the final deadline. - **Card replacement removes input state.** When the card refreshes after an action, any text the user typed in other input fields is lost. Keep input fields minimal on approval cards. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview - https://learn.microsoft.com/en-us/graph/api/user-list-manager - https://adaptivecards.io/explorer/Action.Execute.html ## instructions Use this expert when building in-channel approval workflows. Covers approval state machine, single/sequential/parallel routing, role-specific card views with refresh, approval history, optional comments, escalation timers, and requester notification. Pair with `workflow.message-native-records-ts.md` for the card-as-record pattern, `workflow.sharepoint-lists-ts.md` for persisting approval records, and `runtime.proactive-messaging-ts.md` for notifications. ## research Deep Research prompt: "Write a micro expert on in-channel approval workflows in Microsoft Teams using Adaptive Cards with Action.Execute (TypeScript). Cover: approval state machine, single/sequential/parallel routing patterns, role-specific card views with refresh.userIds, approval history tracking, optional comments, escalation timers with manager lookup via Graph, requester notification on resolution, and optimistic concurrency for parallel decisions. Include complete patterns for the approval record type, card builder, action handler with routing evaluation, and escalation timer." -
workflow.message-native-records-ts.md 10.6 KB
# workflow.message-native-records-ts ## purpose Implement the "structured records as message objects" pattern where workflow state renders inline as durable, updatable Adaptive Cards tied to backing store rows and anchored in threads. ## rules 1. **Every workflow record is an Adaptive Card backed by a store row.** The card is the visual representation; the list/dataverse row is the source of truth. Card actions read from and write to the store, then refresh the card to reflect current state. 2. **Use `Action.Execute` with `verb` for all record mutations.** `Action.Execute` triggers a server-side `adaptiveCard/action` invoke, allowing the bot to update the backing store and return a refreshed card in one round-trip. Never use `Action.Submit` for records — it doesn't support card refresh. [adaptivecards.io -- Action.Execute](https://adaptivecards.io/explorer/Action.Execute.html) 3. **Return the updated card from the invoke response.** The `adaptiveCard/action` invoke handler must return `{ status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.card.adaptive", value: <updated-card> } }` (Teams SDK v2 contract). Teams replaces the original card in-place — no new message needed. 4. **Store the record ID in `Action.Execute.data`.** Every action button must include the backing store record ID (e.g., `{ verb: "approve", recordId: "item-123" }`) so the handler can look up and mutate the correct row. 5. **Embed record metadata in the card body.** Display the record's key fields (status, requester, timestamps) directly in the card using `TextBlock` and `FactSet`. Users should see the full record state without clicking or navigating. 6. **Use `refresh` property for user-specific views.** Adaptive Cards support a `refresh` block that triggers an automatic `adaptiveCard/action` invoke when specific users view the card. Use this to show role-specific actions (approver sees approve/reject; requester sees cancel). [learn.microsoft.com -- Universal Actions](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview) 7. **Anchor records to threads, not top-level messages.** When a workflow creates a record, send the card as a reply to the originating message. This keeps the record contextually linked to the conversation that triggered it. Store the reply `activityId` on the backing row for future updates. 8. **Update existing cards via `activity.updateActivity()`.** When the backing store changes (webhook, timer, external update), look up the stored `activityId` and `conversationId`, then call `updateActivity()` to refresh the card in-place. This keeps the thread's record card always current. 9. **Design cards for three lifecycle states.** Every record card should have variants for: (a) **Active** — shows current data + action buttons, (b) **Completed** — shows final state, no action buttons, (c) **Error** — shows what went wrong + retry button. Map these to the backing store's status field. 10. **Keep cards self-contained.** A record card should display enough information that users never need to open the SharePoint list or external system. The card IS the workflow interface. 11. **Use `ColumnSet` for compact record layouts.** For list views (multiple records in one message), use `ColumnSet` with `Column` elements to create table-like layouts. Each row is a record summary with an inline action button. ## patterns ### Record card with Action.Execute ```typescript function buildRecordCard(record: WorkflowRecord): object { const isActive = record.status === "Pending"; return { type: "AdaptiveCard", version: "1.5", refresh: { action: { type: "Action.Execute", verb: "refreshRecord", data: { recordId: record.id }, }, userIds: [record.approverId], // Only approver gets auto-refresh }, body: [ { type: "TextBlock", text: record.title, weight: "Bolder", size: "Medium", }, { type: "FactSet", facts: [ { title: "Requester", value: record.requesterName }, { title: "Status", value: record.status }, { title: "Created", value: new Date(record.created).toLocaleDateString() }, ...(record.resolvedBy ? [{ title: "Resolved by", value: record.resolvedBy }] : []), ], }, { type: "TextBlock", text: record.description, wrap: true }, ], actions: isActive ? [ { type: "Action.Execute", title: "Approve", verb: "approve", data: { recordId: record.id }, style: "positive", }, { type: "Action.Execute", title: "Reject", verb: "reject", data: { recordId: record.id }, style: "destructive", }, ] : [], // No actions on completed/rejected records }; } ``` ### Handle Action.Execute invoke and refresh card ```typescript app.on("card.action", async (ctx) => { const { verb, recordId } = ctx.activity.value?.action?.data ?? {}; if (verb === "approve" || verb === "reject") { // Update backing store const newStatus = verb === "approve" ? "Approved" : "Rejected"; await graphClient .api(`/sites/${siteId}/lists/${listId}/items/${recordId}/fields`) .patch({ Status: newStatus, ApprovedBy: ctx.activity.from?.name, ResolvedDate: new Date().toISOString(), }); // Fetch updated record const updated = await graphClient .api(`/sites/${siteId}/lists/${listId}/items/${recordId}`) .expand("fields") .get(); // Return refreshed card (replaces original in-place) return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.card.adaptive", value: buildRecordCard(mapListItemToRecord(updated)), }, }; } if (verb === "refreshRecord") { const item = await graphClient .api(`/sites/${siteId}/lists/${listId}/items/${recordId}`) .expand("fields") .get(); return { status: 200, body: { statusCode: 200, type: "application/vnd.microsoft.card.adaptive", value: buildRecordCard(mapListItemToRecord(item)), }, }; } }); ``` ### Update a card in-place when backing store changes ```typescript async function updateRecordCardInThread( adapter: any, record: WorkflowRecord ) { const conversationRef = { channelId: "msteams", conversation: { id: record.conversationId }, serviceUrl: record.serviceUrl, }; await adapter.continueConversation(conversationRef, async (turnContext: any) => { const updatedCard = buildRecordCard(record); const activity = { id: record.cardActivityId, // stored when card was first sent type: "message", attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: updatedCard, }], }; await turnContext.updateActivity(activity); }); } ``` ### Multi-record list view ```typescript function buildRecordListCard(records: WorkflowRecord[]): object { return { type: "AdaptiveCard", version: "1.5", body: [ { type: "TextBlock", text: `Pending Requests (${records.length})`, weight: "Bolder", size: "Medium", }, ...records.map((r) => ({ type: "ColumnSet", columns: [ { type: "Column", width: "stretch", items: [ { type: "TextBlock", text: r.title, weight: "Bolder" }, { type: "TextBlock", text: `${r.requesterName} - ${r.status}`, isSubtle: true, spacing: "None" }, ], }, { type: "Column", width: "auto", items: [ { type: "ActionSet", actions: [{ type: "Action.Execute", title: "View", verb: "viewRecord", data: { recordId: r.id }, }], }, ], }, ], })), ], }; } ``` ## pitfalls - **`Action.Submit` does not refresh cards.** Only `Action.Execute` returns an updated card via invoke response. Using `Action.Submit` sends a regular message activity — the original card stays unchanged. - **`refresh.userIds` is limited to 60 users.** Cards with more than 60 users in the refresh list will not auto-refresh for anyone beyond the limit. For high-traffic channels, use explicit update calls instead of refresh. - **Card update requires the original `activityId`.** You must store the `activityId` returned when the card was first sent. Without it, you cannot update the card in-place. Store it on the backing row immediately after sending. - **Card size limit: 40 KB.** Adaptive Cards cannot exceed 40 KB. Multi-record list views must paginate. Show 5-10 records per card with "Show more" action. - **Thread replies require `replyToId`.** To anchor a record card in a thread, set `activity.replyToId` to the parent message's `activityId`. Without this, the card posts as a new top-level message. - **Universal Actions require bot registration.** `Action.Execute` only works when the card is sent by a registered bot. Cards sent via connectors or webhooks cannot use `Action.Execute`. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview - https://adaptivecards.io/explorer/Action.Execute.html - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/update-and-delete-bot-messages - https://learn.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model ## instructions Use this expert when implementing the card-as-record pattern: Adaptive Cards that represent durable workflow records, update in-place via Action.Execute, and stay anchored in threads. Pair with `workflow.sharepoint-lists-ts.md` for the backing store, `workflow.approvals-inline-ts.md` for approval-specific record flows, and `ui.adaptive-cards-ts.md` for general card construction patterns. ## research Deep Research prompt: "Write a micro expert on implementing structured records as message objects in Microsoft Teams using Adaptive Cards with Action.Execute (TypeScript). Cover: card-as-record pattern with backing store sync, Action.Execute invoke handling with card refresh, user-specific refresh views, in-place card updates via updateActivity, thread anchoring with replyToId, multi-record list views, and lifecycle state management (active/completed/error). Include canonical patterns for approval records and query result rendering." -
workflow.sharepoint-lists-ts.md 9 KB
# workflow.sharepoint-lists-ts ## purpose Integrate SharePoint Lists as the structured state store for message-native workflows in Teams bots, covering CRUD via Graph API, inline card rendering, and bidirectional sync between threads and list rows. ## rules 1. **Use Microsoft Graph REST API for all List operations.** Access SharePoint Lists via `https://graph.microsoft.com/v1.0/sites/{site-id}/lists/{list-id}/items`. Requires `Sites.ReadWrite.All` or `Sites.Manage.All` delegated/application permissions. [learn.microsoft.com -- Lists API](https://learn.microsoft.com/en-us/graph/api/resources/list) 2. **Prefer app-only tokens for bot-initiated List operations.** Bot workflows typically run without user context. Use client credentials flow (`ConfidentialClientApplication`) to get app-only tokens. This avoids per-user consent and works for background automation. [learn.microsoft.com -- App-only access](https://learn.microsoft.com/en-us/graph/auth-v2-service) 3. **Create lists programmatically with column definitions.** Define list schemas in code for reproducible deployment. Use `POST /sites/{site-id}/lists` with `columns` array specifying `text`, `number`, `dateTime`, `choice`, `personOrGroup`, and `boolean` column types. [learn.microsoft.com -- Create list](https://learn.microsoft.com/en-us/graph/api/list-create) 4. **Map workflow fields to list columns explicitly.** Each workflow record type (PTO request, equipment booking, account status) should have a corresponding list with typed columns. Use `personOrGroup` for requester/approver, `dateTime` for timestamps, `choice` for status enums (pending/approved/rejected). 5. **Store the thread activity ID on the list item.** Add a `ThreadActivityId` text column to every workflow list. When a workflow record is created from a message, store the originating `activity.id` and `conversation.id`. This enables bidirectional linking: card-to-record and record-to-thread. 6. **Use `$filter` and `$orderby` for querying.** Graph supports OData filtering on list items: `GET /sites/{site-id}/lists/{list-id}/items?$filter=fields/Status eq 'pending'&$orderby=fields/Created desc&$expand=fields`. Always `$expand=fields` to get column values. [learn.microsoft.com -- Query items](https://learn.microsoft.com/en-us/graph/api/listitem-list) 7. **Batch multiple list operations with JSON batching.** When a workflow step creates/updates multiple records, use Graph JSON batching (`POST /$batch`) to send up to 20 requests in one call. Reduces latency and avoids per-request throttling. [learn.microsoft.com -- Batching](https://learn.microsoft.com/en-us/graph/json-batching) 8. **Subscribe to list changes via Graph webhooks.** Use `POST /subscriptions` with `changeType: "updated,created"` on `/sites/{site-id}/lists/{list-id}/items` to receive notifications when records change outside the bot (e.g., direct list edits). Post updates back to the originating thread. [learn.microsoft.com -- Webhooks](https://learn.microsoft.com/en-us/graph/webhooks) 9. **Handle throttling with retry-after headers.** Graph API returns 429 with `Retry-After` header when throttled. Implement exponential backoff. SharePoint-specific limits: 600 requests per minute per app per tenant for app-only; tighter per-user limits for delegated. [learn.microsoft.com -- Throttling](https://learn.microsoft.com/en-us/graph/throttling) 10. **Use Lists for SMB/Frontline; Dataverse for enterprise.** Lists are included in M365 licensing with no extra cost. Dataverse requires Power Platform premium licensing. For the message-native workflow vision targeting SMB (2.6% adoption) and Frontline (0.5%), Lists are the right default. 11. **Render list records as Adaptive Cards, not raw text.** Every query result should return a structured Adaptive Card with field labels, values, and action buttons (edit, approve, archive). This makes records first-class message objects per the message-native vision. ## patterns ### Create a workflow list programmatically ```typescript import { Client } from "@microsoft/microsoft-graph-client"; async function createWorkflowList(graphClient: Client, siteId: string) { const list = await graphClient.api(`/sites/${siteId}/lists`).post({ displayName: "PTO Requests", list: { template: "genericList" }, columns: [ { name: "Requester", personOrGroup: {} }, { name: "StartDate", dateTime: { format: "dateOnly" } }, { name: "EndDate", dateTime: { format: "dateOnly" } }, { name: "Status", choice: { choices: ["Pending", "Approved", "Rejected"] } }, { name: "ApprovedBy", personOrGroup: {} }, { name: "ThreadActivityId", text: {} }, { name: "ConversationId", text: {} }, { name: "HoursRequested", number: {} }, ], }); return list.id; } ``` ### Create a list item from a message handler ```typescript app.message(/^\/pto\s+(\d{4}-\d{2}-\d{2})\s+to\s+(\d{4}-\d{2}-\d{2})$/i, async (ctx) => { const [, startDate, endDate] = ctx.activity.text!.match( /\/pto\s+(\d{4}-\d{2}-\d{2})\s+to\s+(\d{4}-\d{2}-\d{2})/i )!; const item = await graphClient .api(`/sites/${siteId}/lists/${listId}/items`) .post({ fields: { Title: `PTO - ${ctx.activity.from?.name}`, StartDate: startDate, EndDate: endDate, Status: "Pending", RequesterId: ctx.activity.from?.aadObjectId, ThreadActivityId: ctx.activity.id, ConversationId: ctx.activity.conversation?.id, }, }); await ctx.send({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: buildPtoCard(item.fields, item.id), }], }); }); ``` ### Query list items and render as cards ```typescript async function queryPendingRequests(graphClient: Client, siteId: string, listId: string) { const response = await graphClient .api(`/sites/${siteId}/lists/${listId}/items`) .filter("fields/Status eq 'Pending'") .orderby("fields/Created desc") .expand("fields") .top(10) .get(); return response.value.map((item: any) => ({ id: item.id, ...item.fields, })); } ``` ### Subscribe to list changes ```typescript async function subscribeToListChanges(graphClient: Client, siteId: string, listId: string, webhookUrl: string) { await graphClient.api("/subscriptions").post({ changeType: "created,updated", notificationUrl: webhookUrl, resource: `/sites/${siteId}/lists/${listId}/items`, expirationDateTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days max clientState: "workflow-list-subscription", }); } ``` ## pitfalls - **`$expand=fields` is mandatory.** Without it, list item responses contain only metadata (id, createdDateTime) — no column values. Every query must include `$expand=fields`. - **Column internal names differ from display names.** SharePoint generates internal names by removing spaces and special characters. `"Start Date"` becomes `StartDate` on creation, but existing lists may have `Start_x0020_Date`. Always verify internal names via `GET /sites/{site-id}/lists/{list-id}/columns`. - **PersonOrGroup columns store IDs, not names.** You must resolve display names separately via Graph user lookups. Store the AAD object ID and resolve at render time. - **List item limit: 30 million items per list.** Sufficient for most SMB workflows, but high-volume frontline operations (break logs across thousands of stores) may approach this. Archive completed records periodically. - **Graph webhook subscriptions expire.** Maximum lifetime is 30 days for list items. Implement a renewal timer that refreshes subscriptions before expiry. - **Delegated vs app-only permissions differ.** App-only tokens cannot use `$filter` on certain column types (personOrGroup) in some tenants. Test thoroughly with your permission model. ## references - https://learn.microsoft.com/en-us/graph/api/resources/list - https://learn.microsoft.com/en-us/graph/api/listitem-list - https://learn.microsoft.com/en-us/graph/api/list-create - https://learn.microsoft.com/en-us/graph/json-batching - https://learn.microsoft.com/en-us/graph/webhooks - https://learn.microsoft.com/en-us/graph/throttling - https://learn.microsoft.com/en-us/graph/auth-v2-service ## instructions Use this expert when building Teams bot workflows that persist structured state to SharePoint Lists. Covers list creation, CRUD operations via Graph, querying with OData filters, webhook subscriptions for change notifications, and rendering records as Adaptive Cards. Pair with `workflow.message-native-records-ts.md` for card-as-record patterns, `workflow.approvals-inline-ts.md` for approval state persistence, and `ai.conversational-query-ts.md` for NL querying over list data. ## research Deep Research prompt: "Write a micro expert on SharePoint Lists integration from Microsoft Teams bots using Graph API (TypeScript). Cover: list creation with typed columns, CRUD operations on list items, OData filtering and sorting, Graph JSON batching, webhook subscriptions for change notifications, app-only vs delegated permissions, throttling and retry patterns, and rendering list records as Adaptive Cards. Include canonical patterns for PTO request and equipment tracking workflows." -
workflow.state-driven-events-ts.md 12.2 KB
# workflow.state-driven-events-ts ## purpose Wire up state-driven workflow triggers from Teams-native operational signals — presence changes, Shifts events, and call queue changes — via Microsoft Graph subscriptions and change notifications. ## rules 1. **Use Graph change notifications (webhooks) for all state-driven triggers.** Subscribe to resource changes via `POST /subscriptions`. The bot receives HTTP POST callbacks when subscribed resources change. This is the foundation for presence, Shifts, and call queue triggers. [learn.microsoft.com -- Change notifications](https://learn.microsoft.com/en-us/graph/webhooks) 2. **Presence changes: subscribe to `/communications/presences/{userId}`.** Requires `Presence.Read.All` application permission. Notifications fire when a user's availability changes (Available, Away, Busy, DoNotDisturb, Offline). Use for break management and availability-based routing. [learn.microsoft.com -- Presence subscriptions](https://learn.microsoft.com/en-us/graph/api/subscription-post-subscriptions) 3. **Shifts events: subscribe to `/teams/{teamId}/schedule/shifts`.** Requires `Schedule.Read.All` or `Schedule.ReadWrite.All` application permission. Notifications fire when shifts are created, updated, or deleted. Use for schedule-based automation (shift start/end, coverage gaps). [learn.microsoft.com -- Shifts API](https://learn.microsoft.com/en-us/graph/api/resources/shift) 4. **Time-off requests: use `/teams/{teamId}/schedule/timeOffRequests`.** Subscribe to changes on time-off requests for automated approval workflows. Notifications include the request state (pending, approved, declined). [learn.microsoft.com -- TimeOff](https://learn.microsoft.com/en-us/graph/api/resources/timeoffrequest) 5. **Call queue membership: monitor via `/communications/callRecords`.** Direct call queue subscriptions are limited. Use call record notifications (`/communications/callRecords`) to detect when agents join/leave queues. Alternatively, poll `/communications/callQueues` at intervals. Requires `CallRecords.Read.All`. [learn.microsoft.com -- Call records](https://learn.microsoft.com/en-us/graph/api/resources/callrecords-api-overview) 6. **Validate webhook endpoints with the validation token handshake.** Graph sends a validation request with a `validationToken` query parameter on subscription creation. The endpoint must return the token as `text/plain` within 10 seconds. Without this, subscription creation fails. [learn.microsoft.com -- Webhook validation](https://learn.microsoft.com/en-us/graph/webhooks#notification-endpoint-validation) 7. **Decrypt rich notifications for presence data.** Presence subscriptions require `includeResourceData: true` and encryption. Provide `encryptionCertificate` (public key) and `encryptionCertificateId` in the subscription. Decrypt notification payloads with the corresponding private key. [learn.microsoft.com -- Rich notifications](https://learn.microsoft.com/en-us/graph/webhooks-with-resource-data) 8. **Renew subscriptions before expiry.** Maximum subscription lifetimes: presence = 60 minutes, Shifts = 4230 minutes (~3 days). Implement a renewal timer that calls `PATCH /subscriptions/{id}` with a new `expirationDateTime` before the current one expires. 9. **Bridge webhook notifications to proactive bot messages.** When a state change notification arrives, look up the relevant channel's conversation reference and send a proactive message. The notification handler is an HTTP endpoint; the proactive message uses the bot adapter. 10. **Implement escalation timers for time-bound workflows.** For break management: start a timer on presence change to "Away". If presence doesn't return to "Available" within the threshold (e.g., 15 min), send a reminder. At 20 min, escalate to manager. Use `setTimeout` or a durable task queue. 11. **Use the `clientState` field for subscription routing.** Set `clientState` to a unique identifier (e.g., `"presence-break-workflow"`) on each subscription. Verify it in incoming notifications to route to the correct workflow handler and reject spoofed callbacks. 12. **Handle notification batching.** Graph may batch multiple notifications into a single POST. The payload contains a `value` array of `changeNotification` objects. Process all entries, not just the first. ## patterns ### Subscribe to presence changes ```typescript import { Client } from "@microsoft/microsoft-graph-client"; import { readFileSync } from "fs"; async function subscribeToPresence( graphClient: Client, userId: string, webhookUrl: string ) { const cert = readFileSync("./certs/public.pem", "utf-8") .replace(/-----BEGIN CERTIFICATE-----/, "") .replace(/-----END CERTIFICATE-----/, "") .replace(/\n/g, ""); const subscription = await graphClient.api("/subscriptions").post({ changeType: "updated", notificationUrl: webhookUrl, resource: `/communications/presences/${userId}`, expirationDateTime: new Date(Date.now() + 55 * 60 * 1000).toISOString(), // 55 min (max 60) clientState: "presence-break-workflow", includeResourceData: true, encryptionCertificate: cert, encryptionCertificateId: "break-workflow-cert-1", }); return subscription.id; } ``` ### Webhook endpoint with validation and notification handling ```typescript import express from "express"; import crypto from "crypto"; import { readFileSync } from "fs"; const router = express.Router(); router.post("/api/webhooks/graph", (req, res) => { // Validation handshake if (req.query.validationToken) { res.set("Content-Type", "text/plain"); res.send(req.query.validationToken); return; } const notifications = req.body.value ?? []; // Acknowledge receipt quickly; Graph expects a 2xx within ~3 seconds res.sendStatus(202); // Process notifications asynchronously to avoid request timeouts setImmediate(() => { for (const notification of notifications) { if (notification.clientState !== "presence-break-workflow") { continue; // Ignore unknown subscriptions } // Decrypt resource data const decryptedData = decryptNotification(notification); handlePresenceChange(notification.resource, decryptedData); } }); }); function decryptNotification(notification: any): any { const symmetricKey = crypto.privateDecrypt( { key: readFileSync("./certs/private.pem"), padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(notification.encryptedContent.dataKey, "base64") ); const decipher = crypto.createDecipheriv( "aes-256-cbc", symmetricKey.subarray(0, 32), Buffer.alloc(16, 0) // IV ); const decrypted = Buffer.concat([ decipher.update(Buffer.from(notification.encryptedContent.data, "base64")), decipher.final(), ]); return JSON.parse(decrypted.toString("utf-8")); } ``` ### Break management workflow (presence-driven) ```typescript const breakTimers = new Map<string, NodeJS.Timeout>(); const REMINDER_MS = 15 * 60 * 1000; const ESCALATION_MS = 20 * 60 * 1000; async function handlePresenceChange(resource: string, data: any) { const userId = resource.split("/").pop()!; const availability = data.availability; // Available, Away, Busy, etc. if (availability === "Away") { // Start break tracking const reminderTimer = setTimeout(async () => { await sendProactiveMessage(userId, "channel", { text: `Reminder: ${data.displayName} has been on break for 15 minutes.`, }); }, REMINDER_MS); const escalationTimer = setTimeout(async () => { await sendProactiveMessage(userId, "manager", { text: `Escalation: ${data.displayName} has exceeded 20-minute break limit.`, }); }, ESCALATION_MS); breakTimers.set(`${userId}-reminder`, reminderTimer); breakTimers.set(`${userId}-escalation`, escalationTimer); // Remove from call queue await removeFromCallQueue(userId); // Log break start await createBreakRecord(userId, "started"); } if (availability === "Available") { // Clear timers clearTimeout(breakTimers.get(`${userId}-reminder`)); clearTimeout(breakTimers.get(`${userId}-escalation`)); breakTimers.delete(`${userId}-reminder`); breakTimers.delete(`${userId}-escalation`); // Re-add to call queue await addToCallQueue(userId); // Log break end await updateBreakRecord(userId, "ended"); } } ``` ### Subscribe to Shifts changes ```typescript async function subscribeToShifts( graphClient: Client, teamId: string, webhookUrl: string ) { const subscription = await graphClient.api("/subscriptions").post({ changeType: "created,updated,deleted", notificationUrl: webhookUrl, resource: `/teams/${teamId}/schedule/shifts`, expirationDateTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(), // ~3 days clientState: "shifts-workflow", }); return subscription.id; } ``` ### Subscription renewal timer ```typescript async function renewSubscription(graphClient: Client, subscriptionId: string, lifetimeMs: number) { const renewBeforeMs = 5 * 60 * 1000; // 5 minutes before expiry setInterval(async () => { try { await graphClient.api(`/subscriptions/${subscriptionId}`).patch({ expirationDateTime: new Date(Date.now() + lifetimeMs).toISOString(), }); } catch (err: any) { if (err.statusCode === 404) { // Subscription lost — recreate console.error("Subscription expired, recreating..."); } } }, lifetimeMs - renewBeforeMs); } ``` ## pitfalls - **Presence subscriptions expire in 60 minutes max.** You must renew aggressively. A 55-minute renewal interval is recommended to account for clock drift and network latency. - **Rich notifications require encryption setup upfront.** You cannot subscribe to presence with `includeResourceData: true` without providing encryption keys. Generate a self-signed certificate for development; use a proper cert in production. - **Webhook must respond within 3 seconds.** Graph expects a 2xx response within 3 seconds. Do all processing asynchronously — immediately return 202, then process the notification. Blocking responses cause subscription deactivation after repeated timeouts. - **Shifts API requires team-level scheduling enabled.** If Shifts is not enabled for the team, API calls return 404. Verify Shifts is provisioned before subscribing. - **Call queue operations are limited.** There's no direct Graph subscription for call queue membership changes. The workaround is monitoring presence (agents go Busy when on calls) or call records. Direct queue add/remove requires Teams admin APIs or PowerShell. - **In-memory timers don't survive restarts.** The break management timer pattern using `setTimeout` loses state on process restart. For production, use Azure Durable Functions timers, a Redis-backed job queue, or persist timer state to the backing store with a polling reconciliation loop. ## references - https://learn.microsoft.com/en-us/graph/webhooks - https://learn.microsoft.com/en-us/graph/webhooks-with-resource-data - https://learn.microsoft.com/en-us/graph/api/subscription-post-subscriptions - https://learn.microsoft.com/en-us/graph/api/resources/shift - https://learn.microsoft.com/en-us/graph/api/resources/timeoffrequest - https://learn.microsoft.com/en-us/graph/api/resources/callrecords-api-overview - https://learn.microsoft.com/en-us/graph/api/resources/presence ## instructions Use this expert when building workflows triggered by operational state changes: presence, Shifts, time-off, or call queues. Covers Graph change notification subscriptions, webhook validation, rich notification decryption, escalation timers, and call queue integration. Pair with `workflow.triggers-compose-ts.md` for the full trigger surface, `workflow.sharepoint-lists-ts.md` for persisting event records, and `runtime.proactive-messaging-ts.md` for sending state-change notifications to channels. ## research Deep Research prompt: "Write a micro expert on state-driven workflow triggers in Microsoft Teams using Graph change notifications (TypeScript). Cover: presence change subscriptions with rich notification decryption, Shifts API subscriptions, time-off request monitoring, call queue integration patterns, webhook endpoint validation, subscription renewal, escalation timers, and bridging notifications to proactive bot messages. Include a complete break management workflow example driven by presence changes." -
workflow.triggers-compose-ts.md 10.3 KB
# workflow.triggers-compose-ts ## purpose Unify workflow initiation at the Teams compose surface — message-driven, scheduled, and state-driven triggers all accessible from the compose box, message extensions, and bot commands. ## rules 1. **The compose box is the primary trigger surface.** Users should initiate workflows by typing commands, using message extension search, or submitting compose actions. Never require users to leave the channel to start a workflow. 2. **Use bot commands for direct workflow initiation.** Register keyword patterns (e.g., `/pto`, `/book`, `/standup`) via `app.message()` regex handlers. Commands capture inline parameters and immediately launch the workflow. This is the simplest trigger type. 3. **Use message extension action commands for form-based initiation.** Action commands open a task module (dialog) from the compose area or message context menu. The form collects structured input, and the submit handler creates the workflow record. Declare in manifest under `composeExtensions[].commands` with `type: "action"`. [learn.microsoft.com -- Action commands](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command) 4. **Use message extension search commands for record lookup.** Search commands let users type queries in the compose box extension and see matching records. Results insert as cards into the conversation. Use for "show customer ABC" or "lookup ticket 4821" patterns. [learn.microsoft.com -- Search commands](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command) 5. **Use `Action.Execute` on existing cards to trigger follow-on workflows.** A record card's action buttons can initiate new workflow steps (escalate, reassign, clone). This chains workflows together from the message surface without additional commands. 6. **Use proactive messaging for scheduled triggers.** Timer-based workflows (daily standup, weekly status) use `setInterval` or a job scheduler (node-cron, Azure Functions timer trigger) to send proactive messages at cadence. Store conversation references at bot install time. [learn.microsoft.com -- Proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) 7. **Use Graph change notifications for state-driven triggers.** Subscribe to presence changes, Shifts events, or list updates via Graph webhooks. When a notification fires, the bot sends a proactive message to the relevant channel. See `workflow.state-driven-events-ts.md` for details. 8. **Manifest declares all trigger surfaces.** Bot commands go in `bots[].commandLists`. Message extension commands go in `composeExtensions[].commands`. Ensure the manifest declares every trigger the workflow uses. [learn.microsoft.com -- Manifest](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) 9. **Provide command suggestions in the compose box.** Teams shows command suggestions when users type in the compose box if `bots[].commandLists` is populated. List the most common workflow triggers with descriptions so users can discover them without documentation. 10. **Combine trigger types for the same workflow.** A single workflow (e.g., standup) can be initiated by scheduled message (automatic), bot command (manual), or message extension action (ad-hoc). All paths should create the same record type and render the same card. ## patterns ### Bot command trigger with inline parameters ```typescript // "/pto 2024-03-15 to 2024-03-20" triggers PTO workflow app.message(/^\/pto\s+(.+)/i, async (ctx) => { const params = ctx.activity.text!.match( /\/pto\s+(\d{4}-\d{2}-\d{2})\s+to\s+(\d{4}-\d{2}-\d{2})/i ); if (!params) { await ctx.send("Usage: /pto YYYY-MM-DD to YYYY-MM-DD"); return; } const record = await createPtoRecord({ requester: ctx.activity.from!, startDate: params[1], endDate: params[2], conversationId: ctx.activity.conversation!.id, }); await ctx.send({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: buildPtoCard(record), }], }); }); ``` ### Message extension action command (form-based trigger) ```typescript // Manifest: composeExtensions[].commands[] = { commandId: "createPto", type: "action", ... } app.on("message.ext.submit", async (ctx) => { const { commandId } = ctx.activity.value ?? {}; if (commandId === "createPto") { const { startDate, endDate, reason } = ctx.activity.value?.data ?? {}; const record = await createPtoRecord({ requester: ctx.activity.from!, startDate, endDate, reason, conversationId: ctx.activity.conversation!.id, }); // Return card to insert into compose return { composeExtension: { type: "result", attachmentLayout: "list", attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: buildPtoCard(record), preview: { contentType: "application/vnd.microsoft.card.thumbnail", content: { title: `PTO: ${startDate} - ${endDate}` }, }, }], }, }; } }); ``` ### Message extension search command (record lookup) ```typescript app.on("message.ext.query", async (ctx) => { const { commandId } = ctx.activity.value ?? {}; const query = ctx.activity.value?.queryOptions?.searchText ?? ""; if (commandId === "lookupRecord") { const records = await searchWorkflowRecords(query); return { composeExtension: { type: "result", attachmentLayout: "list", attachments: records.map((r) => ({ contentType: "application/vnd.microsoft.card.adaptive", content: buildRecordCard(r), preview: { contentType: "application/vnd.microsoft.card.thumbnail", content: { title: r.title, text: `${r.status} - ${r.requesterName}`, }, }, })), }, }; } }); ``` ### Scheduled proactive trigger ```typescript import cron from "node-cron"; // Store conversation references at bot install const conversationRefs = new Map<string, any>(); app.on("install.add", async (ctx) => { conversationRefs.set( ctx.activity.conversation!.id, { channelId: ctx.activity.channelId, conversation: ctx.activity.conversation, serviceUrl: ctx.activity.serviceUrl, } ); }); // Daily standup at 9 AM cron.schedule("0 9 * * 1-5", async () => { for (const [, ref] of conversationRefs) { await adapter.continueConversation(ref, async (turnContext) => { await turnContext.sendActivity({ attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: buildStandupPromptCard(), }], }); }); } }); ``` ### Manifest command list for discoverability ```json { "bots": [{ "commandLists": [{ "scopes": ["team"], "commands": [ { "title": "/pto", "description": "Request time off: /pto YYYY-MM-DD to YYYY-MM-DD" }, { "title": "/book", "description": "Reserve equipment: /book [item name]" }, { "title": "/standup", "description": "Start a standup check-in" }, { "title": "/status", "description": "Show pending workflow items" } ] }] }], "composeExtensions": [{ "commands": [ { "id": "createPto", "type": "action", "title": "New PTO Request", "description": "Submit a time-off request", "fetchTask": true, "context": ["compose"] }, { "id": "lookupRecord", "type": "query", "title": "Find Record", "description": "Search workflow records", "initialRun": false, "parameters": [{ "name": "search", "title": "Search", "description": "Search by name, ID, or status" }] } ] }] } ``` ## pitfalls - **Bot command lists max 10 commands.** The manifest allows up to 10 commands per scope per bot. Prioritize the most common workflow triggers. Use message extension search for long-tail lookups. - **Message extension action `fetchTask: true` is required for forms.** Without `fetchTask: true`, Teams won't open a task module. The bot must handle the `composeExtension/fetchTask` invoke and return the form definition. - **Scheduled triggers require persistent conversation references.** If the bot restarts, in-memory references are lost. Persist them to the backing store (SharePoint List, Cosmos DB) at install time. - **Command suggestions only appear after `@mention`.** In channels, bot command suggestions show after the user `@mentions` the bot. In personal/group chats, they appear on `/` or when clicking the bot icon. Educate users on discovery. - **Message extension search has a 10-result limit in the flyout.** The compose extension search UI shows at most 10 results. Implement server-side filtering to return the most relevant matches. ## references - https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command - https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command - https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages - https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema ## instructions Use this expert when unifying workflow triggers at the compose surface. Covers bot commands, message extension actions (form-based initiation), message extension search (record lookup), scheduled proactive triggers, and manifest configuration for discoverability. Pair with `workflow.state-driven-events-ts.md` for presence/Shifts/call queue triggers, `workflow.message-native-records-ts.md` for the cards those triggers produce, and `runtime.manifest-ts.md` for manifest details. ## research Deep Research prompt: "Write a micro expert on unifying workflow triggers at the Microsoft Teams compose surface (TypeScript). Cover: bot command patterns with regex handlers, message extension action commands for form-based workflow initiation, message extension search commands for record lookup, scheduled proactive messaging with node-cron, manifest command list configuration, and combining multiple trigger types for the same workflow. Include patterns for PTO, equipment booking, and daily standup triggers."
-
-
analyzer.md 14 KB
# analyzer ## purpose Scan a project codebase, identify its technology stack, and recommend micro-experts to create based on coverage gaps against the existing `.experts/` inventory. ## rules 1. **Scan manifests first.** Start with package manifests and lock files — they reveal the full dependency tree in seconds. Priority order: `package.json` / `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml`, `Cargo.toml` / `Cargo.lock`, `go.mod` / `go.sum`, `pyproject.toml` / `requirements.txt` / `Pipfile`, `pom.xml` / `build.gradle`, `*.csproj` / `*.sln`, `Gemfile`, `Package.swift`, `build.gradle.kts`. 2. **Examine directory structure for framework signals.** Look for conventional directories: `src/app/` or `app/` (Next.js/Remix), `src/routes/` (SvelteKit), `pages/` (Next.js Pages Router), `components/`, `middleware/`, `migrations/`, `prisma/`, `terraform/`, `.github/workflows/`, `.circleci/`, `docker/`, `k8s/`, `helm/`. 3. **Read config files for tooling signals.** Check for: `tsconfig.json`, `.eslintrc.*`, `.prettierrc`, `jest.config.*`, `vitest.config.*`, `playwright.config.*`, `cypress.config.*`, `.dockerignore`, `Dockerfile`, `docker-compose.yml`, `nginx.conf`, `webpack.config.*`, `vite.config.*`, `tailwind.config.*`, `.env.example`. 4. **Catalog the full tech stack.** Produce a structured inventory: language(s), framework(s), build tool(s), test framework(s), CI/CD platform(s), infrastructure/deployment tool(s), notable libraries (ORM, HTTP client, state management, etc.). 5. **Cross-reference against existing `.experts/` inventory.** Read every domain `index.md` and list all expert files. Map each technology in the stack to the expert(s) that cover it. Mark technologies with no expert coverage as gaps. 6. **Score gaps by usage frequency and impact.** A framework used across every file (React, Express) scores higher than a dev-only tool used in one config file (Husky). Prioritize gaps that affect daily development decisions. 7. **Route library/framework experts to `languages/{lang}/libraries/`, not `.project/`.** When a gap is a language-specific framework or library (Next.js, Django, Spring Boot, Axum, etc.), place the expert under the relevant language's `libraries/` subfolder — e.g., `languages/typescript/libraries/nextjs.md`. This keeps framework knowledge co-located with the language it's written in and lets the language router load it alongside idioms and patterns. Reserve `.project/` for truly cross-cutting project-specific concerns that don't belong to a single language (CI/CD pipelines, infrastructure, project-specific workflows, multi-language prompt template conventions). 8. **Distinguish recommendation types.** Group into three categories: (a) **Populate stubs** — existing expert files that are placeholders; (b) **New project experts** — topics specific to this codebase's stack (frameworks, ORMs, CI/CD, etc.); (c) **General expert gaps** — topics that would benefit the general system (flag these but don't auto-create; they require broader applicability review). 9. **Output structured recommendations.** Each recommendation must include: filename, target domain (`languages/{lang}/libraries/` for language-specific frameworks, `.project/` for cross-cutting concerns), evidence (which files/deps triggered it), priority (high/medium/low), and a one-line expert purpose. 10. **Prioritize populating existing stubs over creating new experts.** Stubs represent already-identified knowledge gaps that the system is designed to hold. Filling them first maximizes coverage per effort. 11. **Update the target domain's `index.md` as experts are created.** After each expert is built, add it to the appropriate router's task clusters and file inventory. For library experts, update `languages/{lang}/libraries/index.md`. For cross-cutting experts, update `.project/index.md`. Keep routers current so the system can find new experts. 12. **Pair output with builder.md for handoff.** The analyzer identifies *what* to build; builder.md handles *how* to build it. Format recommendations so they can be directly fed into builder.md's Phase 1 scoping with the target domain pre-filled (`languages/{lang}/libraries/` or `.project/`). 13. **Scan for prompt template patterns.** Projects that use LLMs almost always have a prompt templating layer — and the implementation varies wildly. Scan for: LLM SDK imports (OpenAI, Anthropic, Azure OpenAI, LangChain, LlamaIndex, Semantic Kernel, Vercel AI SDK, etc.), prompt file conventions (a `prompts/` or `templates/` directory, `.prompt`, `.hbs`, `.jinja2`, `.mustache` files containing LLM instructions), string construction patterns (template literals, f-strings, or concatenation building system/user messages), and prompt management utilities (helper functions that assemble, format, or inject variables into prompts). When any of these signals are found, recommend a prompt template expert that documents: where templates live, which templating mechanism is used, how variables are injected, how system/user/assistant messages are constructed, and which LLM SDK the project calls. If the project uses a single language for LLM calls, place the expert under `languages/{lang}/libraries/prompt-templates.md`. If prompt construction spans multiple languages, place it under `.project/prompt-templates.md`. This expert pairs with `tools/prompt-engineer.md` — the general expert provides the design principles, the project expert provides the local conventions. 14. **Score prompt template gaps as high priority when LLM usage is core.** If the project's primary purpose involves LLM calls (an AI agent, a chatbot, a RAG pipeline, a prompt-driven workflow), the prompt template expert is high priority — it affects nearly every feature. If LLM calls are peripheral (e.g., a single summarization endpoint in a larger app), score it as medium. ## patterns ### Manifest scanning sequence ``` 1. List root directory → identify project type 2. Read primary manifest: - Node.js → package.json (dependencies, devDependencies, scripts) - Rust → Cargo.toml (dependencies, features) - Go → go.mod (require, module path) - Python → pyproject.toml or requirements.txt - Java → pom.xml or build.gradle - C# → *.csproj (PackageReference) - Ruby → Gemfile 3. Read secondary signals: - CI/CD → .github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile - Infra → Dockerfile, docker-compose.yml, terraform/, k8s/ - Config → tsconfig.json, .eslintrc.*, vite.config.*, etc. 4. Scan src/ structure for framework conventions 5. Check for monorepo signals: workspaces, lerna.json, nx.json, turbo.json ``` ### Prompt template scanning sequence ``` 1. Check for LLM SDK dependencies in manifests: - Node.js → openai, @anthropic-ai/sdk, @azure/openai, langchain, llamaindex, @ai-sdk/*, semantic-kernel - Python → openai, anthropic, langchain, llama-index, semantic-kernel, guidance, promptflow - C# → Azure.AI.OpenAI, Anthropic, Microsoft.SemanticKernel, Microsoft.Extensions.AI - Go → github.com/sashabaranov/go-openai, github.com/anthropics/... - Rust → async-openai, anthropic-rs 2. Scan for prompt file conventions: - Directories: prompts/, templates/, agents/, instructions/ - File types: *.prompt, *.txt, *.md, *.hbs, *.jinja2, *.mustache, *.liquid containing LLM instructions - Naming: *system*, *prompt*, *agent*, *instruction* in filenames 3. Scan source code for prompt construction patterns: - Template literals / f-strings building message content - System/user/assistant role message arrays - Section tag patterns: <SECTION_NAME> style markers - Variable interpolation: {{var}}, {var}, ${var}, {{ var }} - Prompt builder/formatter utility functions or classes 4. Identify the prompt architecture: - Storage: files on disk, inline in code, database, CMS - Templating: native string interpolation, Handlebars, Jinja2, Mustache, Liquid, custom - Structure: section tags, markdown headers, XML tags, plain text - Multi-turn: message array construction, conversation history mgmt - Variables: how context is injected (retrieval, user input, state) 5. Catalog findings for the project prompt template expert: - SDK + client setup pattern - Where templates live (path conventions) - Templating mechanism + variable syntax - Message construction pattern (system/user/assistant) - Section/structure conventions used in prompts ``` ### Coverage gap output template ```markdown ## Expert Coverage Analysis ### Tech Stack | Category | Technology | Version | |-------------|-----------------|----------| | Language | TypeScript | 5.x | | Framework | Next.js | 14.x | | ORM | Prisma | 5.x | | Testing | Vitest | 1.x | | CI/CD | GitHub Actions | — | ### Coverage Map | Technology | Expert Coverage | Status | |-----------------|------------------------------|--------| | TypeScript | languages/typescript/*.md | ✅ Full | | Git workflows | tools/git.md | ✅ Full | | Prompt design | tools/prompt-engineer.md | ✅ General | | Next.js | — | ❌ Gap | | Prisma | — | ❌ Gap | | Prompt templates | — | ❌ Gap (project-specific) | | Vitest | — | ❌ Gap | | GitHub Actions | — | ❌ Gap | ### Recommendations | # | File | Domain | Priority | Purpose | |---|----------------------|---------------------------------|----------|--------------------------------------------| | 1 | nextjs.md | languages/typescript/libraries/ | High | Next.js App Router patterns and conventions | | 2 | prisma.md | languages/typescript/libraries/ | High | Prisma schema design, queries, migrations | | 3 | prompt-templates.md | .project/ | High | Project prompt template conventions, SDK patterns, variable injection (pairs with tools/prompt-engineer.md) | | 4 | vitest.md | languages/typescript/libraries/ | Medium | Vitest configuration and testing patterns | | 5 | github-actions.md | .project/ | Medium | GitHub Actions workflow patterns (cross-cutting, not language-specific) | ``` ### Builder.md handoff After generating recommendations, offer to start building: ``` To create any of these experts, I'll hand off to builder.md with the scoping already pre-filled: "Create expert: {filename} in {target domain} — {purpose}. Evidence: {manifest signals}. Priority: {level}." Which experts should I create? (Select numbers, or "all high priority") ``` ## pitfalls - **Don't recommend experts for one-off dependencies.** A single `lodash` import or a `chalk` dependency doesn't warrant an expert. Focus on technologies that shape architectural decisions and daily workflows. - **`devDependencies` don't always mean active use.** Many projects accumulate unused dev dependencies. Cross-reference with config files and import statements before recommending experts based on devDependencies alone. - **Stubs are not coverage.** An expert file that exists but contains only a research prompt (stub) provides zero guidance. Count stubs as gaps when assessing coverage. - **Prioritize ruthlessly in monorepos.** A monorepo with 50 packages and 20 technologies needs 4-6 high-impact experts, not 20. Focus on shared technologies that affect the most packages. - **Don't confuse project-specific config with general expertise.** A project's custom webpack config doesn't need an expert — webpack itself might. Experts cover reusable knowledge, not project-specific setup. - **Don't skip prompt template scanning because "it's just strings."** Prompt construction is often spread across utility functions, config files, and inline code with no obvious directory convention. Projects that use LLMs always have prompt patterns — they're just not always in a `prompts/` folder. Search SDK imports and message construction calls, not just file names. - **Don't duplicate `tools/prompt-engineer.md` in the project expert.** The project prompt template expert captures *how this project* builds prompts (file locations, template syntax, SDK setup, variable injection). The general `prompt-engineer.md` expert covers *how to design prompts well*. The project expert should reference and pair with the general expert, not restate its principles. ## instructions Use this expert when the developer wants to assess their project's technology stack and identify which micro-experts would be most valuable to create. **Trigger phrases:** "explore the codebase," "recommend experts," "analyze project," "audit experts," "expert coverage," "gap analysis," "what experts should I create," "scan my project." Pair with: `builder.md` for creating the recommended experts. The analyzer produces the roadmap; the builder executes it. Pair with: `tools/prompt-engineer.md` — when a project prompt template expert is created in `.project/`, it should declare `Pair with: tools/prompt-engineer.md` so the general prompt design principles are loaded alongside the project-specific conventions. ## research Deep Research prompt: "Write a meta-expert for scanning software project codebases and recommending micro-experts to create. Cover: manifest file scanning strategies (package.json, Cargo.toml, go.mod, pyproject.toml, pom.xml, *.csproj, Gemfile), directory structure analysis for framework detection, config file signals for tooling identification, tech stack cataloging methodology, coverage gap analysis against an existing expert inventory, recommendation prioritization (usage frequency, architectural impact, daily development relevance), structured output formats for recommendations, handoff protocol to an expert-building workflow, and common analysis pitfalls (one-off deps, unused devDependencies, monorepo sprawl, stubs vs coverage)." -
builder.md 11.1 KB
# builder ## purpose Guided workflow for creating new micro-experts — from scoping and research through drafting, validation, and wiring into the routing system. ## rules 1. **One expert, one topic.** Each expert file covers a single, well-bounded topic. If the scope needs an "and" to describe it, split into two experts. 2. **Minimum depth threshold.** Only create a standalone expert if the topic warrants 8+ rules and 2+ code patterns. Below that threshold, add the knowledge to an existing expert instead. 3. **Reusability over specificity.** The expert must apply to future tasks, not just the current one-off request. If the knowledge is project-specific, it belongs in a CLAUDE.md or README, not an expert. 4. **Research before writing.** Never draft rules or patterns from memory alone. Every rule must trace to official docs, SDK source, or verified behavior. If you cannot confirm a claim, mark it `[unverified]`. 5. **Language-agnostic filenames when appropriate.** Use `{topic}-ts.md` when the expert is TypeScript-specific. Use `{topic}.md` (no language suffix) when the expert applies regardless of language (e.g., architecture patterns, workflow guides, platform concepts). 6. **Canonical section order.** Every expert MUST follow the section layout in the expert structure reference below. Omit optional sections entirely rather than leaving them empty. 7. **Rules are imperatives, not observations.** Write "Always call `ack()` before async work" not "ack is important." Each rule must tell the reader exactly what to do or avoid. 8. **Patterns are minimal and self-contained.** Each code snippet demonstrates one concept with all necessary imports. No "see above" references between patterns. 9. **Pitfalls earn their place.** Only include pitfalls that are non-obvious, have bitten real users, or contradict reasonable assumptions. "Don't forget to save the file" is not a pitfall. 10. **No fabricated API signatures.** If a web search yields no confirmation for an API shape, omit it or mark it `[unverified]`. Wrong patterns are worse than missing patterns. 11. **Wire it or it doesn't exist.** An expert that isn't reachable through the routing system (domain `index.md` + root `index.md` signals) will never be loaded. Integration is not optional. 12. **Keep files under 300 lines.** If an expert grows beyond 300 lines, split it into focused sub-experts under the same domain. ## interview ### Q1 — Topic & Language ``` question: "What topic should this expert cover, and is it language-specific?" header: "Topic" options: - label: "TypeScript-specific" description: "Expert targets TypeScript patterns and APIs. File will be named {topic}-ts.md." - label: "Language-agnostic" description: "Expert covers concepts that apply across languages. File will be named {topic}.md." - label: "You Decide Everything" description: "Accept recommended defaults for all decisions and skip remaining questions." multiSelect: false ``` ### Q2 — Research Depth ``` question: "How much research should go into this expert before drafting?" header: "Research" options: - label: "Full deep research (Recommended)" description: "Web search official docs, SDK source, and community guides for each rule and pattern. Thorough but slower." - label: "Light research" description: "Quick scan of official docs only. Good when you already have strong domain knowledge." - label: "Stub only" description: "Create the file structure with a research prompt but no content yet. Fill in later with the researcher workflow." multiSelect: false ``` ### Q3 — Placement ``` question: "Where should this expert live in the folder structure?" header: "Placement" options: - label: "Existing domain folder" description: "Place in an existing domain (languages/, tools/, .project/). You'll specify which." - label: "New domain folder" description: "Create a new domain folder. Only if 3+ experts will belong to it and it has distinct signal words." - label: "Root .experts/ folder" description: "Place at the root level alongside fallback.md. For system-level utilities only." multiSelect: false ``` ### defaults table | Question | Default | |---|---| | Q1 | TypeScript-specific (`{topic}-ts.md`) | | Q2 | Full deep research | | Q3 | Existing domain folder | ## workflow ### phase 1 — scope 1. Run the interview above (or use defaults if the developer opted out). 2. Confirm the topic doesn't overlap with an existing expert. Read the target domain's `index.md` file inventory and scan for coverage. 3. If overlap exists, recommend updating the existing expert instead and stop. 4. Decide the filename: `{topic}-ts.md` (language-specific) or `{topic}.md` (language-agnostic). 5. Decide the target folder: existing domain, new domain, or root `.experts/`. ### phase 2 — research 1. Write a Deep Research prompt for the topic. Include: SDK/platform name, key concepts, specific APIs to cover, and pattern areas. 2. Execute the prompt as a series of targeted web searches: - Break into discrete topics (one per API surface, concept, or pattern area). - Search each individually. Prefer official docs, SDK source, and type definitions. - For each result, capture: API signatures, parameter types, return types, defaults, and gotchas. 3. If interview answer was "Stub only," write the research prompt into `## research` and skip to phase 5 (integration). The expert will be a stub. 4. If interview answer was "Light research," do a quick scan of official docs only — skip community guides and deep dives. ### phase 3 — draft Write the expert file following the canonical section layout from the expert structure reference below. 1. **`## purpose`** — One line. What does this expert cover? 2. **`## rules`** — Numbered list of actionable imperatives. Minimum 8 rules for a non-stub expert. Each rule should cite its source (doc link or observed SDK behavior). 3. **`## interview`** (optional) — Include only if the expert requires developer decisions before implementation. Follow the AskUserQuestion format shown in the expert structure reference below. 4. **`## patterns`** — Code snippets showing canonical usage. Each snippet is self-contained with imports. Minimum 2 patterns for a non-stub expert. 5. **`## pitfalls`** — Non-obvious mistakes, breaking changes, version gotchas. 6. **`## references`** — URLs to official docs and SDK source used during research. 7. **`## instructions`** — When to use this expert, what it pairs with (`Pair with: {other-expert}`). 8. **`## research`** — The Deep Research prompt (preserved for future re-research). ### phase 4 — validate Run through this checklist before considering the expert done: - [ ] **Minimum depth**: 8+ rules, 2+ patterns (unless intentionally a stub). - [ ] **Pattern isolation**: Every code snippet compiles in isolation (imports included, no "see above"). - [ ] **No fabrication**: Every API signature confirmed via research. Unverified claims marked `[unverified]`. - [ ] **File size**: Under 300 lines. If over, identify split points. - [ ] **Section completeness**: All required sections present (`purpose`, `rules`, `instructions`, `research`). Optional sections either fully populated or entirely absent. - [ ] **Rules are imperatives**: Each rule tells the reader what to do/avoid, not what "is" or "exists." - [ ] **Pitfalls are non-obvious**: No trivial advice. Each pitfall would surprise a competent developer. - [ ] **Cross-references set**: `## instructions` includes `Pair with:` entries for related experts. ### phase 5 — integrate Wire the new expert into the routing system so it's reachable: 1. **Domain `index.md`** — Open the target domain's `index.md`: - Add the file to the appropriate task cluster's `Read:` list (or create a new cluster with a `When:` description). - Add `Depends on:` / `Cross-domain deps:` if applicable. - Add the filename to `## file inventory` in alphabetical order. 2. **Root `index.md`** — Open `.experts/index.md`: - If the new expert introduces signal words not already in the domain's `Signals:` line, add them. - If this is a new domain, add a full routing entry under `## routing rules`. 3. **Verify routing** — Mentally trace a request that should reach this expert: root router signals → domain router → task cluster → expert file. Confirm the path is unbroken. ## expert structure reference This is the canonical section layout every expert must follow. Required sections are marked; optional sections should be omitted entirely if not needed. ``` # {topic}-ts | {topic} ← filename without .md ## purpose ← REQUIRED. One line. ## rules ← REQUIRED. Numbered imperatives. ## interview ← OPTIONAL. Delete if no upfront decisions needed. ### Q1 — {Decision} (AskUserQuestion format) ### defaults table (Required if interview exists) ## patterns ← REQUIRED for non-stubs. Code snippets. ## pitfalls ← RECOMMENDED. Non-obvious gotchas. ## references ← RECOMMENDED. Source URLs. ## instructions ← REQUIRED. When to use, Pair with. ## research ← REQUIRED. Deep Research prompt. ``` ## pitfalls - **Creating experts for one-off knowledge.** If the topic won't come up again, don't create an expert. Add a note to the relevant domain expert or CLAUDE.md instead. - **Skipping integration (phase 5).** The most common failure mode. An expert that isn't wired into the routing system is invisible and will never be loaded. - **Writing rules from memory without research.** Even experienced developers misremember API details. Always verify against current docs — APIs change between SDK versions. - **Cramming multiple topics into one file.** An expert on "state management and adaptive cards and function calling" should be three experts. The scope test: can you describe it without "and"? - **Empty optional sections.** An empty `## pitfalls` section signals the author didn't try. Either populate it with real gotchas or omit the section entirely. - **Forgetting the language suffix decision.** A TypeScript-specific expert named `caching.md` (without `-ts`) will confuse future users about whether it's language-agnostic. Be deliberate about the naming. ## instructions Use this expert when creating any new micro-expert file. **Trigger phrases:** "create expert," "new expert," "build expert," "add expert," "make expert," "write expert." Pair with: `fallback.md` (if the builder is invoked because fallback detected a knowledge gap that warrants a new expert). ## research Deep Research prompt: "Write a meta-expert for creating micro-expert prompt files in a modular AI expert system. Cover: scoping criteria (when to create vs. update), research methodology (web search strategies for SDK docs, source code, type definitions), canonical section layout for expert files, quality validation checklists (minimum rules, pattern isolation, no fabrication), integration steps (domain router wiring, signal word updates), and common failure modes in expert authoring. Include guidance on language-agnostic vs. language-specific naming conventions." -
fallback.md 2.1 KB
# fallback ## purpose Safety-net invoked when the routed experts don't fully cover the user's request. Runs a two-phase recovery to fill knowledge gaps. ## when to use - The domain router's experts answered part of the question but left gaps. - The root router picked a domain but the user's request spans multiple domains. - The user explicitly says the answer is incomplete or asks for more detail. ## phase 1 — scan for missed experts 1. List every domain router: `teams/index.md`, `slack/index.md`, `bridge/index.md`, `convert/index.md`, `security/index.md`, `deploy/index.md`, `models/index.md`. 2. For each router, read its `## task clusters` section and compare every **When:** line against the current request. 3. Collect any expert files whose **When:** signals match but were **not** loaded in the initial routing pass. 4. Read those missed expert files and incorporate their guidance into the response. ### example User asks: "Add an Adaptive Card action that calls a function tool and stores the result." Initial route → `teams/index.md` → loaded `ui.adaptive-cards-ts.md`. Phase 1 re-scan finds: - `ai.function-calling-implementation-ts.md` (signal: "function implementation") - `state.storage-patterns-ts.md` (signal: "store … result") Load both and merge their guidance. ## phase 2 — web search After Phase 1, identify any remaining knowledge gaps that **no** expert file covers: 1. Formulate a targeted search query for each gap (SDK name + version + specific API/concept). 2. Execute web searches. 3. Synthesize the results into the response, citing sources. ### search tips - Prefix queries with the SDK package name and version (e.g., `@microsoft/teams-ai v2`). - Prefer official docs (`learn.microsoft.com`, `api.slack.com`, SDK GitHub repos). - If a search returns outdated results, add the current year to the query. ## constraints - Do NOT fabricate API signatures. If a web search yields no answer, say so. - Do NOT re-read expert files already loaded in the initial pass — only add missed ones. - Keep Phase 1 fast: scan headings only, do not read full expert file bodies until you confirm a match. -
index.md 18.9 KB
# experts-router ## purpose You are the **root task router**. Before doing any work, interview the developer to understand scope and preferences. Then classify intent and load exactly one domain router. Do NOT load micro-expert files directly from here. ## pre-task interview (mandatory) **Every task starts here.** Before routing, loading experts, or writing any code, interview the developer. The depth scales with the task — a small bug fix may need one confirmation; a multi-file migration needs detailed scoping. ### How it works 1. **Assess complexity.** Read the developer's message. Determine if the task is small (single file, clear intent), medium (multi-file, some ambiguity), or large (architectural, multi-step, cross-cutting). 2. **Ask the right questions.** Use `AskUserQuestion` to walk through the applicable question blocks below. Skip any question the developer's message already answers unambiguously. 3. **Always offer the escape hatch.** Every question set MUST include a **"You decide everything"** (first question) or **"You decide everything else"** (subsequent questions) option. Selecting it means: use your best judgment for all remaining decisions, apply expert defaults, and proceed without further questions. 4. **Record and carry forward.** Store all answers. They shape routing, expert selection, and implementation decisions for the entire task. ### Question blocks Pick from these based on complexity. You don't need all of them — use judgment. #### Q1 — Scope & intent (always ask) ``` header: "Scope" question: "Before I start, let me confirm what you need. Which best describes this task?" options: - label: "You decide everything" description: "I trust your judgment — assess the task, make all decisions, and just do it." - label: "Quick fix / small change" description: "Single file or minor tweak. Just need it done." - label: "Feature / migration step" description: "Multi-file change with some decisions to make." - label: "Architectural / large task" description: "Significant scope — I want to review the approach before you start." multiSelect: false ``` If the developer picks **"You decide everything"** → skip all remaining questions, proceed with expert defaults. #### Q2 — Approach preferences (medium+ tasks) ``` header: "Approach" question: "Any preferences on how I tackle this?" options: - label: "You decide everything else" description: "No preferences — use best practices and expert defaults." - label: "Minimal changes" description: "Touch as few files as possible. Keep the diff small." - label: "Do it right" description: "Refactor if needed. Prioritize correctness and clean code." - label: "Let me review first" description: "Show me a plan before writing any code." multiSelect: false ``` If **"Let me review first"** → enter plan mode (`EnterPlanMode`) before implementation. #### Q3 — Specifics (large tasks or when ambiguity remains) ``` header: "Details" question: "A few specifics to nail down:" options: - label: "You decide everything else" description: "Use your best judgment for all remaining decisions." - label: "I'll answer each" description: "Walk me through the decisions one at a time." multiSelect: false ``` If **"I'll answer each"** → proceed to any expert-level `## interview` sections after routing. If **"You decide everything else"** → fill expert interviews with defaults and skip them. ### Interaction with expert-level interviews The pre-task interview gates the entire workflow. Expert-level `## interview` sections (described in the auto-interview protocol below) handle domain-specific decisions. If the developer chose "You decide everything" or "You decide everything else" at the pre-task level, those expert interviews are auto-filled with defaults and skipped. ## routing rules Scan the user's message for signal words. Pick the **first matching domain**; if signals overlap, prefer the domain whose signals appear more often. ### Teams — build or modify a Teams bot / agent Signals: Teams SDK, `@microsoft/teams-ai`, Adaptive Cards, ChatPrompt, Graph API, MCP, A2A, `app.ts`, manifest, proactive message, message extension, dialog, task module, Agents Toolkit, Bot Framework, SSO, OAuth, streaming, citations, RAG, function calling, memory, state, storage, `microsoft_teams`, `microsoft_teams.apps`, `microsoft_teams.ai`, `ActivityContext`, `@app.on_message`, `OpenAICompletionsAIModel`, `Microsoft.Teams.Apps`, `Microsoft.Teams.AI`, `AddTeams()`, `UseTeams()`, `IContext<TActivity>`, `OnMessage`, `OpenAIChatPrompt`, Teams Python, Teams .NET, Teams C#, `m365agents.yml`, `atk provision`, `atk deploy`, `atk publish`, `atk preview`, `atk validate`, `atk package`, `atk new`, Agents Playground, `.m365agentsplayground.yml`, lifecycle hooks, `env/.env` → Read `experts/teams/index.md` ### Slack — build or modify a Slack app Signals: Slack Bolt, `@slack/bolt`, Block Kit, `ack()`, Slack events, `app.message`, `app.command`, `app.event`, `app.action`, `app.shortcut`, `app.view`, slash command, Slack OAuth, `InstallProvider`, `InstallationStore`, multi-workspace, `app.assistant`, Assistant container, `threadStarted`, `userMessage`, `setSuggestedPrompts`, `setStatus`, `setTitle`, `getThreadContext`, Socket Mode, `socketMode`, `appToken`, `@slack/socket-mode`, `SocketModeReceiver`, `chat.postMessage`, `chat.update`, `chat.postEphemeral`, proactive message, `WebClient`, `app.client`, `filesUploadV2`, global shortcut, message shortcut, `message_action`, modal, `views.open`, `views.update`, `views.push`, `view_submission`, `view_closed`, `private_metadata`, `slack_bolt`, `AsyncApp`, `SocketModeHandler`, `chat_postMessage`, `views_open`, `slack-bolt-java`, `com.slack.api.bolt`, `AppConfig.builder()`, `MethodsClient`, Slack Python, Slack Java, Slack CLI, `slack create`, `slack run`, `slack deploy`, `slack activity`, `slack trigger`, `slack datastore`, `slack env`, `slack manifest`, `slack auth login`, `slack doctor`, `slack app install`, `slack collaborator`, `slack function distribute`, `slack project`, `.slack/`, `project.json`, `manifest.ts`, `slack.json`, trigger definition, Slack hosted platform → Read `experts/slack/index.md` ### Bridge — cross-platform bridging between Slack and Teams, or AWS and Azure Signals: bridge, cross-platform, migrate, migration, convert, port, Slack to Teams, Teams to Slack, Slack→Teams, Teams→Slack, Slack↔Teams, Block Kit to Adaptive Cards, Adaptive Cards to Block Kit, AWS to Azure, Azure to AWS, Lambda to Functions, S3 to Blob, CloudWatch to App Insights, middleware chain, ack(), Socket Mode, RTM, transport, WebSocket to HTTPS, InstallationService, OAuthStateService, views.open, viewSubmission, viewClosed, App Home, views.publish, attachmentAction, legacy attachments, callback_id, modals to dialogs, task module, replace_original, delete_original, chat.update, chat.postEphemeral, response_url, ephemeral, files.upload, file_shared, link_shared, chat.unfurl, unfurl, shortcut, global shortcut, message shortcut, scheduleMessage, reminders.add, conversations.create, conversations.archive, conversations.invite, channel ops, Workflow Builder, workflow_step_execute, Power Automate, App Directory, InstallationStore, sideloading, rate limit, retry, 429, throttle, circuit breaker, Dapr, FileConsentCard, add Teams support, add Slack support, help me migrate, cross-platform advisor, bridging decisions, assess bridging, bridging scope, dual bot, single server, shared Express, REST API, Java, C#, Go, no SDK, raw HTTP, Bot Framework REST, manual JWT, signature verification, Python dual-platform, Python cross-platform, `slack_bolt` + `microsoft_teams`, unified Python server → Read `experts/bridge/index.md` ### Models — configure and call AI models from any provider Signals: OpenAI, Azure OpenAI, GPT-4o, GPT-4, Anthropic, Claude, `@anthropic-ai/sdk`, Bedrock, `@aws-sdk/client-bedrock-runtime`, Converse API, Foundry Local, `foundry model`, `foundry service`, Foundry cloud, MaaS, model-as-a-service, GitHub Models, Ollama, vLLM, LM Studio, llama.cpp, TGI, LocalAI, open-source model, local LLM, OpenAI-compatible, `/v1/chat/completions`, `openai` npm, model provider, AI model, LLM, chat completions, tool use, function calling, Bedrock agents, Bedrock guardrails, Phi-4, Qwen, Llama, Mistral, DeepSeek, embeddings, model selection, Transformers.js, `@huggingface/transformers`, in-process inference, browser inference, WASM, WebGPU, local embeddings, local classification, pipeline API, HuggingFace → Read `experts/models/index.md` ### Deploy — deploy a bot to Azure or AWS Signals: deploy, deployment, provision, hosting, publish, go live, production, `az login`, `aws configure`, App Service, Azure Functions, Container Apps, Lambda, EC2, ECS, Fargate, Elastic Beanstalk, Azure Bot, `atk provision`, `atk deploy`, Agents Toolkit deploy, zip deploy, SAM deploy, CDK deploy, API Gateway, deploy to Azure, deploy to AWS, push to cloud, CloudFormation → Read `experts/deploy/index.md` ### Convert — rewrite source code from another language to TypeScript Signals: JS to TS, Ruby to TS, Java to TS, Kotlin to TS, convert language, transpile, rewrite in TypeScript, port code, language conversion, gems to npm, Maven to npm, Gradle to npm, type annotations, require to import, add types, snake_case to camelCase, Lombok, @Data, @Builder, Gson, Jackson, @SerializedName, CompletableFuture, bulk conversion, large-scale, trailing lambda, `it` parameter, `trimIndent`, `when` expression, data class, companion object, sealed class, extension function → Read `experts/convert/index.md` ### Security — harden inputs, secrets, or credentials Signals: input validation, sanitize, secrets, credentials, key vault, PII, injection, XSS → Read `experts/security/index.md` ## auto-interview protocol (expert-level) After loading any expert, check for a `## interview` section. If one exists **and the developer did NOT choose "You decide everything" / "You decide everything else" in the pre-task interview**, execute the expert interview before writing any code or giving implementation advice. If the developer DID choose an escape hatch in the pre-task interview, auto-fill all expert interview answers with the expert's documented defaults and skip the questions. ### How it works 1. **Check pre-task answers.** If the developer already opted out of detailed questions, skip to implementation using defaults. 2. **Detect.** After reading an expert file, scan for `## interview`. If missing, proceed directly to implementation. 3. **Execute.** Walk through each question block (`### Q1`, `### Q2`, ...) in order. Use `AskUserQuestion` for each one, following the `header`, `question`, `options`, and `multiSelect` fields exactly as specified. 4. **Record.** Store the developer's answers. Pass them into the expert's rules and patterns as context — the answers shape which code paths, patterns, and configurations apply. 5. **Escape hatch.** Every expert interview MUST include a "You decide everything else" option. Selecting it fills all remaining answers with the expert's documented defaults and skips remaining questions. 6. **Context-skip.** If the developer's original message already answers an interview question unambiguously (e.g., "use Azure OpenAI" when the question is "OpenAI or Azure OpenAI?"), skip that question — don't re-ask what's already known. ### Experts with embedded interviews Some experts (like `cross-platform-advisor-ts.md`) have the interview woven into their phased workflow rather than in a separate `## interview` section. These already satisfy the protocol — their Phase 2 / decision walkthrough IS the interview. ## ambiguity tiebreaker If the request mixes **bridge** signals with a target-platform signal (e.g., "convert Block Kit to Adaptive Cards"), route to **bridge** first — bridge experts already reference the target platform's patterns. If the request mixes **convert** signals with **bridge** signals (e.g., "rewrite this Ruby Slack bot in TypeScript for Teams"), route to **convert** first for language translation, then **bridge** for platform mapping. The convert domain's combining rule handles this layering. ## fallback If no domain matches, ask **one** clarifying question: > "Are you working on a Teams bot, a Slack app, a migration between them, or something else?" If the routed experts don't fully cover the request (gaps remain after the initial pass), read `experts/fallback.md` for a two-phase recovery: re-scan all domain routers for missed experts, then web-search for any remaining gaps. ## expert evolution The expert system is **self-evolving**. As conversations reveal knowledge gaps, outdated patterns, or new topic areas, update the system in-place. Follow the rules below. ### Naming conventions | Convention | Meaning | |---|---| | `topic-ts.md` | Normal expert — can be created, updated, or replaced | | `topic-ts.locked.md` | **Locked** — read-only, do NOT edit. Rename to remove `.locked` only with explicit user approval | | `_filename.md` | System/template file — not a routable expert | | `index.md` | Domain router — update when adding/removing experts in that domain | To lock an expert: rename `topic-ts.md` → `topic-ts.locked.md`. The routing system treats `.locked.md` identically for reads — only writes are blocked. ### When to UPDATE an existing expert Update an expert when **any** of these are true during a conversation: 1. **Corrected mistake** — you discover a rule, code pattern, or pitfall is wrong and fix it in practice. Backport the correction to the expert. 2. **New pattern emerged** — you write a working pattern that isn't covered by any existing expert. Add it to the most relevant expert's `## patterns` section. 3. **SDK/API changed** — the user provides or you discover updated API signatures, new options, or deprecated features. Update the affected rules and patterns. 4. **Missing pitfall** — you hit a gotcha during implementation that the expert didn't warn about. Add it to `## pitfalls`. 5. **Cross-reference gap** — an expert should reference another but doesn't. Add a `Pair with` entry to `## instructions` and update the domain `index.md` cluster. **Do NOT update locked experts** (`*.locked.md`). If a locked expert needs changes, flag it to the user. ### When to CREATE a new expert Create a new expert when **all** of these are true: 1. **No existing expert covers the topic** — the knowledge doesn't fit as an addition to any current file. 2. **The topic is reusable** — it will apply to future tasks, not just the current one-off request. 3. **Sufficient depth** — the topic warrants 8+ rules and 2+ code patterns. If it's only 2-3 rules, add them to an existing expert instead. **Creation steps:** 1. Copy the template from `experts/_expert-ts.md`. 2. Name it `{topic}-ts.md` in the appropriate domain folder. 3. Fill in all sections: purpose, rules, patterns, pitfalls, references, instructions (with `Pair with` cross-refs), research. 4. Complete the **post-creation checklist** from `_expert-ts.md`: - Add to domain `index.md` task cluster `Read:` list (with `Depends on:` / `Cross-domain deps:` if applicable). - Add to domain `index.md` file inventory. - Add signal words to root `index.md` if the new expert introduces terms not already covered. ### When to CREATE a new domain folder Create a new domain (folder + `index.md` router) when **all** of these are true: 1. **3+ experts** would belong to the new domain — a domain with 1-2 experts should stay as a cluster within an existing domain. 2. **Distinct signal words** — the domain's topics wouldn't naturally route through any existing domain's signals. 3. **Separable routing** — moving these experts out of an existing domain simplifies that domain's router, not complicates it. **Creation steps:** 1. Create the folder: `experts/{domain-name}/`. 2. Create `experts/{domain-name}/index.md` following the router pattern from any existing domain index (purpose, task clusters with `When:`/`Read:`, combining rule, file inventory). 3. Move or create the expert files in the new folder. 4. Add a new routing entry to this root `index.md` under `## routing rules` with `Signals:` and a `→ Read` directive. 5. Remove any moved experts from their old domain's `index.md`. ### Evolution audit trail When you modify the expert system, add a one-line comment at the bottom of the affected domain's `index.md`: ``` <!-- Updated YYYY-MM-DD: {what changed and why} --> ``` This lets future sessions see what evolved and when, without cluttering the routing logic. ## utilities These files support the expert system itself — not a specific domain. ### Fallback recovery → Read `experts/fallback.md` When the initial routing pass leaves knowledge gaps. ### New expert template → Read `experts/_expert-ts.md` When creating a new micro-expert file. Provides the canonical stub structure and a post-creation checklist. ### Research workflow → Read `experts/researcher.md` When fleshing out a stub expert with real content. Provides the step-by-step Deep Research workflow. <!-- Updated 2026-02-11: Added convert domain (js-to-ts, ruby-to-ts, java-to-ts, dependency-mapping, type-mapping) for language conversion to TypeScript --> <!-- Updated 2026-02-11: Expanded convert domain (json-serialization, bulk-conversion-strategy) and migrate domain (slack-middleware-to-teams, slack-transport-to-teams); updated java-to-ts with Lombok/CompletableFuture; updated slack-identity-to-aad with OAuth impl code --> <!-- Updated 2026-02-11: Added kotlin-to-ts to convert domain; added slack-modals-to-teams-dialogs, slack-app-home-to-teams, slack-legacy-attachments-to-teams to migrate domain --> <!-- Updated 2026-02-11: Added auto-interview protocol — experts with ## interview sections now auto-trigger AskUserQuestion before implementation. Added interviews to bulk-conversion-strategy-ts, compat.botbuilder-interop-ts. Updated _expert-ts template with interview format. --> <!-- Updated 2026-02-11: Added mandatory pre-task interview — every task now starts with a developer interview (scope, approach, specifics) before routing. Always includes "You decide everything" escape hatch. Expert-level interviews respect pre-task choices. --> <!-- Updated 2026-02-27: Added Slack experts (bolt-assistant, bolt-events, bolt-oauth-distribution) from @slack/bolt v4.6.0 source. Added bridge experts (cross-platform-architecture, rest-only-integration) for dual-bot hosting and SDK-less Java/C#/Go patterns. Updated Slack and bridge signal words. --> <!-- Updated 2026-02-28: Added deploy domain (azure-bot-deploy, aws-bot-deploy) for step-by-step cloud deployment walkthroughs with CLI setup, provisioning, and verification. Includes cloud provider interview. --> <!-- Updated 2026-02-28: Added models domain (openai-azure-openai, anthropic, bedrock, foundry-local, foundry-cloud, oss-openai-compatible) for AI model provider integration. Covers 6 providers with unified OpenAI SDK patterns and provider abstraction. --> <!-- Updated 2026-03-01: Added Teams Agents Toolkit signals (m365agents.yml, teamsapp CLI, Agents Playground, env/.env) and Slack CLI signals (slack create/run/deploy/trigger/datastore/env/manifest/auth/app/collaborator) --> -
prompt-engineer.md 14.7 KB
# prompt-engineer ## purpose Design prompts as information architecture over a 1D token stream — reducing semantic distance, compressing before reasoning, and constraining expansion during writing. ## rules 1. **Treat prompting as a distance design problem.** LLMs see a flat token stream, not tables, headers, or sections. Every prompt decision is about reducing token traversal distance between a question and its relevant data, a field name and its value, an instruction and its constraint, an example and its expected output. 2. **Reformat structured data into records, not tables.** Tables look organized to humans but scatter related values across distant token positions. Convert tabular data into per-record blocks where each label sits immediately next to its value. The model retrieves fields by proximity, not by column headers. 3. **Compress before reasoning.** Reasoning is collapsing many possible interpretations into one. Before asking the model to reason, reduce irrelevant tokens, remove noise, surface only task-relevant facts, and force discrete decisions (Yes/No, choose one, rank). Every token of noise increases entropy and degrades the compression. 4. **Use compression mechanisms deliberately.** RAG retrieval, summarization, scratchpads, chain-of-thought, entity extraction, and tool calls are all compression mechanisms. Choose the one that matches the bottleneck: retrieval for finding, summarization for condensing, chain-of-thought for multi-step inference, tool calls for grounding. 5. **Constrain decompression explicitly.** Writing is controlled expansion from a compressed representation. Unconstrained expansion drifts toward generic filler. Always specify: target audience, tone, length, format, required elements, and output schema. Each constraint reduces degrees of freedom and increases output quality. 6. **Diagnose the failure mode before redesigning.** Three distinct failure categories require different fixes. If the model can't find information → distance problem (move things closer). If the model draws wrong conclusions → compression problem (improve intermediate structure). If the output reads poorly → decompression problem (add constraints). Never redesign the whole prompt when only one layer is broken. 7. **Design for positional attention.** Attention is strongest at the edges of context (beginning and end) and weakest in the middle. Put critical instructions at the top. Put the user's question at the bottom. Inject retrieved data near the query. Never bury high-signal content in the middle of long context. 8. **Prefer structure over volume.** More tokens do not mean better performance. Intentional compression, proximity engineering, context rewriting, and selective retrieval outperform longer prompts with more raw content. If adding context doesn't reduce distance or improve compression, it adds noise. 9. **Place labels adjacent to values.** Any time the model must associate a name with a piece of data (field/value, question/answer, instruction/example), put them directly next to each other in the token stream. Separation creates retrieval failures the model cannot recover from. 10. **Force discrete outputs for reasoning steps.** Open-ended intermediate steps increase entropy. When chaining reasoning, constrain each step to a discrete decision — a classification, a yes/no, a selection from enumerated options. Each forced decision compresses the possibility space for the next step. 11. **Scope retrieved context to the task.** RAG and context injection should deliver only what the current query needs. Retrieving "everything related" adds noise tokens the model must traverse. Filter, re-rank, and truncate retrieved content before injecting it into the prompt. 12. **Write prompts as systems, not sentences.** Prompting is information architecture — pipelines, latent plans, context transformations, compression→latent→decompression flows. Design token flow the way you'd design a data pipeline: each stage transforms the representation toward the output. 13. **Use open-only `<SECTION>` tags to structure prompts.** Delineate prompt regions with `<SECTION_NAME>` tags — no closing `</SECTION_NAME>` tag. The open tag acts as a label that the model pattern-matches against. Closing tags add tokens without adding signal. Each distinct data type gets its own named section (`<DOCUMENT>`, `<USER_PROFILE>`, `<SEARCH_RESULTS>`, etc.). 14. **Put all data sections at the top, `<INSTRUCTIONS>` at the bottom.** Data sections occupy the top of the prompt where they're loaded into context. The `<INSTRUCTIONS>` block goes at the bottom — the high-attention end of the token stream. This separates *what the model knows* from *what the model should do*. 15. **Reference section names inside `<INSTRUCTIONS>` using the same tag format.** When a rule in `<INSTRUCTIONS>` refers to data, use the exact `<SECTION_NAME>` tag from the data section. Writing "Use the information in `<DOCUMENT>` to..." reinforces the pattern match between the instruction and the data it targets. The repeated tag acts as a semantic anchor — the model doesn't search for meaning, it matches the token pattern. 16. **Move `<INSTRUCTIONS>` to user messages for multi-turn flows.** In multi-turn conversations where each turn needs different instructions, place the data sections in the system prompt (stable across turns) and the `<INSTRUCTIONS>` block in the user message (changes per turn). This lets you re-instruct the model at each step without duplicating context. 17. **Place persona declarations above the first section tag, if used at all.** Persona framing ("You are an expert at...") is rarely necessary — constraints and instructions are more effective. When persona is needed, place it at the very top of the prompt before the first `<SECTION>` tag so it colors everything that follows. ## interview ### Q1 — Task type ``` question: "What kind of prompting task are you working on?" header: "Task type" options: - label: "You decide everything" description: "Use your best judgment for all decisions — skip remaining questions." - label: "Data extraction / retrieval" description: "Getting the model to find and return specific information from context." - label: "Reasoning / analysis" description: "Getting the model to draw conclusions, classify, or make decisions." - label: "Content generation" description: "Getting the model to produce structured text, code, or creative output." multiSelect: false ``` ### Q2 — Failure mode ``` question: "What's going wrong with your current prompt (if anything)?" header: "Diagnosis" options: - label: "You decide everything else" description: "No specific failure — I want a prompt designed from scratch." - label: "Can't find the right info" description: "The model misses or ignores relevant data in the context. (Distance problem)" - label: "Wrong conclusions" description: "The model finds the data but reasons incorrectly. (Compression problem)" - label: "Bad output quality" description: "The reasoning is fine but the output format/style/tone is wrong. (Decompression problem)" multiSelect: false ``` ### defaults table | Question | Default | |---|---| | Q1 | Content generation | | Q2 | No specific failure — design from scratch | ## patterns ### Converting a table to proximity-optimized records ```markdown # BAD — table scatters related values across token positions | Name | Role | Department | Start Date | |---------|-----------|------------|------------| | Alice | Engineer | Platform | 2023-01-15 | | Bob | Designer | Product | 2022-06-01 | # GOOD — record format keeps each entity's fields adjacent ## Employee: Alice - Role: Engineer - Department: Platform - Start Date: 2023-01-15 ## Employee: Bob - Role: Designer - Department: Product - Start Date: 2022-06-01 ``` ### Section-tag prompt layout (single turn) ```markdown <USER_PROFILE> Name: Alice Role: Engineering Manager Team: Platform Preferences: concise answers, no jargon <DOCUMENT> [retrieved documentation chunks, pre-filtered and re-ranked] <INSTRUCTIONS> Using the information in <DOCUMENT>, answer the user's question. Tailor your response to the reader described in <USER_PROFILE>. Keep the answer to 2-3 sentences. Cite the document section by name. If the answer is not in <DOCUMENT>, say "I don't have that information." ``` ### Section-tag prompt layout (multi-turn) ```markdown # --- System prompt (stable across turns) --- <USER_PROFILE> Name: Alice Role: Engineering Manager Team: Platform <DOCUMENT> [retrieved documentation — stays in context across turns] # --- User message turn 1 --- <INSTRUCTIONS> Summarize the key points in <DOCUMENT> relevant to the reader in <USER_PROFILE>. Use bullet points, max 5 bullets. # --- User message turn 2 (new instructions, same data) --- <INSTRUCTIONS> Based on <DOCUMENT>, draft a 2-sentence Slack message from the person in <USER_PROFILE> announcing the most important change to their team. Tone: direct and positive. ``` ### Compression chain: open-ended question → constrained reasoning ```markdown # --- Turn 1: Extract (compression) --- <BUG_REPORT> {raw bug report text} <INSTRUCTIONS> From the report in <BUG_REPORT>, extract: - Component affected: (one of: auth, api, ui, database) - Severity: (critical / high / medium / low) - Reproducible: (yes / no / unknown) # --- Turn 2: Reason over compressed representation --- <TRIAGE> Component: {extracted_component} Severity: {extracted_severity} Reproducible: {extracted_reproducible} <INSTRUCTIONS> Using the fields in <TRIAGE>, select the response action: - If critical + reproducible → "hotfix: page on-call" - If critical + not reproducible → "investigate: assign senior engineer" - If high + reproducible → "prioritize: next sprint" - Otherwise → "triage: add to backlog" # --- Turn 3: Expand with constraints (decompression) --- <TRIAGE> Component: {extracted_component} Severity: {extracted_severity} Action: {selected_action} <INSTRUCTIONS> Using the fields in <TRIAGE>, write a 2-sentence Slack message to the engineering channel. Tone: urgent but calm. Include: component, severity, chosen action. ``` ### Diagnostic checklist prompt ```markdown # When a prompt fails, run through this diagnostic: ## 1. Distance check - Is the relevant data within ~500 tokens of the question? - Are labels directly adjacent to their values? - Is anything critical buried in the middle of a long context? → Fix: restructure data, move fields closer, trim irrelevant context. ## 2. Compression check - Is the model asked to reason over raw, unstructured input? - Are intermediate steps unconstrained (free-text instead of discrete)? - Is there more context than the task actually requires? → Fix: pre-extract, force classifications, reduce to task-relevant facts. ## 3. Decompression check - Did you specify: audience, tone, length, format, required elements? - Is there an output schema or example? - Could two equally skilled people interpret the prompt differently? → Fix: add constraints, provide a concrete output example. ``` ## pitfalls - **Assuming the model "sees" your formatting.** Markdown headers, table borders, and indentation carry weak signal at best. The model processes tokens sequentially — visual structure doesn't create semantic structure. Always design for the token stream, not the rendered view. - **Adding more context to fix retrieval failures.** When the model can't find information, the instinct is to add more. This usually makes it worse — more tokens means greater traversal distance. Instead, remove irrelevant content and move the relevant data closer to the query. - **Using free-text intermediate steps.** Asking the model to "think through" a problem in free text generates unconstrained tokens that expand rather than compress the possibility space. Force intermediate outputs into discrete categories, structured fields, or enumerated options. - **Placing instructions in the middle of context.** The "Lost in the Middle" effect is well-documented. Instructions, constraints, and critical data placed in the middle of a long context are reliably degraded. Use the top and bottom of the prompt for high-signal content. - **Treating all prompt failures the same.** Rewriting an entire prompt because the output is wrong wastes effort and obscures the root cause. A distance failure, a compression failure, and a decompression failure require different fixes. Diagnose first. - **Over-engineering with chain-of-thought.** Chain-of-thought is a compression mechanism for multi-step reasoning. Applying it to simple retrieval or generation tasks adds unnecessary tokens without improving quality. Match the mechanism to the bottleneck. - **Adding closing `</SECTION>` tags.** Closing tags waste tokens without adding signal. The next open `<SECTION>` tag implicitly ends the previous section. The model pattern-matches on the open tag — the closing tag is noise. - **Not referencing `<SECTION>` names in instructions.** If `<INSTRUCTIONS>` says "use the document" instead of "use <DOCUMENT>", the model must infer which section you mean. The repeated tag pattern creates a direct token-level link between the rule and the data it operates on. Always use the exact tag name. ## instructions Use this expert when the developer is designing, debugging, or optimizing prompts for LLMs — whether for application prompts, system prompts, RAG pipelines, agent instructions, or any task involving prompt architecture. **Trigger phrases:** "write a prompt," "prompt engineering," "prompt design," "fix my prompt," "prompt not working," "LLM prompt," "system prompt," "improve prompt," "prompt template," "RAG prompt," "optimize prompt," "prompt debugging." Pair with: any language expert from `../languages/` when implementing prompt pipelines in code. Pair with: `json-yaml.md` when working with structured prompt templates or output schemas. ## research Deep Research prompt: "Write a micro-expert for LLM prompt engineering based on an information-architecture mental model. Core framework: prompting is a distance design problem over a 1D token stream. Cover: token proximity as the fundamental retrieval mechanism (not visual structure), table-to-record reformatting for distance reduction, compression before reasoning (RAG retrieval, summarization, chain-of-thought, entity extraction, tool calls as compression mechanisms), constrained decompression for writing (audience, tone, length, format, schema), the three-category diagnostic framework (distance problems, compression problems, decompression problems), positional attention design (Lost in the Middle effect, edge placement), structure vs. volume tradeoffs, discrete vs. free-text intermediate steps, and prompt-as-system-design rather than wordsmithing. Include patterns for RAG prompt layout, table-to-record conversion, compression chains, and diagnostic checklists." -
README.md 8.6 KB
# Bot Platform Expert System A curated knowledge base for building conversational bots and AI agents across Slack and Microsoft Teams. These micro-experts guide AI coding assistants (Claude, Copilot, etc.) to produce correct, idiomatic code by loading only the relevant expertise for each task. ## Goals 1. **Accelerate bot development** by giving AI assistants deep, verified knowledge of both Slack and Teams SDKs — eliminating hallucinated APIs and outdated patterns. 2. **Support cross-platform scenarios** where a single team needs to ship bots on both Slack and Teams from the same codebase. 3. **Cover the full stack** from SDK initialization and webhook plumbing through AI integration, media handling, and infrastructure migration — not just "hello world" examples. 4. **Stay language-pragmatic** by focusing on TypeScript (the only language with first-class SDK support on both platforms) while providing REST-level guidance for Java, C#, Go, and other languages. ## SDK Language Matrix | Language | Slack Bolt | Teams SDK | Recommendation | |--------------------|-----------|-----------|---------------------------------------------------------------| | TypeScript / JS | Full | Full | Best choice for dual-platform — both SDKs are first-class | | Python | Full | Preview | Good for AI/ML workloads; Teams SDK still maturing | | Java / JVM | Full | None | Use REST-only patterns for Teams (see `rest-only-integration`) | | C# / .NET | None | Full | Use REST-only patterns for Slack (see `rest-only-integration`) | | Go, Ruby, etc. | None | None | REST-only for both platforms | ## Scenarios ### 1. Build a Teams bot (TypeScript) Load the **Teams** domain. 28 micro-experts cover app initialization, routing, Adaptive Cards, dialogs, message extensions, OAuth/SSO, Graph API, AI (ChatPrompt, function calling, RAG, streaming, memory), MCP, A2A, and more. **Key experts:** `teams/runtime.app-init-ts.md`, `teams/runtime.routing-handlers-ts.md`, `teams/ui.adaptive-cards-ts.md` ### 2. Build a Slack bot (TypeScript) Load the **Slack** domain. 7 micro-experts cover Bolt.js app setup, handler registration, ack rules, slash commands, Block Kit UI, Events API, Assistant containers, and OAuth/multi-workspace distribution. **Key experts:** `slack/runtime.bolt-foundations-ts.md`, `slack/bolt-events-ts.md`, `slack/bolt-assistant-ts.md` ### 3. Host both bots in a single server Load the **Bridge** domain's architecture cluster. Covers shared Express server with route separation, Socket Mode + HTTP dual receiver, platform-agnostic service layer, and identity normalization. **Key expert:** `bridge/cross-platform-architecture-ts.md` ### 4. Integrate from Java, C#, or Go (no native SDK) Load the **Bridge** domain's REST-only cluster. Language-agnostic pseudocode for Bot Framework REST API (Teams) and Slack Events API + Web API — manual JWT validation, HMAC signature verification, token acquisition, and message sending. **Key expert:** `bridge/rest-only-integration-ts.md` ### 5. Bridge features between Slack and Teams Load the **Bridge** domain. 25 micro-experts cover bidirectional mapping of every feature: Block Kit ↔ Adaptive Cards, commands, events ↔ activities, identity, modals ↔ dialogs, files, shortcuts ↔ extensions, workflows ↔ Power Automate, infrastructure (Lambda ↔ Functions, S3 ↔ Blob), and more. **Key expert:** `bridge/cross-platform-advisor-ts.md` (orchestrates the full bridging workflow) ### 6. Deploy your bot to Azure or AWS Load the **Deploy** domain. The router interviews you on cloud provider preference (Azure or AWS) and bot platform (Teams, Slack, or both), then loads the matching expert for a step-by-step walkthrough from CLI installation through verified deployment. **Key experts:** `deploy/azure-bot-deploy-ts.md`, `deploy/aws-bot-deploy-ts.md` ### 7. Convert code from another language to TypeScript Load the **Convert** domain. 8 micro-experts cover JS→TS, Ruby→TS, Java→TS, Kotlin→TS, type mapping, dependency mapping, JSON serialization, and bulk conversion strategy. **Key experts:** `convert/java-to-ts-ts.md`, `convert/kotlin-to-ts-ts.md`, `convert/type-mapping-ts.md` ## Expert Inventory ### Root (6 files) | File | Purpose | |------|---------| | `index.md` | Root task router — interviews developer, routes to domain | | `fallback.md` | Recovery when no domain matches | | `_expert-ts.md` | Template for creating new experts | | `researcher.md` | Deep research workflow for fleshing out experts | | `analyzer.md` | Analyze project and recommend new experts | | `builder.md` | Build new experts from analysis recommendations | ### Slack Domain (18 files) Covers: Bolt.js foundations, ack rules, slash commands, shortcuts, Socket Mode, Block Kit, modals lifecycle, events API, assistant containers, OAuth/distribution, Web API/proactive messaging, Slack CLI (getting started, app management, manifest/triggers, datastore/env, local dev/deploy), Bolt for Python, Bolt for Java. ### Teams Domain (35 files) Covers: app init, routing, manifest, proactive messaging, Adaptive Cards, dialogs, message extensions, OAuth/SSO, Graph API, state/storage, AI (ChatPrompt, model setup, function calling, RAG, streaming, citations, memory), MCP (server, client, security, expose tools), A2A (server, client, orchestrator), BotBuilder interop, debug/test, scaffolding, Agents Toolkit (playground, environments, lifecycle CLI, publish), Teams for Python, Teams for .NET. ### Bridge Domain (26 files) Covers: Block Kit ↔ Adaptive Cards, commands, events ↔ activities, identity/OAuth bridge, middleware ↔ handlers, modals ↔ dialogs, App Home ↔ personal tab, legacy attachments, transport, infrastructure (compute, storage, secrets, observability), interactive responses, files, link unfurl ↔ preview, shortcuts ↔ extensions, scheduling, channel ops, workflows ↔ automation, distribution/packaging, rate limiting, cross-platform advisor, cross-platform architecture, REST-only integration, Python cross-platform. ### Convert Domain (8 files) Covers: JS→TS, Ruby→TS, Java→TS, Kotlin→TS, type mapping, dependency mapping, JSON serialization, bulk conversion strategy. ### Models Domain (7 files) Covers: OpenAI/Azure OpenAI, Anthropic, AWS Bedrock, Azure AI Foundry (cloud), Foundry Local, OSS/OpenAI-compatible, Transformers.js. ### Deploy Domain (4 files) Covers: Azure deployment (App Service, Functions, Agents Toolkit), AWS deployment (Lambda, API Gateway, ECS, SAM), Azure CLI reference, AWS CLI reference. ### Security Domain (2 files) Covers: input validation, secrets management. ## How It Works 1. **Developer sends a task** → root `index.md` interviews for scope and preferences 2. **Signal words are scanned** → task routes to exactly one domain router 3. **Domain router matches clusters** → loads only the relevant micro-expert files 4. **Expert-level interviews** (if present) → clarify implementation decisions 5. **Implementation** → expert rules, patterns, and pitfalls guide code generation ## Eval Harness The [`evals/`](../evals/) directory contains an automated test harness that validates the expert system across three dimensions: | Dimension | What it checks | LLM required? | |-----------|---------------|----------------| | **Patterns** | TypeScript code blocks in experts still compile | No | | **Routing** | User queries route to the correct domain/clusters/experts | Optional (improves accuracy) | | **Completeness** | Experts cover all required concepts for their domain | Yes | ```bash cd evals && npm install npm run eval:patterns # fast, no API key npm run eval # all dimensions (needs OPENAI_API_KEY in .env) ``` Current results: 294/294 patterns compile, 41/51 routing cases pass (all 7 domains covered), 9/9 completeness cases pass. The ~10 routing failures are LLM judge scoring edge cases where the deterministic router is correct but the judge scores conservatively on ambiguous or cross-domain queries. See [`evals/README.md`](../evals/README.md) for details. After adding or editing experts, run `npm run eval:patterns` to verify code examples still compile. For new domains or significant expert changes, add test cases to `evals/cases/` and run the full suite. ## Adding New Experts Use the `analyzer.md` → `builder.md` workflow: 1. Run `analyzer.md` against a codebase to identify coverage gaps 2. Hand off recommendations to `builder.md` to create expert files 3. New experts auto-wire into domain routers via the post-creation checklist in `_expert-ts.md` 4. Run `cd evals && npm run eval:patterns` to verify new code examples compile -
researcher.md 2.8 KB
# researcher ## purpose Step-by-step workflow for fleshing out any stub micro-expert. Use this when the user says "research expert X" or when you encounter a stub that needs content. ## how to identify a stub A file is a stub if its `## instructions` section contains only a web-search placeholder and its `## rules` section is missing or empty. ## workflow ### step 1 — read the stub Read the target expert file. Locate its `## research` section and extract the Deep Research prompt. ### step 2 — execute research Run the Deep Research prompt as a series of web searches: 1. Break the prompt into discrete topics (each SDK concept, API surface, or pattern mentioned). 2. Search for each topic individually. Prefer: - Official documentation (`learn.microsoft.com`, `api.slack.com`, SDK GitHub repos) - SDK source code and type definitions - Recent blog posts or guides (add current year to query if results are stale) 3. For each search, capture: API signatures, parameter types, return types, default values, and gotchas. ### step 3 — synthesize into canonical sections Replace the stub content with real content using these sections: - **`## rules`** — Numbered list of do/don't rules derived from the research. Each rule should be actionable (e.g., "Always call `ack()` before async work" not "ack is important"). - **`## patterns`** — Code snippets (TypeScript) showing canonical usage. Use fenced code blocks. Keep each snippet minimal and focused on one concept. - **`## pitfalls`** — Common mistakes, breaking changes, or version-specific gotchas. - **`## references`** — URLs to official docs, SDK source, or authoritative blog posts used during research. ### step 4 — update instructions section Replace the `## instructions` placeholder content with a concise summary of what the expert covers and when to use it. This is the "quick reference" an agent reads first. ### step 5 — preserve the research prompt Keep the `## research` section intact with the original Deep Research prompt. This allows future re-research if the SDK changes. ### step 6 — rollup to domain index Open the domain's `index.md` and verify: 1. The expert file appears in the correct task cluster's `Read:` list. 2. The `When:` description for that cluster still accurately reflects the expert's content (update if the scope changed during research). 3. The expert file appears in the `## file inventory` list. ## quality checks - Every `## rules` entry must cite a source (doc link or SDK behavior). - Every `## patterns` code snippet must be valid TypeScript that compiles in isolation (imports included). - No fabricated API signatures — if you cannot confirm a signature, note it as unverified. - Keep the total file under 300 lines. Split into multiple experts if it grows larger. -
update-experts.md 10 KB
# update-experts ## purpose Scan the `.experts/` directory tree, detect expert files that were added or removed since the index files were last updated, and reconcile every `index.md` to match the actual file system. ## rules 1. **Scan the full directory tree first.** Walk every folder under `.experts/` and collect all `.md` files. Separate them into three categories: index files (`index.md`), utility files (root-level system files like `builder.md`, `analyzer.md`, `fallback.md`, `update-experts.md`), and expert files (everything else that isn't prefixed with `_`). 2. **Build the expected inventory from the file system.** For each domain folder (`tools/`, `languages/`, `languages/{lang}/`, `.project/`), list every expert `.md` file present on disk (excluding `index.md`). This is the source of truth. 3. **Build the registered inventory from each index file.** Parse each domain's `index.md` to extract: (a) the `## file inventory` list and (b) expert filenames referenced in `Read:` or `→ Read` directives within `## task clusters`. This is what the routing system currently knows about. 4. **Diff the two inventories per domain.** Identify: (a) **New experts** — files on disk not in the index, (b) **Removed experts** — files in the index not on disk, (c) **Orphaned references** — `Read:` directives pointing to files that don't exist. 5. **For each new expert, read the file to extract routing metadata.** Open the new expert file and extract: the `# title` line, the `## purpose` line, trigger phrases from `## instructions`, and any `When:` signal words. This metadata is needed to write the index entry. 6. **Update domain `index.md` files for new experts.** For each new expert: (a) Add a task cluster entry under `## task clusters` with a `When:` line derived from the expert's trigger phrases and a `→ Read` directive pointing to the file. (b) Add the filename to `## file inventory` in alphabetical order. 7. **Update domain `index.md` files for removed experts.** For each removed expert: (a) Delete its task cluster entry from `## task clusters`. (b) Remove the filename from `## file inventory`. (c) Remove any `Depends on:` references to it from other clusters. 8. **Update the root `index.md` for signal word changes.** After updating domain indexes, check if new experts introduced signal words not already present in the root `index.md` routing rules for that domain. Add them. Similarly, remove signal words that only belonged to a now-deleted expert. 9. **Update the root `index.md` utilities section.** If a new root-level utility file was added (not inside a domain folder), add it to `## utilities` with a `→ Read` directive, signals line, and one-line description. If a utility was removed, delete its entry. 10. **Handle language sub-domains.** Languages have a two-level structure: `languages/index.md` routes to `languages/{lang}/index.md`, which routes to individual expert files. New language folders need entries in `languages/index.md`. New expert files within a language folder need entries in that language's `index.md`. 11. **Detect new domain folders.** If a folder exists under `.experts/` that contains an `index.md` but has no routing entry in the root `index.md`, flag it as a new domain and add a routing entry with signals derived from its `index.md` purpose and task clusters. 12. **Detect orphaned domain folders.** If the root `index.md` references a domain folder that doesn't exist on disk, remove the routing entry and warn the developer. 13. **Never modify expert files themselves.** This utility only touches `index.md` files. Expert content, rules, patterns, and instructions are never altered. 14. **Report all changes.** After updating, output a structured summary showing: files scanned, new experts wired, removed experts unwired, signal words added/removed, and any warnings (orphaned references, missing metadata). ## workflow ### phase 1 — scan 1. List all folders under `.experts/` recursively. 2. For each folder, list all `.md` files. 3. Categorize every file: index, utility, or expert. 4. Build the file-system inventory: `{ domain → [expert files] }`. ### phase 2 — parse indexes 1. Read every `index.md` file found in phase 1. 2. Extract registered experts from `## file inventory` and `Read:` / `→ Read` directives. 3. Build the index inventory: `{ domain → [registered files] }`. ### phase 3 — diff 1. For each domain, compute: - `added = filesystem - index` - `removed = index - filesystem` - `orphaned_refs = Read directives pointing to missing files` 2. For root-level files, compare against `## utilities` in the root `index.md`. ### phase 4 — gather metadata for new experts 1. For each file in `added`, read the expert file. 2. Extract: title, purpose, trigger phrases from `## instructions`, signal words. 3. If the expert lacks `## instructions` or trigger phrases, derive signal words from the title and purpose. ### phase 5 — update indexes 1. Apply additions and removals to each domain `index.md`: - Add/remove task cluster entries. - Add/remove file inventory entries. - Clean up `Depends on:` / `Cross-domain deps:` references to removed files. 2. Apply signal word changes to root `index.md` routing rules. 3. Apply utility additions/removals to root `index.md` `## utilities` section. ### phase 6 — report Output a structured summary: ``` ## Update Report ### Scanned - Domains: {count} - Expert files: {count} - Index files: {count} ### Changes #### New experts wired - {domain}/index.md ← {filename} (signals: {words}) #### Removed experts unwired - {domain}/index.md → {filename} removed #### Signal words updated - Root index.md: added {words} to {domain} signals - Root index.md: removed {words} from {domain} signals #### Utilities updated - Root index.md: added {filename} to utilities - Root index.md: removed {filename} from utilities ### Warnings - {any orphaned references, missing metadata, etc.} ### No changes needed - {domains where filesystem matches index} ``` ## patterns ### Parsing file inventory from an index ``` # File inventory formats to recognize: # Pipe-delimited (tools/index.md style): # git.md | json-yaml.md | prompt-engineer.md → Split on ` | `, strip backticks # Backtick-delimited (languages/typescript/index.md style): # `idioms.md` | `patterns.md` | `pitfalls.md` | `type-system.md` → Split on ` | `, strip backticks # Comment placeholder (.project/index.md style): # <!-- empty — populated by analyzer.md + builder.md --> → Empty list ``` ### Parsing Read directives from task clusters ``` # Single-file directive (tools style): # → Read `.experts/tools/git.md` → Extract path, derive filename: git.md # Multi-file directive (language sub-domain style): # Read: # - `idioms.md` # - `patterns.md` → Extract each filename from bullet list # Domain-level directive (root index.md style): # → Read `.experts/languages/index.md` → This points to an index, not an expert — skip when inventorying experts ``` ### Deriving signal words from an expert file ``` # Priority order for extracting signals: 1. ## instructions → "Trigger phrases:" line → Parse the comma-separated quoted phrases 2. ## interview → question text → Extract domain-specific keywords 3. ## purpose → one-line description → Extract nouns and noun phrases 4. # title → filename stem → Use as a last-resort signal word ``` ## pitfalls - **Confusing index files with expert files.** Every domain has an `index.md` that is a router, not an expert. Never add `index.md` to a file inventory or create a task cluster pointing to an index within its own domain. - **Missing the two-level language structure.** `languages/index.md` routes to `languages/{lang}/index.md`, which routes to expert files. A new file in `languages/python/` must update `languages/python/index.md`, not `languages/index.md` directly. A new language folder must update `languages/index.md`. - **Overwriting hand-crafted cluster descriptions.** When adding a new expert to an existing index, don't rewrite the existing task clusters. Only add the new entry and update the file inventory. - **Duplicating signal words in root index.** Before adding signal words to a domain's entry in the root `index.md`, check that those words don't already appear in another domain's signals. Duplicate signals cause ambiguous routing. - **Forgetting locked files.** `*.locked.md` files are valid experts that should appear in indexes. Don't skip them during scanning — they route identically to unlocked files. - **Treating `_prefixed.md` files as experts.** Files starting with `_` are system/template files, not routable experts. Exclude them from inventory and index updates. ## instructions Use this expert when the developer wants to synchronize the routing indexes with the actual expert files on disk. This is the maintenance counterpart to `builder.md` — the builder creates experts, this utility ensures the routing system reflects what exists. **Trigger phrases:** "update experts," "sync indexes," "update index," "refresh routing," "fix index files," "new experts not routed," "clean up indexes," "reconcile experts." Pair with: `builder.md` (run update-experts after bulk expert creation to wire everything at once). Pair with: `analyzer.md` (run update-experts after the analyzer recommends and creates project experts). ## research Deep Research prompt: "Write a meta-expert for maintaining a modular AI expert routing system's index files. Cover: recursive directory scanning to discover expert files, parsing index.md files to extract registered file inventories and Read directives, diffing filesystem state against index state, extracting routing metadata (signal words, trigger phrases) from expert files, updating multi-level index hierarchies (root router, domain routers, sub-domain routers), handling additions and removals symmetrically, managing signal word propagation from domain indexes to root index, reporting changes in a structured format, and common maintenance pitfalls (index/expert confusion, two-level language routing, locked files, underscore-prefixed system files, duplicate signal words across domains)." -
_expert-ts.md 2.2 KB
# {topic}-ts \## purpose {One-line description of what this expert covers.} \## rules 1. {Core rule or pattern #1.} 2. {Core rule or pattern #2.} 3. {Add or remove rules as needed.} \## interview (optional — delete if not needed) <!-- Include this section ONLY if the expert requires developer decisions before implementation. The auto-interview protocol in index.md will detect this section and execute it via AskUserQuestion BEFORE any code is written. Delete this section entirely if the expert can proceed without upfront decisions. --> \### Q1 — {Decision Topic} ``` question: "{Clear question ending with ?}" header: "{Short label, max 12 chars}" options: - label: "{Option A} (Recommended)" description: "{What this option means and effort/tradeoff}" - label: "{Option B}" description: "{What this option means and effort/tradeoff}" - label: "You Decide Everything" description: "Accept recommended defaults for all decisions and skip remaining questions." multiSelect: false ``` \### defaults table (required if interview exists) | Question | Default | |---|---| | Q1 | {Option A — the recommended choice} | \## instructions Do a web search for: \- "{SDK or library name} {specific API or concept} TypeScript {additional keywords}" \## research Deep Research prompt: "{Write a micro expert on {topic} in {SDK/platform} (TypeScript). Cover {key areas}. Include canonical patterns for: {pattern list}.}" --- \## post-creation checklist After creating a new expert from this template, you MUST complete these steps: 1. **Add to domain `index.md`** — Open the domain's `index.md` (e.g., `teams/index.md`). Either: - Append the new file to an existing task cluster's `Read:` list, OR - Create a new task cluster with a `When:` description and `Read:` entry. - Append the filename to the `## file inventory` list (alphabetical order). 2. **Update root `index.md` signals** — If the new expert introduces signal words not already covered by the domain's signals list in `.experts/index.md`, add them to the domain's `Signals:` line. 3. **Verify** — Confirm the file appears in both the domain `index.md` file inventory and the appropriate task cluster `Read:` list.
-
-
provision-deploy
-
provision-deploy.md 4.3 KB
# Provision and Deploy Provision Azure and M365 resources, then deploy your agent to the cloud. ## Local Provisioning (for Teams testing) ```bash atk provision --env local -i false atk deploy --env local -i false ``` This runs actions in `m365agents.local.yml` — registers Teams app, creates bot AAD app, and writes runtime config to `.localConfigs`. ### Post-Provisioning Verification (Required) ATK's `aadApp/create` may not write `TENANT_ID` to `.localConfigs`. After provisioning, always verify: ```bash # 1. Check TENANT_ID is in .localConfigs grep TENANT_ID .localConfigs # 2. If missing, copy it from the env file (aadApp/create writes it there) grep TENANT_ID env/.env.local # Then add: TENANT_ID=<tenant-id> to .localConfigs ``` > **Why this matters:** Without `TENANT_ID` in `.localConfigs`, the SDK acquires tokens from the wrong authority (`botframework.com` instead of your tenant), causing 401 from Bot Connector. The tenant ID is available in `env/.env.local` after provisioning — copy it to `.localConfigs` if missing. See [troubleshoot.md](../troubleshoot/troubleshoot.md) for details. > **If you hit `AADSTS7000229` / `invalid_client`:** Your `aadApp/create` action is missing `generateServicePrincipal: true`. Add it to the YAML and re-provision: > ```yaml > - uses: aadApp/create > with: > generateServicePrincipal: true # ← add this > ``` > Then run `atk provision --env local -i false` again. If you still get 401 after fixing this, your devtunnel URL may be blacklisted — create a fresh tunnel and update `BOT_ENDPOINT`. ## Cloud Deployment Workflow ### Prerequisites 1. Azure subscription — set `AZURE_SUBSCRIPTION_ID` in `env/.env.dev` 2. Azure login — `atk auth login azure` 3. Resource group — `az group create --name <rg> --location <region>` (if needed) 4. Verify accounts match: `az account show` vs `atk auth list` ### Steps ```bash # Step 1: Copy required env vars from env/.env.local to env/.env.dev # Look at m365agents.yml for ${{VAR_NAME}} references # Step 2: Provision Azure + M365 resources atk provision --env dev --resource-group <rg> --region <region> -i false # Step 3: Deploy code to Azure atk deploy --env dev -i false ``` Both commands can take several minutes — wait for completion (timeout 120000ms+). ## Quick Reference | Task | Command | |------|---------| | Provision local | `atk provision --env local -i false` | | Deploy local | `atk deploy --env local -i false` | | Provision cloud | `atk provision --env dev --resource-group <rg> --region <region> -i false` | | Deploy cloud | `atk deploy --env dev -i false` | | Login M365 | `atk auth login m365` | | Login Azure | `atk auth login azure` | | Check login | `atk auth list` | ## Azure OpenAI Configuration For custom engine agents using Azure OpenAI, add env vars to the YAML and set their values — see [../toolkit/manifest-and-yaml.md](../toolkit/manifest-and-yaml.md) for details. ## References - For YAML structure and env var flow → see [../toolkit/manifest-and-yaml.md](../toolkit/manifest-and-yaml.md) - For package, validate, share, collaborate → see [../toolkit/commands.md](../toolkit/commands.md) - If something goes wrong → see [../troubleshoot/troubleshoot.md](../troubleshoot/troubleshoot.md) ## Expert Deep Dives > **Applies to all ATK projects** — the lifecycle/environments/publish experts cover the YAML-driven `atk provision/deploy/publish` flow used by every template (declarative agents, API plugins, Copilot connectors, Teams bots, tabs). > > The **azure-bot-deploy** expert applies only to projects that deploy code to Azure (Teams bots, custom engine agents, RAG agents, message extensions). Declarative agents and pure-API-plugin projects don't deploy compute and can ignore it. | Topic | Expert | |---|---| | `m365agents.yml` action catalog, lifecycle stages, full `atk` CLI reference | [../toolkit/lifecycle-cli.md](../toolkit/lifecycle-cli.md) | | `env/` files, `${{VAR}}` resolution, `SECRET_` prefix, multi-environment isolation | [../toolkit/environments.md](../toolkit/environments.md) | | Sideload → org catalog → Teams Store publish flow, `atk publish`, version bumping | [../toolkit/publish.md](../toolkit/publish.md) | | Manual Azure deployment walkthrough (what `atk provision` automates) — Teams bots only | [../experts/deploy/azure-bot-deploy-ts.md](../experts/deploy/azure-bot-deploy-ts.md) |
-
-
slack-to-teams
-
SKILL.md 8.9 KB
--- name: slack-to-teams description: "Sub-skill of microsoft-365-agents-toolkit. Routed expert system with 100+ micro-expert files for migrating Slack bots to Teams, cross-platform bridging, and dual-platform bot development. USE FOR: migrating Slack bot to Teams, adding Teams support to Slack bot, building dual-platform bots, converting Block Kit to Adaptive Cards, identity/OAuth bridging, deploying bots to Azure or AWS, configuring AI model providers. DO NOT USE FOR: general web development, non-bot projects, standalone Teams development without Slack (use parent skill instead)." --- # Slack to Teams Expert System A routed expert system with 100+ micro-expert files for migrating Slack bots to Teams and building cross-platform bots. > **Parent skill:** For ATK CLI setup and routing, see [../SKILL.md](../SKILL.md). For local testing, see [../test-playground/test-playground.md](../test-playground/test-playground.md) or [../test-teams/test-teams.md](../test-teams/test-teams.md). For cloud deploy, see [../provision-deploy/provision-deploy.md](../provision-deploy/provision-deploy.md). For troubleshooting, see [../troubleshoot/troubleshoot.md](../troubleshoot/troubleshoot.md). ## When to Use - Building a new Slack bot, Teams bot, or dual-platform bot - Adding Teams support to an existing Slack bot (or vice versa) - Migrating a bot between platforms - Deploying a bot to Azure or AWS - Configuring AI model providers for a bot - Converting UI between Block Kit and Adaptive Cards - Bridging identity, events, files, or transport between platforms - Making a Teams bot project compatible with Microsoft Agents Toolkit (m365agents.yml, env/, appPackage placeholders) ## Procedure ### Step 1: Determine project type Assess whether the developer has an existing codebase or is starting fresh. - **New project** → go to [New Project Flow](#new-project-flow) - **Existing project** → go to [Existing Project Flow](#existing-project-flow) --- ### New Project Flow #### 1a: Platform selection Ask the developer which platform(s) to support: - Slack + Teams (dual-platform) - Slack only - Teams only #### 1b: Load the expert system 1. Read [experts/index.md](../experts/index.md) — the root router. 2. Execute the **pre-task interview** defined in that file. 3. Route based on platform choice: - **Slack + Teams** → Read [experts/bridge/index.md](../experts/bridge/index.md). Load [cross-platform-advisor](../experts/bridge/cross-platform-advisor-ts.md). - **Slack only** → Read [experts/slack/index.md](../experts/slack/index.md). - **Teams only** → Read [experts/teams/index.md](../experts/teams/index.md). #### 1c: Architecture setup 1. Read [cross-platform-architecture](../experts/bridge/cross-platform-architecture-ts.md) (even for single-platform — establishes patterns for adding a second platform later). 2. Let the domain router and advisor take over. 3. Write a `PLAN.md` in the target project root with platform, features, experts loaded. #### 1d: Implementation Follow the advisor's or domain router's output. Implement feature by feature: 1. Pick the next feature from the prioritized list. 2. Load the expert(s) specified for that feature. 3. Implement using the expert's patterns and rules. 4. Verify against the expert's pitfalls section. --- ### Existing Project Flow #### 2a: Analyze the project Run these four sub-analyses **in parallel**: **Detect language** — Scan for `package.json`+`tsconfig.json` (TypeScript), `pom.xml`/`build.gradle` (Java), `*.csproj`/`*.sln` (C#), `go.mod` (Go), `requirements.txt`/`pyproject.toml` (Python), `Gemfile` (Ruby), `Cargo.toml` (Rust). **Detect current platform** — Scan dependencies for SDK indicators: - Slack: `@slack/bolt`, `@slack/web-api`, `slack_bolt`, `app.message`, `app.command`, `ack()` - Teams: `@microsoft/teams-ai`, `botbuilder`, `TeamsActivityHandler`, `app.turn`, Adaptive Cards **Detect features** — Scan for slash commands, Block Kit/Adaptive Cards, action handlers, OAuth, file upload/download, scheduling, threading, AI/LLM calls, proactive messages. **Detect architecture** — Scan for web framework (Express, Fastify, etc.), hosting target (Azure, AWS, Docker), cloud provider, architecture pattern (single bot, dual-bot, monolith). #### 2b: Language gate Classify the detected language into SDK tiers: | Tier | Languages | Guidance | |---|---|---| | **1: Full SDK** | TypeScript / JavaScript | Full expert system available | | **2: Adapt** | Python | Both SDKs exist — adapt TS patterns. Load [bolt-python](../experts/slack/bolt-python.md), [teams-python](../experts/teams/teams-python.md), [python-cross-platform](../experts/bridge/python-cross-platform.md) | | **3: Split SDK** | Java, C# | One platform has SDK, other needs REST. Load [bolt-java](../experts/slack/bolt-java.md) or [teams-dotnet](../experts/teams/teams-dotnet.md) + [rest-only](../experts/bridge/rest-only-integration-ts.md) | | **4: No SDK** | Go, Ruby, Rust | REST-only for both. Load [rest-only](../experts/bridge/rest-only-integration-ts.md) | #### 2c: Expert coverage gap analysis 1. Read [experts/analyzer.md](../experts/analyzer.md). 2. Execute the analyzer workflow: scan manifests, dependencies, source files. 3. Cross-reference detected tech against existing experts. 4. Present gap analysis — covered vs uncovered technologies. #### 2d: Build missing experts (if needed) 1. Read [experts/builder.md](../experts/builder.md). 2. For each gap: analyze project usage → read package source → draft expert → validate → wire into routing. 3. Use [experts/_expert-ts.md](../experts/_expert-ts.md) as the template. #### 2e: Load the expert system 1. Read [experts/index.md](../experts/index.md) — the root router. 2. Execute the pre-task interview, pre-filling with analysis results from 2a. 3. Route based on detected platform and task intent: - Has Slack, wants Teams → **Bridge** domain - Has Teams, wants Slack → **Bridge** domain - Has both → Route by task (bridge refinement, deploy, models, etc.) - Has neither → Ask which platform(s) to target 4. Load [cross-platform-advisor](../experts/bridge/cross-platform-advisor-ts.md) if bridging. Feed analysis results into Phase 1. #### 2f: Architecture and implementation 1. Read [cross-platform-architecture](../experts/bridge/cross-platform-architecture-ts.md). 2. Write a `PLAN.md` with analysis results, routing decisions, and feature migration order. 3. Implement feature by feature using the advisor's prioritized list. ## Agents Toolkit Compatibility For ATK-compatible project structure (m365agents.yml, env/ files, appPackage), see the parent skill: - **[Parent SKILL.md](../SKILL.md)** — ATK CLI setup, sub-skill routing, workflow chains - **[manifest-and-yaml.md](../toolkit/manifest-and-yaml.md)** — Project files, YAML config, env vars, .localConfigs flow - **[commands.md](../toolkit/commands.md)** — Package, validate, share, collaborate, environment management - **[templates.md](../toolkit/templates.md)** — All templates with language support For Teams manifest schema and packaging, see [runtime.manifest-ts](../experts/teams/runtime.manifest-ts.md). ## Error Recovery If the expert system fails to cover a topic: 1. Read [experts/fallback.md](../experts/fallback.md). 2. Phase 1: Re-scan all domain routers for missed experts. 3. Phase 2: Web-search for remaining knowledge gaps. 4. Consider creating a new expert using [experts/builder.md](../experts/builder.md). ## Expert Domains | Domain | Index | Description | |---|---|---| | Slack | [experts/slack/index.md](../experts/slack/index.md) | Bolt framework, events, OAuth, commands, UI, CLI | | Teams | [experts/teams/index.md](../experts/teams/index.md) | Teams AI SDK, Adaptive Cards, Graph, MCP, A2A, deploy | | Bridge | [experts/bridge/index.md](../experts/bridge/index.md) | 27 cross-platform conversion experts (the core differentiator) | | Deploy | [experts/deploy/index.md](../experts/deploy/index.md) | Azure & AWS deployment walkthroughs | | Models | [experts/models/index.md](../experts/models/index.md) | AI model providers (OpenAI, Anthropic, Bedrock, etc.) | | Convert | [experts/convert/index.md](../experts/convert/index.md) | Language conversion to TypeScript | | Security | [experts/security/index.md](../experts/security/index.md) | Input validation, secrets management | ## Platform Comparison Docs Reference guides for side-by-side platform comparison: - [UI Components](../docs/ui-components.md) — Block Kit vs Adaptive Cards - [Messaging & Commands](../docs/messaging-and-commands.md) - [Identity & Auth](../docs/identity-and-auth.md) - [Interactive Responses](../docs/interactive-responses.md) - [Files & Links](../docs/files-and-links.md) - [Middleware & Handlers](../docs/middleware-and-handlers.md) - [Infrastructure](../docs/infrastructure.md) - [Advanced Features](../docs/advanced-features.md) - [Feature Gaps](../docs/feature-gaps.md) - [Workflow Scenarios](../docs/workflows.md) — Message-native workflow patterns (triggers, state, logic, AI, visibility) for Teams bots
-
-
test-playground
-
playground-cli.md 4.6 KB
# Automated Testing with playground-cli Run programmatic integration tests against your bot using `@microsoft/m365agentsplayground-cli` — a headless, browser-free API that drives bot conversations via code. Use this for CI pipelines, multi-turn conversation testing, and verifying bot behavior without manual interaction. ## Installation ```bash npm install --save-dev @microsoft/m365agentsplayground-cli ``` ## Quick Start (TypeScript) ```typescript import { TestClient } from "@microsoft/m365agentsplayground-cli"; import { expect } from "chai"; describe("MyBot", () => { let client: TestClient; before(async () => { client = new TestClient({ botEndpoint: "http://localhost:3978/api/messages", timeout: 15000, deliveryMode: "expectReplies", }); await client.start(); }); after(async () => { await client.stop(); }); beforeEach(() => { client.newConversation(); // isolate each test }); it("should greet the user", async () => { const [response] = await client.sendMessage("Hello"); expect(response.text).to.include("Hello"); }); it("should return a card for /help", async () => { const responses = await client.sendMessage("/help"); const cardResponse = responses.find((r) => r.attachments?.length); expect(cardResponse).to.exist; }); }); ``` ### BotResponse fields | Field | Type | Description | |---|---|---| | `text` | `string` | Plain text content | | `attachments` | `Attachment[]` | Adaptive Cards, Hero Cards, etc. | | `suggestedActions` | `object` | Suggested action buttons | | `type` | `string` | Activity type (`message`, `typing`, etc.) | ### ConversationServer (HTTP API) For non-Node test runners (Python, curl, any language): ```typescript import { createConversationServer } from "@microsoft/m365agentsplayground-cli"; const server = createConversationServer({ port: 9000 }); // server keeps running; call server.close() in teardown ``` ```bash # Verify health curl http://localhost:9000/health # → {"status":"ok"} ``` **POST /run-conversation** ```json { "config": { "botEndpoint": "http://localhost:3978/api/messages", "timeout": 30000, "deliveryMode": "expectReplies", "personas": { "alice": { "id": "user-alice", "name": "Alice", "email": "alice@example.com" } } }, "scenario": "smoke-test", "input": { "turns": [ { "test_id": "t1", "prompt": "Hello" }, { "test_id": "t2", "prompt": "What can you do?", "turn_type": "chat" }, { "test_id": "t3", "prompt": "", "turn_type": "install" }, { "test_id": "t4", "prompt": "<html>Order shipped</html>", "turn_type": "sendEmail", "persona": "alice" } ] } } ``` **Response:** ```json { "turns": [ { "test_id": "t1", "status": "Completed", "actual_response": "Hello! I'm your assistant..." }, { "test_id": "t2", "status": "Completed", "actual_response": "I can help you with..." }, { "test_id": "t3", "status": "Completed", "actual_response": "Welcome! I'm installed..." }, { "test_id": "t4", "status": "TimedOut", "actual_response": "" } ] } ``` Turn statuses: `Completed` | `TimedOut` | `Errored` | `Skipped` (skipped = previous turn failed) ## Turn Types | `turn_type` | Simulates | |---|---| | `"chat"` | Normal user message (default) | | `"sendEmail"` | Email notification received | | `"mentionInWord"` | @mention in Word document | | `"install"` | Bot installation event | | `"userAdded"` | Member added to conversation | | `"botAdded"` | Bot added to team | | `"channelCreated"` | New channel created | | `"teamRenamed"` | Team renamed | ## Key Configuration Notes | Setting | When to use | |---|---| | `deliveryMode: "expectReplies"` | Required for `@microsoft/teams-ai` and `teams.ts` bots | | `timeout: 30000` | Increase for bots calling external APIs or LLMs | | `streamingSettleDelayMs: 2000` | Increase only if LLM pauses > 800ms between stream chunks | | `personas` | Required for testing notification bots with specific `from` identity | ## Common Pitfalls - **No `await client.start()`** → `sendMessage()` throws immediately - **`deliveryMode` missing for teams-ai bots** → `sendMessage()` returns `[]` - **No `client.newConversation()` between tests** → bot state bleeds between test cases - **ConversationServer not started before HTTP tests** → connection refused; verify `/health` first - **Parallel tests sharing one `TestClient`** → mixed-up responses; use separate instances ## References - Manual interactive testing → [playground.md](playground.md) - npm package → [`@microsoft/m365agentsplayground-cli`](https://www.npmjs.com/package/@microsoft/m365agentsplayground-cli) -
playground.md 4.5 KB
# Manual Testing with Agents Playground Test your bot interactively using the Microsoft 365 Agents Playground — a web-based sandbox that requires no M365 account, Azure tunnel, or app registration. ## Installation **Windows:** ```powershell winget install agentsplayground ``` **Linux:** ```bash curl -LO https://github.com/OfficeDev/microsoft-365-agents-toolkit/releases/download/microsoft-365-agents-playground%400.2.23/agentsplayground-linux-x64.zip unzip agentsplayground-linux-x64.zip agentsplayground chmod +x agentsplayground sudo mv agentsplayground /usr/local/bin/ ``` **npm:** ```bash npm install -g @microsoft/m365agentsplayground ``` ## Quick Start ```bash # 1. For ATK projects, deploy playground config first atk deploy --env playground -i false # 2. Start your bot service (this will HANG the terminal — expected!) # Run as a background process since the server keeps running cd my-bot npm run dev:teamsfx:playground # For ATK projects # npm run dev # For customized projects # 3. Use a NEW/separate terminal to start Agents Playground agentsplayground -e http://localhost:3978/api/messages -c msteams ``` **Note:** The bot service start command keeps running and will not return to the prompt. This is expected — the server must stay running. Always start the service in a background terminal, then verify it started by checking the output for "listening on port" or "server started". Use a **new terminal** for Agents Playground. ## CLI Options | Option | Short | Required | Description | |--------|-------|----------|-------------| | `--app-endpoint` | `-e` | Recommended | Bot endpoint URL (e.g., http://localhost:3978/api/messages) | | `--channel-id` | `-c` | Optional | Channel to emulate: msteams, emulator, webchat, directline | | `--port` | `-p` | Optional | Server port (default: 56150, auto-fallback if occupied) | | `--client-id` | `--cid` | Optional | Azure app client ID (for authenticated agents) | | `--client-secret` | `--cs` | Optional | Azure app client secret (for authenticated agents) | | `--tenant-id` | `--tid` | Optional | Azure tenant ID for authentication | | `--enable-events-recording` | `--er` | Optional | Enable events recording (default: false) | ## Examples ```bash # Basic start with Teams channel agentsplayground -e http://localhost:3978/api/messages -c msteams # With authentication agentsplayground -e http://localhost:3978/api/messages -c emulator \ --client-id <CLIENT_ID> \ --client-secret <CLIENT_SECRET> \ --tenant-id <TENANT_ID> # Test different channels agentsplayground -e http://localhost:3978/api/messages -c webchat agentsplayground -e http://localhost:3978/api/messages -c emulator ``` ## Features - **No Setup Required**: Works with HTTP localhost endpoints - **Adaptive Card Preview**: See how cards render in Teams - **Chat Interface**: Simulate user messages and bot responses - **Context Mocking**: Mock Teams APIs (team members, channels, etc.) - **Message Inspection**: View request/response payloads in real-time ## Limitations - Application manifest not processed (command menus unavailable) - Some Adaptive Card features unsupported (people picker, user mentions, stage view) - SSO not supported - Only Adaptive Cards supported (not Hero/Thumbnail cards) ## Configuration File Create `.m365agentsplayground.yml` in project root to mock Teams context: ```yaml version: "0.1.1" tenantId: 00000000-0000-0000-0000-0000000000001 bot: id: 00000000-0000-0000-0000-00000000000011 name: Test Bot currentUser: id: user-id-0 name: Alex Wilber email: alexw@example.com users: - id: user-id-1 name: Megan Bowen email: meganb@example.com personalChat: id: personal-chat-id groupChat: id: group-chat-id team: id: team-id name: My Team channels: - id: channel-announcements-id name: Announcements ``` ## Environment Variables | Variable | Description | |----------|-------------| | `BOT_ENDPOINT` | Bot endpoint URL | | `DEFAULT_CHANNEL_ID` | Channel type (emulator, webchat, msteams) | | `AUTH_CLIENT_ID` | Azure app client ID for authentication | | `AUTH_CLIENT_SECRET` | Azure app client secret for authentication | | `AUTH_TENANT_ID` | Azure tenant ID for authentication | ## References - For project file details → [../toolkit/manifest-and-yaml.md](../toolkit/manifest-and-yaml.md) - If something goes wrong → [../troubleshoot/troubleshoot.md](../troubleshoot/troubleshoot.md) - To test on real Teams instead → [../test-teams/test-teams.md](../test-teams/test-teams.md) - For automated/CI testing → [playground-cli.md](playground-cli.md) -
test-playground.md 1.1 KB
# Test with Agents Playground Test your bot locally using the Microsoft 365 Agents Playground toolset. No M365 account, Azure tunnel, or app registration required. **Default: use [playground.md](playground.md) (manual interactive testing)**, unless user explicitly asks for automated or CI testing. > **Applies to: code-based Teams bots/agents only.** Declarative agents and API plugins must be tested in M365 Copilot via [test-teams](../test-teams/test-teams.md). ## Intent Router | User Intent | Read | |---|---| | "test my bot", "run locally", "chat with the bot", explore responses, manual testing | → [playground.md](playground.md) *(default)* | | "automated tests", "CI pipeline", "smoke tests", "programmatic testing", `TestClient`, `ConversationServer` | → [playground-cli.md](playground-cli.md) | ## References - For project file details → [../toolkit/manifest-and-yaml.md](../toolkit/manifest-and-yaml.md) - If something goes wrong → [../troubleshoot/troubleshoot.md](../troubleshoot/troubleshoot.md) - To test on real Teams instead → [../test-teams/test-teams.md](../test-teams/test-teams.md)
-
-
test-teams
-
test-teams.md 4.9 KB
# Test on Teams Test your agent in the actual Microsoft Teams environment. Requires M365 account and HTTPS endpoint. **Use this when user explicitly asks to run on Teams.** For quick local testing, recommend [Agents Playground](../test-playground/test-playground.md) first. ## Requirements - Microsoft 365 account with sideloading enabled - HTTPS endpoint (for bots, dev tunnels must be started first) ## Quick Start (Bot Projects) ### Step 1: Start devtunnel ```bash # CRITICAL: devtunnel host NEVER exits on its own — MUST use isBackground=true # It is a persistent tunnel process that runs until manually killed devtunnel host -p 3978 --allow-anonymous # After starting, check terminal output for the tunnel URL # Copy the tunnel URL and set BOT_ENDPOINT in env/.env.local before provisioning ``` ### Step 2: Provision and deploy ```bash atk provision --env local -i false atk deploy --env local -i false ``` ### Step 3: Start your local service ```bash # This will HANG the terminal — expected! # Run as a background process (isBackground=true) since the server keeps running # Check package.json scripts for the appropriate start command: # - If project uses .localConfigs: use `npm run dev:teamsfx` or equivalent # - If project uses .env directly: use `npm run dev` or `npm start` # Common patterns: npm run dev:teamsfx, npm run dev, npm start, python app.py, dotnet run ``` ### Step 4: Open Teams ```bash # Use a NEW/separate terminal! # Get TEAMS_APP_ID and TENANT_ID from env/.env.local # Open: https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TENANT_ID}}&login_hint=${{USER_EMAIL}} ``` ## Quick Start (Declarative Agents — No Backend) ```bash # Just provision/deploy and open directly atk provision --env local -i false atk deploy --env local -i false # Then open Teams and find your agent in the app list ``` ## Opening in Different Hosts Get your app IDs from `env/.env.local`, then open: | Host | URL | |------|-----| | Teams web | `https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TENANT_ID}}&login_hint=${{USER_EMAIL}}` | | Outlook web | `https://outlook.office.com/host/${{M365_APP_ID}}` | | Office web | `https://www.office.com/m365apps/${{M365_APP_ID}}` | ## Declarative Agents in M365 Copilot Declarative agents use `M365_APP_ID` (not `TEAMS_APP_ID`), acquired after `teamsApp/extendToM365` runs during provisioning. **Sideloading URL format:** ``` https://m365.cloud.microsoft/chat/entity1-d870f6cd-4aa5-4d42-9626-ab690c041429/${agent-hint}?auth=2&developerMode=Basic ``` Where `${agent-hint}` is Base64-encoded JSON: ```json {"id": "${M365_APP_ID}", "scenario": "launchcopilotextension", "properties": {"clickTimestamp": "2/6/2026, 10:30:45 AM"}, "version": 1} ``` ## Dev Tunnels for Bots **IMPORTANT**: For bot projects, you must start a public devtunnel BEFORE provisioning. The tunnel must be public/anonymous so Teams can reach your bot: ```bash # CRITICAL: devtunnel host NEVER exits on its own — MUST use isBackground=true # It is a persistent tunnel process that runs until manually killed devtunnel host -p 3978 --allow-anonymous ``` Then set `BOT_ENDPOINT` in `env/.env.local` with the tunnel URL before running `atk provision`. ## Comparison: Playground vs Teams | Feature | Agents Playground | Teams Direct Launch | |---------|-------------------|---------------------| | Setup complexity | Simple | Requires provisioning | | M365 account needed | No | Yes | | HTTPS required | No | Yes (for bots) | | Real Teams environment | No (simulated) | Yes | | SSO testing | No | Yes | | Speed | Fast | Slower (tunnel setup) | | Recommended for | Testing first (recommended) | When user explicitly asks to run on Teams | ## References - For project file details → see [../toolkit/manifest-and-yaml.md](../toolkit/manifest-and-yaml.md) - If something goes wrong → see [../troubleshoot/troubleshoot.md](../troubleshoot/troubleshoot.md) ## Expert Deep Dives > **Applies to: code-based Teams bots/agents only.** > > For **declarative agents** and **API plugins**, the experts below do not apply — there is no bot endpoint, no devtunnel, no `App` constructor, and no SDK auth. Use the "Declarative Agents in M365 Copilot" section above (sideloading via `M365_APP_ID`) and consult the [Microsoft 365 Copilot extensibility docs](https://learn.microsoft.com/microsoft-365-copilot/extensibility/) for capability-specific guidance (instructions, knowledge, conversation starters, action authentication). | Topic | Expert | |---|---| | Sideloading URL anatomy, devtunnel, `TENANT_ID`/`TEAMS_APP_TENANT_ID` mapping, `skipAuth` | [../experts/teams/dev.debug-test-ts.md](../experts/teams/dev.debug-test-ts.md) | | Teams app manifest schema (scopes, valid domains, webApplicationInfo) | [../experts/teams/runtime.manifest-ts.md](../experts/teams/runtime.manifest-ts.md) | | OAuth/SSO flow for in-Teams sign-in scenarios | [../experts/teams/auth.oauth-sso-ts.md](../experts/teams/auth.oauth-sso-ts.md) |
-
-
toolkit
-
commands.md 2.6 KB
# ATK CLI Commands Reference ## Package and Validate ```bash # Validate app atk validate --env dev -i false # Create app package atk package --env dev -i false # Sideload app atk install --file-path ./appPackage.zip -i false # Uninstall atk uninstall --mode env --env dev --folder . -i false ``` ## Share and Collaborate ```bash # Share with entire tenant atk share --scope tenant -i false # Share with specific users atk share --scope users --email 'user@example.com' -i false # Grant collaborator access atk collaborator grant -i false # Check collaborator status atk collaborator status ``` ## Environment Management ```bash # List environments atk env list # Add new environment atk env add staging # Reset environment atk env reset --env dev -i false ``` ## Adding Actions to Declarative Agents `atk add action` adds an API action to an existing declarative agent project. **Required parameters:** | Option | Description | |--------|-------------| | `--api-plugin-type api-spec` | Must be set explicitly (CLI bug: default is invalid) | | `--openapi-spec-type` | How to specify the API: `enter-url-or-open-local-file` or `search-api` | | `--openapi-spec-location -a` | OpenAPI spec file path or URL (for `enter-url-or-open-local-file`) | **Optional parameters:** | Option | Description | |--------|-------------| | `--api-operation -o` | Select specific operation(s) Copilot can interact with | | `--search-openapi-spec-query` | Search query (when using `search-api`) | | `--select-openapi-spec` | Select from search results (when using `search-api`) | | `--manifest-file -t` | App manifest path. Default: `./appPackage/manifest.json` | | `--folder -f` | Project folder. Default: `./` | ```bash # Add API action from local file atk add action --api-plugin-type api-spec --openapi-spec-type enter-url-or-open-local-file -a ./openapi.yaml -i false # Add API action from URL atk add action --api-plugin-type api-spec --openapi-spec-type enter-url-or-open-local-file -a https://example.com/openapi.yaml -i false # Add authentication config atk add auth-config -i false # Regenerate action after modifying OpenAPI spec atk regenerate action -i false ``` ## Troubleshooting ```bash # Check system prerequisites atk doctor # Validate app manifest atk validate --env dev -i false # Upgrade project to latest toolkit version atk upgrade -i false ``` **Port already in use:** ```powershell # Windows: Find and kill process using port 3978 Get-NetTCPConnection -LocalPort 3978 | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force } ``` ```bash # macOS/Linux lsof -ti:3978 | xargs kill -9 ``` ## Get Help ```bash atk --help atk new --help atk add action --help ``` -
environments.md 12.6 KB
# Environments and `.localConfigs` ## purpose Multi-environment management with environment files, `${{VAR}}` variable resolution, the `SECRET_` prefix convention, and the `.localConfigs` runtime-config flow for Agents Toolkit projects. ## rules 1. **Environment files live in `env/`.** Each environment has a pair of files: `env/.env.{name}` for shared config and `env/.env.{name}.user` for personal/secret values. The `environmentFolderPath` in `m365agents.yml` points to this folder. 2. **Default environment is `dev`.** Scaffolded projects create `env/.env.dev` and `env/.env.dev.user`. Additional environments (staging, production) are created by running `atk provision --env <name>`. 3. **Variable syntax is `${{VAR_NAME}}`.** Both `manifest.json` and `m365agents.yml` use `${{VAR_NAME}}` placeholders. At build/provision/deploy time, the toolkit resolves them from the active environment's `.env` files. 4. **`SECRET_` prefix marks sensitive values.** Variables prefixed with `SECRET_` (e.g., `SECRET_BOT_PASSWORD`, `SECRET_AAD_APP_CLIENT_SECRET`) are stored only in `.env.*.user` files, which are gitignored. Never put `SECRET_` values in `.env.{name}`. 5. **`.env.*.user` files are gitignored by default.** The scaffold includes a `.gitignore` entry for `env/.env.*.user`. These files contain developer-specific credentials and secrets. Never commit them. 6. **Built-in environment variables are auto-populated.** Lifecycle actions write outputs to env files via `writeToEnvironmentFile`. Common auto-populated vars: `TEAMS_APP_ID`, `BOT_ID`, `SECRET_BOT_PASSWORD`, `AAD_APP_CLIENT_ID`, `SECRET_AAD_APP_CLIENT_SECRET`, `AAD_APP_OBJECT_ID`, `AAD_APP_TENANT_ID`. 7. **Azure resource variables must be set per environment.** `AZURE_SUBSCRIPTION_ID` and `AZURE_RESOURCE_GROUP_NAME` are required for provisioning. Set them in `.env.{name}` or pass via CLI/CI environment. 8. **Custom environments mirror the dev structure.** To create a staging environment, run `atk provision --env staging`. This creates `env/.env.staging` and `env/.env.staging.user` with the same variable structure as dev but pointing to separate cloud resources. 9. **VS Code sidebar switches environments.** The Agents Toolkit VS Code extension shows a dropdown in the sidebar to switch the active environment. This changes which `.env.{name}` files are used for provision, deploy, and preview commands. 10. **Manifest placeholders resolve at package time.** When `atk package` or `teamsApp/zipAppPackage` runs, all `${{VAR}}` placeholders in `manifest.json` are replaced with values from the active environment. The output zip contains a fully resolved manifest. 11. **Environment-specific resource isolation.** Each environment should use separate Azure resource groups to avoid resource conflicts. Use naming conventions like `rg-mybot-dev`, `rg-mybot-staging`, `rg-mybot-prod`. 12. **`.localConfigs` is the runtime config for local development.** `atk deploy --env local` generates `.localConfigs` by reading values from `env/.env.local` and transforming them via `file/createOrUpdateEnvironmentFile` in `m365agents.local.yml`. Your app reads `.localConfigs` at runtime — NOT `env/.env.local`. If `TENANT_ID` is missing from `.localConfigs`, copy the tenant value from the source variable in `env/.env.local` (for example, `TEAMS_APP_TENANT_ID`) into `TENANT_ID`, or update the local lifecycle mapping so it writes `TENANT_ID` into `.localConfigs`. ## patterns ### Pattern 1: Environment file structure ``` project-root/ ├── m365agents.yml ├── env/ │ ├── .env.dev # Shared dev config (committed) │ ├── .env.dev.user # Dev secrets (gitignored) │ ├── .env.staging # Shared staging config (committed) │ ├── .env.staging.user # Staging secrets (gitignored) │ ├── .env.production # Shared production config (committed) │ └── .env.production.user # Production secrets (gitignored) └── appPackage/ └── manifest.json # Uses ${{VAR}} placeholders ``` ```ini # env/.env.dev — shared config (safe to commit) APP_ENV=dev TEAMS_APP_NAME=MyBot-Dev TEAMS_APP_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx BOT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx BOT_DOMAIN=mybot-dev.azurewebsites.net BOT_ENDPOINT=https://mybot-dev.azurewebsites.net/api/messages AZURE_SUBSCRIPTION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx AZURE_RESOURCE_GROUP_NAME=rg-mybot-dev AAD_APP_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx AAD_APP_OBJECT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx AAD_APP_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ``` ```ini # env/.env.dev.user — secrets (gitignored) SECRET_BOT_PASSWORD=your-bot-password-here SECRET_AAD_APP_CLIENT_SECRET=your-client-secret-here ``` ### Pattern 2: Variable resolution in manifest.json ```jsonc // appPackage/manifest.json — placeholders resolve from active env { "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.26/MicrosoftTeams.schema.json", "manifestVersion": "1.26", "version": "1.0.0", "id": "${{TEAMS_APP_ID}}", "name": { "short": "${{TEAMS_APP_NAME}}", "full": "${{TEAMS_APP_NAME}} - Full" }, "bots": [ { "botId": "${{BOT_ID}}", "scopes": ["personal", "team", "groupChat"] } ], "validDomains": [ "${{BOT_DOMAIN}}" ] } ``` ### Pattern 3: Creating and provisioning a new environment ```bash # Step 1: Provision creates the env files and cloud resources atk provision --env staging -i false # This creates: # env/.env.staging (with APP_ENV=staging, resource IDs) # env/.env.staging.user (with SECRET_* values) # Step 2: Set environment-specific overrides # Edit env/.env.staging to adjust resource names, domains, etc. # Step 3: Deploy to the new environment atk deploy --env staging -i false # Step 4: Test against the new environment # Use agentsplayground CLI or sideload in Teams ``` ### Pattern 4: Cross-Platform Environment Variables Cross-platform bots (Teams + Slack) keep both platforms' credentials in a single `.env` file at the project root. Toolkit `${{VAR}}` placeholders live only in `appPackage/manifest.json` — they are **not** the same as runtime `process.env` vars consumed by Slack or Express. ```ini # .env — cross-platform bot (single file, project root) # Teams credentials (used at runtime by @microsoft/teams.apps) CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx CLIENT_SECRET=your-client-secret TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Agents Toolkit vars (used in appPackage/manifest.json ${{VAR}} placeholders) BOT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx BOT_DOMAIN=mybot.azurewebsites.net TEAMS_APP_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Slack credentials (used at runtime by @slack/bolt) SLACK_BOT_TOKEN=xoxb-your-slack-bot-token SLACK_APP_TOKEN=xapp-your-slack-app-token SLACK_SIGNING_SECRET=your-slack-signing-secret # Server PORT=3978 ``` > **Key difference from Toolkit-managed projects:** Standalone cross-platform examples use a single `.env` file (loaded via `dotenv`) instead of the `env/.env.dev` + `env/.env.dev.user` pair. This is intentional — these examples don't ship `m365agents.yml` or run `atk provision`. ## Key Environment Variables | Variable | Where Set | Purpose | |----------|-----------|---------| | `BOT_ID` | `env/.env.local` (by `atk provision`) | Azure AD app client ID | | `SECRET_BOT_PASSWORD` | `env/.env.local.user` (by `atk provision`) | Azure AD app client secret | | `TEAMS_APP_ID` | `env/.env.local` (by `atk provision`) | Teams app ID for sideloading | | `M365_APP_ID` | `env/.env.local` (by `teamsApp/extendToM365`) | M365 app ID for declarative agents | | `TEAMS_APP_TENANT_ID` | `env/.env.local` (by `atk provision`) | Tenant ID | | `BOT_ENDPOINT` | `env/.env.local` (manual — set to devtunnel URL) | Bot HTTPS endpoint for Teams | | `AZURE_SUBSCRIPTION_ID` | `env/.env.dev` (manual) | Azure subscription for cloud deploy | | `AZURE_RESOURCE_GROUP_NAME` | `env/.env.dev` (manual or by `--resource-group`) | Azure resource group for cloud deploy | | `SECRET_AZURE_OPENAI_API_KEY` | `env/.env.local.user` (manual) | Azure OpenAI API key | | `AZURE_OPENAI_ENDPOINT` | `env/.env.local` (manual) | Azure OpenAI endpoint URL | | `AZURE_OPENAI_DEPLOYMENT_NAME` | `env/.env.local` (manual) | Azure OpenAI deployment name | ## .localConfigs runtime config flow (local development) For `local` and `playground` environments, `m365agents.local.yml` uses `file/createOrUpdateEnvironmentFile` to write env vars to `.localConfigs` (not `env/.env.local`). ```yaml # Example from m365agents.local.yml - uses: file/createOrUpdateEnvironmentFile with: target: ./.localConfigs envs: PORT: 3978 CLIENT_ID: ${{BOT_ID}} CLIENT_SECRET: ${{SECRET_BOT_PASSWORD}} TENANT_ID: ${{TEAMS_APP_TENANT_ID}} ``` **Configuration flow:** - `env/.env.local` → source of truth (edited manually or by `atk provision`) - `m365agents.local.yml` → defines how to transform env vars - `.localConfigs` → generated file your app reads at runtime (created by `atk deploy --env local`) ### Missing environment variables at runtime 1. Check `.localConfigs` exists and has the required values 2. Ensure values are set in `env/.env.local` (or `env/.env.local.user` for secrets) 3. Ensure `m365agents.local.yml` maps those values to `.localConfigs` 4. Run `atk deploy --env local -i false` to regenerate `.localConfigs` ## pitfalls - **Committing `.env.*.user` files** — These contain `SECRET_*` values. Verify `.gitignore` includes `env/.env.*.user` before any commit. - **Putting secrets in `.env.{name}` instead of `.env.{name}.user`** — Non-user env files are committed. Always use the `SECRET_` prefix and store in `.user` files. - **Forgetting to set `AZURE_SUBSCRIPTION_ID` for new environments** — Provisioning fails silently or targets the wrong subscription without this variable. - **Reusing resource groups across environments** — Dev and staging sharing a resource group causes resource name conflicts and accidental overwrites during provisioning. - **Stale env files after re-provisioning** — If you delete cloud resources and re-provision, old IDs in env files may not update. Delete the env files and re-provision from scratch. - **`${{VAR}}` vs `${VAR}` confusion** — Agents Toolkit uses double-brace `${{VAR}}`. Shell-style `${VAR}` is not recognized and will not resolve. - **Confusing Toolkit `${{VAR}}` placeholders with Slack runtime env vars** — Toolkit placeholders resolve at package/provision time in manifest files. Slack env vars (`SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`) are read at runtime by `@slack/bolt`. They live in different worlds — don't put Slack tokens inside `${{}}` brackets in the manifest. - **Missing variables at package time** — If a `${{VAR}}` in manifest.json has no matching env entry, packaging fails. Run `atk validate` first to catch these. - **CI/CD without `.user` files** — In CI, secrets come from pipeline secrets, not `.user` files. Set `SECRET_*` vars as environment variables in the CI runner. ## references - [Manage environments in Agents Toolkit](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/toolkit-v4/teamsfx-multi-env-v4) - [m365agents.yml environmentFolderPath](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/m365-agents-yml-file) - [Provision cloud resources](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/provision) - [Teams app manifest placeholders](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/toolkit-v4/teamsfx-preview-and-customize-app-manifest-v4) ## instructions Do a web search for: - "Microsoft 365 Agents Toolkit multi-environment env files configuration 2025" - "atk provision --env staging multiple environments" - "Agents Toolkit SECRET_ prefix environment variables" Pair with: - `lifecycle-cli.md` — lifecycle hooks that consume environment variables - `../experts/teams/runtime.manifest-ts.md` — manifest `${{VAR}}` placeholder resolution - `../experts/teams/project.scaffold-files-ts.md` — scaffolded project includes env/ directory - `publish.md` — publishing requires fully resolved environment variables ## research Deep Research prompt: "Write a micro expert on Microsoft 365 Agents Toolkit environment management (TypeScript). Cover env/.env.{name} and env/.env.{name}.user file pairs, ${{VAR}} variable resolution in manifest.json and m365agents.yml, SECRET_ prefix convention, built-in environment variables (TEAMS_APP_ID, BOT_ID, BOT_PASSWORD, AZURE_SUBSCRIPTION_ID), creating custom environments with atk provision --env, VS Code sidebar environment switching, and CI/CD environment variable injection. Include canonical patterns for: environment file structure, manifest placeholder resolution, multi-environment setup." -
lifecycle-cli.md 17.6 KB
# Lifecycle and `atk` CLI ## purpose M365 Agents Toolkit lifecycle configuration (`m365agents.yml`) and full `atk` CLI command reference for provisioning, deploying, and managing M365 agents (declarative agents, custom engine agents, Teams bots/tabs/message extensions, Copilot connectors, Office add-ins). ## rules 1. **m365agents.yml is the lifecycle manifest.** Every Agents Toolkit project has an `m365agents.yml` at the project root for dev/cloud deployment, and typically an `m365agents.local.yml` for local development. They define the `provision`, `deploy`, and `publish` lifecycle stages — each stage is an ordered list of actions. `atk provision --env local` runs `m365agents.local.yml`; `atk provision --env dev` runs `m365agents.yml`. 2. **Lifecycle stages run in order: provision → deploy → publish.** Provision creates cloud resources (Azure Bot, App Registration, resource groups). Deploy pushes app code to compute targets. Publish submits the app package to the Teams catalog. 3. **All actions use `uses:` — there is no `runs:` syntax.** Built-in actions like `arm/deploy` or `teamsApp/create` are referenced with `uses: <action-name>`. Custom shell commands use the built-in `script` action: `uses: script` with `with.run: <command>`. Every action accepts a `with:` block for parameters. 4. **Built-in actions cover the full lifecycle.** Key actions: `aadApp/create`, `aadApp/update`, `botAadApp/create`, `botFramework/create`, `arm/deploy`, `azureAppService/zipDeploy`, `azureFunctions/zipDeploy`, `teamsApp/create`, `teamsApp/update`, `teamsApp/validateManifest`, `teamsApp/zipAppPackage`, `file/createOrUpdateEnvironmentFile`. The `aadApp/create` action **must include `generateServicePrincipal: true`** — without it, the service principal is not created and the bot gets `AADSTS7000229`. 5. **Use `m365agents.yml` to replace manual Azure portal setup.** A single `provision` stage automates what otherwise requires 10+ manual `az` CLI commands or Azure Portal steps: Entra ID App Registration (`aadApp/create`), bot identity and password (`botAadApp/create`), Bot Service with Teams channel (`botFramework/create`), ARM/Bicep resource deployment (`arm/deploy`), and Teams app registration (`teamsApp/create`). Each action writes its outputs (IDs, secrets) to env files automatically. For the full manual walkthrough these actions replace, see `../experts/deploy/azure-bot-deploy-ts.md` rules 3–12. 6. **`environmentFolderPath`** in `m365agents.yml` points to the `env/` directory. Defaults to `./env`. All `${{VAR}}` placeholders resolve from the active environment's `.env.{name}` files. 7. **`atk new` scaffolds a project.** Creates project structure with `m365agents.yml`, `m365agents.local.yml`, `env/` folder, `appPackage/`, and starter code. Supports `--capability` for predefined templates and `-i false` for non-interactive mode. ATK CLI version must be > 1.1.5-beta — install with `npm i -g @microsoft/m365agentstoolkit-cli@beta`. 8. **`atk provision` creates cloud resources.** Runs the `provision` stage in the environment-specific YAML. Accepts `--env <name>` to target a specific environment (default: `dev`). Always add `-i false` for non-interactive execution. Creates resources defined by ARM templates or built-in actions. 9. **`atk deploy` pushes code to cloud or generates local config.** Runs the `deploy` stage. For cloud (`--env dev`), builds the project and deploys to Azure. For local (`--env local`), writes runtime credentials to `.localConfigs` via `file/createOrUpdateEnvironmentFile`. Always run `atk provision` before first deploy. 10. **`atk publish` submits to the org catalog.** Runs the `publish` stage. Packages the app and submits it to the Teams Admin Center for org-wide distribution. Requires admin approval after submission. 11. **`atk validate` checks the manifest.** Validates `manifest.json` against the Teams schema before packaging. Catches missing fields, invalid scopes, and schema violations early. 12. **`atk package` creates the app zip bundle.** Generates the `.zip` containing `manifest.json`, icons, and resolved placeholders. Use `atk package --env <name> -i false`. This is the artifact uploaded to Teams or Partner Center. 13. **`atk preview` launches local testing.** Starts the Agents Playground for local testing without deploying to Teams. See `playground.md` for the recommended `agentsplayground` CLI alternative that requires no provisioning. 14. **CI/CD integration uses `atk` CLI with `--env` and `-i false` flags.** GitHub Actions and Azure Pipelines call `atk provision --env staging -i false` and `atk deploy --env staging -i false` in sequence. Store credentials in CI secrets, not in `.env.*.user` files. ## patterns ### Pattern 1: m365agents.yml anatomy (cloud deployment) ```yaml # m365agents.yml — lifecycle configuration for dev/cloud version: v1.11 environmentFolderPath: ./env provision: - uses: teamsApp/create with: name: ${{TEAMS_APP_NAME}} writeToEnvironmentFile: teamsAppId: TEAMS_APP_ID - uses: botAadApp/create with: name: ${{BOT_DISPLAY_NAME}} writeToEnvironmentFile: botId: BOT_ID botPassword: SECRET_BOT_PASSWORD - uses: arm/deploy with: subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} templates: - path: ./infra/azure.bicep parameters: ./infra/azure.parameters.json deploymentName: teams-bot writeToEnvironmentFile: botEndpoint: BOT_ENDPOINT - uses: teamsApp/zipAppPackage with: manifestPath: ./appPackage/manifest.json outputZipPath: ./appPackage/build/appPackage.${{APP_ENV}}.zip outputFolder: ./appPackage/build - uses: teamsApp/update with: appPackagePath: ./appPackage/build/appPackage.${{APP_ENV}}.zip deploy: - uses: cli/runNpmCommand with: args: install - uses: azureAppService/zipDeploy with: artifactFolder: . resourceId: ${{AZURE_APP_SERVICE_RESOURCE_ID}} ``` ### Pattern 1b: m365agents.local.yml anatomy (local development) ```yaml # m365agents.local.yml — lifecycle configuration for local version: v1.11 provision: - uses: teamsApp/create with: name: ${{TEAMS_APP_NAME}}-local-debug writeToEnvironmentFile: teamsAppId: TEAMS_APP_ID - uses: aadApp/create with: name: ${{CONFIG__MANIFEST__NAME}}-aad generateClientSecret: true generateServicePrincipal: true # REQUIRED — without this, AADSTS7000229 signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: clientId: BOT_ID clientSecret: SECRET_BOT_PASSWORD objectId: BOT_OBJECT_ID tenantId: TEAMS_APP_TENANT_ID - uses: botFramework/create with: botId: ${{BOT_ID}} name: ${{CONFIG__MANIFEST__NAME}} messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages description: "" # Optional — driver defaults to ""; templates set it explicitly for clarity channels: - name: msteams deploy: - uses: file/createOrUpdateEnvironmentFile with: target: ./.localConfigs envs: PORT: 3978 CLIENT_ID: ${{BOT_ID}} CLIENT_SECRET: ${{SECRET_BOT_PASSWORD}} TENANT_ID: ${{TEAMS_APP_TENANT_ID}} ``` > **Critical:** `.localConfigs` is what your app reads at runtime, NOT `env/.env.local`. The `file/createOrUpdateEnvironmentFile` action transforms env vars from `env/.env.local` into `.localConfigs`. In the example above, `.localConfigs` `TENANT_ID` comes from `TEAMS_APP_TENANT_ID` in `env/.env.local`. If `TENANT_ID` is missing from `.localConfigs` after deploy, copy the value from `TEAMS_APP_TENANT_ID` in `env/.env.local`. ### Pattern 2: Manual steps replaced by m365agents.yml Each `provision` action in `m365agents.yml` replaces one or more manual Azure CLI / Portal steps. This table maps them: | `m365agents.yml` action | Manual equivalent it replaces | What gets auto-created | |---|---|---| | `aadApp/create` | Azure Portal → App Registrations → New registration, or `az ad app create` + `az ad app credential reset` | Entra ID App with client ID + secret, written to env | | `botAadApp/create` | `az ad app create` (separate bot identity) + `az ad app credential reset` | Bot-specific App ID + password, written to env | | `botFramework/create` | `az bot create --app-type SingleTenant` + `az bot msteams create` | Azure Bot Service resource with Teams channel connected | | `arm/deploy` | `az group create` + `az webapp create` + `az webapp config appsettings set` (or equivalent for Functions/Container Apps) | All Bicep/ARM resources (App Service, plan, settings) | | `teamsApp/create` | Teams client → Apps → Upload a custom app, or Teams Admin Center upload | Teams app registration with `TEAMS_APP_ID` | | `azureAppService/zipDeploy` | `az webapp deploy --src-path <zip>` | Code deployed to App Service | | `teamsApp/zipAppPackage` | Manually zip `manifest.json` + icons with resolved placeholders | App package `.zip` ready for sideload or publishing | > **Bottom line:** `atk provision` + `atk deploy` replaces steps 3–12 in `../experts/deploy/azure-bot-deploy-ts.md`. Two commands instead of ten. ### Pattern 2b: Custom shell commands via `uses: script` There is no `runs:` step in `m365agents.yml`. To run an arbitrary shell command, use the built-in `script` action: ```yaml # Set environment variables for local launch (from templates/configs/local/typescript/m365agents.local.yml.tpl) - uses: script with: run: echo "::set-teamsfx-env BOT_DOMAIN=localhost"; echo "::set-teamsfx-env BOT_ENDPOINT=https://localhost:3978"; # Run a build step in a subdirectory - uses: script with: run: npm run build workingDirectory: ./src ``` The `script` driver also supports `shell:` (e.g., `bash`, `pwsh`) and `redirectTo:` for capturing output. ### Pattern 3: CLI command reference ```bash # Check CLI version (must be > 1.1.5-beta) atk --version # Install / update CLI npm i -g @microsoft/m365agentstoolkit-cli@beta # Scaffold a new project atk new # Interactive wizard atk new -c ai-bot -l typescript -i false # Non-interactive # Provision cloud resources atk provision --env dev -i false # Uses m365agents.yml atk provision --env local -i false # Uses m365agents.local.yml atk provision --env dev --resource-group <rg> --region <region> -i false # Azure resources # Deploy application code / generate .localConfigs atk deploy --env dev -i false # Deploy to Azure atk deploy --env local -i false # Generate .localConfigs # Validate and package atk validate --env dev -i false atk package --env dev -i false # Publish to org catalog atk publish --env dev -i false # Local preview / Agents Playground atk preview # Update an existing Teams app registration atk update # Auth management atk auth login m365 atk auth login azure atk auth list ``` ### Pattern 4: GitHub Actions CI/CD pipeline ```yaml # .github/workflows/deploy.yml name: Deploy Teams Bot on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - name: Install Agents Toolkit CLI run: npm install -g @microsoft/m365agentstoolkit-cli - name: Provision run: atk provision --env production -i false env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AZURE_RESOURCE_GROUP_NAME: ${{ secrets.AZURE_RESOURCE_GROUP_NAME }} # M365 credentials for app registration M365_ACCOUNT_NAME: ${{ secrets.M365_ACCOUNT_NAME }} M365_ACCOUNT_PASSWORD: ${{ secrets.M365_ACCOUNT_PASSWORD }} - name: Deploy run: atk deploy --env production -i false env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} ``` ### Pattern 5: Cross-Platform Projects (no m365agents.yml) Standalone cross-platform examples (Teams + Slack) can skip `m365agents.yml` entirely. These projects: - Use a single `.env` file at the project root (loaded via `dotenv`) instead of `env/.env.{name}` pairs - Still include `appPackage/manifest.json` for sideloading into Teams - Run with `tsx watch` or `node` directly — no `atk provision` or `atk deploy` needed - Manage Azure resources manually (Bot Registration, App Service) rather than through lifecycle actions ``` cross-platform-bot/ ├── appPackage/ │ └── manifest.json # v1.26 schema, ${{VAR}} placeholders for sideloading ├── src/ │ ├── adapters/ │ │ ├── teams-bot.ts # @microsoft/teams.apps handler │ │ └── slack-bot.ts # @slack/bolt handler │ └── index.ts # Starts both platforms ├── .env # All credentials (Teams + Slack) in one file ├── package.json └── tsconfig.json # extends @microsoft/teams.config/tsconfig.node.json ``` > **When to add `m365agents.yml`:** Only when you want `atk provision` / `atk deploy` to manage Azure resources automatically. For teaching examples and local development, manual `.env` + sideloading is simpler. ## pitfalls - **Running `deploy` before `provision`** — Cloud resources must exist first. Always provision before the first deploy. Subsequent deploys can skip provision if resources haven't changed. - **Forgetting `writeToEnvironmentFile`** — Built-in actions that create resources output IDs and secrets. Without `writeToEnvironmentFile`, downstream actions can't reference these values. - **Editing `m365agents.yml` action order** — Actions run top-to-bottom within a stage. Moving `arm/deploy` before `botAadApp/create` breaks because the ARM template references the bot ID. - **Inventing a `runs:` field** — There is no top-level `runs:` step in `m365agents.yml`. For custom shell commands, use the built-in `uses: script` action with a `with.run: <command>` block (and an optional `working-directory:`). - **Committing `.env.*.user` files** — These contain secrets (`SECRET_*` vars). They're gitignored by default — don't override this. - **Missing `--env` in CI** — Without `--env`, the CLI uses the `dev` environment. Production pipelines must specify `--env production` explicitly. - **Confusing `atk` with legacy CLI names** — The CLI was previously called `teamsfx`, then `teamsapp`. The current CLI is `atk` (installed as `@microsoft/m365agentstoolkit-cli`). If docs or examples reference `teamsfx` or `teamsapp`, translate to `atk`. - **ARM template parameter mismatches** — `arm/deploy` parameters must match the Bicep/ARM template's expected inputs. Mismatches cause silent failures during provisioning. - **Missing `generateServicePrincipal: true` in `aadApp/create`** — Without this field, no service principal is created. The bot gets `AADSTS7000229` at runtime. Always include it in the local YAML's `aadApp/create` action. - **`TENANT_ID` not written to `.localConfigs`** — The `file/createOrUpdateEnvironmentFile` may not include `TENANT_ID`. Without it, the SDK acquires tokens from the wrong authority, causing 401 from Bot Connector. Copy from `env/.env.local` if missing. - **Devtunnel URL blacklisted after repeated 401s** — Bot Framework may cache a failing tunnel URL. Even after fixing auth, the bot still gets 401. Create a fresh devtunnel, update `BOT_ENDPOINT`, and re-provision. - **`outputJsonPath` in `teamsApp/zipAppPackage`** — This field does not exist. Use `outputFolder` instead. Using the wrong field causes a silent schema validation error. - **Assuming `description` is required in `botFramework/create`** — It is optional. The driver defaults to `""` when not provided. Templates set `description: ""` explicitly only for clarity, not because the schema rejects its omission. - **Using `botAadApp/create` in local YAML** — `botAadApp/create` is for cloud (`m365agents.yml`). Local templates use `aadApp/create` + `botFramework/create` instead. ## references - [M365 Agents Toolkit overview](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [m365agents.yml schema](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/m365-agents-yml-file) - [Provision cloud resources](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/provision) - [Deploy to Azure](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/deploy) - [CI/CD with Agents Toolkit](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/toolkit-v4/use-cicd-template-v4) - [ATK CLI reference](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/toolkit-cli) ## instructions Do a web search for: - "Microsoft 365 Agents Toolkit m365agents.yml lifecycle configuration 2025" - "atk CLI provision deploy publish commands reference" - "Agents Toolkit CI/CD GitHub Actions Azure Pipelines" Pair with: - `../experts/teams/project.scaffold-files-ts.md` — project scaffolding (what `atk new` creates) - `../experts/deploy/azure-bot-deploy-ts.md` — manual Azure deployment as alternative to Agents Toolkit - `environments.md` — environment files consumed by lifecycle hooks - `publish.md` — detailed publishing workflow ## research Deep Research prompt: "Write a micro expert on Microsoft 365 Agents Toolkit lifecycle management (TypeScript). Cover m365agents.yml anatomy, atk CLI commands (new, provision, deploy, publish, validate, package, preview, update), built-in actions (arm/deploy, azureAppService/deploy, aadApp/create, botAadApp/create, teamsApp/create, teamsApp/validateManifest, teamsApp/zipAppPackage), uses: vs runs: hooks, writeToEnvironmentFile, CI/CD integration with GitHub Actions and Azure Pipelines. Include canonical patterns for: complete m365agents.yml config, CLI command reference cheat sheet, GitHub Actions deployment pipeline." -
manifest-and-yaml.md 6 KB
# Manifest and YAML Action Reference Reference for `appPackage/` files (manifest, declarative agent definition) and the field-by-field reference for `m365agents.yml` actions. For environment files, `${{VAR}}` resolution, `.localConfigs` flow, and the env-var catalog, see [environments.md](environments.md). For the lifecycle YAML structure (provision/deploy/publish stages, action ordering, full anatomy), see [lifecycle-cli.md](lifecycle-cli.md). ## Contents - Key Project Files - Schema Versions Used by Templates - YAML Action Field Reference (Common Mistakes) - signInAudience and Tenant Configuration - Azure OpenAI Configuration ## Key Project Files | File | Purpose | |------|---------| | `appPackage/manifest.json` | App metadata and capabilities (used by all capabilities) | | `appPackage/declarativeAgent.json` | Agent instructions, conversation starters (Declarative Agents only) | | `appPackage/color.png` / `outline.png` | Required icons (192x192 color, 32x32 outline with transparency) | | `env/.env.{name}` / `.env.{name}.user` | Environment variables — see [environments.md](environments.md) | | `.localConfigs` | Runtime config generated by `atk deploy --env local` — see [environments.md](environments.md) | | `m365agents.yml` | Lifecycle config for dev/cloud — see [lifecycle-cli.md](lifecycle-cli.md) | | `m365agents.local.yml` | Lifecycle config for local development | | `m365agents.playground.yml` | Lifecycle config for Agents Playground | | `.m365agentsplayground.yml` | Optional Playground UI config — see [playground.md](playground.md) | ## Schema Versions Used by Templates These are the canonical versions written by the current `atk new` templates (verified against `templates/**` in this repo). Use these in any hand-authored or hand-edited file unless you have a specific reason to pin an older one. | File | Field | Value | |------|-------|-------| | `appPackage/manifest.json` | `$schema` | `https://developer.microsoft.com/en-us/json-schemas/teams/v1.26/MicrosoftTeams.schema.json` | | `appPackage/manifest.json` | `manifestVersion` | `1.26` | | `appPackage/declarativeAgent.json` | `$schema` | `https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.7/schema.json` | | `appPackage/declarativeAgent.json` | `version` | `v1.7` | | `m365agents.yml` / `m365agents.local.yml` / `m365agents.playground.yml` | `version` | `v1.11` (TS/Python templates) — some C# templates still ship `v1.9` | **Notes:** - The Teams `manifestVersion: 1.26` schema is also offered as `vDevPreview` (3 templates use it for early-access features). Stick with `1.26` unless a feature you need only exists in `vDevPreview`. - The `declarativeAgent.json` `version` field is the **schema** version (e.g., `"v1.7"`), not your app version. The app version still lives in `manifest.json`'s top-level `version` field. ```jsonc // appPackage/declarativeAgent.json — minimal v1.7 example { "$schema": "https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.7/schema.json", "version": "v1.7", "name": "My Agent", "description": "Helps with X", "instructions": "You are a helpful assistant that..." } ``` ## YAML Action Field Reference (Common Mistakes) These field names are verified against official ATK templates. Wrong field names cause silent provisioning failures. | Action | Correct Fields | Common Mistake | |--------|---------------|----------------| | `teamsApp/zipAppPackage` | `manifestPath`, `outputZipPath`, `outputFolder` | Using `outputJsonPath` (does not exist — use `outputFolder`) | | `aadApp/create` | `name`, `generateClientSecret`, `generateServicePrincipal`, `signInAudience` | **Must include `generateServicePrincipal: true`** — without it, no service principal is created and bot gets `AADSTS7000229` | | `aadApp/create` writeToEnvironmentFile | `clientId`, `clientSecret`, `objectId` → `BOT_OBJECT_ID` | Writing objectId to `AAD_APP_OBJECT_ID` (wrong — use `BOT_OBJECT_ID` in local templates) | | `botFramework/create` | `botId`, `name`, `messagingEndpoint`, `channels`; optional: `description` | Only `messagingEndpoint` is strictly required by the driver; `description` defaults to `""` if omitted. Templates ship `description: ""` explicitly for clarity. | | `botAadApp/create` | `name` | Only available in cloud (`m365agents.yml`), not used in local templates | | `teamsApp/extendToM365` | `appPackagePath` | Required for declarative agents to surface in M365 Copilot — writes `M365_APP_ID` to env file | ## signInAudience and Tenant Configuration The `aadApp/create` action's `signInAudience` controls which tenants can authenticate: | signInAudience | Use When | Bot Framework Behavior | |---------------|----------|----------------------| | `AzureADMultipleOrgs` | Multi-tenant bots (default in templates) | Bot tokens use audience `{appId}` or `api://{appId}` | | `AzureADMyOrg` | Single-tenant bots | Bot tokens use audience `api://botid-{appId}` — requires custom JWT validation | **Single-tenant gotcha:** If you change `signInAudience` to `AzureADMyOrg`, the Bot Framework sends tokens with audience `api://botid-{appId}`. The Teams SDK v2 (`@microsoft/teams.apps`) only validates `{appId}` and `api://{appId}` by default, causing 401 errors. Workaround: create a custom `HttpPlugin` with `skipAuth: true` and add manual JWT validation that also accepts `api://botid-{appId}`. ## Azure OpenAI Configuration For custom engine agents using Azure OpenAI, add these env vars to the YAML's `file/createOrUpdateEnvironmentFile` action: ```yaml # Add to m365agents.local.yml or m365agents.playground.yml - uses: file/createOrUpdateEnvironmentFile with: target: ./.localConfigs envs: AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} AZURE_OPENAI_DEPLOYMENT_NAME: ${{AZURE_OPENAI_DEPLOYMENT_NAME}} ``` Then set values in `env/.env.local`: ```ini SECRET_AZURE_OPENAI_API_KEY=your-api-key AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o ``` After updating the YAML, run `atk deploy --env local -i false` to write values to `.localConfigs`. -
playground.md 7.6 KB
# Agents Playground ## purpose Agents Playground — the local web-based test harness for testing M365 agents (Teams bots, custom engine agents, message extensions) without deploying to the Teams client. Note: declarative agents and Office add-ins are not supported by the Playground — see [test-teams](../test-teams/) for those. ## rules 1. **Agents Playground is a local web UI for testing.** It provides a browser-based chat interface that simulates a Teams conversation. No Teams client, sideloading, or M365 account required. Recommend Agents Playground first for testing — use Teams only when the user explicitly requests it. 2. **Use the `agentsplayground` CLI to start.** Install with `winget install agentsplayground` (Windows), or `npm install -g @microsoft/m365agentsplayground`. Start with `agentsplayground -e http://localhost:3978/api/messages -c msteams`. The `atk preview` command is an alternative that also opens the playground. 3. **`.m365agentsplayground.yml` configures the playground.** This optional config file in the project root customizes playground behavior — bot endpoint URL, display settings, and test scenarios. 4. **The playground connects to your local bot endpoint.** By default it connects to `http://localhost:3978/api/messages` (or whatever port your bot runs on). Ensure your bot server is running before or alongside the playground. 5. **Send messages to test conversation flows.** Type messages in the playground chat to simulate user input. The bot processes them through the same handler pipeline as in production Teams. 6. **Card actions work in the playground.** Adaptive Card actions (submit, execute) are supported. Test card interactions without deploying to Teams. 7. **Activity simulation for advanced testing.** The playground can simulate Teams-specific activities like `conversationUpdate` (member added/removed), `messageReaction`, and `invoke` activities that are hard to trigger manually. 8. **The playground does NOT replace Teams client testing.** It simulates core messaging and card interactions but does not support: SSO/OAuth popups, message extensions, task modules, meeting-specific features, or the full Teams app manifest experience. Always do a final validation in the real Teams client. 9. **`agentsplayground` supports environment-specific config.** Pass `--client-id`, `--client-secret`, and `--tenant-id` flags to test authenticated agents, or use `--channel-id` to emulate different channels (`msteams`, `emulator`, `webchat`, `directline`). 10. **Hot reload works with the playground.** If your bot server supports hot reload (e.g., `nodemon` or `tsx watch`), changes to bot code are reflected immediately without restarting the playground. ## patterns ### Pattern 1: Starting Agents Playground ```bash # Option 1: agentsplayground CLI (recommended — no provisioning needed) npm run dev # Start bot in background agentsplayground -e http://localhost:3978/api/messages -c msteams # New terminal # Option 2: With authentication credentials agentsplayground -e http://localhost:3978/api/messages -c msteams \ --client-id <CLIENT_ID> --client-secret <CLIENT_SECRET> --tenant-id <TENANT_ID> ``` ### Pattern 2: .m365agentsplayground.yml configuration ```yaml # .m365agentsplayground.yml — optional playground configuration version: v1.0 # Bot endpoint the playground connects to botEndpoint: http://localhost:3978/api/messages # Display name shown in the playground chat header botName: My Teams Bot # Optional: pre-configured test messages testScenarios: - name: "Greeting" message: "Hello" - name: "Help command" message: "help" - name: "Complex query" message: "What are the sales figures for Q4?" ``` ### Pattern 3: Local development workflow with playground ```jsonc // package.json — scripts for playground development { "scripts": { "dev": "tsx watch src/index.ts", "playground": "agentsplayground -e http://localhost:3978/api/messages -c msteams" } } ``` ```typescript // src/index.ts — bot entry point import { Application, TurnState } from '@microsoft/teams-ai'; const app = new Application<TurnState>({ // In playground mode, the bot runs locally // No special config needed — same code works in playground and Teams }); app.message('/test', async (ctx) => { await ctx.send('Playground test successful!'); }); app.message(/.*/, async (ctx) => { await ctx.send(`You said: ${ctx.activity.text}`); }); // Start the server const port = process.env.PORT || 3978; app.listen(port, () => { console.log(`Bot running at http://localhost:${port}`); }); ``` ## pitfalls - **Bot server not running when playground starts** — The playground connects to your local bot endpoint. If the server isn't running, you'll see connection errors. Start the bot first or use `concurrently`. - **Wrong port in playground config** — If your bot runs on a non-default port, update `.m365agentsplayground.yml` or the `BOT_ENDPOINT` env variable. - **Testing SSO in the playground** — OAuth/SSO flows require the real Teams client. The playground cannot simulate the Teams SSO token exchange. Use the playground for message/card testing, Teams client for auth flows. - **Assuming playground = Teams client** — Message extensions, task modules, meeting features, and app installation flows are not available in the playground. Always validate in Teams before publishing. - **Forgetting `--env` for environment-specific testing** — Without `--env`, `atk preview` uses the default dev environment. For the `agentsplayground` CLI, pass auth credentials explicitly via `--client-id`, `--client-secret`, `--tenant-id`. - **Firewall blocking localhost** — Some corporate networks block local WebSocket connections. If the playground can't connect, check firewall rules for localhost ports. - **Hot reload not configured** — Without `tsx watch` or `nodemon`, code changes require manual server restart. Set up hot reload for efficient playground development. - **Card rendering differences** — Adaptive Card rendering in the playground may differ slightly from the Teams client. Complex card layouts should be verified in Teams. ## references - [Test with Agents Playground](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/debug-overview) - [ATK preview command](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/toolkit-cli) - [Local debug overview](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/debug-overview) - [Agents Toolkit VS Code extension](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/install-teams-toolkit) ## instructions Do a web search for: - "Microsoft 365 Agents Playground local testing Teams bot 2025" - "atk preview Agents Playground configuration" - ".m365agentsplayground.yml configuration options" Pair with: - `../experts/teams/dev.debug-test-ts.md` — broader debugging and testing patterns - `lifecycle-cli.md` — `atk preview` is part of the CLI command set - `environments.md` — playground uses environment-specific config - `../experts/teams/runtime.app-init-ts.md` — bot entry point that playground connects to ## research Deep Research prompt: "Write a micro expert on Microsoft 365 Agents Playground for testing Teams bots locally (TypeScript). Cover what the playground is, how to start it (atk preview, VS Code command), .m365agentsplayground.yml configuration, testing capabilities (messages, card actions, activity simulation), limitations vs real Teams client (no SSO, no message extensions, no task modules), hot reload workflow, and environment-specific preview. Include canonical patterns for: starting the playground, playground config file, local dev workflow with concurrent bot server and playground." -
publish.md 9.3 KB
# Publishing ## purpose Publishing workflow for M365 agents — from local sideloading through org catalog distribution to public Teams Store / Microsoft AppSource submission. Applies to Teams apps, declarative agents, message extensions, and Copilot connectors; the org-catalog and Store stages are the same for all of them. ## rules 1. **Three publishing stages: sideload → org catalog → Teams Store.** Each stage expands the audience. Sideload is developer-only. Org catalog reaches your tenant. Teams Store is public to all Teams users. 2. **Sideloading is for development and testing.** Upload the app package directly in Teams (`Apps → Manage your apps → Upload a custom app`). No admin approval needed, but only you can see the app. Requires "Upload custom apps" policy to be enabled. 3. **`atk publish` submits to the org catalog.** The command packages the app and submits it to the Teams Admin Center. An admin must approve the submission before the app appears in the org's app catalog. 4. **Admin approval happens in Teams Admin Center.** After `atk publish`, admins review the submission at `admin.teams.microsoft.com → Teams apps → Manage apps`. They can approve, reject, or request changes. 5. **`atk validate` catches manifest errors before publishing.** Always validate before publishing. The command checks the manifest against the Teams schema, verifies required fields, and flags common issues. Fix all validation errors before submitting. 6. **`atk package` creates the submission artifact.** Generates a `.zip` bundle containing the resolved `manifest.json` and icon files. This is the file that gets uploaded to the org catalog or Partner Center. 7. **Version bumping is required for updates.** When publishing an update to an already-published app, increment the `version` field in `manifest.json`. The org catalog and Teams Store reject submissions with the same version as an existing entry. 8. **`atk update` pushes changes to an existing Teams app.** Updates the app registration without creating a new one. Use this after changing manifest properties, bot endpoints, or permissions. 9. **Teams Store submission goes through Partner Center.** To publish publicly, submit the app package at `partner.microsoft.com`. Microsoft reviews the app against validation policies (functionality, security, compliance). Review takes 1-2+ weeks. 10. **Teams Store validation requirements are strict.** The app must: work correctly in all declared scopes, handle errors gracefully, not crash or hang, follow Teams design guidelines, include privacy policy and terms of use URLs, and pass automated testing. 11. **Pre-submission checklist.** Before any publishing: validate manifest (`atk validate`), test in real Teams client (not just playground), verify all URLs are HTTPS and reachable, confirm icons meet size requirements (192x192 color, 32x32 outline), and ensure the app works in all declared scopes (personal, team, groupChat). 12. **App update propagation varies by stage.** Sideloaded updates are immediate. Org catalog updates require admin re-approval. Teams Store updates require Microsoft re-review. ## patterns ### Pattern 1: Publishing to org catalog ```bash # Step 1: Validate the manifest atk validate --manifest-file ./appPackage/manifest.json # Fix any reported errors before continuing # Step 2: Package the app atk package --manifest-file ./appPackage/manifest.json \ --output-package-file ./build/appPackage.zip # Step 3: Publish to org catalog (submits for admin approval) atk publish --env dev -i false # Step 4: Notify your Teams admin to approve in Admin Center # admin.teams.microsoft.com → Teams apps → Manage apps → search for your app # Step 5: After approval, users find the app in Teams → Apps → Built for your org ``` ### Pattern 2: App update workflow ```bash # Step 1: Bump the version in manifest.json # Before: "version": "1.0.0" # After: "version": "1.1.0" # Step 2: Validate the updated manifest atk validate --manifest-file ./appPackage/manifest.json # Step 3: Update the Teams app registration atk update # Step 4: Re-package with the new version atk package --manifest-file ./appPackage/manifest.json \ --output-package-file ./build/appPackage.zip # Step 5: Re-publish (triggers admin re-approval for org catalog) atk publish ``` ### Pattern 3: Manifest fields required for publishing ```jsonc // appPackage/manifest.json — fields required for org catalog and Teams Store { "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.26/MicrosoftTeams.schema.json", "manifestVersion": "1.26", "version": "1.0.0", // Must increment for updates "id": "${{TEAMS_APP_ID}}", "developer": { "name": "Your Company", // Required "websiteUrl": "https://yourcompany.com", // Required — HTTPS "privacyUrl": "https://yourcompany.com/privacy", // Required for Store "termsOfUseUrl": "https://yourcompany.com/terms" // Required for Store }, "name": { "short": "My Bot", // Max 30 chars "full": "My Bot for Teams" // Max 100 chars }, "description": { "short": "A helpful Teams bot", // Max 80 chars, required "full": "Detailed description of what the bot does, its features, and how to use it. This appears in the Teams Store listing." // Max 4000 chars }, "icons": { "color": "color.png", // 192x192 px, full color "outline": "outline.png" // 32x32 px, transparent + white only }, "bots": [ { "botId": "${{BOT_ID}}", "scopes": ["personal", "team", "groupChat"], "commandLists": [ { "scopes": ["personal"], "commands": [ { "title": "help", "description": "Show help information" }, { "title": "start", "description": "Start a new conversation" } ] } ] } ], "permissions": ["identity", "messageTeamMembers"], "validDomains": ["${{BOT_DOMAIN}}"] } ``` ## pitfalls - **Publishing without validating first** — `atk validate` catches schema errors, missing fields, and invalid URLs. Skipping it means surprises during admin review or Store rejection. - **Same version number on update** — The org catalog and Store reject duplicate versions. Always bump `version` in manifest.json before re-publishing. - **Missing privacy/terms URLs** — Required for Teams Store submission. Org catalog may accept without them, but add them early to avoid rework. - **Icons wrong size or format** — Color icon must be 192x192 PNG. Outline icon must be 32x32 PNG with only white and transparent pixels. Wrong sizes cause validation failure. - **Not testing in all declared scopes** — If manifest declares `personal`, `team`, and `groupChat` scopes, the app must work correctly in all three. Store review tests all declared scopes. - **Forgetting admin approval step** — `atk publish` only submits. The app isn't available until an admin approves it in the Teams Admin Center. Plan for this delay. - **Testing only in playground before publishing** — The playground doesn't cover SSO, message extensions, or Teams-specific behaviors. Always do a full sideload test in the real Teams client. - **Partner Center submission without meeting all policies** — Microsoft's validation checks functionality, security, performance, and compliance. Read the [validation guidelines](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/prepare/teams-store-validation-guidelines) before submitting. ## references - [Publish Teams apps overview](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-publish-overview) - [Publish to org catalog](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) - [Submit to Teams Store](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/publish) - [Teams Store validation guidelines](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/prepare/teams-store-validation-guidelines) - [ATK publish command](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/toolkit-cli) - [App manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) ## instructions Do a web search for: - "Microsoft Teams app publishing org catalog admin approval 2025" - "atk publish validate package CLI commands" - "Teams Store submission Partner Center validation requirements" Pair with: - `../experts/teams/runtime.manifest-ts.md` — manifest structure and schema requirements - `lifecycle-cli.md` — CLI commands for validate, package, publish - `environments.md` — environment-specific publishing (dev vs production) - `../experts/deploy/azure-bot-deploy-ts.md` — deploy must succeed before publishing ## research Deep Research prompt: "Write a micro expert on Microsoft Teams app publishing workflow (TypeScript). Cover the three publishing stages (sideload, org catalog, Teams Store), atk publish / validate / package / update commands, admin approval flow in Teams Admin Center, Partner Center submission for Teams Store, validation requirements (manifest schema, icons, scopes, privacy/terms URLs), version bumping for updates, and the complete pre-submission checklist. Include canonical patterns for: org catalog publishing steps, app update workflow, manifest fields required for Store submission." -
README.md 4.2 KB
<!-- SOURCE OF TRUTH: microsoft/microsoft-365-agents-toolkit PATH: packages/vscode-extension/skills/microsoft-365-agents-toolkit/toolkit/ This folder is the canonical reference for the M365 Agents Toolkit toolchain. Other repos may sync this folder via their knowledge-sync workflow — do NOT edit copies downstream; open a PR upstream instead. --> # M365 Agents Toolkit — Toolchain Knowledge Reference material for the **toolchain** itself: the `atk` CLI, the `m365agents.yml` lifecycle, environment files, project templates, manifests, the Agents Playground, and publishing. This content is **capability-agnostic** — it applies to every project type the toolkit supports: Teams bots, declarative agents, API plugins, Copilot connectors, Office add-ins, custom engine agents, RAG agents, message extensions, tabs. For SDK code patterns (handlers, AI prompts, Adaptive Cards, MCP, OAuth, etc.), see the sibling [../experts/](../experts/) folder. For Slack-vs-Teams platform comparison, see [../docs/](../docs/). ## Files | File | Scope | |---|---| | [templates.md](templates.md) | Full `atk new -c` capability catalog: declarative agents (8 variants), Copilot connectors, Office add-ins, Teams bots/tabs/message extensions, custom engine agents, RAG agents | | [commands.md](commands.md) | `atk` CLI reference outside the lifecycle: `add action`, `add auth-config`, `regenerate action`, `share`, `collaborator`, `env`, `install/uninstall`, `upgrade`, `doctor` | | [lifecycle-cli.md](lifecycle-cli.md) | Lifecycle CLI commands (`provision`, `deploy`, `package`, `validate`, `preview`) and the full `m365agents.yml` action catalog | | [manifest-and-yaml.md](manifest-and-yaml.md) | `appPackage/manifest.json` + `appPackage/declarativeAgent.json`, YAML action field reference, common-mistake table, `signInAudience` configuration | | [environments.md](environments.md) | `env/.env.{name}` + `.user` files, `${{VAR}}` resolution, `SECRET_` prefix, `.localConfigs` flow, multi-environment isolation | | [playground.md](playground.md) | Agents Playground (`agentsplayground` CLI, `.m365agentsplayground.yml`, channel emulation, activity simulation) | | [publish.md](publish.md) | Publishing workflow: sideload → org catalog (`atk publish`) → Teams Store / Partner Center; version bumping, validation requirements | ## Capability matrix | Capability | Templates | What applies from this folder | |---|---|---| | **Declarative agents** | `declarative-agent`, `declarative-agent-action*`, `declarative-agent-with-*`, `declarative-agent-meta-os-*`, `declarative-agent-typespec` | All except `playground.md` (DAs run in M365 Copilot, not Playground). Sideload via `M365_APP_ID`. | | **API plugins** | `declarative-agent-action-from-existing-api`, `add action` | All. Use `commands.md` for `atk add action` and `manifest-and-yaml.md` for OpenAPI integration. | | **Copilot connectors** | `copilot-connector` | `templates.md`, `commands.md`, `lifecycle-cli.md`, `environments.md`. | | **Custom engine agents** | `basic-custom-engine-agent`, `weather-agent`, `foundry-agent-to-m365`, `coffee-agent`, `data-analyst-agent-v2` | All. Compute deploy via `lifecycle-cli.md` (`arm/deploy` + `azureAppService/zipDeploy`). | | **Teams bots / tabs / message extensions** | `bot`, `tab`, `message-extension`, `teams-agent*`, `teams-collaborator-agent`, `bot-sso` | All. Pair with [../experts/teams/](../experts/teams/) for SDK code patterns. | | **Office add-ins** | `office-addin-outlook-taskpane`, `office-addin-wxpo-taskpane`, `office-addin-excel-cfshortcut`, `office-addin-config` | `templates.md`, `commands.md`, `lifecycle-cli.md`. Add-in-specific runtime is out of scope here. | ## Cross-references - Workflow how-tos that consume this knowledge live one level up: [../create-project/](../create-project/), [../test-playground/](../test-playground/), [../test-teams/](../test-teams/), [../provision-deploy/](../provision-deploy/), [../troubleshoot/](../troubleshoot/). - For Teams-bot SDK code (DevtoolsPlugin, ConsoleLogger, runtime handlers, Adaptive Cards): see [../experts/teams/](../experts/teams/). - For deploying without ATK (manual `az` CLI walkthrough): see [../experts/deploy/azure-bot-deploy-ts.md](../experts/deploy/azure-bot-deploy-ts.md). -
templates.md 6.7 KB
# Agent Templates Reference ## Contents - CLI Capabilities (all atk new -c options) - Declarative Agents (creating, options) - Custom Engine Agents (creating, languages) - Teams Agents (creating, languages) - Other Templates (bot, tab, message extension) - Best Practices (language matching) - Template Selection Guide ## CLI Capabilities (atk new -c) Use `atk new -c <capability>` to create projects. Available capabilities: | Capability | Description | |------------|-------------| | `declarative-agent` | Declarative Agent | | `declarative-agent-action` | Declarative Agent with Action from Scratch | | `declarative-agent-action-bearer` | Declarative Agent with Action from Scratch (Bearer Token) | | `declarative-agent-action-oauth` | Declarative Agent with Action from Scratch (OAuth) | | `declarative-agent-action-from-existing-api` | Declarative Agent with Action from Existing API | | `declarative-agent-with-action-from-mcp` | Declarative Agent with Action from MCP Server | | `declarative-agent-with-graph-connector` | Declarative Agent with Copilot Connector | | `declarative-agent-meta-os-new-project` | Declarative Agent for MetaOS (New Project) | | `declarative-agent-meta-os-upgrade-project` | Declarative Agent for MetaOS (Upgrade Project) | | `declarative-agent-typespec` | Declarative Agent from TypeSpec | | `basic-custom-engine-agent` | Basic Custom Engine Agent | | `weather-agent` | Weather Agent | | `foundry-agent-to-m365` | Foundry Agent to M365 | | `copilot-connector` | Copilot Connector | | `teams-agent` | General Teams Agent | | `teams-agent-rag-customize` | Teams Agent with Data from Customized Source | | `teams-agent-rag-azure-ai-search` | Teams Agent with Data from Azure AI Search | | `teams-agent-rag-custom-api` | Teams Agent with Data from Custom API using OpenAPI Spec | | `teams-collaborator-agent` | Teams Collaborator Agent | | `tab` | Tab | | `bot` | Simple Bot | | `message-extension` | Message Extension | | `office-addin-outlook-taskpane` | Outlook Task Pane Add-in | | `office-addin-wxpo-taskpane` | Office Task Pane Add-in | | `office-addin-excel-cfshortcut` | Excel Custom Functions | | `office-addin-config` | Office Add-in Common Configuration | ## Declarative Agents (Copilot Extensions) ### Creating a Declarative Agent ```bash # Basic declarative agent (no backend service needed) atk new -c declarative-agent -n myagent -i false # Declarative agent with new API plugin (creates backend) atk new -c declarative-agent-action -l typescript -n myagent -i false # Declarative agent with existing OpenAPI spec (requires -a and -o with operation IDs) # First inspect the OpenAPI spec to find operation IDs, then pass them: atk new -c declarative-agent-action-from-existing-api -n myagent -a <openapi-spec-url-or-path> -o "GET /repairs" -o "POST /repairs" -i false # Declarative agent with MCP Server atk new -c declarative-agent-with-action-from-mcp -n myagent -i false ``` **Important Notes:** - Basic declarative agents (`declarative-agent`) do NOT require a programming language - `declarative-agent-action`: Use `-l typescript/javascript/csharp` (creates new backend API) - `declarative-agent-action-from-existing-api`: Requires `-a` (OpenAPI spec) and `-o` (operation IDs from the spec, e.g., `"GET /repairs"`) ### Declarative Agent Options | Option | Values | Description | |--------|--------|-------------| | `--openapi-spec-location -a` | file path or URL | **Required for existing API**: OpenAPI spec location | | `--api-operation -o` | operation IDs (e.g., `"GET /path"`) | **Required for existing API**: Actual operation IDs from OpenAPI spec. Use multiple `-o` for multiple operations | | `--api-auth` | `none`, `api-key`, `bearer-token`, `oauth` | API authentication type | ## Custom Engine Agents (M365 SDK-based) ### Creating a Custom Engine Agent ```bash # Basic custom engine agent atk new -c basic-custom-engine-agent -l typescript -n myagent -i false # Weather agent sample atk new -c weather-agent -l typescript -n myagent -i false ``` | Capability | Languages | Description | |------------|-----------|-------------| | `basic-custom-engine-agent` | typescript, javascript, python | Basic agent with M365 SDK and LLM | | `weather-agent` | typescript, javascript, csharp | Weather forecast agent with LangChain | ## Teams Agents (Teams AI Library) ### Creating a Teams Agent ```bash # Basic Teams chatbot atk new -c teams-agent -l typescript -n mybot -i false # Teams Agent with RAG (custom data source) atk new -c teams-agent-rag-customize -l typescript -n mybot -i false # Teams Agent with Azure AI Search atk new -c teams-agent-rag-azure-ai-search -l typescript -n mybot -i false ``` | Capability | Languages | Description | |------------|-----------|-------------| | `teams-agent` | typescript, javascript, csharp, python | General Teams Agent | | `teams-agent-rag-customize` | typescript, javascript, csharp, python | Teams Agent with Customized Data Source | | `teams-agent-rag-azure-ai-search` | typescript, javascript, csharp, python | Teams Agent with Azure AI Search | | `teams-agent-rag-custom-api` | typescript, javascript, csharp, python | Teams Agent with Custom API | | `teams-collaborator-agent` | typescript, csharp | Teams Collaborator Agent | ## Other Templates ```bash # Simple Bot atk new -c bot -l typescript -n mybot -i false # Tab atk new -c tab -l typescript -n mytab -i false # Message Extension atk new -c message-extension -l typescript -n myme -i false ``` | Capability | Languages | Description | |------------|-----------|-------------| | `bot` | typescript, javascript, python, csharp | Simple Bot | | `tab` | typescript, csharp | Tab | | `message-extension` | typescript, python, csharp | Message Extension | | `copilot-connector` | typescript, csharp | Copilot Connector | ## Best Practices ### Before Creating a Project 1. **Use non-interactive mode** - Always use `-i false` for scripted creation 2. **Match language to capability**: - Basic declarative agents (`declarative-agent`): NO language flag needed - API plugin agents (`declarative-agent-action`): `-l typescript/javascript/csharp` - Custom Engine agents: `-l typescript/javascript/python` - Teams agents: `-l typescript/javascript/csharp/python` ## Template Selection Guide **Choose Declarative Agents when:** - Extending Microsoft 365 Copilot with custom instructions - Integrating APIs as actions without running custom code - Need zero-infrastructure deployment **Choose Custom Engine Agents when:** - Need custom LLM integration (Azure OpenAI, OpenAI, etc.) - Require complex multi-turn conversations - Building with LangChain or other AI frameworks **Choose Teams Agents when:** - Building chat bots specifically for Microsoft Teams - Need RAG (Retrieval Augmented Generation) capabilities - Require Teams-specific features (channels, meetings, etc.)
-
-
troubleshoot
-
troubleshoot.md 20.2 KB
# Troubleshooting Consolidated troubleshooting for ATK projects — provisioning, runtime, Playground, and Teams issues. ## Error Code Quick Reference | Error Code | Section | |------------|---------| | `Ext.FindProcessError` | [Port already in use](#port-already-in-use) | | `Ext.PortsConflictError` | [Port already in use](#port-already-in-use) | | `fileCreateOrUpdateEnvironmentFile.MissingEnvironmentVariablesError` | [Missing environment variables at runtime](#missing-environment-variables-at-runtime) | | `botFrameworkCreate.MissingEnvironmentVariablesError` | [Missing environment variables at runtime](#missing-environment-variables-at-runtime) | | `devToolInstall.TestToolInstallationError` | [Agents Playground installation failed](#agents-playground-installation-failed) | | `devToolInstall.FuncInstallationError` | [Azure Functions Core Tools installation failed](#azure-functions-core-tools-installation-failed) | | `Ext.DebugTestToolFailedToStartError` | [Playground won't start](#playground-wont-start) | | `AppStudioPlugin.ManifestValidationFailed` | [Manifest validation failed](#manifest-validation-failed) | | `armDeploy.DeployArmError` | [ARM deployment failed](#arm-deployment-failed) | | `Ext.DevTunnelOperationError` | [Dev tunnel operation failed](#dev-tunnel-operation-failed) | ## Common Provisioning Issues | Symptom | Cause | Fix | |---------|-------|-----| | YAML schema validation error during `atk provision` | Wrong field names in `m365agents.yml` or `m365agents.local.yml` | Check [field reference](../toolkit/manifest-and-yaml.md). Common: `outputJsonPath` → `outputFolder`, missing `description: ""` in `botFramework/create` | | `teamsApp/validateManifest` fails with network error | Schema URL (`https://developer.microsoft.com/...`) unreachable | Remove `teamsApp/validateManifest` from local YAML, or retry with network access | | `AADSTS7000229: missing service principal` | `aadApp/create` missing `generateServicePrincipal: true` | Add `generateServicePrincipal: true` to `aadApp/create` in YAML, re-provision — see [Missing Service Principal](#missing-service-principal-aadsts7000229) | | 401 from Bot Connector (bot receives messages but can't reply) | `TENANT_ID` missing from `.localConfigs` → SDK uses wrong token authority | Copy `TENANT_ID` from `env/.env.local` to `.localConfigs` — see [Missing TENANT_ID](#missing-tenant_id-wrong-token-authority--401) | | Bot still gets 401 after fixing auth issues | Devtunnel URL blacklisted by Bot Framework due to repeated prior failures | Create a fresh devtunnel (`devtunnel delete` + `devtunnel create`), update `BOT_ENDPOINT`, re-provision — see [Blacklisted Devtunnel URL](#blacklisted-devtunnel-url) | | `Authorization: Bearer null` (401) at runtime | `clientId`/`clientSecret` not passed to Teams SDK `App` constructor | Pass credentials explicitly: `new App({ adapter: { credentials: { clientId, clientSecret, tenantId } } })` | | 401 after changing to single-tenant (`AzureADMyOrg`) | Tenant mismatch — SDK doesn't accept `api://botid-{appId}` audience | Add custom JWT middleware accepting all audience formats, or stay with `AzureADMultipleOrgs` | | Stale bot after re-provisioning | Old AAD app still referenced by Bot Framework registration | Delete `env/.env.local` and `env/.env.local.user`, re-run `atk provision --env local -i false` + `atk deploy --env local -i false` | | Bot works in Playground but not in Teams | Missing dev tunnel or wrong `BOT_ENDPOINT` | Start `devtunnel host -p 3978 --allow-anonymous`, set `BOT_ENDPOINT` in `env/.env.local` before provisioning | | Manifest v1.25 validation fails with `"team"` scope | `supportsChannelFeatures` required at runtime but rejected by v1.25 schema | Use `"personal"` scope only in v1.25, or use devPreview schema that defines the property | ## YAML Schema Errors Common field name mistakes in `m365agents.local.yml`: - `outputJsonPath` does not exist — use `outputFolder` in `teamsApp/zipAppPackage` - `AAD_APP_OBJECT_ID` — use `BOT_OBJECT_ID` in local YAML's `aadApp/create` writeToEnvironmentFile - Missing `description: ""` in `botFramework/create` — this field is required See [../toolkit/manifest-and-yaml.md](../toolkit/manifest-and-yaml.md) for the full field reference. ## Known ATK Pitfalls | Pitfall | Symptom | Fix | |---------|---------|-----| | `aadApp/create` missing `generateServicePrincipal: true` | `AADSTS7000229: missing service principal in tenant` when bot calls Bot Connector | Add `generateServicePrincipal: true` to `aadApp/create` in YAML, then re-provision | | `TENANT_ID` not written to `.localConfigs` | SDK defaults to `botframework.com` tenant → 401 from Bot Connector (wrong issuer/tid in token) | Copy `TENANT_ID` from `env/.env.local` (where `aadApp/create` writes it) into `.localConfigs` | | Devtunnel URL blacklisted after repeated 401s | Bot still gets 401 even after fixing auth — Bot Framework cached the tunnel URL as failing | Delete old tunnel, create a fresh one, update `BOT_ENDPOINT`, re-provision | ## Authorization / 401 Issues ### Missing Service Principal (AADSTS7000229) The `aadApp/create` action in `m365agents.local.yml` must include `generateServicePrincipal: true` to create the service principal (enterprise application) alongside the app registration. Without it, the client credentials grant fails: ``` AADSTS7000229: The client application <BOT_ID> is missing service principal in the tenant <TENANT_ID> ``` **Fix — add `generateServicePrincipal: true` to your YAML:** ```yaml - uses: aadApp/create with: name: ${{CONFIG__MANIFEST__NAME}}-aad generateClientSecret: true generateServicePrincipal: true # ← REQUIRED — without this, no SP is created signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: clientId: BOT_ID clientSecret: SECRET_BOT_PASSWORD objectId: BOT_OBJECT_ID ``` Then re-provision: ```bash atk provision --env local -i false ``` > **Manual fallback** (if you can't re-provision): `az ad sp create --id <BOT_ID>` ### Blacklisted Devtunnel URL After repeated 401 failures (e.g., from a missing service principal), Bot Framework may blacklist the devtunnel URL. Even after fixing the auth issue, the bot continues to get 401. **Fix — create a fresh devtunnel:** ```bash devtunnel delete <old-tunnel-id> devtunnel create --allow-anonymous devtunnel port create -p 3978 devtunnel host ``` Update `BOT_ENDPOINT` in `env/.env.local` with the new tunnel URL, then re-provision: ```bash atk provision --env local -i false atk deploy --env local -i false ``` ### Missing TENANT_ID (wrong token authority → 401) When `TENANT_ID` is not set in `.localConfigs` or environment, the Teams SDK (both Python and Node) may default to acquiring tokens from the shared `botframework.com` tenant (`d6d49420-f39b-4df7-a1dc-d59a935871db`) instead of your home tenant. The resulting token has: - Wrong `iss` (issuer) and `tid` (tenant) claims - No `roles` assigned Bot Connector rejects this token with **401 Unauthorized**. **Diagnose:** ```bash # Check if TENANT_ID is set grep TENANT_ID .localConfigs # Or in env file grep TENANT_ID env/.env.local ``` **Fix:** ```bash # Copy TENANT_ID from env file (aadApp/create writes it there, not to .localConfigs) grep TENANT_ID env/.env.local # Add the value to .localConfigs echo TENANT_ID=<tenant-id-from-env-file> >> .localConfigs ``` This ensures the SDK uses `https://login.microsoftonline.com/<your-tenant-id>` instead of `https://login.microsoftonline.com/botframework.com`. > **Python SDK note:** `TokenManager._resolve_tenant_id()` falls back to `botframework.com` when `TENANT_ID` is unset. Always set it explicitly. ### `Authorization: Bearer null` The Teams SDK v2 `App` constructor requires explicit credentials. If `clientId`/`clientSecret` are not passed, the auth header will be `Bearer null`: ```typescript const app = new App({ adapter: { credentials: { clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId: process.env.TENANT_ID, // required for single-tenant }, }, }); ``` Ensure `.localConfigs` has `CLIENT_ID` and `CLIENT_SECRET`. Run `atk deploy --env local -i false` to regenerate. ### 401 with single-tenant bots (`AzureADMyOrg`) If `aadApp/create` uses `signInAudience: AzureADMyOrg`, Bot Framework tokens have audience `api://botid-{appId}`. The Teams SDK v2 only validates `{appId}` and `api://{appId}` by default, causing 401 errors. Solutions: 1. **Stay with `AzureADMultipleOrgs`** (recommended for most scenarios) 2. **Create custom auth middleware** with `skipAuth: true` on the `HttpPlugin`, then manually validate JWT tokens accepting all three audience formats: `{appId}`, `api://{appId}`, `api://botid-{appId}` ## Stale Bot Framework Registration If you delete and re-create Azure AD apps, the Bot Framework registration may still reference the old app ID. Fix: 1. Delete `env/.env.local` and `env/.env.local.user` 2. Re-run `atk provision --env local -i false` 3. Re-run `atk deploy --env local -i false` 4. Re-sideload the Teams app ## Playground Issues ### Playground won't start **Error code:** `Ext.DebugTestToolFailedToStartError` Check if port 56150 is in use: ```bash # Windows netstat -ano | findstr :56150 # macOS / Linux lsof -i :56150 ``` The playground will automatically find an available port if 56150 is taken. If it still fails to start: 1. Check the output/terminal for error messages. 2. Ensure Agents Playground is installed correctly — see [Agents Playground installation failed](#agents-playground-installation-failed). 3. Try launching manually: `./devTools/playground/node_modules/.bin/agentsplayground start` ### Bot not responding in Playground 1. Verify bot is running on specified endpoint 2. Check bot logs for errors 3. Ensure your bot endpoint is accessible: ```bash curl http://localhost:3978/api/messages ``` ## Teams Issues ### Teams shows "app not available" This usually means BOT_ENDPOINT requires HTTPS. Use Agents Playground instead, or ensure dev tunnel is running and BOT_ENDPOINT is properly configured. ### App not loading Verify `M365_APP_ID` (for declarative agents) or `TEAMS_APP_ID` (for bots/tabs) exists in `env/.env.local`. ### Manifest validation failed **Error code:** `AppStudioPlugin.ManifestValidationFailed` The app manifest (`appPackage/manifest.json`) failed validation against the Teams schema. 1. **Check the error details** — the output lists which fields are invalid. 2. **Common fixes:** - Missing required fields (e.g., `description`, `version`, `icons`). - Invalid scope — e.g., `"team"` scope with `supportsChannelFeatures` on schema v1.25 (use `"personal"` only, or switch to devPreview schema). - Schema URL mismatch — ensure `$schema` points to the correct manifest version. 3. **Validate locally:** ```bash atk validate --env <env> ``` 4. **Re-build and re-provision after fixing:** ```bash atk provision --env local -i false ``` ## Runtime Issues ### Port already in use **Error codes:** `Ext.FindProcessError`, `Ext.PortsConflictError` Common ports used by ATK projects: **3978** (bot), **9239** (Node debugger), **56150** (Agents Playground). **Find and release occupied ports:** ```bash # Check which ports are in use (replace PORT with 3978, 9239, 56150, etc.) # Windows netstat -ano | findstr :PORT # macOS / Linux lsof -i :PORT # Kill the process occupying the port # Windows (replace PID with the process ID from above) taskkill /PID PID /F # macOS / Linux kill -9 PID ``` If the issue continues after releasing those three ports, inspect your bot logs and code for additional ports (e.g., custom API servers, function hosts on port 7071). Release them the same way. ### Missing environment variables at runtime **Error codes:** `fileCreateOrUpdateEnvironmentFile.MissingEnvironmentVariablesError`, `botFrameworkCreate.MissingEnvironmentVariablesError` Check that environment config files exist and contain all required values: - For **local debug**: check `.localConfigs` - For **Agents Playground debug**: also check `.localConfigs.playground` Run `atk deploy --env local -i false` (or `--env playground` for Playground) to regenerate. If a specific variable is reported missing, locate it in the relevant config file. Either fill in the correct value or remove the variable reference from your YAML if it is not needed. ### Agents Playground installation failed **Error code:** `devToolInstall.TestToolInstallationError` If automatic installation of Agents Playground fails, install it manually. **Option 1 — npm (recommended):** ```bash npm install -g @microsoft/m365agentsplayground ``` **Option 2 — winget (Windows only):** ```bash winget install agentsplayground ``` **Option 3 — script (Linux only):** ```bash curl -s https://raw.githubusercontent.com/OfficeDev/microsoft-365-agents-toolkit/dev/.github/scripts/install-agentsplayground-linux.sh | bash ``` **If installation still fails**, clear cached installation files and retry: - npm version cache: `~/.fx/bin/testTool/` - Binary version cache: `~/.fx/bin/testToolBinary/` ```bash # Clear caches (adjust path separator for your OS) rm -rf ~/.fx/bin/testTool rm -rf ~/.fx/bin/testToolBinary ``` Then reinstall using one of the options above. **Verify installation structure (npm version):** A valid npm installation looks like: ``` ~/.fx/bin/testTool/<version>/ ``` where `<version>` is the npm version of `@microsoft/m365agentsplayground`, and the folder contains the installed package contents. After installation, create a symlink under your project root: ```bash # From your project root ln -s ~/.fx/bin/testTool/<version> devTools/playground ``` **Verify installation structure (binary version):** A valid binary installation looks like: ``` ~/.fx/bin/testToolBinary/<version>/agentsplayground.exe ``` To install manually, download the release from: `https://github.com/OfficeDev/microsoft-365-agents-toolkit/releases/tag/teams-app-test-tool%40<version>` (e.g., `teams-app-test-tool%400.2.25` for version 0.2.25). Extract `teamsapptester-win32-x64.zip` and place the contents (including the `.exe`) into `~/.fx/bin/testToolBinary/<version>/`. If you encounter permission or OS-level errors (e.g., "access denied", "not recognized as executable"), try: ```bash # Windows — unblock the downloaded file powershell -Command "Unblock-File -Path '$HOME\.fx\bin\testToolBinary\<version>\agentsplayground.exe'" # macOS / Linux — set executable permission chmod +x ~/.fx/bin/testToolBinary/<version>/agentsplayground ``` If the issue persists, check your OS security settings and unblock the file manually. ### Azure Functions Core Tools installation failed **Error code:** `devToolInstall.FuncInstallationError` The `devTool/install` action in `m365agents.local.yml` installs Azure Functions Core Tools. The version range is defined in your YAML, for example: ```yaml - uses: devTool/install with: func: version: ^4.0.5530 symlinkDir: ./devTools/func writeToEnvironmentFile: funcPath: FUNC_PATH ``` **Manual installation steps:** 1. **Check your required version range** in `m365agents.local.yml` under `devTool/install → func → version`. 2. **Install via npm:** ```bash npm install azure-functions-core-tools@<version> --prefix ~/.fx/bin/azfunc/<version> --no-audit ``` Replace `<version>` with a version matching your YAML range (e.g., `4.0.5530`). 3. **Create the sentinel file** (marks the installation as valid): ```bash touch ~/.fx/bin/azfunc/<version>/node_modules/azure-functions-core-tools/bin/func-sentinel ``` 4. **Create the project symlink:** ```bash # From your project root ln -s ~/.fx/bin/azfunc/<version>/node_modules/azure-functions-core-tools/bin devTools/func ``` 5. **Verify the installation:** ```bash ./devTools/func/func --version ``` If npm is not available, install it first (`npm` ships with Node.js). On Linux, the npm-based portable installation is not supported — install Azure Functions Core Tools via the system package manager instead (see [Azure docs](https://learn.microsoft.com/azure/azure-functions/functions-run-local)). ### ARM deployment failed **Error code:** `armDeploy.DeployArmError` ARM deployment errors usually come from invalid Bicep templates or Azure resource configuration issues. 1. **Check the deployment log** — the error message includes the log file path (typically under `.fx/` or the output pane). Open it and look for the first error entry. 2. **Common causes and fixes:** - **Invalid parameter or resource property**: open the Bicep files under `infra/` (e.g., `azure.bicep`, `azure.parameters.json`) and fix the flagged property. - **Resource name conflict**: Azure resource names must be globally unique. Change the name in your Bicep parameters. - **Quota or region limitation**: check if the target region supports the requested SKU or resource type. - **Missing role assignment**: ensure the deploying identity has Contributor (or required) role on the target resource group. 3. **Validate Bicep locally before re-deploying:** ```bash az bicep build --file infra/azure.bicep az deployment group validate --resource-group <rg-name> --template-file infra/azure.bicep --parameters infra/azure.parameters.json ``` 4. **Re-deploy after fixing:** ```bash atk provision --env <env> ``` ### Dev tunnel operation failed **Error code:** `Ext.DevTunnelOperationError` This error occurs when a dev tunnel operation (create, delete, host, list) fails. Common causes: 1. **Not logged in to dev tunnels:** ```bash devtunnel user login ``` 2. **Tunnel limit reached** — free accounts have a limit on active tunnels. List and delete unused ones: ```bash devtunnel list devtunnel delete <tunnel-id> ``` 3. **Port already hosted by another tunnel session:** ```bash # Check if another devtunnel process is running # Windows tasklist | findstr devtunnel # macOS / Linux ps aux | grep devtunnel # Kill stale sessions # Windows taskkill /IM devtunnel.exe /F # macOS / Linux killall devtunnel ``` 4. **Network or proxy issues** — dev tunnels require outbound HTTPS. If behind a corporate proxy, configure it: ```bash set HTTPS_PROXY=http://proxy:port # Windows export HTTPS_PROXY=http://proxy:port # macOS / Linux ``` 5. **Stale tunnel state** — if the tunnel was deleted externally or is in a bad state, create a fresh one: ```bash devtunnel create --allow-anonymous devtunnel port create -p 3978 devtunnel host ``` Update `BOT_ENDPOINT` in `env/.env.local` with the new tunnel URL, then re-provision: ```bash atk provision --env local -i false atk deploy --env local -i false ``` See also [Blacklisted Devtunnel URL](#blacklisted-devtunnel-url) if you continue to get 401 errors after fixing tunnel issues. ## Diagnostics Commands ```bash atk doctor # Check ATK installation and dependencies atk validate --env <env> # Validate project configuration atk auth list # Check logged-in accounts agentsplayground --help # Playground CLI help atk provision --help # Provision help atk deploy --help # Deploy help ``` ## Expert Deep Dives > **Applicability per row**: lifecycle / environments rows apply to **all ATK projects**. The dev-debug, OAuth/SSO, and manifest rows apply only to **code-based Teams bots/agents**. Declarative-agent and API-plugin troubleshooting (Copilot recognition, instructions tuning, action invocation) is not covered by these experts — use the [Microsoft 365 Copilot extensibility docs](https://learn.microsoft.com/microsoft-365-copilot/extensibility/) and the in-product Copilot developer mode logs. | Symptom area | Expert | |---|---| | YAML actions, `aadApp/create` options, `m365agents.yml` field reference (all projects) | [../toolkit/lifecycle-cli.md](../toolkit/lifecycle-cli.md) | | `.localConfigs` vs `env/.env.local`, `TENANT_ID` mapping, `SECRET_` files (all projects) | [../toolkit/environments.md](../toolkit/environments.md) | | DevTools plugin, sideloading URL, `skipAuth`, devtunnel debugging (Teams bots only) | [../experts/teams/dev.debug-test-ts.md](../experts/teams/dev.debug-test-ts.md) | | 401 / `Bearer null` / single-tenant audience issues, JWT validation (Teams bots only) | [../experts/teams/auth.oauth-sso-ts.md](../experts/teams/auth.oauth-sso-ts.md) | | Manifest validation errors, scope/permission rejections (Teams bots / tabs / message extensions) | [../experts/teams/runtime.manifest-ts.md](../experts/teams/runtime.manifest-ts.md) |
-
-
SKILL.md 7.6 KB
--- name: teams-app-developer description: "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." --- # 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](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: ```bash 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 ```bash atk --version # Must be > 1.1.5-beta ``` If ATK is not found or version is too old: ```bash npm i -g @microsoft/m365agentstoolkit-cli@beta ``` ## CLI 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](create-project/create-project.md) | | **test-playground** | Test locally with Agents Playground, `agentsplayground`, quick testing | [test-playground/test-playground.md](test-playground/test-playground.md) | | **test-teams** | Run on Teams, devtunnel, sideload, Teams testing, test in Copilot | [test-teams/test-teams.md](test-teams/test-teams.md) | | **provision-deploy** | Provision Azure resources, deploy to cloud, `atk provision`, `atk deploy` | [provision-deploy/provision-deploy.md](provision-deploy/provision-deploy.md) | | **troubleshoot** | Fix errors, 401, port conflicts, YAML errors, stale bots | [troubleshoot/troubleshoot.md](troubleshoot/troubleshoot.md) | | **slack-to-teams** | Migrate Slack bot to Teams, cross-platform bridging, Block Kit to Adaptive Cards | [slack-to-teams/SKILL.md](slack-to-teams/SKILL.md) | > **MANDATORY:** Before executing any workflow, read the corresponding sub-skill document. ## Shared References - [manifest-and-yaml.md](toolkit/manifest-and-yaml.md) — Project files, YAML config, env vars, .localConfigs flow - [commands.md](toolkit/commands.md) — ATK CLI commands: package, validate, share, collaborate - [templates.md](toolkit/templates.md) — Complete template catalog with language support - [experts/](experts/index.md) — 100+ micro-expert files: Teams SDK, Slack SDK, cross-platform bridging, deploy, AI models, security, language conversion - [docs/](docs/README.md) — 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](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](toolkit/manifest-and-yaml.md) for full config-file details.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.