Messaging & Commands
Message Handling
| Aspect | Slack | Teams |
|---|---|---|
| Handler | app.message(pattern, handler) |
app.on("message", handler) |
| Pattern matching | String (substring), RegExp, or catch-all | RegExp or manual text.match() |
| Reply to channel | say(text) |
ctx.send(text) |
| Reply in thread | say({ text, thread_ts }) |
ctx.reply(text) |
| Get message text | message.text |
ctx.activity.text |
| Get sender | message.user (Slack ID) |
ctx.activity.from.id (AAD ID) |
Rating: GREEN — direct mapping in both directions.
Mitigation: Extract message handling into a platform-agnostic service layer that receives (text, userId, platform) and returns structured data. Each adapter converts to the platform's native format.
Slash Commands
| Aspect | Slack | Teams |
|---|---|---|
| Invocation | /command args |
No native equivalent |
| Handler | app.command("/cmd", handler) |
app.on("message") with text pattern matching |
| Acknowledgement | Must ack() within 3 seconds |
Automatic — no ack() |
| Default response | Ephemeral (user-only) | Visible to everyone |
| Modal trigger | trigger_id from command → views.open() |
dialog.open handler or Adaptive Card form |
| Registration | Slack app dashboard + commands scope |
Manifest commands[] array (bot commands, not slash) |
Rating: YELLOW — functional equivalent exists but UX is fundamentally different.
Impact
- Slash commands are a core Slack interaction pattern with no Teams counterpart
- Teams bot commands appear in a command menu but don't use
/prefix - Ephemeral responses don't exist in Teams
Mitigation Strategies
| Strategy | How | Effort |
|---|---|---|
| Text commands (Recommended) | Detect command-like patterns in app.on("message"). Accept both weather and /weather. |
2–4 hrs |
| Manifest bot commands | Add commands[] to manifest for discoverability in Teams command menu. Users type the command name. |
1–2 hrs |
| Message extension | Use composeExtensions for a richer command experience with search results or task modules. |
8–12 hrs |
Reverse Direction (Teams → Slack)
Teams bot commands map directly to Slack slash commands via app.command(). Add ack() calls (required in Slack, absent in Teams) and configure the command in the Slack app dashboard.
Events / Activities
| Slack Event | Teams Activity | Notes |
|---|---|---|
message |
message |
Direct mapping |
app_mention |
message (in channel) |
Teams channels require @mention by default |
member_joined_channel |
conversationUpdate (membersAdded) |
Different event shape |
member_left_channel |
conversationUpdate (membersRemoved) |
Different event shape |
reaction_added |
messageReaction |
Teams has only 6 fixed reactions |
app_home_opened |
install.add (closest) |
No "opened" event in Teams |
channel_created |
No equivalent | Use Graph API subscription |
team_join |
conversationUpdate (membersAdded) |
Same event, different context |
Rating: GREEN for most events, RED for custom emoji reactions.
@Mention Behavior
| Aspect | Slack | Teams |
|---|---|---|
| Channel messages | Bot receives all messages in joined channels | Bot only receives messages with @mention (default) |
| Override | Default behavior | Add ChannelMessage.Read.Group RSC permission to manifest |
| Mention stripping | Not needed | Set activity.mentions.stripText: true in App options |
Mitigation: To receive all channel messages in Teams without @mention, add RSC permission to the manifest. This is a config-only change (1–2 hrs).
Threading
| Aspect | Slack | Teams |
|---|---|---|
| Reply in thread | say({ thread_ts: message.ts }) |
ctx.reply(text) |
| Thread broadcast | say({ thread_ts, reply_broadcast: true }) |
Two API calls: reply() + send() |
| Get thread replies | conversations.replies({ ts }) |
Graph API GET /messages/{id}/replies |
| Thread discovery | Native API | Requires ChannelMessage.Read.All Graph permission |
Rating: GREEN for basic threading, YELLOW for broadcast and discovery.
Mitigation for Thread Broadcast
Slack's reply_broadcast posts in both the thread and the channel in one call. Teams requires two separate calls: reply() for the thread and send() for the channel. Wrap in a helper:
async function replyWithBroadcast(ctx, text: string): Promise<void> {
await ctx.reply(text); // Thread reply
await ctx.send(text); // Channel message
}Effort: 1–2 hrs.