Skip to content

Commit ce182fb

Browse files
fix(slack): paginate v2 conversation listing
1 parent 7e85965 commit ce182fb

13 files changed

Lines changed: 549 additions & 335 deletions

File tree

apps/docs/content/docs/integrations/slack.mdx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ Rename the Slack agent session associated with a thread.
956956

957957
### Slack List Channels
958958

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

961961
#### Input
962962

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

972973
#### Output
973974

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

10051008
### Slack List Channel Members
10061009

apps/sim/blocks/blocks/slack.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,28 @@ describe('Slack block release', () => {
189189
expect(isSlackV2SubBlockVisible('agentChannel', repurposedValues)).toBe(true)
190190
expect(selectTool(repurposedValues)).toBe('slack_set_suggested_prompts_v2')
191191
})
192+
193+
it('maps bounded cursor pagination for list channels', () => {
194+
const values = { operation: 'list_channels' }
195+
expect(isSlackV2SubBlockVisible('channelMaxPages', values)).toBe(true)
196+
expect(isSlackV2SubBlockVisible('paginationCursor', values)).toBe(true)
197+
expect(
198+
mapSlackV2Params({
199+
...values,
200+
channelLimit: '50',
201+
channelMaxPages: '4',
202+
paginationCursor: ' cursor-1 ',
203+
})
204+
).toMatchObject({
205+
limit: 50,
206+
maxPages: 4,
207+
cursor: 'cursor-1',
208+
})
209+
expect(() => mapSlackV2Params({ ...values, channelLimit: '201' })).toThrow(
210+
'Conversations per page must be an integer between 1 and 200'
211+
)
212+
expect(() => mapSlackV2Params({ ...values, channelMaxPages: '11' })).toThrow(
213+
'Max pages must be an integer between 1 and 10'
214+
)
215+
})
192216
})

apps/sim/blocks/blocks/slack.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,11 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
163163
{ text: ', with heading', field: 'promptsTitle' },
164164
],
165165
list_channels: [
166+
'List Slack conversations',
166167
{
167-
text: 'List up to',
168+
text: ', in pages of',
168169
field: 'channelLimit',
169-
after: 'channels',
170-
core: true,
170+
after: 'items',
171171
},
172172
],
173173
list_members: [
@@ -762,13 +762,25 @@ Do not include any explanations, markdown formatting, or other text outside the
762762
},
763763
{
764764
id: 'channelLimit',
765-
title: 'Channel Limit',
765+
title: 'Conversations Per Page',
766766
type: 'short-input',
767767
placeholder: '100',
768768
condition: {
769769
field: 'operation',
770770
value: 'list_channels',
771771
},
772+
mode: 'advanced',
773+
},
774+
{
775+
id: 'channelMaxPages',
776+
title: 'Max Pages',
777+
type: 'short-input',
778+
placeholder: '10',
779+
condition: {
780+
field: 'operation',
781+
value: 'list_channels',
782+
},
783+
mode: 'advanced',
772784
},
773785
// List Members specific fields
774786
{
@@ -1911,6 +1923,7 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
19111923
emojiName,
19121924
includePrivate,
19131925
channelLimit,
1926+
channelMaxPages,
19141927
memberLimit,
19151928
includeDeleted,
19161929
userLimit,
@@ -2127,7 +2140,19 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
21272140
case 'list_channels': {
21282141
baseParams.includePrivate = includePrivate !== 'false'
21292142
baseParams.excludeArchived = true
2130-
baseParams.limit = channelLimit ? Number.parseInt(channelLimit, 10) : 100
2143+
const parsedLimit =
2144+
channelLimit === undefined || channelLimit === '' ? 100 : Number(channelLimit)
2145+
if (!Number.isInteger(parsedLimit) || parsedLimit < 1 || parsedLimit > 200) {
2146+
throw new Error('Conversations per page must be an integer between 1 and 200')
2147+
}
2148+
baseParams.limit = parsedLimit
2149+
if (channelMaxPages !== undefined && channelMaxPages !== '') {
2150+
const parsedMaxPages = Number(channelMaxPages)
2151+
if (!Number.isInteger(parsedMaxPages) || parsedMaxPages < 1 || parsedMaxPages > 10) {
2152+
throw new Error('Max pages must be an integer between 1 and 10')
2153+
}
2154+
baseParams.maxPages = parsedMaxPages
2155+
}
21312156
if (paginationCursor) {
21322157
baseParams.cursor = String(paginationCursor).trim()
21332158
}
@@ -2393,7 +2418,8 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
23932418
thread_ts: { type: 'string', description: 'Thread timestamp for reply' },
23942419
// List Channels inputs
23952420
includePrivate: { type: 'string', description: 'Include private channels (true/false)' },
2396-
channelLimit: { type: 'string', description: 'Maximum number of channels to return' },
2421+
channelLimit: { type: 'string', description: 'Conversations to request per Slack page' },
2422+
channelMaxPages: { type: 'string', description: 'Maximum Slack pages to fetch (max 10)' },
23972423
// List Members inputs
23982424
memberLimit: { type: 'string', description: 'Maximum number of members to return' },
23992425
// List Users inputs
@@ -2600,13 +2626,13 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
26002626
},
26012627
hasMore: {
26022628
type: 'boolean',
2603-
description: 'Whether there are more messages in the thread',
2629+
description: 'Whether more provider pages remain beyond the fetched window',
26042630
},
26052631

26062632
// slack_get_channel_history / slack_get_thread_replies pagination outputs
26072633
pages: {
26082634
type: 'number',
2609-
description: 'Number of pages fetched during a paginated history/replies read',
2635+
description: 'Number of provider pages fetched during a paginated read',
26102636
},
26112637
threadTs: {
26122638
type: 'string',

apps/sim/lib/internal/slack/execute-tool.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ const mocks = vi.hoisted(() => ({
88
addReaction: vi.fn(),
99
deleteMessage: vi.fn(),
1010
download: vi.fn(),
11+
listConversations: vi.fn(),
1112
readMessages: vi.fn(),
1213
removeReaction: vi.fn(),
1314
sendEphemeral: vi.fn(),
1415
sendMessage: vi.fn(),
1516
updateMessage: vi.fn(),
1617
}))
1718

19+
vi.mock('@/lib/internal/slack/operations/list-conversations', () => ({
20+
executeSlackListConversationsOperation: mocks.listConversations,
21+
}))
22+
1823
vi.mock('@/lib/internal/slack/operations', () => ({
1924
executeSlackAddReaction: mocks.addReaction,
2025
executeSlackDeleteMessage: mocks.deleteMessage,
@@ -40,6 +45,7 @@ const INPUTS = {
4045
},
4146
slack_delete_message: { accessToken: 'token', channel: 'C1', timestamp: '1.0' },
4247
slack_download: { accessToken: 'token', fileId: 'F1', fileName: 'report.pdf' },
48+
slack_list_channels: { accessToken: 'token', limit: 100, maxPages: 10 },
4349
slack_ephemeral_message: {
4450
accessToken: 'token',
4551
channel: 'C1',
@@ -66,6 +72,7 @@ const DISPATCH = {
6672
slack_add_reaction: mocks.addReaction,
6773
slack_delete_message: mocks.deleteMessage,
6874
slack_download: mocks.download,
75+
slack_list_channels: mocks.listConversations,
6976
slack_ephemeral_message: mocks.sendEphemeral,
7077
slack_message: mocks.sendMessage,
7178
slack_message_reader: mocks.readMessages,
@@ -115,6 +122,13 @@ describe('executeSlackTool', () => {
115122
signal: controller.signal,
116123
userId: 'user-1',
117124
})
125+
} else if (toolId === 'slack_list_channels') {
126+
expect(DISPATCH[toolId].mock.calls[0]?.[1]).toBe(controller.signal)
127+
expect(DISPATCH[toolId].mock.calls[0]?.[2]).toMatchObject({
128+
workflowId: 'workflow-1',
129+
workspaceId: 'workspace-1',
130+
userId: 'user-1',
131+
})
118132
} else {
119133
expect(DISPATCH[toolId].mock.calls[0]?.[1]).toBe(controller.signal)
120134
}

apps/sim/lib/internal/slack/execute-tool.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from '@/lib/internal/slack/operations'
2727
import { executeSlackGetChannelHistoryOperation } from '@/lib/internal/slack/operations/get-channel-history'
2828
import { executeSlackGetThreadRepliesOperation } from '@/lib/internal/slack/operations/get-thread-replies'
29+
import { executeSlackListConversationsOperation } from '@/lib/internal/slack/operations/list-conversations'
2930
import { executeToolOperationImplementation } from '@/lib/internal/tool-operations/execute'
3031
import { parseInternalToolInput } from '@/lib/internal/tool-operations/parse-input'
3132
import type {
@@ -94,6 +95,8 @@ export const executeSlackTool: InternalToolOperationHandler = async (request) =>
9495
return executeToolOperationImplementation(executeSlackGetChannelHistoryOperation, request)
9596
case 'slack_get_thread_replies':
9697
return executeToolOperationImplementation(executeSlackGetThreadRepliesOperation, request)
98+
case 'slack_list_channels':
99+
return executeToolOperationImplementation(executeSlackListConversationsOperation, request)
97100
case 'slack_ephemeral_message':
98101
return executeOperation(slackSendEphemeralContract, request, (input) =>
99102
executeSlackSendEphemeral(input, request.signal)

0 commit comments

Comments
 (0)