From 22347fe2dd5eb8a9ceaed45a3d44be5f800a6f3d Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 20 Feb 2026 15:48:43 -0800 Subject: [PATCH 1/5] feat(slack): added ephemeral message send tool, updated ci, updated docs --- .github/workflows/ci.yml | 2 - .github/workflows/images.yml | 4 +- .github/workflows/test-build.yml | 6 + .../docs/en/tools/jira_service_management.mdx | 2 +- apps/docs/content/docs/en/tools/slack.mdx | 26 ++++- .../api/tools/slack/send-ephemeral/route.ts | 93 ++++++++++++++++ apps/sim/blocks/blocks/slack.ts | 85 ++++++++++---- apps/sim/tools/registry.ts | 2 + apps/sim/tools/slack/ephemeral_message.ts | 105 ++++++++++++++++++ apps/sim/tools/slack/index.ts | 2 + apps/sim/tools/slack/types.ts | 15 +++ 11 files changed, 314 insertions(+), 28 deletions(-) create mode 100644 apps/sim/app/api/tools/slack/send-ephemeral/route.ts create mode 100644 apps/sim/tools/slack/ephemeral_message.ts 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..7a7fc2792e 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -38,6 +38,12 @@ jobs: key: ${{ github.repository }}-node-modules path: ./node_modules + - name: Mount Next.js cache (Sticky Disk) + uses: useblacksmith/stickydisk@v1 + with: + key: ${{ github.repository }}-next-cache + path: ./apps/sim/.next/cache + - name: Install dependencies run: bun install --frozen-lockfile 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..73edd6b438 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. @@ -146,6 +146,28 @@ 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. | + +#### 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. 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..00ca2c9e32 --- /dev/null +++ b/apps/sim/app/api/tools/slack/send-ephemeral/route.ts @@ -0,0 +1,93 @@ +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(), +}) + +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 }), + }), + }) + + 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/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index c4337fab4c..7cf4b2c944 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,17 @@ 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: 'text', title: 'Message', @@ -182,7 +206,7 @@ export const SlackBlock: BlockConfig = { placeholder: 'Enter your message (supports Slack mrkdwn)', condition: { field: 'operation', - value: 'send', + value: ['send', 'ephemeral'], }, required: true, }, @@ -193,7 +217,7 @@ export const SlackBlock: BlockConfig = { placeholder: 'Reply to thread (e.g., 1405894322.002768)', condition: { field: 'operation', - value: 'send', + value: ['send', 'ephemeral'], }, required: false, }, @@ -499,6 +523,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 +542,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': @@ -561,6 +588,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, oldest, files, threadTs, + ephemeralUser, updateTimestamp, updateText, deleteTimestamp, @@ -614,6 +642,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, break } + case 'ephemeral': { + baseParams.text = text + baseParams.user = ephemeralUser ? String(ephemeralUser).trim() : '' + if (threadTs) { + baseParams.threadTs = threadTs + } + break + } + case 'canvas': baseParams.title = title baseParams.content = content @@ -731,6 +768,8 @@ 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' }, // Get User inputs userId: { type: 'string', description: 'User ID to look up' }, // Get Message inputs @@ -758,6 +797,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..5e1600a5e0 --- /dev/null +++ b/apps/sim/tools/slack/ephemeral_message.ts @@ -0,0 +1,105 @@ +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.', + }, + }, + + 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, + }), + }, + + 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/types.ts b/apps/sim/tools/slack/types.ts index 73ecccbad1..d45e8f11c7 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -584,6 +584,13 @@ export interface SlackGetMessageParams extends SlackBaseParams { timestamp: string } +export interface SlackEphemeralMessageParams extends SlackBaseParams { + channel: string + user: string + text: string + threadTs?: string +} + export interface SlackGetThreadParams extends SlackBaseParams { channel: string threadTs: string @@ -831,6 +838,13 @@ export interface SlackGetMessageResponse extends ToolResponse { } } +export interface SlackEphemeralMessageResponse extends ToolResponse { + output: { + messageTs: string + channel: string + } +} + export interface SlackGetThreadResponse extends ToolResponse { output: { parentMessage: SlackMessage @@ -853,5 +867,6 @@ export type SlackResponse = | SlackListMembersResponse | SlackListUsersResponse | SlackGetUserResponse + | SlackEphemeralMessageResponse | SlackGetMessageResponse | SlackGetThreadResponse From 85f56e7db3ad9cd25e96a8dedc0423e84b509907 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 20 Feb 2026 16:20:45 -0800 Subject: [PATCH 2/5] added block kit support --- .github/workflows/test-build.yml | 8 +-- apps/docs/content/docs/en/tools/slack.mdx | 3 + .../api/tools/slack/send-ephemeral/route.ts | 3 + .../app/api/tools/slack/send-message/route.ts | 2 + .../api/tools/slack/update-message/route.ts | 3 + apps/sim/app/api/tools/slack/utils.ts | 11 ++-- apps/sim/blocks/blocks/slack.ts | 63 +++++++++++++++++-- apps/sim/tools/slack/ephemeral_message.ts | 9 +++ apps/sim/tools/slack/message.ts | 11 ++++ apps/sim/tools/slack/types.ts | 3 + apps/sim/tools/slack/update_message.ts | 9 +++ docker/app.Dockerfile | 2 +- 12 files changed, 110 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 7a7fc2792e..10dd6f0b01 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -38,12 +38,6 @@ jobs: key: ${{ github.repository }}-node-modules path: ./node_modules - - name: Mount Next.js cache (Sticky Disk) - uses: useblacksmith/stickydisk@v1 - with: - key: ${{ github.repository }}-next-cache - path: ./apps/sim/.next/cache - - name: Install dependencies run: bun install --frozen-lockfile @@ -116,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/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 73edd6b438..c51fea7ff2 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -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 @@ -160,6 +161,7 @@ Send an ephemeral message visible only to a specific user in a channel. Optional | `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 @@ -704,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 index 00ca2c9e32..6d443e5039 100644 --- a/apps/sim/app/api/tools/slack/send-ephemeral/route.ts +++ b/apps/sim/app/api/tools/slack/send-ephemeral/route.ts @@ -14,6 +14,7 @@ const SlackSendEphemeralSchema = z.object({ 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) { @@ -58,6 +59,8 @@ export async function POST(request: NextRequest) { user: validatedData.user, text: validatedData.text, ...(validatedData.thread_ts && { thread_ts: validatedData.thread_ts }), + ...(validatedData.blocks && + validatedData.blocks.length > 0 && { blocks: validatedData.blocks }), }), }) 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 7cf4b2c944..eaa8e2d369 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -199,6 +199,20 @@ export const SlackBlock: BlockConfig = { }, 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', @@ -207,8 +221,29 @@ export const SlackBlock: BlockConfig = { condition: { field: 'operation', 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', + 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' }, }, - required: true, }, { id: 'threadTs', @@ -480,8 +515,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 { @@ -581,12 +621,14 @@ 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, @@ -630,10 +672,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) { @@ -643,11 +688,14 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, } case 'ephemeral': { - baseParams.text = text + baseParams.text = messageFormat === 'blocks' && !text ? ' ' : text baseParams.user = ephemeralUser ? String(ephemeralUser).trim() : '' if (threadTs) { baseParams.threadTs = threadTs } + if (blocks) { + baseParams.blocks = blocks + } break } @@ -717,7 +765,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': @@ -736,6 +787,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' }, @@ -770,6 +822,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 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 diff --git a/apps/sim/tools/slack/ephemeral_message.ts b/apps/sim/tools/slack/ephemeral_message.ts index 5e1600a5e0..7f5a6c7d40 100644 --- a/apps/sim/tools/slack/ephemeral_message.ts +++ b/apps/sim/tools/slack/ephemeral_message.ts @@ -64,6 +64,13 @@ export const slackEphemeralMessageTool: ToolConfig< 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: { @@ -78,6 +85,8 @@ export const slackEphemeralMessageTool: ToolConfig< user: params.user?.trim(), text: params.text, thread_ts: params.threadTs || undefined, + blocks: + typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks || undefined, }), }, 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 Date: Fri, 20 Feb 2026 16:21:30 -0800 Subject: [PATCH 3/5] upgrade turborepo --- turbo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/turbo.json b/turbo.json index 8a73392ae2..848371da3c 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://v2-8-0.turborepo.dev/schema.json", + "$schema": "https://v2-8-10.turborepo.dev/schema.json", "envMode": "loose", "tasks": { "build": { From b8af0da94c8a50a30a131f87f26886f9d7e4df02 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 20 Feb 2026 16:23:43 -0800 Subject: [PATCH 4/5] added wandConfig for slack block kit --- apps/sim/blocks/blocks/slack.ts | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index eaa8e2d369..f1bb5713ce 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -233,6 +233,7 @@ export const SlackBlock: BlockConfig = { id: 'blocks', title: 'Block Kit Blocks', type: 'code', + language: 'json', placeholder: 'JSON array of Block Kit blocks', condition: { field: 'operation', @@ -244,6 +245,54 @@ export const SlackBlock: BlockConfig = { value: ['send', 'ephemeral', 'update'], and: { field: 'messageFormat', value: 'blocks' }, }, + generationType: 'json-object', + wandConfig: { + enabled: true, + maintainHistory: true, + generationType: 'json-object', + 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...', + }, }, { id: 'threadTs', From 08643c4d5e790e65067f04876fe3a676d97bdccf Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 20 Feb 2026 16:30:53 -0800 Subject: [PATCH 5/5] fix generation type --- apps/sim/blocks/blocks/slack.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index f1bb5713ce..77c44a21bf 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -245,11 +245,9 @@ export const SlackBlock: BlockConfig = { value: ['send', 'ephemeral', 'update'], and: { field: 'messageFormat', value: 'blocks' }, }, - generationType: 'json-object', wandConfig: { enabled: true, maintainHistory: true, - generationType: 'json-object', 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 ].