diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts
index 2d749862756..b356834eacd 100644
--- a/apps/sim/lib/webhooks/utils.server.ts
+++ b/apps/sim/lib/webhooks/utils.server.ts
@@ -530,6 +530,66 @@ export async function validateTwilioSignature(
const SLACK_MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB
const SLACK_MAX_FILES = 15
+/**
+ * Fetches the original message text for a reaction event using conversations.history.
+ * Reaction events don't include message text, so we need to fetch it separately.
+ * Returns empty string if permissions are missing or fetch fails.
+ */
+async function fetchSlackReactionMessageText(
+ channel: string,
+ messageTs: string,
+ botToken: string
+): Promise<{ text: string; user?: string }> {
+ try {
+ const url = new URL('https://slack.com/api/conversations.history')
+ url.searchParams.append('channel', channel)
+ url.searchParams.append('oldest', messageTs)
+ url.searchParams.append('limit', '1')
+ url.searchParams.append('inclusive', 'true')
+
+ const response = await fetch(url.toString(), {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${botToken}`,
+ },
+ })
+
+ const data = (await response.json()) as {
+ ok: boolean
+ error?: string
+ messages?: Array<{ text?: string; user?: string }>
+ }
+
+ if (!data.ok) {
+ if (data.error === 'missing_scope') {
+ logger.debug('Missing scope for fetching reaction message text - channels:history required')
+ } else if (data.error === 'channel_not_found') {
+ logger.debug('Channel not found when fetching reaction message text', { channel })
+ } else {
+ logger.warn('Failed to fetch reaction message text', { error: data.error, channel })
+ }
+ return { text: '' }
+ }
+
+ const messages = data.messages || []
+ if (messages.length === 0) {
+ return { text: '' }
+ }
+
+ return {
+ text: messages[0].text || '',
+ user: messages[0].user,
+ }
+ } catch (error) {
+ logger.warn('Error fetching reaction message text', {
+ error: error instanceof Error ? error.message : String(error),
+ channel,
+ })
+ return { text: '' }
+ }
+}
+
/**
* Resolves the full file object from the Slack API when the event payload
* only contains a partial file (e.g. missing url_private due to file_access restrictions).
@@ -953,6 +1013,51 @@ export async function formatWebhookInput(
})
}
+ const eventType = rawEvent?.type || body?.type || 'unknown'
+
+ // Handle reaction events (reaction_added, reaction_removed) which have a different payload structure
+ const isReactionEvent = eventType === 'reaction_added' || eventType === 'reaction_removed'
+
+ if (isReactionEvent) {
+ // Reaction events have item.channel instead of channel, and item.ts for the message timestamp
+ const itemChannel = rawEvent?.item?.channel || ''
+ const itemTs = rawEvent?.item?.ts || ''
+ const reaction = rawEvent?.reaction || ''
+ const itemUser = rawEvent?.item_user || ''
+
+ // Fetch the original message text if bot token is available
+ let messageText = ''
+ let messageAuthor = itemUser
+ if (botToken && itemChannel && itemTs) {
+ const messageData = await fetchSlackReactionMessageText(itemChannel, itemTs, botToken)
+ messageText = messageData.text
+ if (messageData.user) {
+ messageAuthor = messageData.user
+ }
+ }
+
+ return {
+ event: {
+ event_type: eventType,
+ channel: itemChannel,
+ channel_name: '',
+ user: rawEvent?.user || '',
+ user_name: '',
+ text: messageText,
+ reaction,
+ item_user: messageAuthor,
+ timestamp: itemTs,
+ event_ts: rawEvent?.event_ts || '',
+ thread_ts: '',
+ team_id: body?.team_id || rawEvent?.team || '',
+ event_id: body?.event_id || '',
+ hasFiles: false,
+ files: [],
+ },
+ }
+ }
+
+ // Standard message events
const rawFiles: any[] = rawEvent?.files ?? []
const hasFiles = rawFiles.length > 0
@@ -965,7 +1070,7 @@ export async function formatWebhookInput(
return {
event: {
- event_type: rawEvent?.type || body?.type || 'unknown',
+ event_type: eventType,
channel: rawEvent?.channel || '',
channel_name: '',
user: rawEvent?.user || '',
diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts
index 3d22e3be20f..4f891131266 100644
--- a/apps/sim/triggers/slack/webhook.ts
+++ b/apps/sim/triggers/slack/webhook.ts
@@ -67,7 +67,7 @@ export const slackWebhookTrigger: TriggerConfig = {
'Go to Slack Apps page',
'If you don\'t have an app:
app_mentions:read - For viewing messages that tag your bot with an @chat:write - To send messages to channels your bot is a part offiles:read - To access files and images shared in messagesapp_mentions:read - For viewing messages that tag your bot with an @chat:write - To send messages to channels your bot is a part offiles:read - To access files and images shared in messageschannels:history - To fetch message text for reaction eventsreactions:read - To receive reaction eventsapp_mention to listen to messages that mention your botxoxb-) and paste it in the Bot Token field above to enable file downloads.',
@@ -90,7 +90,8 @@ export const slackWebhookTrigger: TriggerConfig = {
properties: {
event_type: {
type: 'string',
- description: 'Type of Slack event (e.g., app_mention, message)',
+ description:
+ 'Type of Slack event (e.g., app_mention, message, reaction_added, reaction_removed)',
},
channel: {
type: 'string',
@@ -102,7 +103,8 @@ export const slackWebhookTrigger: TriggerConfig = {
},
user: {
type: 'string',
- description: 'User ID who triggered the event',
+ description:
+ 'User ID who triggered the event (for reactions, the user who added/removed the reaction)',
},
user_name: {
type: 'string',
@@ -110,11 +112,26 @@ export const slackWebhookTrigger: TriggerConfig = {
},
text: {
type: 'string',
- description: 'Message text content',
+ description:
+ 'Message text content. For reaction events, fetched via API if bot token has channels:history scope.',
+ },
+ reaction: {
+ type: 'string',
+ description: 'Emoji name for reaction events (e.g., "thumbsup", "heart")',
+ },
+ item_user: {
+ type: 'string',
+ description: 'User ID of the message author (for reaction events)',
},
timestamp: {
type: 'string',
- description: 'Message timestamp from the triggering event',
+ description:
+ 'Message timestamp. For reactions, this is the timestamp of the reacted-to message (item.ts).',
+ },
+ event_ts: {
+ type: 'string',
+ description:
+ 'Event timestamp. For reactions, this is when the reaction was added/removed.',
},
thread_ts: {
type: 'string',