feat(slack): added ephemeral message send tool, updated ci, updated docs (#3278)

* feat(slack): added ephemeral message send tool, updated ci, updated docs

* added block kit support

* upgrade turborepo

* added wandConfig for slack block kit

* fix generation type
This commit is contained in:
Waleed
2026-02-20 16:53:10 -08:00
committed by GitHub
parent 3fa4bb4c12
commit 2fc2e12cb2
18 changed files with 465 additions and 39 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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\) |

View File

@@ -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

View File

@@ -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 }
)
}
}

View File

@@ -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,

View File

@@ -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 }),
}),
})

View File

@@ -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' }

View File

@@ -9,10 +9,10 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
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<SlackResponse> = {
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<SlackResponse> = {
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<string, unknown>) => {
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<SlackResponse> = {
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<string, unknown>) => {
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<SlackResponse> = {
},
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<SlackResponse> = {
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., <blockName.output>.
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<SlackResponse> = {
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' },

View File

@@ -1541,6 +1541,7 @@ import {
slackCanvasTool,
slackDeleteMessageTool,
slackDownloadTool,
slackEphemeralMessageTool,
slackGetMessageTool,
slackGetThreadTool,
slackGetUserTool,
@@ -2216,6 +2217,7 @@ export const tools: Record<string, ToolConfig> = {
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,

View File

@@ -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',
},
},
}

View File

@@ -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,

View File

@@ -63,6 +63,13 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
visibility: 'user-or-llm',
description: 'Thread timestamp to reply to (creates 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.',
},
files: {
type: 'file[]',
required: false,
@@ -85,6 +92,10 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
userId: isDM ? params.dmUserId : params.userId,
text: params.text,
thread_ts: params.threadTs || undefined,
blocks:
typeof params.blocks === 'string'
? JSON.parse(params.blocks)
: params.blocks || undefined,
files: params.files || null,
}
},

View File

@@ -517,6 +517,7 @@ export interface SlackMessageParams extends SlackBaseParams {
userId?: string
text: string
threadTs?: string
blocks?: string
files?: UserFile[]
}
@@ -546,6 +547,7 @@ export interface SlackUpdateMessageParams extends SlackBaseParams {
channel: string
timestamp: string
text: string
blocks?: string
}
export interface SlackDeleteMessageParams extends SlackBaseParams {
@@ -584,6 +586,14 @@ export interface SlackGetMessageParams extends SlackBaseParams {
timestamp: string
}
export interface SlackEphemeralMessageParams extends SlackBaseParams {
channel: string
user: string
text: string
threadTs?: string
blocks?: string
}
export interface SlackGetThreadParams extends SlackBaseParams {
channel: string
threadTs: string
@@ -831,6 +841,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 +870,6 @@ export type SlackResponse =
| SlackListMembersResponse
| SlackListUsersResponse
| SlackGetUserResponse
| SlackEphemeralMessageResponse
| SlackGetMessageResponse
| SlackGetThreadResponse

View File

@@ -53,6 +53,13 @@ export const slackUpdateMessageTool: ToolConfig<
visibility: 'user-or-llm',
description: 'New message text (supports Slack mrkdwn formatting)',
},
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: {
@@ -66,6 +73,8 @@ export const slackUpdateMessageTool: ToolConfig<
channel: params.channel,
timestamp: params.timestamp,
text: params.text,
blocks:
typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks || undefined,
}),
},

View File

@@ -124,7 +124,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
# Create .next/cache directory with correct ownership
RUN mkdir -p apps/sim/.next/cache && \
chown -R nextjs:nodejs /app
chown -R nextjs:nodejs apps/sim/.next/cache
# Switch to non-root user
USER nextjs

View File

@@ -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": {