diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f369210ec..73e5e852fb 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 e3136510eb..44e8636d90 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 96480e7f2b..10dd6f0b01 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 9814f81036..cd294152d3 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 0f4285e2a1..c51fea7ff2 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 0000000000..6d443e5039 --- /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 21f60faf6c..a6b8a3db71 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 4edd983a56..ccf0a04529 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 b635c49d8f..4049a3fe0d 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 c4337fab4c..77c44a21bf 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 2bf07caeb4..c206509aca 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 0000000000..7f5a6c7d40 --- /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 2bc0f249ef..e4beed2a89 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 b1f0474033..5f0a3b31b5 100644 --- a/apps/sim/tools/slack/message.ts +++ b/apps/sim/tools/slack/message.ts @@ -63,6 +63,13 @@ export const slackMessageTool: ToolConfig