diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f369210ec0..73e5e852fb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,7 +144,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} provenance: false sbom: false - no-cache: true # Build ARM64 images for GHCR (main branch only, runs in parallel) build-ghcr-arm64: @@ -205,7 +204,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} provenance: false sbom: false - no-cache: true # Create GHCR multi-arch manifests (only for main, after both builds) create-ghcr-manifests: diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index e3136510eb1..44e8636d909 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -97,7 +97,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} provenance: false sbom: false - no-cache: true build-ghcr-arm64: name: Build ARM64 (GHCR Only) @@ -144,11 +143,10 @@ jobs: tags: ${{ steps.meta.outputs.tags }} provenance: false sbom: false - no-cache: true create-ghcr-manifests: name: Create GHCR Manifests - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: blacksmith-2vcpu-ubuntu-2404 needs: [build-amd64, build-ghcr-arm64] if: github.ref == 'refs/heads/main' strategy: diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 96480e7f2bb..10dd6f0b012 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -110,7 +110,7 @@ jobs: RESEND_API_KEY: 'dummy_key_for_ci_only' AWS_REGION: 'us-west-2' ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only - run: bun run build + run: bunx turbo run build --filter=sim - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 diff --git a/apps/docs/content/docs/en/tools/jira_service_management.mdx b/apps/docs/content/docs/en/tools/jira_service_management.mdx index 9814f81036f..cd294152d3e 100644 --- a/apps/docs/content/docs/en/tools/jira_service_management.mdx +++ b/apps/docs/content/docs/en/tools/jira_service_management.mdx @@ -116,7 +116,7 @@ Create a new service request in Jira Service Management | `summary` | string | Yes | Summary/title for the service request | | `description` | string | No | Description for the service request | | `raiseOnBehalfOf` | string | No | Account ID of customer to raise request on behalf of | -| `requestFieldValues` | json | No | Custom field values as key-value pairs \(overrides summary/description if provided\) | +| `requestFieldValues` | json | No | Request field values as key-value pairs \(overrides summary/description if provided\) | | `requestParticipants` | string | No | Comma-separated account IDs to add as request participants | | `channel` | string | No | Channel the request originates from \(e.g., portal, email\) | diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 0f4285e2a16..c51fea7ff2c 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -1,6 +1,6 @@ --- title: Slack -description: Send, update, delete messages, add reactions in Slack or trigger workflows from Slack events +description: Send, update, delete messages, send ephemeral messages, add reactions in Slack or trigger workflows from Slack events --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -59,7 +59,7 @@ If you encounter issues with the Slack integration, contact us at [help@sim.ai]( ## Usage Instructions -Integrate Slack into the workflow. Can send, update, and delete messages, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel. +Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel. @@ -80,6 +80,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format | `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) | | `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) | | `threadTs` | string | No | Thread timestamp to reply to \(creates thread reply\) | +| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. | | `files` | file[] | No | Files to attach to the message | #### Output @@ -146,6 +147,29 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format | `fileCount` | number | Number of files uploaded \(when files are attached\) | | `files` | file[] | Files attached to the message | +### `slack_ephemeral_message` + +Send an ephemeral message visible only to a specific user in a channel. Optionally reply in a thread. The message does not persist across sessions. + +#### 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\) | +| `user` | string | Yes | User ID who will see the ephemeral message \(e.g., U1234567890\). Must be a member of the channel. | +| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) | +| `threadTs` | string | No | Thread timestamp to reply in. When provided, the ephemeral message appears as a thread reply. | +| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `messageTs` | string | Timestamp of the ephemeral message \(cannot be used with chat.update\) | +| `channel` | string | Channel ID where the ephemeral message was sent | + ### `slack_canvas` Create and share Slack canvases in channels. Canvases are collaborative documents within Slack. @@ -682,6 +706,7 @@ Update a message previously sent by the bot in Slack | `channel` | string | Yes | Channel ID where the message was posted \(e.g., C1234567890\) | | `timestamp` | string | Yes | Timestamp of the message to update \(e.g., 1405894322.002768\) | | `text` | string | Yes | New message text \(supports Slack mrkdwn formatting\) | +| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. | #### Output diff --git a/apps/sim/app/api/tools/slack/send-ephemeral/route.ts b/apps/sim/app/api/tools/slack/send-ephemeral/route.ts new file mode 100644 index 00000000000..6d443e5039a --- /dev/null +++ b/apps/sim/app/api/tools/slack/send-ephemeral/route.ts @@ -0,0 +1,96 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SlackSendEphemeralAPI') + +const SlackSendEphemeralSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().min(1, 'Channel ID is required'), + user: z.string().min(1, 'User ID is required'), + text: z.string().min(1, 'Message text is required'), + thread_ts: z.string().optional().nullable(), + blocks: z.array(z.record(z.unknown())).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Slack ephemeral send attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated Slack ephemeral send request via ${authResult.authType}`, + { userId: authResult.userId } + ) + + const body = await request.json() + const validatedData = SlackSendEphemeralSchema.parse(body) + + logger.info(`[${requestId}] Sending ephemeral message`, { + channel: validatedData.channel, + user: validatedData.user, + threadTs: validatedData.thread_ts ?? undefined, + }) + + const response = await fetch('https://slack.com/api/chat.postEphemeral', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: JSON.stringify({ + channel: validatedData.channel, + user: validatedData.user, + text: validatedData.text, + ...(validatedData.thread_ts && { thread_ts: validatedData.thread_ts }), + ...(validatedData.blocks && + validatedData.blocks.length > 0 && { blocks: validatedData.blocks }), + }), + }) + + const data = await response.json() + + if (!data.ok) { + logger.error(`[${requestId}] Slack API error:`, data.error) + return NextResponse.json( + { success: false, error: data.error || 'Failed to send ephemeral message' }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Ephemeral message sent successfully`) + + return NextResponse.json({ + success: true, + output: { + messageTs: data.message_ts, + channel: validatedData.channel, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error sending ephemeral message:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/slack/send-message/route.ts b/apps/sim/app/api/tools/slack/send-message/route.ts index 21f60faf6c7..a6b8a3db71c 100644 --- a/apps/sim/app/api/tools/slack/send-message/route.ts +++ b/apps/sim/app/api/tools/slack/send-message/route.ts @@ -17,6 +17,7 @@ const SlackSendMessageSchema = z userId: z.string().optional().nullable(), text: z.string().min(1, 'Message text is required'), thread_ts: z.string().optional().nullable(), + blocks: z.array(z.record(z.unknown())).optional().nullable(), files: RawFileInputArraySchema.optional().nullable(), }) .refine((data) => data.channel || data.userId, { @@ -63,6 +64,7 @@ export async function POST(request: NextRequest) { userId: validatedData.userId ?? undefined, text: validatedData.text, threadTs: validatedData.thread_ts ?? undefined, + blocks: validatedData.blocks ?? undefined, files: validatedData.files ?? undefined, }, requestId, diff --git a/apps/sim/app/api/tools/slack/update-message/route.ts b/apps/sim/app/api/tools/slack/update-message/route.ts index 4edd983a565..ccf0a045294 100644 --- a/apps/sim/app/api/tools/slack/update-message/route.ts +++ b/apps/sim/app/api/tools/slack/update-message/route.ts @@ -13,6 +13,7 @@ const SlackUpdateMessageSchema = z.object({ channel: z.string().min(1, 'Channel is required'), timestamp: z.string().min(1, 'Message timestamp is required'), text: z.string().min(1, 'Message text is required'), + blocks: z.array(z.record(z.unknown())).optional().nullable(), }) export async function POST(request: NextRequest) { @@ -57,6 +58,8 @@ export async function POST(request: NextRequest) { channel: validatedData.channel, ts: validatedData.timestamp, text: validatedData.text, + ...(validatedData.blocks && + validatedData.blocks.length > 0 && { blocks: validatedData.blocks }), }), }) diff --git a/apps/sim/app/api/tools/slack/utils.ts b/apps/sim/app/api/tools/slack/utils.ts index b635c49d8fe..4049a3fe0db 100644 --- a/apps/sim/app/api/tools/slack/utils.ts +++ b/apps/sim/app/api/tools/slack/utils.ts @@ -11,7 +11,8 @@ export async function postSlackMessage( accessToken: string, channel: string, text: string, - threadTs?: string | null + threadTs?: string | null, + blocks?: unknown[] | null ): Promise<{ ok: boolean; ts?: string; channel?: string; message?: any; error?: string }> { const response = await fetch('https://slack.com/api/chat.postMessage', { method: 'POST', @@ -23,6 +24,7 @@ export async function postSlackMessage( channel, text, ...(threadTs && { thread_ts: threadTs }), + ...(blocks && blocks.length > 0 && { blocks }), }), }) @@ -220,6 +222,7 @@ export interface SlackMessageParams { userId?: string text: string threadTs?: string | null + blocks?: unknown[] | null files?: any[] | null } @@ -242,7 +245,7 @@ export async function sendSlackMessage( } error?: string }> { - const { accessToken, text, threadTs, files } = params + const { accessToken, text, threadTs, blocks, files } = params let { channel } = params if (!channel && params.userId) { @@ -258,7 +261,7 @@ export async function sendSlackMessage( if (!files || files.length === 0) { logger.info(`[${requestId}] No files, using chat.postMessage`) - const data = await postSlackMessage(accessToken, channel, text, threadTs) + const data = await postSlackMessage(accessToken, channel, text, threadTs, blocks) if (!data.ok) { logger.error(`[${requestId}] Slack API error:`, data.error) @@ -282,7 +285,7 @@ export async function sendSlackMessage( if (fileIds.length === 0) { logger.warn(`[${requestId}] No valid files to upload, sending text-only message`) - const data = await postSlackMessage(accessToken, channel, text, threadTs) + const data = await postSlackMessage(accessToken, channel, text, threadTs, blocks) if (!data.ok) { return { success: false, error: data.error || 'Failed to send message' } diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index c4337fab4c8..77c44a21bff 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -9,10 +9,10 @@ export const SlackBlock: BlockConfig = { type: 'slack', name: 'Slack', description: - 'Send, update, delete messages, add reactions in Slack or trigger workflows from Slack events', + 'Send, update, delete messages, send ephemeral messages, add reactions in Slack or trigger workflows from Slack events', authMode: AuthMode.OAuth, longDescription: - 'Integrate Slack into the workflow. Can send, update, and delete messages, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.', + 'Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.', docsLink: 'https://docs.sim.ai/tools/slack', category: 'tools', bgColor: '#611f69', @@ -25,6 +25,7 @@ export const SlackBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'Send Message', id: 'send' }, + { label: 'Send Ephemeral Message', id: 'ephemeral' }, { label: 'Create Canvas', id: 'canvas' }, { label: 'Read Messages', id: 'read' }, { label: 'Get Message', id: 'get_message' }, @@ -116,15 +117,21 @@ export const SlackBlock: BlockConfig = { placeholder: 'Select Slack channel', mode: 'basic', dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }, - condition: { - field: 'operation', - value: ['list_channels', 'list_users', 'get_user'], - not: true, - and: { - field: 'destinationType', - value: 'dm', + condition: (values?: Record) => { + const op = values?.operation as string + if (op === 'ephemeral') { + return { field: 'operation', value: 'ephemeral' } + } + return { + field: 'operation', + value: ['list_channels', 'list_users', 'get_user'], not: true, - }, + and: { + field: 'destinationType', + value: 'dm', + not: true, + }, + } }, required: true, }, @@ -135,15 +142,21 @@ export const SlackBlock: BlockConfig = { canonicalParamId: 'channel', placeholder: 'Enter Slack channel ID (e.g., C1234567890)', mode: 'advanced', - condition: { - field: 'operation', - value: ['list_channels', 'list_users', 'get_user'], - not: true, - and: { - field: 'destinationType', - value: 'dm', + condition: (values?: Record) => { + const op = values?.operation as string + if (op === 'ephemeral') { + return { field: 'operation', value: 'ephemeral' } + } + return { + field: 'operation', + value: ['list_channels', 'list_users', 'get_user'], not: true, - }, + and: { + field: 'destinationType', + value: 'dm', + not: true, + }, + } }, required: true, }, @@ -175,6 +188,31 @@ export const SlackBlock: BlockConfig = { }, required: true, }, + { + id: 'ephemeralUser', + title: 'Target User', + type: 'short-input', + placeholder: 'User ID who will see the message (e.g., U1234567890)', + condition: { + field: 'operation', + value: 'ephemeral', + }, + required: true, + }, + { + id: 'messageFormat', + title: 'Message Format', + type: 'dropdown', + options: [ + { label: 'Plain Text', id: 'text' }, + { label: 'Block Kit', id: 'blocks' }, + ], + value: () => 'text', + condition: { + field: 'operation', + value: ['send', 'ephemeral', 'update'], + }, + }, { id: 'text', title: 'Message', @@ -182,9 +220,77 @@ export const SlackBlock: BlockConfig = { placeholder: 'Enter your message (supports Slack mrkdwn)', condition: { field: 'operation', - value: 'send', + value: ['send', 'ephemeral'], + and: { field: 'messageFormat', value: 'blocks', not: true }, + }, + required: { + field: 'operation', + value: ['send', 'ephemeral'], + and: { field: 'messageFormat', value: 'blocks', not: true }, + }, + }, + { + id: 'blocks', + title: 'Block Kit Blocks', + type: 'code', + language: 'json', + placeholder: 'JSON array of Block Kit blocks', + condition: { + field: 'operation', + value: ['send', 'ephemeral', 'update'], + and: { field: 'messageFormat', value: 'blocks' }, + }, + required: { + field: 'operation', + value: ['send', 'ephemeral', 'update'], + and: { field: 'messageFormat', value: 'blocks' }, + }, + wandConfig: { + enabled: true, + maintainHistory: true, + prompt: `You are an expert at Slack Block Kit. +Generate ONLY a valid JSON array of Block Kit blocks based on the user's request. +The output MUST be a JSON array starting with [ and ending with ]. + +Current blocks: {context} + +Available block types for messages: +- "section": Displays text with an optional accessory element. Text uses { "type": "mrkdwn", "text": "..." } or { "type": "plain_text", "text": "..." }. +- "header": Large text header. Text must be plain_text. +- "divider": A horizontal rule separator. No fields needed besides type. +- "image": Displays an image. Requires "image_url" and "alt_text". +- "context": Contextual info with an "elements" array of image and text objects. +- "actions": Interactive elements like buttons. Each button needs "type": "button", a "text" object, and an "action_id". +- "rich_text": Structured rich text with "elements" array of rich_text_section objects. + +Example output: +[ + { + "type": "header", + "text": { "type": "plain_text", "text": "Order Confirmation" } + }, + { + "type": "section", + "text": { "type": "mrkdwn", "text": "Your order *#1234* has been confirmed." } + }, + { "type": "divider" }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { "type": "plain_text", "text": "View Order" }, + "action_id": "view_order", + "url": "https://example.com/orders/1234" + } + ] + } +] + +You can reference workflow variables using angle brackets, e.g., . +Do not include any explanations, markdown formatting, or other text outside the JSON array.`, + placeholder: 'Describe the Block Kit layout you want to create...', }, - required: true, }, { id: 'threadTs', @@ -193,7 +299,7 @@ export const SlackBlock: BlockConfig = { placeholder: 'Reply to thread (e.g., 1405894322.002768)', condition: { field: 'operation', - value: 'send', + value: ['send', 'ephemeral'], }, required: false, }, @@ -456,8 +562,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, condition: { field: 'operation', value: 'update', + and: { field: 'messageFormat', value: 'blocks', not: true }, + }, + required: { + field: 'operation', + value: 'update', + and: { field: 'messageFormat', value: 'blocks', not: true }, }, - required: true, }, // Delete Message specific fields { @@ -499,6 +610,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, tools: { access: [ 'slack_message', + 'slack_ephemeral_message', 'slack_canvas', 'slack_message_reader', 'slack_get_message', @@ -517,6 +629,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, switch (params.operation) { case 'send': return 'slack_message' + case 'ephemeral': + return 'slack_ephemeral_message' case 'canvas': return 'slack_canvas' case 'read': @@ -554,13 +668,16 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, destinationType, channel, dmUserId, + messageFormat, text, title, content, limit, oldest, files, + blocks, threadTs, + ephemeralUser, updateTimestamp, updateText, deleteTimestamp, @@ -602,10 +719,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, switch (operation) { case 'send': { - baseParams.text = text + baseParams.text = messageFormat === 'blocks' && !text ? ' ' : text if (threadTs) { baseParams.threadTs = threadTs } + if (blocks) { + baseParams.blocks = blocks + } // files is the canonical param from attachmentFiles (basic) or files (advanced) const normalizedFiles = normalizeFileInput(files) if (normalizedFiles) { @@ -614,6 +734,18 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, break } + case 'ephemeral': { + baseParams.text = messageFormat === 'blocks' && !text ? ' ' : text + baseParams.user = ephemeralUser ? String(ephemeralUser).trim() : '' + if (threadTs) { + baseParams.threadTs = threadTs + } + if (blocks) { + baseParams.blocks = blocks + } + break + } + case 'canvas': baseParams.title = title baseParams.content = content @@ -680,7 +812,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, case 'update': baseParams.timestamp = updateTimestamp - baseParams.text = updateText + baseParams.text = messageFormat === 'blocks' && !updateText ? ' ' : updateText + if (blocks) { + baseParams.blocks = blocks + } break case 'delete': @@ -699,6 +834,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, }, inputs: { operation: { type: 'string', description: 'Operation to perform' }, + messageFormat: { type: 'string', description: 'Message format: text or blocks' }, authMethod: { type: 'string', description: 'Authentication method' }, destinationType: { type: 'string', description: 'Destination type (channel or dm)' }, credential: { type: 'string', description: 'Slack access token' }, @@ -731,6 +867,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, // List Users inputs includeDeleted: { type: 'string', description: 'Include deactivated users (true/false)' }, userLimit: { type: 'string', description: 'Maximum number of users to return' }, + // Ephemeral message inputs + ephemeralUser: { type: 'string', description: 'User ID who will see the ephemeral message' }, + blocks: { type: 'json', description: 'Block Kit layout blocks as a JSON array' }, // Get User inputs userId: { type: 'string', description: 'User ID to look up' }, // Get Message inputs @@ -758,6 +897,12 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, }, files: { type: 'file[]', description: 'Files attached to the message' }, + // slack_ephemeral_message outputs (ephemeral operation) + messageTs: { + type: 'string', + description: 'Timestamp of the ephemeral message (cannot be used to update or delete)', + }, + // slack_canvas outputs canvas_id: { type: 'string', description: 'Canvas identifier for created canvases' }, title: { type: 'string', description: 'Canvas title' }, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 2bf07caeb4d..c206509aca7 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1541,6 +1541,7 @@ import { slackCanvasTool, slackDeleteMessageTool, slackDownloadTool, + slackEphemeralMessageTool, slackGetMessageTool, slackGetThreadTool, slackGetUserTool, @@ -2216,6 +2217,7 @@ export const tools: Record = { slack_get_thread: slackGetThreadTool, slack_canvas: slackCanvasTool, slack_download: slackDownloadTool, + slack_ephemeral_message: slackEphemeralMessageTool, slack_update_message: slackUpdateMessageTool, slack_delete_message: slackDeleteMessageTool, slack_add_reaction: slackAddReactionTool, diff --git a/apps/sim/tools/slack/ephemeral_message.ts b/apps/sim/tools/slack/ephemeral_message.ts new file mode 100644 index 00000000000..7f5a6c7d40c --- /dev/null +++ b/apps/sim/tools/slack/ephemeral_message.ts @@ -0,0 +1,114 @@ +import type { + SlackEphemeralMessageParams, + SlackEphemeralMessageResponse, +} from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackEphemeralMessageTool: ToolConfig< + SlackEphemeralMessageParams, + SlackEphemeralMessageResponse +> = { + id: 'slack_ephemeral_message', + name: 'Slack Ephemeral Message', + description: + 'Send an ephemeral message visible only to a specific user in a channel. Optionally reply in a thread. The message does not persist across sessions.', + 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-or-llm', + description: 'Slack channel ID (e.g., C1234567890)', + }, + user: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'User ID who will see the ephemeral message (e.g., U1234567890). Must be a member of the channel.', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Message text to send (supports Slack mrkdwn formatting)', + }, + threadTs: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Thread timestamp to reply in. When provided, the ephemeral message appears as a thread reply.', + }, + blocks: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text.', + }, + }, + + request: { + url: '/api/tools/slack/send-ephemeral', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: SlackEphemeralMessageParams) => ({ + accessToken: params.accessToken || params.botToken, + channel: params.channel, + user: params.user?.trim(), + text: params.text, + thread_ts: params.threadTs || undefined, + blocks: + typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks || undefined, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to send ephemeral message') + } + return { + success: true, + output: data.output, + } + }, + + outputs: { + messageTs: { + type: 'string', + description: 'Timestamp of the ephemeral message (cannot be used with chat.update)', + }, + channel: { + type: 'string', + description: 'Channel ID where the ephemeral message was sent', + }, + }, +} diff --git a/apps/sim/tools/slack/index.ts b/apps/sim/tools/slack/index.ts index 2bc0f249ef6..e4beed2a895 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 { slackEphemeralMessageTool } from '@/tools/slack/ephemeral_message' import { slackGetMessageTool } from '@/tools/slack/get_message' import { slackGetThreadTool } from '@/tools/slack/get_thread' import { slackGetUserTool } from '@/tools/slack/get_user' @@ -17,6 +18,7 @@ export { slackCanvasTool, slackMessageReaderTool, slackDownloadTool, + slackEphemeralMessageTool, slackUpdateMessageTool, slackDeleteMessageTool, slackAddReactionTool, diff --git a/apps/sim/tools/slack/message.ts b/apps/sim/tools/slack/message.ts index b1f04740338..5f0a3b31b5a 100644 --- a/apps/sim/tools/slack/message.ts +++ b/apps/sim/tools/slack/message.ts @@ -63,6 +63,13 @@ export const slackMessageTool: ToolConfig