From ad0d250af5472cb29060f17e12373290c805a77a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 13 Jan 2026 17:14:27 -0800 Subject: [PATCH 1/2] feat(slack): added get message tool --- apps/docs/components/icons.tsx | 30 ++- apps/docs/content/docs/en/tools/a2a.mdx | 5 - apps/docs/content/docs/en/tools/lemlist.mdx | 3 +- apps/docs/content/docs/en/tools/slack.mdx | 19 ++ apps/sim/blocks/blocks/slack.ts | 40 ++++ apps/sim/tools/registry.ts | 2 + apps/sim/tools/slack/get_message.ts | 216 ++++++++++++++++++++ apps/sim/tools/slack/index.ts | 2 + apps/sim/tools/slack/types.ts | 12 ++ 9 files changed, 311 insertions(+), 18 deletions(-) create mode 100644 apps/sim/tools/slack/get_message.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 7addb30eaa..0143e517a5 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1855,17 +1855,25 @@ export function LinearIcon(props: React.SVGProps) { export function LemlistIcon(props: SVGProps) { return ( - - - - + + + + + ) } diff --git a/apps/docs/content/docs/en/tools/a2a.mdx b/apps/docs/content/docs/en/tools/a2a.mdx index 558f1f907e..63393fe019 100644 --- a/apps/docs/content/docs/en/tools/a2a.mdx +++ b/apps/docs/content/docs/en/tools/a2a.mdx @@ -208,8 +208,3 @@ Delete the push notification webhook configuration for a task. | `success` | boolean | Whether deletion was successful | - -## Notes - -- Category: `tools` -- Type: `a2a` diff --git a/apps/docs/content/docs/en/tools/lemlist.mdx b/apps/docs/content/docs/en/tools/lemlist.mdx index c3b38bb720..25e1a4ca11 100644 --- a/apps/docs/content/docs/en/tools/lemlist.mdx +++ b/apps/docs/content/docs/en/tools/lemlist.mdx @@ -49,8 +49,7 @@ Retrieves lead information by email address or lead ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Lemlist API key | -| `email` | string | No | Lead email address \(use either email or id\) | -| `id` | string | No | Lead ID \(use either email or id\) | +| `leadIdentifier` | string | Yes | Lead email address or lead ID | #### Output diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 8c806d4de8..a234b402ab 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -124,6 +124,25 @@ Read the latest messages from Slack channels. Retrieve conversation history with | --------- | ---- | ----------- | | `messages` | array | Array of message objects from the channel | +### `slack_get_message` + +Retrieve a specific message by its timestamp. Useful for getting a thread parent message. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | Slack channel ID \(e.g., C1234567890\) | +| `timestamp` | string | Yes | Message timestamp to retrieve \(e.g., 1405894322.002768\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | object | The retrieved message object | + ### `slack_list_channels` List all channels in a Slack workspace. Returns public and private channels the bot has access to. diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index c7c37d7576..1abc653bcb 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -26,6 +26,7 @@ export const SlackBlock: BlockConfig = { { label: 'Send Message', id: 'send' }, { label: 'Create Canvas', id: 'canvas' }, { label: 'Read Messages', id: 'read' }, + { label: 'Get Message', id: 'get_message' }, { label: 'List Channels', id: 'list_channels' }, { label: 'List Channel Members', id: 'list_members' }, { label: 'List Users', id: 'list_users' }, @@ -316,6 +317,32 @@ export const SlackBlock: BlockConfig = { }, required: true, }, + // Get Message specific fields + { + id: 'getMessageTimestamp', + title: 'Message Timestamp', + type: 'short-input', + placeholder: 'Message timestamp (e.g., 1405894322.002768)', + condition: { + field: 'operation', + value: 'get_message', + }, + required: true, + wandConfig: { + enabled: true, + prompt: `Extract or generate a Slack message timestamp from the user's input. +Slack message timestamps are in the format: XXXXXXXXXX.XXXXXX (seconds.microseconds since Unix epoch). +Examples: +- "1405894322.002768" -> 1405894322.002768 (already a valid timestamp) +- "thread_ts from the trigger" -> The user wants to reference a variable, output the original text +- A URL like "https://slack.com/archives/C123/p1405894322002768" -> Extract 1405894322.002768 (remove 'p' prefix, add decimal after 10th digit) + +If the input looks like a reference to another block's output (contains < and >) or a variable, return it as-is. +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Paste a Slack message URL or timestamp...', + generationType: 'timestamp', + }, + }, { id: 'oldest', title: 'Oldest Timestamp', @@ -430,6 +457,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'slack_message', 'slack_canvas', 'slack_message_reader', + 'slack_get_message', 'slack_list_channels', 'slack_list_members', 'slack_list_users', @@ -448,6 +476,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'slack_canvas' case 'read': return 'slack_message_reader' + case 'get_message': + return 'slack_get_message' case 'list_channels': return 'slack_list_channels' case 'list_members': @@ -498,6 +528,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, includeDeleted, userLimit, userId, + getMessageTimestamp, ...rest } = params @@ -574,6 +605,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, break } + case 'get_message': + if (!getMessageTimestamp) { + throw new Error('Message timestamp is required for get message operation') + } + baseParams.timestamp = getMessageTimestamp + break + case 'list_channels': { baseParams.includePrivate = includePrivate !== 'false' baseParams.excludeArchived = true @@ -679,6 +717,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, userLimit: { type: 'string', description: 'Maximum number of users to return' }, // Get User inputs userId: { type: 'string', description: 'User ID to look up' }, + // Get Message inputs + getMessageTimestamp: { type: 'string', description: 'Message timestamp to retrieve' }, }, outputs: { // slack_message outputs (send operation) diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 6570eea636..5f3b0e2dda 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1180,6 +1180,7 @@ import { slackCanvasTool, slackDeleteMessageTool, slackDownloadTool, + slackGetMessageTool, slackGetUserTool, slackListChannelsTool, slackListMembersTool, @@ -1731,6 +1732,7 @@ export const tools: Record = { slack_list_members: slackListMembersTool, slack_list_users: slackListUsersTool, slack_get_user: slackGetUserTool, + slack_get_message: slackGetMessageTool, slack_canvas: slackCanvasTool, slack_download: slackDownloadTool, slack_update_message: slackUpdateMessageTool, diff --git a/apps/sim/tools/slack/get_message.ts b/apps/sim/tools/slack/get_message.ts new file mode 100644 index 0000000000..d1baa844f5 --- /dev/null +++ b/apps/sim/tools/slack/get_message.ts @@ -0,0 +1,216 @@ +import type { SlackGetMessageParams, SlackGetMessageResponse } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackGetMessageTool: ToolConfig = { + id: 'slack_get_message', + name: 'Slack Get Message', + description: + 'Retrieve a specific message by its timestamp. Useful for getting a thread parent message.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Slack channel ID (e.g., C1234567890)', + }, + timestamp: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Message timestamp to retrieve (e.g., 1405894322.002768)', + }, + }, + + request: { + url: (params: SlackGetMessageParams) => { + const url = new URL('https://slack.com/api/conversations.replies') + url.searchParams.append('channel', params.channel?.trim() ?? '') + url.searchParams.append('ts', params.timestamp?.trim() ?? '') + url.searchParams.append('limit', '1') + url.searchParams.append('inclusive', 'true') + return url.toString() + }, + method: 'GET', + headers: (params: SlackGetMessageParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please check the channel ID.') + } + if (data.error === 'thread_not_found') { + throw new Error('Message not found. Please check the timestamp.') + } + throw new Error(data.error || 'Failed to get message from Slack') + } + + const messages = data.messages || [] + if (messages.length === 0) { + throw new Error('Message not found') + } + + const msg = messages[0] + const message = { + type: msg.type ?? 'message', + ts: msg.ts, + text: msg.text ?? '', + user: msg.user ?? null, + bot_id: msg.bot_id ?? null, + username: msg.username ?? null, + channel: msg.channel ?? null, + team: msg.team ?? null, + thread_ts: msg.thread_ts ?? null, + parent_user_id: msg.parent_user_id ?? null, + reply_count: msg.reply_count ?? null, + reply_users_count: msg.reply_users_count ?? null, + latest_reply: msg.latest_reply ?? null, + subscribed: msg.subscribed ?? null, + last_read: msg.last_read ?? null, + unread_count: msg.unread_count ?? null, + subtype: msg.subtype ?? null, + reactions: msg.reactions ?? [], + is_starred: msg.is_starred ?? false, + pinned_to: msg.pinned_to ?? [], + files: (msg.files ?? []).map((f: any) => ({ + id: f.id, + name: f.name, + mimetype: f.mimetype, + size: f.size, + url_private: f.url_private ?? null, + permalink: f.permalink ?? null, + mode: f.mode ?? null, + })), + attachments: msg.attachments ?? [], + blocks: msg.blocks ?? [], + edited: msg.edited ?? null, + permalink: msg.permalink ?? null, + } + + return { + success: true, + output: { + message, + }, + } + }, + + outputs: { + message: { + type: 'object', + description: 'The retrieved message object', + properties: { + type: { type: 'string', description: 'Message type' }, + ts: { type: 'string', description: 'Message timestamp' }, + text: { type: 'string', description: 'Message text content' }, + user: { type: 'string', description: 'User ID who sent the message' }, + bot_id: { type: 'string', description: 'Bot ID if sent by a bot', optional: true }, + username: { type: 'string', description: 'Display username', optional: true }, + channel: { type: 'string', description: 'Channel ID', optional: true }, + team: { type: 'string', description: 'Team ID', optional: true }, + thread_ts: { type: 'string', description: 'Thread parent timestamp', optional: true }, + parent_user_id: { type: 'string', description: 'User ID of thread parent', optional: true }, + reply_count: { type: 'number', description: 'Number of thread replies', optional: true }, + reply_users_count: { + type: 'number', + description: 'Number of users who replied', + optional: true, + }, + latest_reply: { type: 'string', description: 'Timestamp of latest reply', optional: true }, + subtype: { type: 'string', description: 'Message subtype', optional: true }, + reactions: { + type: 'array', + description: 'Array of reactions on this message', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Emoji name' }, + count: { type: 'number', description: 'Number of reactions' }, + users: { + type: 'array', + description: 'User IDs who reacted', + items: { type: 'string' }, + }, + }, + }, + }, + is_starred: { type: 'boolean', description: 'Whether message is starred', optional: true }, + pinned_to: { + type: 'array', + description: 'Channel IDs where message is pinned', + items: { type: 'string' }, + optional: true, + }, + files: { + type: 'array', + description: 'Files attached to message', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'File ID' }, + name: { type: 'string', description: 'File name' }, + mimetype: { type: 'string', description: 'MIME type' }, + size: { type: 'number', description: 'File size in bytes' }, + url_private: { type: 'string', description: 'Private download URL' }, + permalink: { type: 'string', description: 'Permanent link to file' }, + }, + }, + }, + attachments: { + type: 'array', + description: 'Legacy attachments', + items: { type: 'object' }, + }, + blocks: { type: 'array', description: 'Block Kit blocks', items: { type: 'object' } }, + edited: { + type: 'object', + description: 'Edit information if message was edited', + properties: { + user: { type: 'string', description: 'User ID who edited' }, + ts: { type: 'string', description: 'Edit timestamp' }, + }, + optional: true, + }, + permalink: { type: 'string', description: 'Permanent link to message', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/slack/index.ts b/apps/sim/tools/slack/index.ts index ea99459cd3..bf7010cf8a 100644 --- a/apps/sim/tools/slack/index.ts +++ b/apps/sim/tools/slack/index.ts @@ -2,6 +2,7 @@ import { slackAddReactionTool } from '@/tools/slack/add_reaction' import { slackCanvasTool } from '@/tools/slack/canvas' import { slackDeleteMessageTool } from '@/tools/slack/delete_message' import { slackDownloadTool } from '@/tools/slack/download' +import { slackGetMessageTool } from '@/tools/slack/get_message' import { slackGetUserTool } from '@/tools/slack/get_user' import { slackListChannelsTool } from '@/tools/slack/list_channels' import { slackListMembersTool } from '@/tools/slack/list_members' @@ -22,4 +23,5 @@ export { slackListMembersTool, slackListUsersTool, slackGetUserTool, + slackGetMessageTool, } diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 3cbf79b81d..75edf986d5 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -71,6 +71,11 @@ export interface SlackGetUserParams extends SlackBaseParams { userId: string } +export interface SlackGetMessageParams extends SlackBaseParams { + channel: string + timestamp: string +} + export interface SlackMessageResponse extends ToolResponse { output: { // Legacy properties for backward compatibility @@ -305,6 +310,12 @@ export interface SlackGetUserResponse extends ToolResponse { } } +export interface SlackGetMessageResponse extends ToolResponse { + output: { + message: SlackMessage + } +} + export type SlackResponse = | SlackCanvasResponse | SlackMessageReaderResponse @@ -317,3 +328,4 @@ export type SlackResponse = | SlackListMembersResponse | SlackListUsersResponse | SlackGetUserResponse + | SlackGetMessageResponse From 65c16e1b713c9f0ade499db65b63f42451f37089 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 13 Jan 2026 18:30:05 -0800 Subject: [PATCH 2/2] added get thread --- apps/docs/content/docs/en/tools/slack.mdx | 20 ++ apps/sim/blocks/blocks/slack.ts | 80 ++++++++ apps/sim/tools/registry.ts | 2 + apps/sim/tools/slack/get_message.ts | 7 +- apps/sim/tools/slack/get_thread.ts | 224 ++++++++++++++++++++++ apps/sim/tools/slack/index.ts | 2 + apps/sim/tools/slack/types.ts | 17 ++ 7 files changed, 347 insertions(+), 5 deletions(-) create mode 100644 apps/sim/tools/slack/get_thread.ts diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index a234b402ab..1181f24bb7 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -143,6 +143,26 @@ Retrieve a specific message by its timestamp. Useful for getting a thread parent | --------- | ---- | ----------- | | `message` | object | The retrieved message object | +### `slack_get_thread` + +Retrieve an entire thread including the parent message and all replies. Useful for getting full conversation context. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | Slack channel ID \(e.g., C1234567890\) | +| `threadTs` | string | Yes | Thread timestamp \(thread_ts\) to retrieve \(e.g., 1405894322.002768\) | +| `limit` | number | No | Maximum number of messages to return \(default: 100, max: 200\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `parentMessage` | object | The thread parent message | + ### `slack_list_channels` List all channels in a Slack workspace. Returns public and private channels the bot has access to. diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 1abc653bcb..7e432f27f0 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -27,6 +27,7 @@ export const SlackBlock: BlockConfig = { { label: 'Create Canvas', id: 'canvas' }, { label: 'Read Messages', id: 'read' }, { label: 'Get Message', id: 'get_message' }, + { label: 'Get Thread', id: 'get_thread' }, { label: 'List Channels', id: 'list_channels' }, { label: 'List Channel Members', id: 'list_members' }, { label: 'List Users', id: 'list_users' }, @@ -343,6 +344,42 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, generationType: 'timestamp', }, }, + // Get Thread specific fields + { + id: 'getThreadTimestamp', + title: 'Thread Timestamp', + type: 'short-input', + placeholder: 'Thread timestamp (thread_ts, e.g., 1405894322.002768)', + condition: { + field: 'operation', + value: 'get_thread', + }, + required: true, + wandConfig: { + enabled: true, + prompt: `Extract or generate a Slack thread timestamp from the user's input. +Slack thread timestamps (thread_ts) are in the format: XXXXXXXXXX.XXXXXX (seconds.microseconds since Unix epoch). +Examples: +- "1405894322.002768" -> 1405894322.002768 (already a valid timestamp) +- "thread_ts from the trigger" -> The user wants to reference a variable, output the original text +- A URL like "https://slack.com/archives/C123/p1405894322002768" -> Extract 1405894322.002768 (remove 'p' prefix, add decimal after 10th digit) + +If the input looks like a reference to another block's output (contains < and >) or a variable, return it as-is. +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Paste a Slack thread URL or thread_ts...', + generationType: 'timestamp', + }, + }, + { + id: 'threadLimit', + title: 'Message Limit', + type: 'short-input', + placeholder: '100', + condition: { + field: 'operation', + value: 'get_thread', + }, + }, { id: 'oldest', title: 'Oldest Timestamp', @@ -458,6 +495,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'slack_canvas', 'slack_message_reader', 'slack_get_message', + 'slack_get_thread', 'slack_list_channels', 'slack_list_members', 'slack_list_users', @@ -478,6 +516,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'slack_message_reader' case 'get_message': return 'slack_get_message' + case 'get_thread': + return 'slack_get_thread' case 'list_channels': return 'slack_list_channels' case 'list_members': @@ -529,6 +569,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, userLimit, userId, getMessageTimestamp, + getThreadTimestamp, + threadLimit, ...rest } = params @@ -612,6 +654,20 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, baseParams.timestamp = getMessageTimestamp break + case 'get_thread': { + if (!getThreadTimestamp) { + throw new Error('Thread timestamp is required for get thread operation') + } + baseParams.threadTs = getThreadTimestamp + if (threadLimit) { + const parsedLimit = Number.parseInt(threadLimit, 10) + if (!Number.isNaN(parsedLimit) && parsedLimit > 0) { + baseParams.limit = Math.min(parsedLimit, 200) + } + } + break + } + case 'list_channels': { baseParams.includePrivate = includePrivate !== 'false' baseParams.excludeArchived = true @@ -719,6 +775,12 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, userId: { type: 'string', description: 'User ID to look up' }, // Get Message inputs getMessageTimestamp: { type: 'string', description: 'Message timestamp to retrieve' }, + // Get Thread inputs + getThreadTimestamp: { type: 'string', description: 'Thread timestamp to retrieve' }, + threadLimit: { + type: 'string', + description: 'Maximum number of messages to return from thread', + }, }, outputs: { // slack_message outputs (send operation) @@ -746,6 +808,24 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'Array of message objects with comprehensive properties: text, user, timestamp, reactions, threads, files, attachments, blocks, stars, pins, and edit history', }, + // slack_get_thread outputs (get_thread operation) + parentMessage: { + type: 'json', + description: 'The thread parent message with all properties', + }, + replies: { + type: 'json', + description: 'Array of reply messages in the thread (excluding the parent)', + }, + replyCount: { + type: 'number', + description: 'Number of replies returned in this response', + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more messages in the thread', + }, + // slack_list_channels outputs (list_channels operation) channels: { type: 'json', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 5f3b0e2dda..c5137da6e5 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1181,6 +1181,7 @@ import { slackDeleteMessageTool, slackDownloadTool, slackGetMessageTool, + slackGetThreadTool, slackGetUserTool, slackListChannelsTool, slackListMembersTool, @@ -1733,6 +1734,7 @@ export const tools: Record = { slack_list_users: slackListUsersTool, slack_get_user: slackGetUserTool, slack_get_message: slackGetMessageTool, + slack_get_thread: slackGetThreadTool, slack_canvas: slackCanvasTool, slack_download: slackDownloadTool, slack_update_message: slackUpdateMessageTool, diff --git a/apps/sim/tools/slack/get_message.ts b/apps/sim/tools/slack/get_message.ts index d1baa844f5..f651f00a1a 100644 --- a/apps/sim/tools/slack/get_message.ts +++ b/apps/sim/tools/slack/get_message.ts @@ -48,9 +48,9 @@ export const slackGetMessageTool: ToolConfig { - const url = new URL('https://slack.com/api/conversations.replies') + const url = new URL('https://slack.com/api/conversations.history') url.searchParams.append('channel', params.channel?.trim() ?? '') - url.searchParams.append('ts', params.timestamp?.trim() ?? '') + url.searchParams.append('oldest', params.timestamp?.trim() ?? '') url.searchParams.append('limit', '1') url.searchParams.append('inclusive', 'true') return url.toString() @@ -77,9 +77,6 @@ export const slackGetMessageTool: ToolConfig = { + id: 'slack_get_thread', + name: 'Slack Get Thread', + description: + 'Retrieve an entire thread including the parent message and all replies. Useful for getting full conversation context.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Slack channel ID (e.g., C1234567890)', + }, + threadTs: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Thread timestamp (thread_ts) to retrieve (e.g., 1405894322.002768)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of messages to return (default: 100, max: 200)', + }, + }, + + request: { + url: (params: SlackGetThreadParams) => { + const url = new URL('https://slack.com/api/conversations.replies') + url.searchParams.append('channel', params.channel?.trim() ?? '') + url.searchParams.append('ts', params.threadTs?.trim() ?? '') + url.searchParams.append('inclusive', 'true') + const limit = params.limit ? Math.min(Number(params.limit), 200) : 100 + url.searchParams.append('limit', String(limit)) + return url.toString() + }, + method: 'GET', + headers: (params: SlackGetThreadParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please check the channel ID.') + } + if (data.error === 'thread_not_found') { + throw new Error('Thread not found. Please check the thread timestamp.') + } + throw new Error(data.error || 'Failed to get thread from Slack') + } + + const rawMessages = data.messages || [] + if (rawMessages.length === 0) { + throw new Error('Thread not found') + } + + const messages = rawMessages.map((msg: any) => ({ + type: msg.type ?? 'message', + ts: msg.ts, + text: msg.text ?? '', + user: msg.user ?? null, + bot_id: msg.bot_id ?? null, + username: msg.username ?? null, + channel: msg.channel ?? null, + team: msg.team ?? null, + thread_ts: msg.thread_ts ?? null, + parent_user_id: msg.parent_user_id ?? null, + reply_count: msg.reply_count ?? null, + reply_users_count: msg.reply_users_count ?? null, + latest_reply: msg.latest_reply ?? null, + subscribed: msg.subscribed ?? null, + last_read: msg.last_read ?? null, + unread_count: msg.unread_count ?? null, + subtype: msg.subtype ?? null, + reactions: msg.reactions ?? [], + is_starred: msg.is_starred ?? false, + pinned_to: msg.pinned_to ?? [], + files: (msg.files ?? []).map((f: any) => ({ + id: f.id, + name: f.name, + mimetype: f.mimetype, + size: f.size, + url_private: f.url_private ?? null, + permalink: f.permalink ?? null, + mode: f.mode ?? null, + })), + attachments: msg.attachments ?? [], + blocks: msg.blocks ?? [], + edited: msg.edited ?? null, + permalink: msg.permalink ?? null, + })) + + // First message is always the parent + const parentMessage = messages[0] + // Remaining messages are replies + const replies = messages.slice(1) + + return { + success: true, + output: { + parentMessage, + replies, + messages, + replyCount: replies.length, + hasMore: data.has_more ?? false, + }, + } + }, + + outputs: { + parentMessage: { + type: 'object', + description: 'The thread parent message', + properties: { + type: { type: 'string', description: 'Message type' }, + ts: { type: 'string', description: 'Message timestamp' }, + text: { type: 'string', description: 'Message text content' }, + user: { type: 'string', description: 'User ID who sent the message' }, + bot_id: { type: 'string', description: 'Bot ID if sent by a bot', optional: true }, + username: { type: 'string', description: 'Display username', optional: true }, + reply_count: { type: 'number', description: 'Total number of thread replies' }, + reply_users_count: { type: 'number', description: 'Number of users who replied' }, + latest_reply: { type: 'string', description: 'Timestamp of latest reply' }, + reactions: { + type: 'array', + description: 'Array of reactions on the parent message', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Emoji name' }, + count: { type: 'number', description: 'Number of reactions' }, + users: { + type: 'array', + description: 'User IDs who reacted', + items: { type: 'string' }, + }, + }, + }, + }, + files: { + type: 'array', + description: 'Files attached to the parent message', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'File ID' }, + name: { type: 'string', description: 'File name' }, + mimetype: { type: 'string', description: 'MIME type' }, + size: { type: 'number', description: 'File size in bytes' }, + }, + }, + }, + }, + }, + replies: { + type: 'array', + description: 'Array of reply messages in the thread (excluding the parent)', + items: { + type: 'object', + properties: { + ts: { type: 'string', description: 'Message timestamp' }, + text: { type: 'string', description: 'Message text content' }, + user: { type: 'string', description: 'User ID who sent the reply' }, + reactions: { type: 'array', description: 'Reactions on the reply' }, + files: { type: 'array', description: 'Files attached to the reply' }, + }, + }, + }, + messages: { + type: 'array', + description: 'All messages in the thread (parent + replies) in chronological order', + items: { type: 'object' }, + }, + replyCount: { + type: 'number', + description: 'Number of replies returned in this response', + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more messages in the thread (pagination needed)', + }, + }, +} diff --git a/apps/sim/tools/slack/index.ts b/apps/sim/tools/slack/index.ts index bf7010cf8a..2bc0f249ef 100644 --- a/apps/sim/tools/slack/index.ts +++ b/apps/sim/tools/slack/index.ts @@ -3,6 +3,7 @@ import { slackCanvasTool } from '@/tools/slack/canvas' import { slackDeleteMessageTool } from '@/tools/slack/delete_message' import { slackDownloadTool } from '@/tools/slack/download' import { slackGetMessageTool } from '@/tools/slack/get_message' +import { slackGetThreadTool } from '@/tools/slack/get_thread' import { slackGetUserTool } from '@/tools/slack/get_user' import { slackListChannelsTool } from '@/tools/slack/list_channels' import { slackListMembersTool } from '@/tools/slack/list_members' @@ -24,4 +25,5 @@ export { slackListUsersTool, slackGetUserTool, slackGetMessageTool, + slackGetThreadTool, } diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 75edf986d5..4271409aa0 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -76,6 +76,12 @@ export interface SlackGetMessageParams extends SlackBaseParams { timestamp: string } +export interface SlackGetThreadParams extends SlackBaseParams { + channel: string + threadTs: string + limit?: number +} + export interface SlackMessageResponse extends ToolResponse { output: { // Legacy properties for backward compatibility @@ -316,6 +322,16 @@ export interface SlackGetMessageResponse extends ToolResponse { } } +export interface SlackGetThreadResponse extends ToolResponse { + output: { + parentMessage: SlackMessage + replies: SlackMessage[] + messages: SlackMessage[] + replyCount: number + hasMore: boolean + } +} + export type SlackResponse = | SlackCanvasResponse | SlackMessageReaderResponse @@ -329,3 +345,4 @@ export type SlackResponse = | SlackListUsersResponse | SlackGetUserResponse | SlackGetMessageResponse + | SlackGetThreadResponse