Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions apps/docs/content/docs/integrations/slack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ Rename the Slack agent session associated with a thread.

### Slack List Channels

List accessible Slack conversations. Credential-group user tokens also return one-to-one and group direct messages.
List accessible Slack conversations across multiple cursor pages. Credential-group user tokens also return one-to-one and group direct messages.

#### Input

Expand All @@ -966,8 +966,9 @@ List accessible Slack conversations. Credential-group user tokens also return on
| `botToken` | string | No | Bot token for Custom Bot |
| `includePrivate` | boolean | No | Include private channels the bot is a member of \(default: true\) |
| `excludeArchived` | boolean | No | Exclude archived channels \(default: true\) |
| `limit` | number | No | Maximum number of channels to return \(default: 100, max: 200\) |
| `cursor` | string | No | Pagination cursor from a previous response.next_cursor |
| `limit` | number | No | Conversations to request per Slack page \(default: 100, max: 200\) |
| `cursor` | string | No | Pagination cursor from a previous response.nextCursor to resume from |
| `maxPages` | number | No | Maximum number of Slack pages to fetch \(default: 10, max: 10\) |

#### Output

Expand Down Expand Up @@ -999,8 +1000,10 @@ List accessible Slack conversations. Credential-group user tokens also return on
| ↳ `priority` | number | Slack sidebar sort priority |
| `ids` | array | Conversation IDs for every returned channel or DM |
| `names` | array | Names of returned channels and group DMs; one-to-one DMs have no name |
| `count` | number | Total number of conversations returned |
| `nextCursor` | string | Cursor for the next page; null if no more pages |
| `count` | number | Total number of conversations returned across all fetched pages |
| `hasMore` | boolean | Whether more Slack conversation pages remain beyond the fetched window |
| `nextCursor` | string | Cursor to fetch the next page; null when there are no more pages |
| `pages` | number | Number of Slack conversation pages fetched in this invocation |

### Slack List Channel Members

Expand Down
30 changes: 30 additions & 0 deletions apps/sim/blocks/blocks/slack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,34 @@ describe('Slack block release', () => {
expect(isSlackV2SubBlockVisible('agentChannel', repurposedValues)).toBe(true)
expect(selectTool(repurposedValues)).toBe('slack_set_suggested_prompts_v2')
})

it('maps bounded cursor pagination for list channels', () => {
const values = { operation: 'list_channels' }
expect(SlackV2Block.outputs.hasMore.description).toBe(
'Whether more thread messages or provider pages remain beyond the fetched window'
)
expect(isSlackV2SubBlockVisible('channelMaxPages', values)).toBe(true)
expect(isSlackV2SubBlockVisible('paginationCursor', values)).toBe(true)
expect(
mapSlackV2Params({
...values,
channelLimit: '50',
channelMaxPages: '4',
paginationCursor: ' cursor-1 ',
})
).toMatchObject({
limit: 50,
maxPages: 4,
cursor: 'cursor-1',
})
expect(() => mapSlackV2Params({ ...values, channelLimit: '201' })).toThrow(
'Conversations per page must be an integer between 1 and 200'
)
expect(() => mapSlackV2Params({ ...values, channelMaxPages: '11' })).toThrow(
'Max pages must be an integer between 1 and 10'
)
expect(mapSlackV2Params({ ...values, channelLimit: null, channelMaxPages: ' ' })).toMatchObject(
{ limit: 100 }
)
})
})
50 changes: 42 additions & 8 deletions apps/sim/blocks/blocks/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
{ text: ', with heading', field: 'promptsTitle' },
],
list_channels: [
'List Slack conversations',
{
text: 'List up to',
text: ', in pages of',
field: 'channelLimit',
after: 'channels',
core: true,
after: 'items',
},
],
list_members: [
Expand Down Expand Up @@ -762,13 +762,25 @@ Do not include any explanations, markdown formatting, or other text outside the
},
{
id: 'channelLimit',
title: 'Channel Limit',
title: 'Conversations Per Page',
type: 'short-input',
placeholder: '100',
condition: {
field: 'operation',
value: 'list_channels',
},
mode: 'advanced',
},
{
id: 'channelMaxPages',
title: 'Max Pages',
type: 'short-input',
placeholder: '10',
condition: {
field: 'operation',
value: 'list_channels',
},
mode: 'advanced',
},
// List Members specific fields
{
Expand Down Expand Up @@ -1911,6 +1923,7 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
emojiName,
includePrivate,
channelLimit,
channelMaxPages,
memberLimit,
includeDeleted,
userLimit,
Expand Down Expand Up @@ -2127,7 +2140,26 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
case 'list_channels': {
baseParams.includePrivate = includePrivate !== 'false'
baseParams.excludeArchived = true
baseParams.limit = channelLimit ? Number.parseInt(channelLimit, 10) : 100
const hasChannelLimit =
channelLimit !== undefined &&
channelLimit !== null &&
(typeof channelLimit !== 'string' || Boolean(channelLimit.trim()))
const parsedLimit = hasChannelLimit ? Number(channelLimit) : 100
if (!Number.isInteger(parsedLimit) || parsedLimit < 1 || parsedLimit > 200) {
throw new Error('Conversations per page must be an integer between 1 and 200')
}
baseParams.limit = parsedLimit
const hasChannelMaxPages =
channelMaxPages !== undefined &&
channelMaxPages !== null &&
(typeof channelMaxPages !== 'string' || Boolean(channelMaxPages.trim()))
if (hasChannelMaxPages) {
const parsedMaxPages = Number(channelMaxPages)
if (!Number.isInteger(parsedMaxPages) || parsedMaxPages < 1 || parsedMaxPages > 10) {
throw new Error('Max pages must be an integer between 1 and 10')
}
baseParams.maxPages = parsedMaxPages
}
if (paginationCursor) {
baseParams.cursor = String(paginationCursor).trim()
}
Expand Down Expand Up @@ -2393,7 +2425,8 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
thread_ts: { type: 'string', description: 'Thread timestamp for reply' },
// List Channels inputs
includePrivate: { type: 'string', description: 'Include private channels (true/false)' },
channelLimit: { type: 'string', description: 'Maximum number of channels to return' },
channelLimit: { type: 'string', description: 'Conversations to request per Slack page' },
channelMaxPages: { type: 'string', description: 'Maximum Slack pages to fetch (max 10)' },
// List Members inputs
memberLimit: { type: 'string', description: 'Maximum number of members to return' },
// List Users inputs
Expand Down Expand Up @@ -2600,13 +2633,14 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
},
hasMore: {
type: 'boolean',
description: 'Whether there are more messages in the thread',
description:
'Whether more thread messages or provider pages remain beyond the fetched window',
},

// slack_get_channel_history / slack_get_thread_replies pagination outputs
pages: {
type: 'number',
description: 'Number of pages fetched during a paginated history/replies read',
description: 'Number of provider pages fetched during a paginated read',
},
threadTs: {
type: 'string',
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/lib/internal/slack/execute-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ const mocks = vi.hoisted(() => ({
addReaction: vi.fn(),
deleteMessage: vi.fn(),
download: vi.fn(),
listConversations: vi.fn(),
readMessages: vi.fn(),
removeReaction: vi.fn(),
sendEphemeral: vi.fn(),
sendMessage: vi.fn(),
updateMessage: vi.fn(),
}))

vi.mock('@/lib/internal/slack/operations/list-conversations', () => ({
executeSlackListConversationsOperation: mocks.listConversations,
}))

vi.mock('@/lib/internal/slack/operations', () => ({
executeSlackAddReaction: mocks.addReaction,
executeSlackDeleteMessage: mocks.deleteMessage,
Expand All @@ -40,6 +45,7 @@ const INPUTS = {
},
slack_delete_message: { accessToken: 'token', channel: 'C1', timestamp: '1.0' },
slack_download: { accessToken: 'token', fileId: 'F1', fileName: 'report.pdf' },
slack_list_channels: { accessToken: 'token', limit: 100, maxPages: 10 },
slack_ephemeral_message: {
accessToken: 'token',
channel: 'C1',
Expand All @@ -66,6 +72,7 @@ const DISPATCH = {
slack_add_reaction: mocks.addReaction,
slack_delete_message: mocks.deleteMessage,
slack_download: mocks.download,
slack_list_channels: mocks.listConversations,
slack_ephemeral_message: mocks.sendEphemeral,
slack_message: mocks.sendMessage,
slack_message_reader: mocks.readMessages,
Expand Down Expand Up @@ -115,6 +122,13 @@ describe('executeSlackTool', () => {
signal: controller.signal,
userId: 'user-1',
})
} else if (toolId === 'slack_list_channels') {
expect(DISPATCH[toolId].mock.calls[0]?.[1]).toBe(controller.signal)
expect(DISPATCH[toolId].mock.calls[0]?.[2]).toMatchObject({
workflowId: 'workflow-1',
workspaceId: 'workspace-1',
userId: 'user-1',
})
} else {
expect(DISPATCH[toolId].mock.calls[0]?.[1]).toBe(controller.signal)
}
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/lib/internal/slack/execute-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from '@/lib/internal/slack/operations'
import { executeSlackGetChannelHistoryOperation } from '@/lib/internal/slack/operations/get-channel-history'
import { executeSlackGetThreadRepliesOperation } from '@/lib/internal/slack/operations/get-thread-replies'
import { executeSlackListConversationsOperation } from '@/lib/internal/slack/operations/list-conversations'
import { executeToolOperationImplementation } from '@/lib/internal/tool-operations/execute'
import { parseInternalToolInput } from '@/lib/internal/tool-operations/parse-input'
import type {
Expand Down Expand Up @@ -94,6 +95,8 @@ export const executeSlackTool: InternalToolOperationHandler = async (request) =>
return executeToolOperationImplementation(executeSlackGetChannelHistoryOperation, request)
case 'slack_get_thread_replies':
return executeToolOperationImplementation(executeSlackGetThreadRepliesOperation, request)
case 'slack_list_channels':
return executeToolOperationImplementation(executeSlackListConversationsOperation, request)
case 'slack_ephemeral_message':
return executeOperation(slackSendEphemeralContract, request, (input) =>
executeSlackSendEphemeral(input, request.signal)
Expand Down
Loading
Loading