diff --git a/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts
index ef56d496b7..fad1296281 100644
--- a/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts
+++ b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts
@@ -4,6 +4,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing'
import { generateRequestId } from '@/lib/utils'
+import { resolveMentionsForChannel } from '@/tools/microsoft_teams/utils'
export const dynamic = 'force-dynamic'
@@ -140,23 +141,62 @@ export async function POST(request: NextRequest) {
}
let messageContent = validatedData.content
+ let contentType: 'text' | 'html' = 'text'
+ const mentionEntities: Array<{
+ type: 'mention'
+ mentioned: { id: string; name: string }
+ text: string
+ }> = []
+
+ try {
+ const mentionResult = await resolveMentionsForChannel(
+ validatedData.content,
+ validatedData.teamId,
+ validatedData.channelId,
+ validatedData.accessToken
+ )
+
+ if (mentionResult.hasMentions) {
+ contentType = 'html'
+ mentionEntities.push(...mentionResult.entities)
+ logger.info(`[${requestId}] Resolved ${mentionResult.entities.length} mention(s)`)
+ }
+ } catch (error) {
+ logger.warn(`[${requestId}] Failed to resolve mentions, continuing without them:`, error)
+ }
if (attachments.length > 0) {
+ contentType = 'html'
const attachmentTags = attachments
.map((att) => ``)
.join(' ')
messageContent = `${validatedData.content}
${attachmentTags}`
}
- const messageBody = {
+ const messageBody: {
body: {
- contentType: attachments.length > 0 ? 'html' : 'text',
+ contentType: 'text' | 'html'
+ content: string
+ }
+ attachments?: any[]
+ mentions?: Array<{
+ type: 'mention'
+ mentioned: { id: string; name: string }
+ text: string
+ }>
+ } = {
+ body: {
+ contentType,
content: messageContent,
},
}
if (attachments.length > 0) {
- ;(messageBody as any).attachments = attachments
+ messageBody.attachments = attachments
+ }
+
+ if (mentionEntities.length > 0) {
+ messageBody.mentions = mentionEntities
}
logger.info(`[${requestId}] Sending message to Teams channel: ${validatedData.channelId}`)
diff --git a/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts
index f7dd519760..445edf480c 100644
--- a/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts
+++ b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts
@@ -4,6 +4,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing'
import { generateRequestId } from '@/lib/utils'
+import { resolveMentionsForChat } from '@/tools/microsoft_teams/utils'
export const dynamic = 'force-dynamic'
@@ -138,23 +139,61 @@ export async function POST(request: NextRequest) {
}
let messageContent = validatedData.content
+ let contentType: 'text' | 'html' = 'text'
+ const mentionEntities: Array<{
+ type: 'mention'
+ mentioned: { id: string; name: string }
+ text: string
+ }> = []
+
+ try {
+ const mentionResult = await resolveMentionsForChat(
+ validatedData.content,
+ validatedData.chatId,
+ validatedData.accessToken
+ )
+
+ if (mentionResult.hasMentions) {
+ contentType = 'html'
+ mentionEntities.push(...mentionResult.entities)
+ logger.info(`[${requestId}] Resolved ${mentionResult.entities.length} mention(s)`)
+ }
+ } catch (error) {
+ logger.warn(`[${requestId}] Failed to resolve mentions, continuing without them:`, error)
+ }
if (attachments.length > 0) {
+ contentType = 'html'
const attachmentTags = attachments
.map((att) => ``)
.join(' ')
messageContent = `${validatedData.content}
${attachmentTags}`
}
- const messageBody = {
+ const messageBody: {
body: {
- contentType: attachments.length > 0 ? 'html' : 'text',
+ contentType: 'text' | 'html'
+ content: string
+ }
+ attachments?: any[]
+ mentions?: Array<{
+ type: 'mention'
+ mentioned: { id: string; name: string }
+ text: string
+ }>
+ } = {
+ body: {
+ contentType,
content: messageContent,
},
}
if (attachments.length > 0) {
- ;(messageBody as any).attachments = attachments
+ messageBody.attachments = attachments
+ }
+
+ if (mentionEntities.length > 0) {
+ messageBody.mentions = mentionEntities
}
logger.info(`[${requestId}] Sending message to Teams chat: ${validatedData.chatId}`)
diff --git a/apps/sim/tools/microsoft_teams/utils.ts b/apps/sim/tools/microsoft_teams/utils.ts
index 27e25489dc..20b6939318 100644
--- a/apps/sim/tools/microsoft_teams/utils.ts
+++ b/apps/sim/tools/microsoft_teams/utils.ts
@@ -4,6 +4,28 @@ import type { ToolFileData } from '@/tools/types'
const logger = createLogger('MicrosoftTeamsUtils')
+interface ParsedMention {
+ name: string
+ fullTag: string
+ startIndex: number
+ endIndex: number
+}
+
+interface TeamMember {
+ id: string
+ displayName: string
+ userIdentityType?: string
+}
+
+export interface MentionEntity {
+ type: 'mention'
+ mentioned: {
+ id: string
+ name: string
+ }
+ text: string
+}
+
/**
* Transform raw attachment data from Microsoft Graph API
*/
@@ -99,3 +121,161 @@ export async function fetchHostedContentsForChannelMessage(params: {
return []
}
}
+
+function parseMentions(content: string): ParsedMention[] {
+ const mentions: ParsedMention[] = []
+ const mentionRegex = /([^<]+)<\/at>/gi
+ let match: RegExpExecArray | null
+
+ while ((match = mentionRegex.exec(content)) !== null) {
+ const name = match[1].trim()
+ if (name) {
+ mentions.push({
+ name,
+ fullTag: match[0],
+ startIndex: match.index,
+ endIndex: match.index + match[0].length,
+ })
+ }
+ }
+
+ return mentions
+}
+
+async function fetchChatMembers(chatId: string, accessToken: string): Promise {
+ const response = await fetch(
+ `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members`,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ }
+ )
+
+ if (!response.ok) {
+ return []
+ }
+
+ const data = await response.json()
+ return (data.value || []).map((member: TeamMember) => ({
+ id: member.id,
+ displayName: member.displayName || '',
+ userIdentityType: member.userIdentityType,
+ }))
+}
+
+async function fetchChannelMembers(
+ teamId: string,
+ channelId: string,
+ accessToken: string
+): Promise {
+ const response = await fetch(
+ `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/members`,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ }
+ )
+
+ if (!response.ok) {
+ return []
+ }
+
+ const data = await response.json()
+ return (data.value || []).map((member: TeamMember) => ({
+ id: member.id,
+ displayName: member.displayName || '',
+ userIdentityType: member.userIdentityType,
+ }))
+}
+
+function findMemberByName(members: TeamMember[], name: string): TeamMember | undefined {
+ const normalizedName = name.trim().toLowerCase()
+ return members.find((member) => member.displayName.toLowerCase() === normalizedName)
+}
+
+export async function resolveMentionsForChat(
+ content: string,
+ chatId: string,
+ accessToken: string
+): Promise<{ entities: MentionEntity[]; hasMentions: boolean }> {
+ const parsedMentions = parseMentions(content)
+
+ if (parsedMentions.length === 0) {
+ return { entities: [], hasMentions: false }
+ }
+
+ const members = await fetchChatMembers(chatId, accessToken)
+ const entities: MentionEntity[] = []
+ const resolvedNames = new Set()
+
+ for (const mention of parsedMentions) {
+ if (resolvedNames.has(mention.fullTag)) {
+ continue
+ }
+
+ const member = findMemberByName(members, mention.name)
+
+ if (member) {
+ entities.push({
+ type: 'mention',
+ mentioned: {
+ id: member.id,
+ name: member.displayName,
+ },
+ text: mention.fullTag,
+ })
+ resolvedNames.add(mention.fullTag)
+ }
+ }
+
+ return {
+ entities,
+ hasMentions: entities.length > 0,
+ }
+}
+
+export async function resolveMentionsForChannel(
+ content: string,
+ teamId: string,
+ channelId: string,
+ accessToken: string
+): Promise<{ entities: MentionEntity[]; hasMentions: boolean }> {
+ const parsedMentions = parseMentions(content)
+
+ if (parsedMentions.length === 0) {
+ return { entities: [], hasMentions: false }
+ }
+
+ const members = await fetchChannelMembers(teamId, channelId, accessToken)
+ const entities: MentionEntity[] = []
+ const resolvedNames = new Set()
+
+ for (const mention of parsedMentions) {
+ if (resolvedNames.has(mention.fullTag)) {
+ continue
+ }
+
+ const member = findMemberByName(members, mention.name)
+
+ if (member) {
+ entities.push({
+ type: 'mention',
+ mentioned: {
+ id: member.id,
+ name: member.displayName,
+ },
+ text: mention.fullTag,
+ })
+ resolvedNames.add(mention.fullTag)
+ }
+ }
+
+ return {
+ entities,
+ hasMentions: entities.length > 0,
+ }
+}
diff --git a/apps/sim/tools/microsoft_teams/write_channel.ts b/apps/sim/tools/microsoft_teams/write_channel.ts
index 043cda86d7..3b372cc323 100644
--- a/apps/sim/tools/microsoft_teams/write_channel.ts
+++ b/apps/sim/tools/microsoft_teams/write_channel.ts
@@ -73,6 +73,12 @@ export const writeChannelTool: ToolConfig[^<]+<\/at>/i.test(params.content || '')
+ if (hasMentions) {
+ return '/api/tools/microsoft_teams/write_channel'
+ }
+
const encodedTeamId = encodeURIComponent(teamId)
const encodedChannelId = encodeURIComponent(channelId)
@@ -98,7 +104,8 @@ export const writeChannelTool: ToolConfig[^<]+<\/at>/i.test(params.content || '')
if (params.files && params.files.length > 0) {
return {
accessToken: params.accessToken,
@@ -109,6 +116,15 @@ export const writeChannelTool: ToolConfig[^<]+<\/at>/i.test(params.content || '')
+ if (hasMentions) {
+ return '/api/tools/microsoft_teams/write_chat'
+ }
+
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages`
},
method: 'POST',
@@ -82,7 +88,8 @@ export const writeChatTool: ToolConfig[^<]+<\/at>/i.test(params.content || '')
if (params.files && params.files.length > 0) {
return {
accessToken: params.accessToken,
@@ -92,6 +99,14 @@ export const writeChatTool: ToolConfig