feat(slack): add views.open, views.update, views.push, views.publish tools (#3436)

* feat(slack): add views.open, views.update, views.push, views.publish tools

* feat(slack): wire view tools into slack block definition
This commit is contained in:
Waleed
2026-03-05 22:10:02 -08:00
committed by GitHub
parent 2722f0efbf
commit 127968d467
9 changed files with 1297 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
---
title: Slack
description: Send, update, delete messages, add or remove reactions, manage canvases, get channel info and user presence in Slack
description: Send, update, delete messages, manage views and modals, add or remove reactions, manage canvases, get channel info and user presence in Slack
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -39,7 +39,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, send ephemeral messages visible only to a specific user, create canvases, read messages, and add or remove 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, open/update/push modal views, publish Home tab views, create canvases, read messages, and add or remove 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.
@@ -923,4 +923,189 @@ Create a canvas pinned to a Slack channel as its resource hub
| --------- | ---- | ----------- |
| `canvas_id` | string | ID of the created channel canvas |
### `slack_open_view`
Open a modal view in Slack using a trigger_id from an interaction payload. Used to display forms, confirmations, and other interactive modals.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `triggerId` | string | Yes | Exchange a trigger to post to the user. Obtained from an interaction payload \(e.g., slash command, button click\) |
| `interactivityPointer` | string | No | Alternative to trigger_id for posting to user |
| `view` | json | Yes | A view payload object defining the modal. Must include type \("modal"\), title, and blocks array |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `view` | object | The opened modal view object |
| ↳ `id` | string | Unique view identifier |
| ↳ `team_id` | string | Workspace/team ID |
| ↳ `type` | string | View type \(e.g., "modal"\) |
| ↳ `title` | json | Plain text title object with type and text fields |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Title text content |
| ↳ `submit` | json | Plain text submit button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Submit button text |
| ↳ `close` | json | Plain text close button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Close button text |
| ↳ `blocks` | array | Block Kit blocks in the view |
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
| ↳ `block_id` | string | Unique block identifier |
| ↳ `private_metadata` | string | Private metadata string passed with the view |
| ↳ `callback_id` | string | Custom identifier for the view |
| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) |
| ↳ `state` | json | Current state of the view with input values |
| ↳ `hash` | string | View version hash for updates |
| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed |
| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed |
| ↳ `root_view_id` | string | ID of the root view in the view stack |
| ↳ `previous_view_id` | string | ID of the previous view in the view stack |
| ↳ `app_id` | string | Application identifier |
| ↳ `bot_id` | string | Bot identifier |
### `slack_update_view`
Update an existing modal view in Slack. Identify the view by view_id or external_id, and provide the updated view payload.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `viewId` | string | No | Unique identifier of the view to update. Either viewId or externalId is required |
| `externalId` | string | No | Developer-set unique identifier of the view to update \(max 255 chars\). Either viewId or externalId is required |
| `hash` | string | No | View state hash to protect against race conditions. Obtained from a previous views response |
| `view` | json | Yes | A view payload object defining the updated modal. Must include type \("modal"\), title, and blocks array. Use identical block_id and action_id values to preserve input data |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `view` | object | The updated modal view object |
| ↳ `id` | string | Unique view identifier |
| ↳ `team_id` | string | Workspace/team ID |
| ↳ `type` | string | View type \(e.g., "modal"\) |
| ↳ `title` | json | Plain text title object with type and text fields |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Title text content |
| ↳ `submit` | json | Plain text submit button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Submit button text |
| ↳ `close` | json | Plain text close button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Close button text |
| ↳ `blocks` | array | Block Kit blocks in the view |
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
| ↳ `block_id` | string | Unique block identifier |
| ↳ `private_metadata` | string | Private metadata string passed with the view |
| ↳ `callback_id` | string | Custom identifier for the view |
| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) |
| ↳ `state` | json | Current state of the view with input values |
| ↳ `hash` | string | View version hash for updates |
| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed |
| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed |
| ↳ `root_view_id` | string | ID of the root view in the view stack |
| ↳ `previous_view_id` | string | ID of the previous view in the view stack |
| ↳ `app_id` | string | Application identifier |
| ↳ `bot_id` | string | Bot identifier |
### `slack_push_view`
Push a new view onto an existing modal stack in Slack. Limited to 2 additional views after the initial modal is opened.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `triggerId` | string | Yes | Exchange a trigger to post to the user. Obtained from an interaction payload \(e.g., button click within an existing modal\) |
| `interactivityPointer` | string | No | Alternative to trigger_id for posting to user |
| `view` | json | Yes | A view payload object defining the modal to push. Must include type \("modal"\), title, and blocks array |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `view` | object | The pushed modal view object |
| ↳ `id` | string | Unique view identifier |
| ↳ `team_id` | string | Workspace/team ID |
| ↳ `type` | string | View type \(e.g., "modal"\) |
| ↳ `title` | json | Plain text title object with type and text fields |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Title text content |
| ↳ `submit` | json | Plain text submit button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Submit button text |
| ↳ `close` | json | Plain text close button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Close button text |
| ↳ `blocks` | array | Block Kit blocks in the view |
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
| ↳ `block_id` | string | Unique block identifier |
| ↳ `private_metadata` | string | Private metadata string passed with the view |
| ↳ `callback_id` | string | Custom identifier for the view |
| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) |
| ↳ `state` | json | Current state of the view with input values |
| ↳ `hash` | string | View version hash for updates |
| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed |
| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed |
| ↳ `root_view_id` | string | ID of the root view in the view stack |
| ↳ `previous_view_id` | string | ID of the previous view in the view stack |
| ↳ `app_id` | string | Application identifier |
| ↳ `bot_id` | string | Bot identifier |
### `slack_publish_view`
Publish a static view to a user
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `userId` | string | Yes | The user ID to publish the Home tab view to \(e.g., U0BPQUNTA\) |
| `hash` | string | No | View state hash to protect against race conditions. Obtained from a previous views response |
| `view` | json | Yes | A view payload object defining the Home tab. Must include type \("home"\) and blocks array |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `view` | object | The published Home tab view object |
| ↳ `id` | string | Unique view identifier |
| ↳ `team_id` | string | Workspace/team ID |
| ↳ `type` | string | View type \(e.g., "modal"\) |
| ↳ `title` | json | Plain text title object with type and text fields |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Title text content |
| ↳ `submit` | json | Plain text submit button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Submit button text |
| ↳ `close` | json | Plain text close button object |
| ↳ `type` | string | Text object type \(plain_text\) |
| ↳ `text` | string | Close button text |
| ↳ `blocks` | array | Block Kit blocks in the view |
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
| ↳ `block_id` | string | Unique block identifier |
| ↳ `private_metadata` | string | Private metadata string passed with the view |
| ↳ `callback_id` | string | Custom identifier for the view |
| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) |
| ↳ `state` | json | Current state of the view with input values |
| ↳ `hash` | string | View version hash for updates |
| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed |
| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed |
| ↳ `root_view_id` | string | ID of the root view in the view stack |
| ↳ `previous_view_id` | string | ID of the previous view in the view stack |
| ↳ `app_id` | string | Application identifier |
| ↳ `bot_id` | string | Bot identifier |

View File

@@ -9,10 +9,10 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
type: 'slack',
name: 'Slack',
description:
'Send, update, delete messages, add or remove reactions, manage canvases, get channel info and user presence in Slack',
'Send, update, delete messages, manage views and modals, add or remove reactions, manage canvases, get channel info and user presence in Slack',
authMode: AuthMode.OAuth,
longDescription:
'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 or remove 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, open/update/push modal views, publish Home tab views, create canvases, read messages, and add or remove 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',
@@ -43,6 +43,10 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
{ label: 'Get User Presence', id: 'get_user_presence' },
{ label: 'Edit Canvas', id: 'edit_canvas' },
{ label: 'Create Channel Canvas', id: 'create_channel_canvas' },
{ label: 'Open View', id: 'open_view' },
{ label: 'Update View', id: 'update_view' },
{ label: 'Push View', id: 'push_view' },
{ label: 'Publish View', id: 'publish_view' },
],
value: () => 'send',
},
@@ -146,7 +150,17 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
}
return {
field: 'operation',
value: ['list_channels', 'list_users', 'get_user', 'get_user_presence', 'edit_canvas'],
value: [
'list_channels',
'list_users',
'get_user',
'get_user_presence',
'edit_canvas',
'open_view',
'update_view',
'push_view',
'publish_view',
],
not: true,
and: {
field: 'destinationType',
@@ -171,7 +185,17 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
}
return {
field: 'operation',
value: ['list_channels', 'list_users', 'get_user', 'get_user_presence', 'edit_canvas'],
value: [
'list_channels',
'list_users',
'get_user',
'get_user_presence',
'edit_canvas',
'open_view',
'update_view',
'push_view',
'publish_view',
],
not: true,
and: {
field: 'destinationType',
@@ -804,6 +828,157 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
value: 'create_channel_canvas',
},
},
// Open View / Push View specific fields
{
id: 'viewTriggerId',
title: 'Trigger ID',
type: 'short-input',
placeholder: 'Trigger ID from interaction payload',
condition: {
field: 'operation',
value: ['open_view', 'push_view'],
},
required: true,
},
{
id: 'viewInteractivityPointer',
title: 'Interactivity Pointer',
type: 'short-input',
placeholder: 'Alternative to trigger_id (optional)',
condition: {
field: 'operation',
value: ['open_view', 'push_view'],
},
mode: 'advanced',
},
// Update View specific fields
{
id: 'viewId',
title: 'View ID',
type: 'short-input',
placeholder: 'Unique view identifier (either View ID or External ID required)',
condition: {
field: 'operation',
value: 'update_view',
},
},
{
id: 'viewExternalId',
title: 'External ID',
type: 'short-input',
placeholder: 'Developer-set unique identifier (max 255 chars)',
condition: {
field: 'operation',
value: 'update_view',
},
},
// Update View / Publish View hash field
{
id: 'viewHash',
title: 'View Hash',
type: 'short-input',
placeholder: 'View state hash for race condition protection',
condition: {
field: 'operation',
value: ['update_view', 'publish_view'],
},
mode: 'advanced',
},
// Publish View specific fields
{
id: 'publishUserId',
title: 'User',
type: 'user-selector',
canonicalParamId: 'publishUserId',
serviceId: 'slack',
selectorKey: 'slack.users',
placeholder: 'Select user to publish Home tab to',
mode: 'basic',
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] },
condition: {
field: 'operation',
value: 'publish_view',
},
required: true,
},
{
id: 'manualPublishUserId',
title: 'User ID',
type: 'short-input',
canonicalParamId: 'publishUserId',
placeholder: 'Enter Slack user ID (e.g., U0BPQUNTA)',
mode: 'advanced',
condition: {
field: 'operation',
value: 'publish_view',
},
required: true,
},
// View payload (shared across all view operations)
{
id: 'viewPayload',
title: 'View Payload',
type: 'code',
language: 'json',
placeholder: 'JSON view payload with type, title, and blocks',
condition: {
field: 'operation',
value: ['open_view', 'update_view', 'push_view', 'publish_view'],
},
required: true,
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `You are an expert at Slack Block Kit views.
Generate ONLY a valid JSON view payload object based on the user's request.
The output MUST be a JSON object starting with { and ending with }.
Current view: {context}
The view object must include:
- "type": "modal" (for open/update/push) or "home" (for publish)
- "title": { "type": "plain_text", "text": "Title text", "emoji": true } (max 24 chars)
- "blocks": Array of Block Kit blocks
Optional fields:
- "submit": { "type": "plain_text", "text": "Submit" } - Submit button text
- "close": { "type": "plain_text", "text": "Cancel" } - Close button text
- "private_metadata": String up to 3000 chars
- "callback_id": String identifier for interaction handling
- "clear_on_close": true/false
- "notify_on_close": true/false
- "external_id": Unique string per workspace (max 255 chars)
Available block types:
- "section": Text with optional accessory. Text uses { "type": "mrkdwn", "text": "..." } or { "type": "plain_text", "text": "..." }
- "input": Form input with a label and element (plain_text_input, static_select, multi_static_select, datepicker, timepicker, checkboxes, radio_buttons)
- "header": Large text header (plain_text only)
- "divider": Horizontal rule separator
- "image": Requires "image_url" and "alt_text"
- "context": Contextual info with "elements" array
- "actions": Interactive elements like buttons
Example modal:
{
"type": "modal",
"title": { "type": "plain_text", "text": "My Form" },
"submit": { "type": "plain_text", "text": "Submit" },
"close": { "type": "plain_text", "text": "Cancel" },
"blocks": [
{
"type": "input",
"block_id": "input_1",
"label": { "type": "plain_text", "text": "Name" },
"element": { "type": "plain_text_input", "action_id": "name_input" }
}
]
}
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 object.`,
placeholder: 'Describe the view/modal you want to create...',
},
},
...getTrigger('slack_webhook').subBlocks,
],
tools: {
@@ -827,6 +1002,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
'slack_get_user_presence',
'slack_edit_canvas',
'slack_create_channel_canvas',
'slack_open_view',
'slack_update_view',
'slack_push_view',
'slack_publish_view',
],
config: {
tool: (params) => {
@@ -869,6 +1048,14 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
return 'slack_edit_canvas'
case 'create_channel_canvas':
return 'slack_create_channel_canvas'
case 'open_view':
return 'slack_open_view'
case 'update_view':
return 'slack_update_view'
case 'push_view':
return 'slack_push_view'
case 'publish_view':
return 'slack_publish_view'
default:
throw new Error(`Invalid Slack operation: ${params.operation}`)
}
@@ -915,6 +1102,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
canvasTitle,
channelCanvasTitle,
channelCanvasContent,
viewTriggerId,
viewInteractivityPointer,
viewId,
viewExternalId,
viewHash,
publishUserId,
viewPayload,
...rest
} = params
@@ -1081,6 +1275,43 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
baseParams.content = channelCanvasContent
}
break
case 'open_view':
baseParams.triggerId = viewTriggerId
if (viewInteractivityPointer) {
baseParams.interactivityPointer = viewInteractivityPointer
}
baseParams.view = viewPayload
break
case 'update_view':
if (viewId) {
baseParams.viewId = viewId
}
if (viewExternalId) {
baseParams.externalId = viewExternalId
}
if (viewHash) {
baseParams.hash = viewHash
}
baseParams.view = viewPayload
break
case 'push_view':
baseParams.triggerId = viewTriggerId
if (viewInteractivityPointer) {
baseParams.interactivityPointer = viewInteractivityPointer
}
baseParams.view = viewPayload
break
case 'publish_view':
baseParams.userId = publishUserId
if (viewHash) {
baseParams.hash = viewHash
}
baseParams.view = viewPayload
break
}
return baseParams
@@ -1148,6 +1379,23 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
// Create Channel Canvas inputs
channelCanvasTitle: { type: 'string', description: 'Title for channel canvas' },
channelCanvasContent: { type: 'string', description: 'Content for channel canvas' },
// View operation inputs
viewTriggerId: { type: 'string', description: 'Trigger ID from interaction payload' },
viewInteractivityPointer: {
type: 'string',
description: 'Alternative to trigger_id for posting to user',
},
viewId: { type: 'string', description: 'Unique view identifier for update' },
viewExternalId: {
type: 'string',
description: 'Developer-set unique identifier for update (max 255 chars)',
},
viewHash: { type: 'string', description: 'View state hash for race condition protection' },
publishUserId: {
type: 'string',
description: 'User ID to publish Home tab view to',
},
viewPayload: { type: 'json', description: 'View payload object with type, title, and blocks' },
},
outputs: {
// slack_message outputs (send operation)
@@ -1281,6 +1529,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
'Unix timestamp of last detected activity (only available when checking own presence)',
},
// View operation outputs (open_view, update_view, push_view, publish_view)
view: {
type: 'json',
description:
'View object with properties: id, team_id, type, title, submit, close, blocks, private_metadata, callback_id, external_id, state, hash, clear_on_close, notify_on_close, root_view_id, previous_view_id, app_id, bot_id',
},
// Trigger outputs (when used as webhook trigger)
event_type: { type: 'string', description: 'Type of Slack event that triggered the workflow' },
channel_name: { type: 'string', description: 'Human-readable channel name' },

View File

@@ -1817,8 +1817,12 @@ import {
slackListUsersTool,
slackMessageReaderTool,
slackMessageTool,
slackOpenViewTool,
slackPublishViewTool,
slackPushViewTool,
slackRemoveReactionTool,
slackUpdateMessageTool,
slackUpdateViewTool,
} from '@/tools/slack'
import { smsSendTool } from '@/tools/sms'
import { smtpSendMailTool } from '@/tools/smtp'
@@ -2624,6 +2628,10 @@ export const tools: Record<string, ToolConfig> = {
slack_remove_reaction: slackRemoveReactionTool,
slack_get_channel_info: slackGetChannelInfoTool,
slack_get_user_presence: slackGetUserPresenceTool,
slack_open_view: slackOpenViewTool,
slack_update_view: slackUpdateViewTool,
slack_push_view: slackPushViewTool,
slack_publish_view: slackPublishViewTool,
slack_edit_canvas: slackEditCanvasTool,
slack_create_channel_canvas: slackCreateChannelCanvasTool,
github_repo_info: githubRepoInfoTool,

View File

@@ -15,8 +15,12 @@ import { slackListMembersTool } from '@/tools/slack/list_members'
import { slackListUsersTool } from '@/tools/slack/list_users'
import { slackMessageTool } from '@/tools/slack/message'
import { slackMessageReaderTool } from '@/tools/slack/message_reader'
import { slackOpenViewTool } from '@/tools/slack/open_view'
import { slackPublishViewTool } from '@/tools/slack/publish_view'
import { slackPushViewTool } from '@/tools/slack/push_view'
import { slackRemoveReactionTool } from '@/tools/slack/remove_reaction'
import { slackUpdateMessageTool } from '@/tools/slack/update_message'
import { slackUpdateViewTool } from '@/tools/slack/update_view'
export {
slackMessageTool,
@@ -36,6 +40,10 @@ export {
slackListUsersTool,
slackGetUserTool,
slackGetUserPresenceTool,
slackOpenViewTool,
slackUpdateViewTool,
slackPushViewTool,
slackPublishViewTool,
slackGetMessageTool,
slackGetThreadTool,
}

View File

@@ -0,0 +1,166 @@
import type { SlackOpenViewParams, SlackOpenViewResponse } from '@/tools/slack/types'
import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackOpenViewTool: ToolConfig<SlackOpenViewParams, SlackOpenViewResponse> = {
id: 'slack_open_view',
name: 'Slack Open View',
description:
'Open a modal view in Slack using a trigger_id from an interaction payload. Used to display forms, confirmations, and other interactive modals.',
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',
},
triggerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Exchange a trigger to post to the user. Obtained from an interaction payload (e.g., slash command, button click)',
},
interactivityPointer: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Alternative to trigger_id for posting to user',
},
view: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'A view payload object defining the modal. Must include type ("modal"), title, and blocks array',
},
},
request: {
url: 'https://slack.com/api/views.open',
method: 'POST',
headers: (params: SlackOpenViewParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackOpenViewParams) => {
const body: Record<string, unknown> = {
view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view,
}
if (params.triggerId) {
body.trigger_id = params.triggerId.trim()
}
if (params.interactivityPointer) {
body.interactivity_pointer = params.interactivityPointer.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'expired_trigger_id') {
throw new Error(
'The trigger_id has expired. Trigger IDs are only valid for 3 seconds after the interaction.'
)
}
if (data.error === 'invalid_trigger_id') {
throw new Error(
'Invalid trigger_id. Ensure you are using a trigger_id from a valid interaction payload.'
)
}
if (data.error === 'exchanged_trigger_id') {
throw new Error(
'This trigger_id has already been used. Each trigger_id can only be used once.'
)
}
if (data.error === 'view_too_large') {
throw new Error('The view payload is too large. Reduce the number of blocks or content.')
}
if (data.error === 'duplicate_external_id') {
throw new Error(
'A view with this external_id already exists. Use a unique external_id per workspace.'
)
}
if (data.error === 'invalid_arguments') {
const messages = data.response_metadata?.messages ?? []
throw new Error(
`Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}`
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes.'
)
}
if (
data.error === 'invalid_auth' ||
data.error === 'not_authed' ||
data.error === 'token_expired'
) {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to open view in Slack')
}
const view = data.view
return {
success: true,
output: {
view: {
id: view.id,
team_id: view.team_id ?? null,
type: view.type,
title: view.title ?? null,
submit: view.submit ?? null,
close: view.close ?? null,
blocks: view.blocks ?? [],
private_metadata: view.private_metadata ?? null,
callback_id: view.callback_id ?? null,
external_id: view.external_id ?? null,
state: view.state ?? null,
hash: view.hash ?? null,
clear_on_close: view.clear_on_close ?? false,
notify_on_close: view.notify_on_close ?? false,
root_view_id: view.root_view_id ?? null,
previous_view_id: view.previous_view_id ?? null,
app_id: view.app_id ?? null,
bot_id: view.bot_id ?? null,
},
},
}
},
outputs: {
view: {
type: 'object',
description: 'The opened modal view object',
properties: VIEW_OUTPUT_PROPERTIES,
},
},
}

View File

@@ -0,0 +1,163 @@
import type { SlackPublishViewParams, SlackPublishViewResponse } from '@/tools/slack/types'
import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackPublishViewTool: ToolConfig<SlackPublishViewParams, SlackPublishViewResponse> = {
id: 'slack_publish_view',
name: 'Slack Publish View',
description:
"Publish a static view to a user's Home tab in Slack. Used to create or update the app's Home tab experience.",
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',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID to publish the Home tab view to (e.g., U0BPQUNTA)',
},
hash: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'View state hash to protect against race conditions. Obtained from a previous views response',
},
view: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'A view payload object defining the Home tab. Must include type ("home") and blocks array',
},
},
request: {
url: 'https://slack.com/api/views.publish',
method: 'POST',
headers: (params: SlackPublishViewParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackPublishViewParams) => {
const body: Record<string, unknown> = {
user_id: params.userId.trim(),
view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view,
}
if (params.hash) {
body.hash = params.hash.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'not_found') {
throw new Error('User not found. Please check the user ID and try again.')
}
if (data.error === 'not_enabled') {
throw new Error(
'The Home tab is not enabled for this app. Enable it in your app configuration.'
)
}
if (data.error === 'hash_conflict') {
throw new Error(
'The view has been modified since the hash was generated. Retrieve the latest view and try again.'
)
}
if (data.error === 'view_too_large') {
throw new Error(
'The view payload is too large (max 250kb). Reduce the number of blocks or content.'
)
}
if (data.error === 'duplicate_external_id') {
throw new Error(
'A view with this external_id already exists. Use a unique external_id per workspace.'
)
}
if (data.error === 'invalid_arguments') {
const messages = data.response_metadata?.messages ?? []
throw new Error(
`Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}`
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes.'
)
}
if (
data.error === 'invalid_auth' ||
data.error === 'not_authed' ||
data.error === 'token_expired'
) {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to publish view in Slack')
}
const view = data.view
return {
success: true,
output: {
view: {
id: view.id,
team_id: view.team_id ?? null,
type: view.type,
title: view.title ?? null,
submit: view.submit ?? null,
close: view.close ?? null,
blocks: view.blocks ?? [],
private_metadata: view.private_metadata ?? null,
callback_id: view.callback_id ?? null,
external_id: view.external_id ?? null,
state: view.state ?? null,
hash: view.hash ?? null,
clear_on_close: view.clear_on_close ?? false,
notify_on_close: view.notify_on_close ?? false,
root_view_id: view.root_view_id ?? null,
previous_view_id: view.previous_view_id ?? null,
app_id: view.app_id ?? null,
bot_id: view.bot_id ?? null,
},
},
}
},
outputs: {
view: {
type: 'object',
description: 'The published Home tab view object',
properties: VIEW_OUTPUT_PROPERTIES,
},
},
}

View File

@@ -0,0 +1,173 @@
import type { SlackPushViewParams, SlackPushViewResponse } from '@/tools/slack/types'
import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackPushViewTool: ToolConfig<SlackPushViewParams, SlackPushViewResponse> = {
id: 'slack_push_view',
name: 'Slack Push View',
description:
'Push a new view onto an existing modal stack in Slack. Limited to 2 additional views after the initial modal is opened.',
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',
},
triggerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Exchange a trigger to post to the user. Obtained from an interaction payload (e.g., button click within an existing modal)',
},
interactivityPointer: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Alternative to trigger_id for posting to user',
},
view: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'A view payload object defining the modal to push. Must include type ("modal"), title, and blocks array',
},
},
request: {
url: 'https://slack.com/api/views.push',
method: 'POST',
headers: (params: SlackPushViewParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackPushViewParams) => {
const body: Record<string, unknown> = {
view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view,
}
if (params.triggerId) {
body.trigger_id = params.triggerId.trim()
}
if (params.interactivityPointer) {
body.interactivity_pointer = params.interactivityPointer.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'expired_trigger_id') {
throw new Error(
'The trigger_id has expired. Trigger IDs are only valid for 3 seconds after the interaction.'
)
}
if (data.error === 'invalid_trigger_id') {
throw new Error(
'Invalid trigger_id. Ensure you are using a trigger_id from a valid interaction payload.'
)
}
if (data.error === 'exchanged_trigger_id') {
throw new Error(
'This trigger_id has already been used. Each trigger_id can only be used once.'
)
}
if (data.error === 'push_limit_reached') {
throw new Error(
'Cannot push more views. After a modal is opened, only 2 additional views can be pushed onto the stack.'
)
}
if (data.error === 'view_too_large') {
throw new Error(
'The view payload is too large (max 250kb). Reduce the number of blocks or content.'
)
}
if (data.error === 'duplicate_external_id') {
throw new Error(
'A view with this external_id already exists. Use a unique external_id per workspace.'
)
}
if (data.error === 'invalid_arguments') {
const messages = data.response_metadata?.messages ?? []
throw new Error(
`Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}`
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes.'
)
}
if (
data.error === 'invalid_auth' ||
data.error === 'not_authed' ||
data.error === 'token_expired'
) {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to push view in Slack')
}
const view = data.view
return {
success: true,
output: {
view: {
id: view.id,
team_id: view.team_id ?? null,
type: view.type,
title: view.title ?? null,
submit: view.submit ?? null,
close: view.close ?? null,
blocks: view.blocks ?? [],
private_metadata: view.private_metadata ?? null,
callback_id: view.callback_id ?? null,
external_id: view.external_id ?? null,
state: view.state ?? null,
hash: view.hash ?? null,
clear_on_close: view.clear_on_close ?? false,
notify_on_close: view.notify_on_close ?? false,
root_view_id: view.root_view_id ?? null,
previous_view_id: view.previous_view_id ?? null,
app_id: view.app_id ?? null,
bot_id: view.bot_id ?? null,
},
},
}
},
outputs: {
view: {
type: 'object',
description: 'The pushed modal view object',
properties: VIEW_OUTPUT_PROPERTIES,
},
},
}

View File

@@ -478,6 +478,90 @@ export const CANVAS_OUTPUT_PROPERTIES = {
title: { type: 'string', description: 'Canvas title' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for modal view objects
* Based on Slack views.open response structure
*/
export const VIEW_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'Unique view identifier' },
team_id: { type: 'string', description: 'Workspace/team ID', optional: true },
type: { type: 'string', description: 'View type (e.g., "modal")' },
title: {
type: 'json',
description: 'Plain text title object with type and text fields',
optional: true,
properties: {
type: { type: 'string', description: 'Text object type (plain_text)' },
text: { type: 'string', description: 'Title text content' },
},
},
submit: {
type: 'json',
description: 'Plain text submit button object',
optional: true,
properties: {
type: { type: 'string', description: 'Text object type (plain_text)' },
text: { type: 'string', description: 'Submit button text' },
},
},
close: {
type: 'json',
description: 'Plain text close button object',
optional: true,
properties: {
type: { type: 'string', description: 'Text object type (plain_text)' },
text: { type: 'string', description: 'Close button text' },
},
},
blocks: {
type: 'array',
description: 'Block Kit blocks in the view',
items: {
type: 'object',
properties: BLOCK_OUTPUT_PROPERTIES,
},
},
private_metadata: {
type: 'string',
description: 'Private metadata string passed with the view',
optional: true,
},
callback_id: { type: 'string', description: 'Custom identifier for the view', optional: true },
external_id: {
type: 'string',
description: 'Custom external identifier (max 255 chars, unique per workspace)',
optional: true,
},
state: {
type: 'json',
description: 'Current state of the view with input values',
optional: true,
},
hash: { type: 'string', description: 'View version hash for updates', optional: true },
clear_on_close: {
type: 'boolean',
description: 'Whether to clear all views in the stack when this view is closed',
optional: true,
},
notify_on_close: {
type: 'boolean',
description: 'Whether to send a view_closed event when this view is closed',
optional: true,
},
root_view_id: {
type: 'string',
description: 'ID of the root view in the view stack',
optional: true,
},
previous_view_id: {
type: 'string',
description: 'ID of the previous view in the view stack',
optional: true,
},
app_id: { type: 'string', description: 'Application identifier', optional: true },
bot_id: { type: 'string', description: 'Bot identifier', optional: true },
} as const satisfies Record<string, OutputProperty>
/**
* File download output properties
*/
@@ -629,6 +713,31 @@ export interface SlackCreateChannelCanvasParams extends SlackBaseParams {
content?: string
}
export interface SlackOpenViewParams extends SlackBaseParams {
triggerId: string
interactivityPointer?: string
view: object | string
}
export interface SlackUpdateViewParams extends SlackBaseParams {
viewId?: string
externalId?: string
hash?: string
view: object | string
}
export interface SlackPushViewParams extends SlackBaseParams {
triggerId: string
interactivityPointer?: string
view: object | string
}
export interface SlackPublishViewParams extends SlackBaseParams {
userId: string
hash?: string
view: object | string
}
export interface SlackMessageResponse extends ToolResponse {
output: {
// Legacy properties for backward compatibility
@@ -933,6 +1042,51 @@ export interface SlackCreateChannelCanvasResponse extends ToolResponse {
}
}
export interface SlackView {
id: string
team_id?: string | null
type: string
title?: { type: string; text: string } | null
submit?: { type: string; text: string } | null
close?: { type: string; text: string } | null
blocks: SlackBlock[]
private_metadata?: string | null
callback_id?: string | null
external_id?: string | null
state?: Record<string, unknown> | null
hash?: string | null
clear_on_close?: boolean
notify_on_close?: boolean
root_view_id?: string | null
previous_view_id?: string | null
app_id?: string | null
bot_id?: string | null
}
export interface SlackOpenViewResponse extends ToolResponse {
output: {
view: SlackView
}
}
export interface SlackUpdateViewResponse extends ToolResponse {
output: {
view: SlackView
}
}
export interface SlackPushViewResponse extends ToolResponse {
output: {
view: SlackView
}
}
export interface SlackPublishViewResponse extends ToolResponse {
output: {
view: SlackView
}
}
export type SlackResponse =
| SlackCanvasResponse
| SlackMessageReaderResponse
@@ -953,3 +1107,7 @@ export type SlackResponse =
| SlackGetUserPresenceResponse
| SlackEditCanvasResponse
| SlackCreateChannelCanvasResponse
| SlackOpenViewResponse
| SlackUpdateViewResponse
| SlackPushViewResponse
| SlackPublishViewResponse

View File

@@ -0,0 +1,175 @@
import type { SlackUpdateViewParams, SlackUpdateViewResponse } from '@/tools/slack/types'
import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackUpdateViewTool: ToolConfig<SlackUpdateViewParams, SlackUpdateViewResponse> = {
id: 'slack_update_view',
name: 'Slack Update View',
description:
'Update an existing modal view in Slack. Identify the view by view_id or external_id, and provide the updated view payload.',
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',
},
viewId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Unique identifier of the view to update. Either viewId or externalId is required',
},
externalId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Developer-set unique identifier of the view to update (max 255 chars). Either viewId or externalId is required',
},
hash: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'View state hash to protect against race conditions. Obtained from a previous views response',
},
view: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'A view payload object defining the updated modal. Must include type ("modal"), title, and blocks array. Use identical block_id and action_id values to preserve input data',
},
},
request: {
url: 'https://slack.com/api/views.update',
method: 'POST',
headers: (params: SlackUpdateViewParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackUpdateViewParams) => {
const body: Record<string, unknown> = {
view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view,
}
if (params.viewId) {
body.view_id = params.viewId.trim()
}
if (params.externalId) {
body.external_id = params.externalId.trim()
}
if (params.hash) {
body.hash = params.hash.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'not_found') {
throw new Error(
'View not found. The provided view_id or external_id does not match an existing view.'
)
}
if (data.error === 'hash_conflict') {
throw new Error(
'The view has been modified since the hash was generated. Retrieve the latest view and try again.'
)
}
if (data.error === 'view_too_large') {
throw new Error(
'The view payload is too large (max 250kb). Reduce the number of blocks or content.'
)
}
if (data.error === 'duplicate_external_id') {
throw new Error(
'A view with this external_id already exists. Use a unique external_id per workspace.'
)
}
if (data.error === 'invalid_arguments') {
const messages = data.response_metadata?.messages ?? []
throw new Error(
`Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}`
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes.'
)
}
if (
data.error === 'invalid_auth' ||
data.error === 'not_authed' ||
data.error === 'token_expired'
) {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to update view in Slack')
}
const view = data.view
return {
success: true,
output: {
view: {
id: view.id,
team_id: view.team_id ?? null,
type: view.type,
title: view.title ?? null,
submit: view.submit ?? null,
close: view.close ?? null,
blocks: view.blocks ?? [],
private_metadata: view.private_metadata ?? null,
callback_id: view.callback_id ?? null,
external_id: view.external_id ?? null,
state: view.state ?? null,
hash: view.hash ?? null,
clear_on_close: view.clear_on_close ?? false,
notify_on_close: view.notify_on_close ?? false,
root_view_id: view.root_view_id ?? null,
previous_view_id: view.previous_view_id ?? null,
app_id: view.app_id ?? null,
bot_id: view.bot_id ?? null,
},
},
}
},
outputs: {
view: {
type: 'object',
description: 'The updated modal view object',
properties: VIEW_OUTPUT_PROPERTIES,
},
},
}