From d6351ec26372564883e2a9fba785286ef695d8c9 Mon Sep 17 00:00:00 2001 From: Alive-Fish <15262146+Alive-Fish@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:10:52 +0000 Subject: [PATCH] sync: update knowledge from upstream sources --- .github/scripts/validate-derived.sh | 0 .github/sync-state.json | 11 +- .../docs/README.md | 1 + .../docs/workflows.md | 276 +++++++++++++++ .../experts/README.md | 4 +- .../bridge/app-distribution-packaging-ts.md | 11 +- .../bridge/identity-oauth-bridge-ts.md | 3 - .../experts/bridge/index.md | 9 +- .../bridge/workflow.composable-platform-ts.md | 297 ++++++++++++++++ .../experts/deploy/azure-bot-deploy-ts.md | 43 +-- .../experts/deploy/index.md | 7 +- .../experts/index.md | 7 +- .../experts/slack/index.md | 10 +- .../slack/workflow.slack-automations-ts.md | 181 ++++++++++ .../teams/ai.conversational-query-ts.md | 325 ++++++++++++++++++ .../experts/teams/index.md | 80 ++++- .../experts/teams/runtime.manifest-ts.md | 18 +- .../experts/teams/toolkit.environments.md | 170 +++++++++ .../experts/teams/toolkit.lifecycle-cli.md | 223 ++++++++++++ .../experts/teams/toolkit.playground-ts.md | 135 ++++++++ .../experts/teams/toolkit.publish.md | 150 ++++++++ .../teams/workflow.approvals-inline-ts.md | 294 ++++++++++++++++ .../workflow.message-native-records-ts.md | 233 +++++++++++++ .../teams/workflow.sharepoint-lists-ts.md | 138 ++++++++ .../teams/workflow.state-driven-events-ts.md | 231 +++++++++++++ .../teams/workflow.triggers-compose-ts.md | 216 ++++++++++++ .../slack-to-teams/SKILL.md | 1 + 27 files changed, 3009 insertions(+), 65 deletions(-) mode change 100644 => 100755 .github/scripts/validate-derived.sh create mode 100644 skills/microsoft-365-agents-toolkit/docs/workflows.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/bridge/workflow.composable-platform-ts.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/slack/workflow.slack-automations-ts.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/ai.conversational-query-ts.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/toolkit.environments.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/toolkit.lifecycle-cli.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/toolkit.playground-ts.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/toolkit.publish.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/workflow.approvals-inline-ts.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/workflow.message-native-records-ts.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/workflow.sharepoint-lists-ts.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/workflow.state-driven-events-ts.md create mode 100644 skills/microsoft-365-agents-toolkit/experts/teams/workflow.triggers-compose-ts.md diff --git a/.github/scripts/validate-derived.sh b/.github/scripts/validate-derived.sh old mode 100644 new mode 100755 diff --git a/.github/sync-state.json b/.github/sync-state.json index 1d83c48..267be7e 100644 --- a/.github/sync-state.json +++ b/.github/sync-state.json @@ -1,4 +1,13 @@ { "version": 1, - "sources": {} + "sources": { + "slack-plus-teams-experts": { + "lastSyncedCommit": "9ee143ba47bc28dd1eccf19cdbafffeddd51b4ab", + "lastSyncedAt": "2026-04-16T09:09:17Z" + }, + "slack-plus-teams-docs": { + "lastSyncedCommit": "9ee143ba47bc28dd1eccf19cdbafffeddd51b4ab", + "lastSyncedAt": "2026-04-16T09:09:19Z" + } + } } diff --git a/skills/microsoft-365-agents-toolkit/docs/README.md b/skills/microsoft-365-agents-toolkit/docs/README.md index 19b3932..3cad2f3 100644 --- a/skills/microsoft-365-agents-toolkit/docs/README.md +++ b/skills/microsoft-365-agents-toolkit/docs/README.md @@ -7,6 +7,7 @@ A practical guide for developers adding cross-platform support to an existing bo | 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 | diff --git a/skills/microsoft-365-agents-toolkit/docs/workflows.md b/skills/microsoft-365-agents-toolkit/docs/workflows.md new file mode 100644 index 0000000..f576b6d --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/docs/workflows.md @@ -0,0 +1,276 @@ +# 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. diff --git a/skills/microsoft-365-agents-toolkit/experts/README.md b/skills/microsoft-365-agents-toolkit/experts/README.md index e64c98b..10a53b6 100644 --- a/skills/microsoft-365-agents-toolkit/experts/README.md +++ b/skills/microsoft-365-agents-toolkit/experts/README.md @@ -106,7 +106,7 @@ Covers: input validation, secrets management. ## Eval Harness -The `evals/` directory (if present) contains an automated test harness that validates the expert system across three dimensions: +The [`evals/`](../evals/) directory contains an automated test harness that validates the expert system across three dimensions: | Dimension | What it checks | LLM required? | |-----------|---------------|----------------| @@ -120,7 +120,7 @@ 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. +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. diff --git a/skills/microsoft-365-agents-toolkit/experts/bridge/app-distribution-packaging-ts.md b/skills/microsoft-365-agents-toolkit/experts/bridge/app-distribution-packaging-ts.md index e2b6607..ebb2dd7 100644 --- a/skills/microsoft-365-agents-toolkit/experts/bridge/app-distribution-packaging-ts.md +++ b/skills/microsoft-365-agents-toolkit/experts/bridge/app-distribution-packaging-ts.md @@ -11,7 +11,7 @@ Bridges Slack App Directory distribution and Teams app packaging / Admin Center 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. **Microsoft 365 Agents Toolkit (ATK) simplifies packaging, provisioning, and deployment.** ATK (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 --env dev -i false` to generate the app package and `atk share --scope tenant -i false` to distribute. [learn.microsoft.com -- ATK](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +6. **Teams Toolkit simplifies packaging, provisioning, and deployment.** Teams Toolkit (VS Code extension or CLI `teamsapp`) automates: Azure resource provisioning, app package generation, sideloading, and publishing. It replaces the manual Azure Portal + zip file workflow. Use `teamsapp package` to generate the app package and `teamsapp publish` to submit. [learn.microsoft.com -- Teams 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) @@ -115,14 +115,13 @@ app.start(3978); ```shell # Option 1: Sideload for development # Build the app package (manifest.json + icons in a ZIP) -atk package --env dev -i false +teamsapp package --env dev # Upload to Teams: # Teams → Apps → Manage your apps → Upload a custom app -# Or use: atk install --file-path ./appPackage/build/appPackage.dev.zip -i false # Option 2: Submit to organization's app catalog -atk share --scope tenant -i false +teamsapp publish --env staging # IT admin approves in Teams Admin Center → Manage Apps # Option 3: Submit to public Teams App Store (Partner Center) @@ -182,10 +181,10 @@ my-teams-bot.zip ## 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, ATK 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. +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, Teams 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, ATK packaging (atk package / atk share), 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." +"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, Teams 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/bridge/identity-oauth-bridge-ts.md b/skills/microsoft-365-agents-toolkit/experts/bridge/identity-oauth-bridge-ts.md index 211b4fa..a8f45f1 100644 --- a/skills/microsoft-365-agents-toolkit/experts/bridge/identity-oauth-bridge-ts.md +++ b/skills/microsoft-365-agents-toolkit/experts/bridge/identity-oauth-bridge-ts.md @@ -53,9 +53,6 @@ PORT=3978 | `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) | - -> **\u26a0\ufe0f CRITICAL:** `TENANT_ID` must always be set explicitly. If missing, the Teams SDK defaults to acquiring tokens from the shared `botframework.com` tenant instead of your home tenant, causing **401 Unauthorized** from Bot Connector. ATK's `aadApp/create` may not write this to `.localConfigs` — verify and add it manually. - | `PORT` (default 3000) | `PORT` (default 3978) | Different conventional defaults | **Identity concept mapping table:** diff --git a/skills/microsoft-365-agents-toolkit/experts/bridge/index.md b/skills/microsoft-365-agents-toolkit/experts/bridge/index.md index 5c3bc04..6f0259e 100644 --- a/skills/microsoft-365-agents-toolkit/experts/bridge/index.md +++ b/skills/microsoft-365-agents-toolkit/experts/bridge/index.md @@ -126,6 +126,12 @@ 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: @@ -200,9 +206,10 @@ If a request involves both Slack↔Teams app bridging **and** AWS↔Azure infra ## file inventory -`app-distribution-packaging-ts.md` | `channel-ops-graph-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` +`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` + diff --git a/skills/microsoft-365-agents-toolkit/experts/bridge/workflow.composable-platform-ts.md b/skills/microsoft-365-agents-toolkit/experts/bridge/workflow.composable-platform-ts.md new file mode 100644 index 0000000..fe42b55 --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/bridge/workflow.composable-platform-ts.md @@ -0,0 +1,297 @@ +# 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; // 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(); + private graphClient: Client; + private siteId: string; + private lists = new Map(); // 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 = {}; + 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) { + 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 { + 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/deploy/azure-bot-deploy-ts.md b/skills/microsoft-365-agents-toolkit/experts/deploy/azure-bot-deploy-ts.md index 6d70660..cf13a22 100644 --- a/skills/microsoft-365-agents-toolkit/experts/deploy/azure-bot-deploy-ts.md +++ b/skills/microsoft-365-agents-toolkit/experts/deploy/azure-bot-deploy-ts.md @@ -6,7 +6,7 @@ Step-by-step deployment of a Slack bot, Teams bot, or dual bot to Azure. Covers ## rules -1. **Install prerequisites before anything else.** You need: Node.js 20 LTS, Azure CLI (`az`), and optionally the ATK CLI (`npm i -g @microsoft/m365agentstoolkit-cli@beta`). Verify with `az --version`, `node --version`, and `atk --version`. [learn.microsoft.com/cli/azure/install-azure-cli](https://learn.microsoft.com/cli/azure/install-azure-cli) +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/teamsapp-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 `. 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 --location `. 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 ` returns an `appId` (client ID). Then create a secret: `az ad app credential reset --id `. 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) @@ -19,7 +19,7 @@ Step-by-step deployment of a Slack bot, Teams bot, or dual bot to Azure. Covers 11. **Build and deploy.** Run `npm run build` locally, then zip deploy: `az webapp deploy --resource-group --name --src-path --type zip`. For Functions: `func azure functionapp publish `. [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 --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://.azurewebsites.net/api/health`. Then send a test message in Teams or Slack. Check App Service logs: `az webapp log tail --resource-group --name `. [learn.microsoft.com/azure/app-service/troubleshoot-diagnostic-logs](https://learn.microsoft.com/azure/app-service/troubleshoot-diagnostic-logs) -14. **ATK fast path (Teams bots).** Instead of steps 3-12, run `atk provision --env dev --resource-group --region -i false` (creates App Registration, Bot Service, App Service, and all config) then `atk deploy --env dev -i false` (builds and deploys). Two commands replace the entire manual process. Requires a `m365agents.yml` in your project. [learn.microsoft.com -- ATK CLI](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/toolkit-cli) +14. **Agents Toolkit fast path (Teams bots).** Instead of steps 3-12, run `teamsapp provision` (creates App Registration, Bot Service, App Service, and all config) then `teamsapp deploy` (builds and deploys). Two commands replace the entire manual process. Requires a `teamsapp.yml` in your project. [learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-cli](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-cli) ## interview @@ -44,8 +44,8 @@ multiSelect: false question: "How do you want to deploy?" header: "Method" options: - - label: "ATK CLI (Recommended)" - description: "atk provision + atk deploy — automates App Registration, Bot Service, App Service, and manifest sideloading in two commands." + - label: "Agents Toolkit CLI (Recommended)" + description: "teamsapp provision + teamsapp 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" @@ -58,7 +58,7 @@ multiSelect: false | Question | Default | |---|---| | Q1 | App Service | -| Q2 | ATK CLI | +| Q2 | Agents Toolkit CLI | ## patterns @@ -139,30 +139,23 @@ curl https://mybot-app.azurewebsites.net/api/health az webapp log tail --resource-group rg-mybot --name mybot-app ``` -### ATK fast path (Teams bot) +### Agents Toolkit fast path (Teams bot) ```bash -# 1. Install ATK CLI -npm i -g @microsoft/m365agentstoolkit-cli@beta +# 1. Install Agents Toolkit CLI +npm install -g @microsoft/teamsapp-cli -# 2. Login to M365 and Azure -atk auth login m365 -atk auth login azure +# 2. Provision all Azure resources (App Registration, Bot Service, App Service) +teamsapp provision --env dev -# 3. Provision all Azure resources (App Registration, Bot Service, App Service) -atk provision --env dev --resource-group rg-mybot --region eastus -i false +# 3. Build and deploy +teamsapp deploy --env dev -# 4. Build and deploy -atk deploy --env dev -i false +# 4. Sideload to Teams for testing +teamsapp preview --env dev -# 5. Test locally with Agents Playground -agentsplayground -e http://localhost:3978/api/messages -c msteams - -# Or open in Teams: -# https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TENANT_ID}}&login_hint=${{USER_EMAIL}} - -# That's it — three commands from zero to running bot. -# m365agents.yml in your project defines the resource topology. +# That's it — three commands from zero to running bot in Teams. +# teamsapp.yml in your project defines the resource topology. ``` ### Slack bot on Azure App Service @@ -246,7 +239,7 @@ az webapp config appsettings set \ - 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/toolkit-cli +- https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-cli - https://learn.microsoft.com/azure/app-service/troubleshoot-diagnostic-logs ## instructions @@ -259,4 +252,4 @@ Pair with: `../teams/project.scaffold-files-ts.md` (project structure before dep 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, ATK CLI (atk provision + atk deploy) as a fast path, Agents Playground for local testing, 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." +"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 (teamsapp provision + teamsapp 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/deploy/index.md b/skills/microsoft-365-agents-toolkit/experts/deploy/index.md index d1e261e..e4cfc6d 100644 --- a/skills/microsoft-365-agents-toolkit/experts/deploy/index.md +++ b/skills/microsoft-365-agents-toolkit/experts/deploy/index.md @@ -46,11 +46,11 @@ multiSelect: false ## 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`, ATK deploy, deploy Teams bot, deploy Slack bot 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, `teamsapp provision`, `teamsapp 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 ATK automated deployment (alternative to manual Azure CLI), see the parent skill's [../../references/commands.md](../../references/commands.md) and [../../provision-deploy/provision-deploy.md](../../provision-deploy/provision-deploy.md). +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 @@ -88,5 +88,4 @@ If deploying a **Slack-only bot**, either cloud works independently. `aws-bot-deploy-ts.md` | `aws-cli-reference-ts.md` | `azure-bot-deploy-ts.md` | `azure-cli-reference-ts.md` - - + diff --git a/skills/microsoft-365-agents-toolkit/experts/index.md b/skills/microsoft-365-agents-toolkit/experts/index.md index 2ef9a1e..bf934be 100644 --- a/skills/microsoft-365-agents-toolkit/experts/index.md +++ b/skills/microsoft-365-agents-toolkit/experts/index.md @@ -78,11 +78,11 @@ The pre-task interview gates the entire workflow. Expert-level `## interview` se 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, Microsoft 365 Agents Toolkit, ATK, 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`, `OnMessage`, `OpenAIChatPrompt`, Teams Python, Teams .NET, Teams C#, `m365agents.yml`, `atk provision`, `atk deploy`, `atk share`, `atk validate`, `atk package`, `atk new`, `atk auth`, `atk doctor`, Agents Playground, `agentsplayground`, `.m365agentsplayground.yml`, lifecycle hooks, `env/.env`, `.localConfigs` +Signals: Teams SDK, `@microsoft/teams-ai`, Adaptive Cards, ChatPrompt, Graph API, MCP, A2A, `app.ts`, manifest, proactive message, message extension, dialog, task module, Teams 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`, `OnMessage`, `OpenAIChatPrompt`, Teams Python, Teams .NET, Teams C#, `m365agents.yml`, `teamsapp provision`, `teamsapp deploy`, `teamsapp publish`, `teamsapp preview`, `teamsapp validate`, `teamsapp package`, `teamsapp new`, Agents Playground, `.m365agentsplayground.yml`, lifecycle hooks, `env/.env`, SharePoint Lists, workflow state, message-native records, Action.Execute refresh, presence trigger, Shifts trigger, call queue, Graph change notifications, approval workflow, in-channel approval, escalation, conversational query, natural language query, NL retrieval, composable workflow, workflow engine → 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 +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, `DefineFunction`, `DefineWorkflow`, `DefineDatastore`, Slack automation, Slack next-gen platform, Workflow Builder → Read `experts/slack/index.md` ### Bridge — cross-platform bridging between Slack and Teams, or AWS and Azure @@ -94,7 +94,7 @@ Signals: OpenAI, Azure OpenAI, GPT-4o, GPT-4, Anthropic, Claude, `@anthropic-ai/ → 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`, ATK deploy, zip deploy, SAM deploy, CDK deploy, API Gateway, deploy to Azure, deploy to AWS, push to cloud, CloudFormation +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, `teamsapp provision`, `teamsapp 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 @@ -227,4 +227,3 @@ When fleshing out a stub expert with real content. Provides the step-by-step Dee - diff --git a/skills/microsoft-365-agents-toolkit/experts/slack/index.md b/skills/microsoft-365-agents-toolkit/experts/slack/index.md index b1b3f56..0f2137c 100644 --- a/skills/microsoft-365-agents-toolkit/experts/slack/index.md +++ b/skills/microsoft-365-agents-toolkit/experts/slack/index.md @@ -117,6 +117,13 @@ Read: - `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: @@ -133,7 +140,8 @@ If a request spans multiple clusters (e.g., "add a slash command that opens a Bl ## 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` +`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` + diff --git a/skills/microsoft-365-agents-toolkit/experts/slack/workflow.slack-automations-ts.md b/skills/microsoft-365-agents-toolkit/experts/slack/workflow.slack-automations-ts.md new file mode 100644 index 0000000..a90adc2 --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/slack/workflow.slack-automations-ts.md @@ -0,0 +1,181 @@ +# 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/ai.conversational-query-ts.md b/skills/microsoft-365-agents-toolkit/experts/teams/ai.conversational-query-ts.md new file mode 100644 index 0000000..a655e7d --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/ai.conversational-query-ts.md @@ -0,0 +1,325 @@ +# 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/index.md b/skills/microsoft-365-agents-toolkit/experts/teams/index.md index dd54a31..9b8a6a0 100644 --- a/skills/microsoft-365-agents-toolkit/experts/teams/index.md +++ b/skills/microsoft-365-agents-toolkit/experts/teams/index.md @@ -120,6 +120,50 @@ Read: - `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). +### Workflow: SharePoint Lists as State +When: SharePoint Lists integration, list CRUD, workflow state persistence, OData queries on lists, Graph webhooks for list changes +Read: +- `workflow.sharepoint-lists-ts.md` +- `workflow.message-native-records-ts.md` (for rendering list records as cards) +Depends on: `graph.usergraph-appgraph-ts.md` (Graph API auth), `auth.oauth-sso-ts.md` (app-only token setup) + +### Workflow: Message-Native Records +When: card-as-record pattern, structured records in threads, Action.Execute card refresh, in-place card updates, updatable workflow cards +Read: +- `workflow.message-native-records-ts.md` +- `ui.adaptive-cards-ts.md` (card construction patterns) +Depends on: `workflow.sharepoint-lists-ts.md` (backing store), `runtime.routing-handlers-ts.md` (card.action handler) + +### Workflow: Triggers at Compose Surface +When: workflow initiation, bot commands, message extension triggers, compose box, scheduled proactive workflows, workflow discoverability +Read: +- `workflow.triggers-compose-ts.md` +- `ui.message-extensions-ts.md` (message extension details) +- `runtime.proactive-messaging-ts.md` (scheduled trigger infrastructure) +Depends on: `runtime.manifest-ts.md` (command lists and compose extension declarations) + +### Workflow: State-Driven Events +When: presence triggers, Shifts events, call queue, Graph change notifications, break management, operational triggers, webhook subscriptions +Read: +- `workflow.state-driven-events-ts.md` +- `workflow.triggers-compose-ts.md` (for the full trigger surface) +Depends on: `graph.usergraph-appgraph-ts.md` (Graph API), `runtime.proactive-messaging-ts.md` (proactive messaging from webhooks) + +### Workflow: In-Channel Approvals +When: approval workflow, approve/reject, approval routing, sequential approval, parallel approval, escalation, approval state machine +Read: +- `workflow.approvals-inline-ts.md` +- `workflow.message-native-records-ts.md` (card-as-record pattern) +Depends on: `workflow.sharepoint-lists-ts.md` (approval record persistence), `graph.usergraph-appgraph-ts.md` (manager lookup for escalation) + +### AI: Conversational Query +When: natural language query, NL retrieval, "who is on break", "show PTO for March", querying workflow state with AI, function calling for data queries +Read: +- `ai.conversational-query-ts.md` +- `ai.function-calling-design-ts.md` (function schema design) +- `ai.function-calling-implementation-ts.md` (execution patterns) +Depends on: `workflow.sharepoint-lists-ts.md` (underlying data), `workflow.message-native-records-ts.md` (rendering results as cards) + ### Compatibility: BotBuilder Interop When: mixing BotBuilder SDK with Teams AI, legacy bot code, adapter patterns Read: @@ -128,7 +172,7 @@ Read: - `runtime.routing-handlers-ts.md` (for mapping TeamsActivityHandler to SDK v2 routes) ### Dev: Debug & Test -When: debugging, testing, ATK, Agents Toolkit, local tunnel, dev tools, unit tests +When: debugging, testing, Teams Toolkit, local tunnel, dev tools, unit tests Read: - `dev.debug-test-ts.md` - `project.scaffold-files-ts.md` (for npm scripts and build verification) @@ -152,16 +196,33 @@ 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: ATK CLI, Environments, Playground, Publishing -When: `m365agents.yml`, `atk` CLI, `atk provision`, `atk deploy`, `atk share`, `atk new`, `atk auth`, `atk doctor`, lifecycle hooks, CI/CD pipeline, built-in actions, env files, environment variables, `${{VAR}}`, `SECRET_` prefix, multi-environment, Agents Playground, `agentsplayground`, local testing, publish to org, Teams Store, Partner Center +### Toolkit: Lifecycle & CLI +When: `m365agents.yml`, `teamsapp` CLI, `teamsapp provision`, `teamsapp deploy`, `teamsapp publish`, `teamsapp 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, `teamsapp 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, `teamsapp publish`, `teamsapp validate`, `teamsapp package`, `teamsapp update`, app validation, sideload to org, org catalog, version bump Read: -- Parent skill references at [../../references/](../../references/) — covers ATK CLI commands, environment config, local debugging, templates, and publishing -- `runtime.manifest-ts.md` (for manifest.json v1.25 schema and packaging) -Note: ATK toolkit details are maintained in the parent microsoft-365-agents-toolkit skill to avoid duplication. +- `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. For ATK toolkit patterns (environments, lifecycle, playground), see the parent skill's [../../references/](../../references/) folder. +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 @@ -169,6 +230,7 @@ If a request spans multiple clusters (e.g., "add a function-calling tool that re ## 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` | `ui.adaptive-cards-ts.md` | `ui.dialogs-task-modules-ts.md` | `ui.message-extensions-ts.md` +`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.conversational-query-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` | `workflow.approvals-inline-ts.md` | `workflow.message-native-records-ts.md` | `workflow.sharepoint-lists-ts.md` | `workflow.state-driven-events-ts.md` | `workflow.triggers-compose-ts.md` - + + diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/runtime.manifest-ts.md b/skills/microsoft-365-agents-toolkit/experts/teams/runtime.manifest-ts.md index 94b35b2..1b9914f 100644 --- a/skills/microsoft-365-agents-toolkit/experts/teams/runtime.manifest-ts.md +++ b/skills/microsoft-365-agents-toolkit/experts/teams/runtime.manifest-ts.md @@ -6,9 +6,9 @@ Teams app manifest (manifest.json) structure, schema, bots config, permissions, ## rules -1. Use schema version `1.25` with `"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.25/MicrosoftTeams.schema.json"` and `"manifestVersion": "1.25"`. 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) +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. **v1.25 catch-22:** With manifest v1.25, `"team"` scope requires `supportsChannelFeatures: true` at runtime but the v1.25 schema rejects it. Workaround: use `"personal"` scope only in v1.25, or use devPreview/future schema that defines the property. [learn.microsoft.com -- Bots in manifest](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#bots) +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) @@ -24,9 +24,9 @@ Teams app manifest (manifest.json) structure, schema, bots config, permissions, ```typescript // appPackage/manifest.json const manifest = { - "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.25/MicrosoftTeams.schema.json", + "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.20/MicrosoftTeams.schema.json", "version": "1.0.0", - "manifestVersion": "1.25", + "manifestVersion": "1.20", "id": "${{TEAMS_APP_ID}}", "name": { "short": "My Bot", @@ -55,7 +55,7 @@ const manifest = { "bots": [ { "botId": "${{BOT_ID}}", - "scopes": ["personal"], + "scopes": ["personal", "team", "groupChat"], "isNotificationOnly": false, "supportsCalling": false, "supportsVideo": false, @@ -75,9 +75,9 @@ const manifest = { ```typescript // appPackage/manifest.json -- adding composeExtensions for a search command const manifestWithExtensions = { - "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.25/MicrosoftTeams.schema.json", + "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.20/MicrosoftTeams.schema.json", "version": "1.0.0", - "manifestVersion": "1.25", + "manifestVersion": "1.20", "id": "${{TEAMS_APP_ID}}", "name": { "short": "Search Bot", @@ -102,7 +102,7 @@ const manifestWithExtensions = { "bots": [ { "botId": "${{BOT_ID}}", - "scopes": ["personal"], + "scopes": ["personal", "team", "groupChat"], "isNotificationOnly": false } ], @@ -202,4 +202,4 @@ Pair with `project.scaffold-files-ts.md` for the full project file structure and Deep Research prompt: -"Write a micro expert on Microsoft Teams app manifest.json for SDK v2 bots (TypeScript). Cover the v1.25 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." +"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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.environments.md b/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.environments.md new file mode 100644 index 0000000..ff514d4 --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.environments.md @@ -0,0 +1,170 @@ +# toolkit.environments + +## purpose + +Multi-environment management with environment files, variable resolution, and the `SECRET_` prefix convention 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 `teamsapp provision --env `. +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 `teamsapp 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 `teamsapp 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`. + +## 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) +TEAMSFX_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.20/MicrosoftTeams.schema.json", + "manifestVersion": "1.20", + "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 +teamsapp provision --env staging + +# This creates: +# env/.env.staging (with TEAMSFX_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 +teamsapp deploy --env staging + +# Step 4: Preview/test against a specific environment +teamsapp preview --env staging +``` + +### 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 `teamsapp provision`. + +## 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 `teamsapp 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" +- "teamsapp provision --env staging multiple environments" +- "Teams Toolkit SECRET_ prefix environment variables" + +Pair with: +- `toolkit.lifecycle-cli.md` — lifecycle hooks that consume environment variables +- `runtime.manifest-ts.md` — manifest `${{VAR}}` placeholder resolution +- `project.scaffold-files-ts.md` — scaffolded project includes env/ directory +- `toolkit.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 teamsapp 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.lifecycle-cli.md b/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.lifecycle-cli.md new file mode 100644 index 0000000..46bce02 --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.lifecycle-cli.md @@ -0,0 +1,223 @@ +# toolkit.lifecycle-cli + +## purpose + +M365 Agents Toolkit lifecycle configuration (`m365agents.yml`) and full `teamsapp` CLI command reference for provisioning, deploying, and managing Teams agents. + +## rules + +1. **m365agents.yml is the lifecycle manifest.** Every Agents Toolkit project has an `m365agents.yml` at the project root. It defines the `provision`, `deploy`, and `publish` lifecycle stages — each stage is an ordered list of actions. +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. **Actions use `uses:` for built-ins, `runs:` for custom scripts.** Built-in actions like `arm/deploy` or `teamsApp/create` are referenced with `uses: `. Custom shell commands use `runs: `. Both accept 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/deploy`, `azureFunctions/deploy`, `teamsApp/create`, `teamsApp/update`, `teamsApp/validateManifest`, `teamsApp/zipAppPackage`. +5. **`environmentFolderPath`** in `m365agents.yml` points to the `env/` directory. Defaults to `./env`. All `${{VAR}}` placeholders resolve from the active environment's `.env.{name}` files. +6. **`teamsapp new` scaffolds a project.** Creates project structure with `m365agents.yml`, `env/` folder, `appPackage/`, and starter code. Supports `--template` for predefined templates and `--interactive false` for CI. +7. **`teamsapp provision` creates cloud resources.** Runs the `provision` stage in `m365agents.yml`. Accepts `--env ` to target a specific environment (default: `dev`). Creates resources defined by ARM templates or built-in actions. +8. **`teamsapp deploy` pushes code to cloud.** Runs the `deploy` stage. Builds the project, then deploys to the compute target (App Service, Functions, Container Apps). Always run `teamsapp provision` before first deploy. +9. **`teamsapp 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. +10. **`teamsapp validate` checks the manifest.** Validates `manifest.json` against the Teams schema before packaging. Catches missing fields, invalid scopes, and schema violations early. +11. **`teamsapp package` creates the app zip bundle.** Generates the `.zip` containing `manifest.json`, icons, and resolved placeholders. This is the artifact uploaded to Teams or Partner Center. +12. **`teamsapp preview` launches local testing.** Starts the Agents Playground for local testing without deploying to Teams. See `toolkit.playground-ts.md` for details. +13. **CI/CD integration uses `teamsapp` CLI with `--env` flags.** GitHub Actions and Azure Pipelines call `teamsapp provision --env staging` and `teamsapp deploy --env staging` in sequence. Store credentials in CI secrets, not in `.env.*.user` files. + +## patterns + +### Pattern 1: m365agents.yml anatomy + +```yaml +# m365agents.yml — lifecycle configuration +version: v1.7 + +environmentFolderPath: ./env + +provision: + - uses: aadApp/create + with: + name: ${{AAD_APP_NAME}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + clientSecret: SECRET_AAD_APP_CLIENT_SECRET + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_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/create + with: + name: ${{TEAMS_APP_NAME}} + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + +deploy: + - uses: azureAppService/deploy + with: + artifactFolder: . + resourceId: ${{AZURE_APP_SERVICE_RESOURCE_ID}} + +publish: + - uses: teamsApp/validateManifest + with: + manifestPath: ./appPackage/manifest.json + + - uses: teamsApp/zipAppPackage + with: + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + + - uses: teamsApp/update + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip +``` + +### Pattern 2: CLI command reference + +```bash +# Scaffold a new project +teamsapp new # Interactive wizard +teamsapp new --template ai-bot --interactive false # Non-interactive + +# Provision cloud resources +teamsapp provision # Uses default env (dev) +teamsapp provision --env staging # Target specific env + +# Deploy application code +teamsapp deploy # Deploy to default env +teamsapp deploy --env production # Deploy to production + +# Validate and package +teamsapp validate --manifest-path ./appPackage/manifest.json +teamsapp package --manifest-path ./appPackage/manifest.json \ + --output-zip-path ./build/appPackage.zip + +# Publish to org catalog +teamsapp publish + +# Local preview / Agents Playground +teamsapp preview + +# Update an existing Teams app registration +teamsapp update +``` + +### Pattern 3: 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/teamsapp-cli + + - name: Provision + run: teamsapp provision --env production + 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: teamsapp deploy --env production + env: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} +``` + +### Pattern 4: 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 `teamsapp provision` or `teamsapp deploy` needed +- Manage Azure resources manually (Bot Registration, App Service) rather than through lifecycle actions + +``` +cross-platform-bot/ +├── appPackage/ +│ └── manifest.json # v1.20 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 `teamsapp provision` / `teamsapp 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. +- **Using `runs:` for what `uses:` handles** — Built-in actions handle auth, retries, and environment writes automatically. Only use `runs:` for truly custom steps. +- **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 `teamsapp` with `teamsfx`** — The old CLI was `teamsfx`. The current CLI is `teamsapp`. If docs reference `teamsfx`, translate to `teamsapp`. +- **ARM template parameter mismatches** — `arm/deploy` parameters must match the Bicep/ARM template's expected inputs. Mismatches cause silent failures during provisioning. + +## 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) +- [teamsapp 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" +- "teamsapp CLI provision deploy publish commands reference" +- "Agents Toolkit CI/CD GitHub Actions Azure Pipelines" + +Pair with: +- `project.scaffold-files-ts.md` — project scaffolding (what `teamsapp new` creates) +- `../deploy/azure-bot-deploy-ts.md` — manual Azure deployment as alternative to Agents Toolkit +- `toolkit.environments.md` — environment files consumed by lifecycle hooks +- `toolkit.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, teamsapp 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.playground-ts.md b/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.playground-ts.md new file mode 100644 index 0000000..530351b --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.playground-ts.md @@ -0,0 +1,135 @@ +# toolkit.playground-ts + +## purpose + +Agents Playground — the local web-based test harness for testing Teams agents without deploying to the Teams client. + +## 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 or sideloading required for basic message testing. +2. **Start with `teamsapp preview`.** The CLI command starts the local bot server and opens the Agents Playground in the default browser. Alternatively, use the VS Code command palette: "Agents Toolkit: Preview". +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. **`teamsapp preview --env ` uses environment-specific config.** The preview command loads environment variables from the specified env files, allowing you to test against different configurations. +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: CLI — starts bot + opens playground +teamsapp preview + +# Option 2: CLI — target a specific environment +teamsapp preview --env dev + +# Option 3: Start bot separately, then preview +npm run dev # Start bot server on localhost:3978 +teamsapp preview # Opens playground, connects to running bot + +# Option 4: VS Code command palette +# > Agents Toolkit: Preview +``` + +### 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", + "preview": "teamsapp preview", + "dev:playground": "concurrently \"npm run dev\" \"npm run preview\"" + } +} +``` + +```typescript +// src/index.ts — bot entry point +import { Application, TurnState } from '@microsoft/teams-ai'; + +const app = new Application({ + // 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}`); + console.log(`Run "teamsapp preview" to open Agents Playground`); +}); +``` + +## 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`, preview uses the default dev environment. If you need staging credentials or endpoints, specify the environment. +- **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) +- [teamsapp 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" +- "teamsapp preview Agents Playground configuration" +- ".m365agentsplayground.yml configuration options" + +Pair with: +- `dev.debug-test-ts.md` — broader debugging and testing patterns +- `toolkit.lifecycle-cli.md` — `teamsapp preview` is part of the CLI command set +- `toolkit.environments.md` — playground uses environment-specific config +- `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 (teamsapp 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.publish.md b/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.publish.md new file mode 100644 index 0000000..b83436d --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/toolkit.publish.md @@ -0,0 +1,150 @@ +# toolkit.publish + +## purpose + +Publishing workflow for Teams agents — from local sideloading through org catalog distribution to public Teams Store submission. + +## 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. **`teamsapp 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 `teamsapp publish`, admins review the submission at `admin.teams.microsoft.com → Teams apps → Manage apps`. They can approve, reject, or request changes. +5. **`teamsapp 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. **`teamsapp 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. **`teamsapp 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 (`teamsapp 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 +teamsapp validate --manifest-path ./appPackage/manifest.json +# Fix any reported errors before continuing + +# Step 2: Package the app +teamsapp package --manifest-path ./appPackage/manifest.json \ + --output-zip-path ./build/appPackage.zip + +# Step 3: Publish to org catalog (submits for admin approval) +teamsapp publish + +# 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 +teamsapp validate --manifest-path ./appPackage/manifest.json + +# Step 3: Update the Teams app registration +teamsapp update + +# Step 4: Re-package with the new version +teamsapp package --manifest-path ./appPackage/manifest.json \ + --output-zip-path ./build/appPackage.zip + +# Step 5: Re-publish (triggers admin re-approval for org catalog) +teamsapp 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.20/MicrosoftTeams.schema.json", + "manifestVersion": "1.20", + "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** — `teamsapp 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** — `teamsapp 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) +- [teamsapp 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" +- "teamsapp publish validate package CLI commands" +- "Teams Store submission Partner Center validation requirements" + +Pair with: +- `runtime.manifest-ts.md` — manifest structure and schema requirements +- `toolkit.lifecycle-cli.md` — CLI commands for validate, package, publish +- `toolkit.environments.md` — environment-specific publishing (dev vs production) +- `../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), teamsapp 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/workflow.approvals-inline-ts.md b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.approvals-inline-ts.md new file mode 100644 index 0000000..58df89d --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.approvals-inline-ts.md @@ -0,0 +1,294 @@ +# 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/workflow.message-native-records-ts.md b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.message-native-records-ts.md new file mode 100644 index 0000000..c4e9196 --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.message-native-records-ts.md @@ -0,0 +1,233 @@ +# 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: } }` (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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/workflow.sharepoint-lists-ts.md b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.sharepoint-lists-ts.md new file mode 100644 index 0000000..964523e --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.sharepoint-lists-ts.md @@ -0,0 +1,138 @@ +# 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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/workflow.state-driven-events-ts.md b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.state-driven-events-ts.md new file mode 100644 index 0000000..fbcfff3 --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.state-driven-events-ts.md @@ -0,0 +1,231 @@ +# 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(); +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." diff --git a/skills/microsoft-365-agents-toolkit/experts/teams/workflow.triggers-compose-ts.md b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.triggers-compose-ts.md new file mode 100644 index 0000000..6515068 --- /dev/null +++ b/skills/microsoft-365-agents-toolkit/experts/teams/workflow.triggers-compose-ts.md @@ -0,0 +1,216 @@ +# 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(); + +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." diff --git a/skills/microsoft-365-agents-toolkit/slack-to-teams/SKILL.md b/skills/microsoft-365-agents-toolkit/slack-to-teams/SKILL.md index c015f98..b8e8959 100644 --- a/skills/microsoft-365-agents-toolkit/slack-to-teams/SKILL.md +++ b/skills/microsoft-365-agents-toolkit/slack-to-teams/SKILL.md @@ -165,3 +165,4 @@ Reference guides for side-by-side platform comparison: - [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 for Teams bots (triggers, state, logic, AI, visibility)