feat(slack): add conversations.create and conversations.invite tools (#3720)

* feat(slack): add conversations.create and conversations.invite tools

* fix(slack): address PR review comments on conversation tools

* feat(slack): wire create/invite conversation tools into Slack block

* lint

* fix(slack): rename channel output to channelInfo to avoid type collision

The block outputs already declare `channel` as type string (channel ID from
send operation). Rename the object output to `channelInfo` to match the
pattern used by get_channel_info and avoid [object Object] rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(slack): update output key in docs to match channelInfo rename

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(docs): fix lint errors in auto-generated docs files

Sort imports in icon-mapping.ts and add trailing newline to meta.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-03-23 15:32:52 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent f02f85fded
commit 4c8395928a
8 changed files with 534 additions and 1 deletions
+76
View File
@@ -925,6 +925,82 @@ Create a canvas pinned to a Slack channel as its resource hub
| --------- | ---- | ----------- |
| `canvas_id` | string | ID of the created channel canvas |
### `slack_create_conversation`
Create a new public or private channel in a Slack workspace.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `name` | string | Yes | Name of the channel to create \(lowercase, numbers, hyphens, underscores only; max 80 characters\) |
| `isPrivate` | boolean | No | Create a private channel instead of a public one \(default: false\) |
| `teamId` | string | No | Encoded team ID to create the channel in \(required if using an org token\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `channelInfo` | object | The newly created channel object |
| ↳ `id` | string | Channel ID \(e.g., C1234567890\) |
| ↳ `name` | string | Channel name without # prefix |
| ↳ `is_channel` | boolean | Whether this is a channel |
| ↳ `is_private` | boolean | Whether channel is private |
| ↳ `is_archived` | boolean | Whether channel is archived |
| ↳ `is_general` | boolean | Whether this is the general channel |
| ↳ `is_member` | boolean | Whether the bot/user is a member |
| ↳ `is_shared` | boolean | Whether channel is shared across workspaces |
| ↳ `is_ext_shared` | boolean | Whether channel is externally shared |
| ↳ `is_org_shared` | boolean | Whether channel is org-wide shared |
| ↳ `num_members` | number | Number of members in the channel |
| ↳ `topic` | string | Channel topic |
| ↳ `purpose` | string | Channel purpose/description |
| ↳ `created` | number | Unix timestamp when channel was created |
| ↳ `creator` | string | User ID of channel creator |
| ↳ `updated` | number | Unix timestamp of last update |
### `slack_invite_to_conversation`
Invite one or more users to a Slack channel. Supports up to 100 users at a time.
#### 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 | The ID of the channel to invite users to |
| `users` | string | Yes | Comma-separated list of user IDs to invite \(up to 100\) |
| `force` | boolean | No | When true, continues inviting valid users while skipping invalid ones \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `channelInfo` | object | The channel object after inviting users |
| ↳ `id` | string | Channel ID \(e.g., C1234567890\) |
| ↳ `name` | string | Channel name without # prefix |
| ↳ `is_channel` | boolean | Whether this is a channel |
| ↳ `is_private` | boolean | Whether channel is private |
| ↳ `is_archived` | boolean | Whether channel is archived |
| ↳ `is_general` | boolean | Whether this is the general channel |
| ↳ `is_member` | boolean | Whether the bot/user is a member |
| ↳ `is_shared` | boolean | Whether channel is shared across workspaces |
| ↳ `is_ext_shared` | boolean | Whether channel is externally shared |
| ↳ `is_org_shared` | boolean | Whether channel is org-wide shared |
| ↳ `num_members` | number | Number of members in the channel |
| ↳ `topic` | string | Channel topic |
| ↳ `purpose` | string | Channel purpose/description |
| ↳ `created` | number | Unix timestamp when channel was created |
| ↳ `creator` | string | User ID of channel creator |
| ↳ `updated` | number | Unix timestamp of last update |
| `errors` | array | Per-user errors when force is true and some invitations failed |
| ↳ `user` | string | User ID that failed |
| ↳ `ok` | boolean | Always false for error entries |
| ↳ `error` | string | Error code for this user |
### `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.
@@ -9581,6 +9581,14 @@
"name": "Create Channel Canvas",
"description": "Create a canvas pinned to a Slack channel as its resource hub"
},
{
"name": "Create Conversation",
"description": "Create a new public or private channel in a Slack workspace."
},
{
"name": "Invite to Conversation",
"description": "Invite one or more users to a Slack channel. Supports up to 100 users at a time."
},
{
"name": "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."
@@ -9598,7 +9606,7 @@
"description": "Publish a static view to a user"
}
],
"operationCount": 23,
"operationCount": 25,
"triggers": [
{
"id": "slack_webhook",
+108
View File
@@ -46,6 +46,8 @@ 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: 'Create Conversation', id: 'create_conversation' },
{ label: 'Invite to Conversation', id: 'invite_to_conversation' },
{ label: 'Open View', id: 'open_view' },
{ label: 'Update View', id: 'update_view' },
{ label: 'Push View', id: 'push_view' },
@@ -144,6 +146,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
'get_user',
'get_user_presence',
'edit_canvas',
'create_conversation',
'open_view',
'update_view',
'push_view',
@@ -179,6 +182,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
'get_user',
'get_user_presence',
'edit_canvas',
'create_conversation',
'open_view',
'update_view',
'push_view',
@@ -816,6 +820,70 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
value: 'create_channel_canvas',
},
},
// Create Conversation specific fields
{
id: 'conversationName',
title: 'Channel Name',
type: 'short-input',
placeholder: 'e.g., project-updates',
condition: {
field: 'operation',
value: 'create_conversation',
},
required: true,
},
{
id: 'isPrivate',
title: 'Private Channel',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: {
field: 'operation',
value: 'create_conversation',
},
},
{
id: 'teamId',
title: 'Team ID',
type: 'short-input',
placeholder: 'Encoded team ID (org tokens only)',
condition: {
field: 'operation',
value: 'create_conversation',
},
mode: 'advanced',
},
// Invite to Conversation specific fields
{
id: 'inviteUsers',
title: 'User IDs',
type: 'short-input',
placeholder: 'Comma-separated user IDs (e.g., U123,U456)',
condition: {
field: 'operation',
value: 'invite_to_conversation',
},
required: true,
},
{
id: 'inviteForce',
title: 'Skip Invalid Users',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: {
field: 'operation',
value: 'invite_to_conversation',
},
mode: 'advanced',
},
// Open View / Push View specific fields
{
id: 'viewTriggerId',
@@ -990,6 +1058,8 @@ Do not include any explanations, markdown formatting, or other text outside the
'slack_get_user_presence',
'slack_edit_canvas',
'slack_create_channel_canvas',
'slack_create_conversation',
'slack_invite_to_conversation',
'slack_open_view',
'slack_update_view',
'slack_push_view',
@@ -1036,6 +1106,10 @@ Do not include any explanations, markdown formatting, or other text outside the
return 'slack_edit_canvas'
case 'create_channel_canvas':
return 'slack_create_channel_canvas'
case 'create_conversation':
return 'slack_create_conversation'
case 'invite_to_conversation':
return 'slack_invite_to_conversation'
case 'open_view':
return 'slack_open_view'
case 'update_view':
@@ -1090,6 +1164,11 @@ Do not include any explanations, markdown formatting, or other text outside the
canvasTitle,
channelCanvasTitle,
channelCanvasContent,
conversationName,
isPrivate,
teamId,
inviteUsers,
inviteForce,
viewTriggerId,
viewInteractivityPointer,
viewId,
@@ -1264,6 +1343,21 @@ Do not include any explanations, markdown formatting, or other text outside the
}
break
case 'create_conversation':
baseParams.name = conversationName
baseParams.isPrivate = isPrivate === 'true'
if (teamId) {
baseParams.teamId = teamId
}
break
case 'invite_to_conversation':
baseParams.users = inviteUsers
if (inviteForce === 'true') {
baseParams.force = true
}
break
case 'open_view':
baseParams.triggerId = viewTriggerId
if (viewInteractivityPointer) {
@@ -1367,6 +1461,13 @@ Do not include any explanations, markdown formatting, or other text outside the
// Create Channel Canvas inputs
channelCanvasTitle: { type: 'string', description: 'Title for channel canvas' },
channelCanvasContent: { type: 'string', description: 'Content for channel canvas' },
// Create Conversation inputs
conversationName: { type: 'string', description: 'Name for the new channel' },
isPrivate: { type: 'string', description: 'Create as private channel (true/false)' },
teamId: { type: 'string', description: 'Encoded team ID for org tokens' },
// Invite to Conversation inputs
inviteUsers: { type: 'string', description: 'Comma-separated user IDs to invite' },
inviteForce: { type: 'string', description: 'Skip invalid users (true/false)' },
// View operation inputs
viewTriggerId: { type: 'string', description: 'Trigger ID from interaction payload' },
viewInteractivityPointer: {
@@ -1524,6 +1625,13 @@ Do not include any explanations, markdown formatting, or other text outside the
'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',
},
// slack_invite_to_conversation outputs (invite_to_conversation operation)
errors: {
type: 'json',
description:
'Array of per-user error objects when force is true and some invitations failed (user, ok, error)',
},
// 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' },
+4
View File
@@ -1948,6 +1948,7 @@ import {
slackAddReactionTool,
slackCanvasTool,
slackCreateChannelCanvasTool,
slackCreateConversationTool,
slackDeleteMessageTool,
slackDownloadTool,
slackEditCanvasTool,
@@ -1957,6 +1958,7 @@ import {
slackGetThreadTool,
slackGetUserPresenceTool,
slackGetUserTool,
slackInviteToConversationTool,
slackListChannelsTool,
slackListMembersTool,
slackListUsersTool,
@@ -2822,6 +2824,8 @@ export const tools: Record<string, ToolConfig> = {
slack_publish_view: slackPublishViewTool,
slack_edit_canvas: slackEditCanvasTool,
slack_create_channel_canvas: slackCreateChannelCanvasTool,
slack_create_conversation: slackCreateConversationTool,
slack_invite_to_conversation: slackInviteToConversationTool,
github_repo_info: githubRepoInfoTool,
github_repo_info_v2: githubRepoInfoV2Tool,
github_latest_commit: githubLatestCommitTool,
+140
View File
@@ -0,0 +1,140 @@
import type {
SlackCreateConversationParams,
SlackCreateConversationResponse,
} from '@/tools/slack/types'
import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackCreateConversationTool: ToolConfig<
SlackCreateConversationParams,
SlackCreateConversationResponse
> = {
id: 'slack_create_conversation',
name: 'Slack Create Conversation',
description: 'Create a new public or private channel in a Slack workspace.',
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',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Name of the channel to create (lowercase, numbers, hyphens, underscores only; max 80 characters)',
},
isPrivate: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Create a private channel instead of a public one (default: false)',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Encoded team ID to create the channel in (required if using an org token)',
},
},
request: {
url: 'https://slack.com/api/conversations.create',
method: 'POST',
headers: (params: SlackCreateConversationParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackCreateConversationParams) => {
const body: Record<string, unknown> = {
name: params.name?.trim(),
}
if (params.isPrivate != null) {
body.is_private = params.isPrivate
}
if (params.teamId?.trim()) {
body.team_id = params.teamId.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'name_taken') {
throw new Error('A channel with this name already exists in the workspace.')
}
if (
data.error === 'invalid_name' ||
data.error === 'invalid_name_specials' ||
data.error === 'invalid_name_maxlength'
) {
throw new Error(
'Invalid channel name. Use only lowercase letters, numbers, hyphens, and underscores (max 80 characters).'
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
if (data.error === 'restricted_action') {
throw new Error('Workspace policy prevents channel creation.')
}
throw new Error(data.error || 'Failed to create conversation in Slack')
}
const ch = data.channel || {}
return {
success: true,
output: {
channelInfo: {
id: ch.id,
name: ch.name,
is_private: ch.is_private || false,
is_archived: ch.is_archived || false,
is_member: ch.is_member || false,
topic: ch.topic?.value || '',
purpose: ch.purpose?.value || '',
created: ch.created,
creator: ch.creator,
},
},
}
},
outputs: {
channelInfo: {
type: 'object',
description: 'The newly created channel object',
properties: CHANNEL_OUTPUT_PROPERTIES,
},
},
}
+4
View File
@@ -1,6 +1,7 @@
import { slackAddReactionTool } from '@/tools/slack/add_reaction'
import { slackCanvasTool } from '@/tools/slack/canvas'
import { slackCreateChannelCanvasTool } from '@/tools/slack/create_channel_canvas'
import { slackCreateConversationTool } from '@/tools/slack/create_conversation'
import { slackDeleteMessageTool } from '@/tools/slack/delete_message'
import { slackDownloadTool } from '@/tools/slack/download'
import { slackEditCanvasTool } from '@/tools/slack/edit_canvas'
@@ -10,6 +11,7 @@ import { slackGetMessageTool } from '@/tools/slack/get_message'
import { slackGetThreadTool } from '@/tools/slack/get_thread'
import { slackGetUserTool } from '@/tools/slack/get_user'
import { slackGetUserPresenceTool } from '@/tools/slack/get_user_presence'
import { slackInviteToConversationTool } from '@/tools/slack/invite_to_conversation'
import { slackListChannelsTool } from '@/tools/slack/list_channels'
import { slackListMembersTool } from '@/tools/slack/list_members'
import { slackListUsersTool } from '@/tools/slack/list_users'
@@ -25,6 +27,7 @@ import { slackUpdateViewTool } from '@/tools/slack/update_view'
export {
slackMessageTool,
slackCanvasTool,
slackCreateConversationTool,
slackCreateChannelCanvasTool,
slackMessageReaderTool,
slackDownloadTool,
@@ -46,4 +49,5 @@ export {
slackPublishViewTool,
slackGetMessageTool,
slackGetThreadTool,
slackInviteToConversationTool,
}
@@ -0,0 +1,166 @@
import type {
SlackInviteToConversationParams,
SlackInviteToConversationResponse,
} from '@/tools/slack/types'
import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackInviteToConversationTool: ToolConfig<
SlackInviteToConversationParams,
SlackInviteToConversationResponse
> = {
id: 'slack_invite_to_conversation',
name: 'Slack Invite to Conversation',
description: 'Invite one or more users to a Slack channel. Supports up to 100 users at a time.',
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: 'The ID of the channel to invite users to',
},
users: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated list of user IDs to invite (up to 100)',
},
force: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'When true, continues inviting valid users while skipping invalid ones (default: false)',
},
},
request: {
url: 'https://slack.com/api/conversations.invite',
method: 'POST',
headers: (params: SlackInviteToConversationParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackInviteToConversationParams) => {
const body: Record<string, unknown> = {
channel: params.channel?.trim(),
users: params.users?.trim(),
}
if (params.force != null) {
body.force = params.force
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'channel_not_found') {
throw new Error('Channel not found. Please verify the channel ID.')
}
if (data.error === 'user_not_found') {
throw new Error('One or more user IDs were not found.')
}
if (data.error === 'cant_invite_self') {
throw new Error('You cannot invite yourself to a channel.')
}
if (data.error === 'already_in_channel') {
throw new Error('One or more users are already in the channel.')
}
if (data.error === 'is_archived') {
throw new Error('The channel is archived and cannot accept new members.')
}
if (data.error === 'not_in_channel') {
throw new Error('The authenticated user is not a member of this channel.')
}
if (data.error === 'cant_invite') {
throw new Error('This user cannot be invited to the channel.')
}
if (data.error === 'no_permission') {
throw new Error('You do not have permission to invite this user to the channel.')
}
if (data.error === 'org_user_not_in_team') {
throw new Error(
'One or more invited members are in the Enterprise org but not this workspace.'
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to invite users to Slack conversation')
}
const ch = data.channel || {}
return {
success: true,
output: {
channelInfo: {
id: ch.id,
name: ch.name,
is_private: ch.is_private || false,
is_archived: ch.is_archived || false,
is_member: ch.is_member || false,
topic: ch.topic?.value || '',
purpose: ch.purpose?.value || '',
created: ch.created,
creator: ch.creator,
},
...(data.errors?.length ? { errors: data.errors } : {}),
},
}
},
outputs: {
channelInfo: {
type: 'object',
description: 'The channel object after inviting users',
properties: CHANNEL_OUTPUT_PROPERTIES,
},
errors: {
type: 'array',
description: 'Per-user errors when force is true and some invitations failed',
optional: true,
items: {
type: 'object',
properties: {
user: { type: 'string', description: 'User ID that failed' },
ok: { type: 'boolean', description: 'Always false for error entries' },
error: { type: 'string', description: 'Error code for this user' },
},
},
},
},
}
+27
View File
@@ -709,6 +709,18 @@ export interface SlackGetUserPresenceParams extends SlackBaseParams {
userId: string
}
export interface SlackCreateConversationParams extends SlackBaseParams {
name: string
isPrivate?: boolean
teamId?: string
}
export interface SlackInviteToConversationParams extends SlackBaseParams {
channel: string
users: string
force?: boolean
}
export interface SlackEditCanvasParams extends SlackBaseParams {
canvasId: string
operation: string
@@ -1030,6 +1042,19 @@ export interface SlackGetChannelInfoResponse extends ToolResponse {
}
}
export interface SlackCreateConversationResponse extends ToolResponse {
output: {
channelInfo: SlackChannel
}
}
export interface SlackInviteToConversationResponse extends ToolResponse {
output: {
channelInfo: SlackChannel
errors?: Array<{ user: string; ok: boolean; error: string }>
}
}
export interface SlackGetUserPresenceResponse extends ToolResponse {
output: {
presence: string
@@ -1118,6 +1143,8 @@ export type SlackResponse =
| SlackGetUserPresenceResponse
| SlackEditCanvasResponse
| SlackCreateChannelCanvasResponse
| SlackCreateConversationResponse
| SlackInviteToConversationResponse
| SlackOpenViewResponse
| SlackUpdateViewResponse
| SlackPushViewResponse