Skip to content

Commit 77bbee9

Browse files
fix(slack): drain conversation pagination safely
1 parent 8656228 commit 77bbee9

9 files changed

Lines changed: 92 additions & 26 deletions

File tree

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

Lines changed: 4 additions & 4 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 across multiple cursor pages. Credential-group user tokens also return one-to-one and group direct messages.
959+
List up to 10,000 accessible Slack conversations across as many cursor pages as Slack supplies, capped at 200 provider pages. Credential-group user tokens also return one-to-one and group direct messages.
960960

961961
#### Input
962962

@@ -968,13 +968,13 @@ List accessible Slack conversations across multiple cursor pages. Credential-gro
968968
| `excludeArchived` | boolean | No | Exclude archived channels \(default: true\) |
969969
| `limit` | number | No | Conversations to request per Slack page \(default: 100, max: 200\) |
970970
| `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\) |
971+
| `maxPages` | number | No | Maximum number of Slack pages to fetch \(default: 200, max: 200\) |
972972

973973
#### Output
974974

975975
| Parameter | Type | Description |
976976
| --------- | ---- | ----------- |
977-
| `channels` | array | Accessible public and private channels, plus direct and group DMs for credential-group user tokens |
977+
| `channels` | array | Up to 10,000 accessible public and private channels, plus direct and group DMs for credential-group user tokens |
978978
|`id` | string | Conversation ID \(for example, C123, D123, or G123\) |
979979
|`name` | string | Channel or group-DM name; omitted for one-to-one direct messages |
980980
|`is_channel` | boolean | Whether this is a channel |
@@ -1000,7 +1000,7 @@ List accessible Slack conversations across multiple cursor pages. Credential-gro
10001000
|`priority` | number | Slack sidebar sort priority |
10011001
| `ids` | array | Conversation IDs for every returned channel or DM |
10021002
| `names` | array | Names of returned channels and group DMs; one-to-one DMs have no name |
1003-
| `count` | number | Total number of conversations returned across all fetched pages |
1003+
| `count` | number | Total number of conversations returned across all fetched pages, up to 10,000 |
10041004
| `hasMore` | boolean | Whether more Slack conversation pages remain beyond the fetched window |
10051005
| `nextCursor` | string | Cursor to fetch the next page; null when there are no more pages |
10061006
| `pages` | number | Number of Slack conversation pages fetched in this invocation |

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ describe('Slack block release', () => {
212212
expect(() => mapSlackV2Params({ ...values, channelLimit: '201' })).toThrow(
213213
'Conversations per page must be an integer between 1 and 200'
214214
)
215-
expect(() => mapSlackV2Params({ ...values, channelMaxPages: '11' })).toThrow(
216-
'Max pages must be an integer between 1 and 10'
215+
expect(() => mapSlackV2Params({ ...values, channelMaxPages: '201' })).toThrow(
216+
'Max pages must be an integer between 1 and 200'
217217
)
218218
expect(mapSlackV2Params({ ...values, channelLimit: null, channelMaxPages: ' ' })).toMatchObject(
219219
{ limit: 100 }

apps/sim/blocks/blocks/slack.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,7 @@ Do not include any explanations, markdown formatting, or other text outside the
775775
id: 'channelMaxPages',
776776
title: 'Max Pages',
777777
type: 'short-input',
778-
placeholder: '10',
778+
placeholder: '200',
779779
condition: {
780780
field: 'operation',
781781
value: 'list_channels',
@@ -2155,8 +2155,8 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
21552155
(typeof channelMaxPages !== 'string' || Boolean(channelMaxPages.trim()))
21562156
if (hasChannelMaxPages) {
21572157
const parsedMaxPages = Number(channelMaxPages)
2158-
if (!Number.isInteger(parsedMaxPages) || parsedMaxPages < 1 || parsedMaxPages > 10) {
2159-
throw new Error('Max pages must be an integer between 1 and 10')
2158+
if (!Number.isInteger(parsedMaxPages) || parsedMaxPages < 1 || parsedMaxPages > 200) {
2159+
throw new Error('Max pages must be an integer between 1 and 200')
21602160
}
21612161
baseParams.maxPages = parsedMaxPages
21622162
}
@@ -2426,7 +2426,7 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
24262426
// List Channels inputs
24272427
includePrivate: { type: 'string', description: 'Include private channels (true/false)' },
24282428
channelLimit: { type: 'string', description: 'Conversations to request per Slack page' },
2429-
channelMaxPages: { type: 'string', description: 'Maximum Slack pages to fetch (max 10)' },
2429+
channelMaxPages: { type: 'string', description: 'Maximum Slack pages to fetch (max 200)' },
24302430
// List Members inputs
24312431
memberLimit: { type: 'string', description: 'Maximum number of members to return' },
24322432
// List Users inputs
@@ -2657,7 +2657,7 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
26572657
channels: {
26582658
type: 'json',
26592659
description:
2660-
'Array of accessible conversation objects. Credential-group user tokens also include direct and group DMs, with type fields (is_channel, is_im, is_mpim) and DM participant field user.',
2660+
'Array of up to 10,000 accessible conversation objects. Credential-group user tokens also include direct and group DMs, with type fields (is_channel, is_im, is_mpim) and DM participant field user.',
26612661
},
26622662
count: {
26632663
type: 'number',

apps/sim/lib/internal/slack/operations/list-conversations.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DEFAULT_CONVERSATION_PAGE_LIMIT,
77
MAX_CONVERSATION_PAGE_LIMIT,
88
MAX_CONVERSATION_PAGES,
9+
MAX_CONVERSATIONS,
910
} from '@/tools/slack/list_channels'
1011
import type { SlackListChannelsParams, SlackListChannelsResponse } from '@/tools/slack/types'
1112
import {
@@ -130,15 +131,16 @@ export const executeSlackListConversationsOperation: InternalToolOperationImplem
130131
let nextCursor: string | null = null
131132
let pages = 0
132133

133-
while (pages < maxPages) {
134+
while (pages < maxPages && channels.length < MAX_CONVERSATIONS) {
135+
const pageLimit = Math.min(limit, MAX_CONVERSATIONS - channels.length)
134136
const { data } = await requestSlackApi({
135137
accessToken,
136138
method: 'conversations.list',
137139
httpMethod: 'GET',
138140
query: {
139141
types,
140142
exclude_archived: String(excludeArchived),
141-
limit,
143+
limit: pageLimit,
142144
cursor,
143145
},
144146
signal,
@@ -148,6 +150,9 @@ export const executeSlackListConversationsOperation: InternalToolOperationImplem
148150
if (!parsed.channels) {
149151
throw new Error('Slack returned a malformed conversations list')
150152
}
153+
if (parsed.channels.length > pageLimit) {
154+
throw new Error(`Slack returned more than the requested ${pageLimit} conversations`)
155+
}
151156

152157
channels.push(...parsed.channels.map(mapSlackConversation))
153158
pages += 1

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/generated/tool-outputs.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/slack/list_channels.test.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,60 @@ describe('Slack list channels', () => {
178178
})
179179
})
180180

181+
it('continues beyond ten pages by default until Slack exhausts its cursor', async () => {
182+
fetchMock.mockImplementation(async (input) => {
183+
const cursor = new URL(String(input)).searchParams.get('cursor')
184+
const page = cursor ? Number(cursor.replace('cursor-', '')) : 1
185+
return slackResponse({
186+
ok: true,
187+
channels: [{ id: `C${page}` }],
188+
response_metadata: { next_cursor: page < 12 ? `cursor-${page + 1}` : '' },
189+
})
190+
})
191+
192+
const result = await executeSlackListConversationsOperation(BASE_PARAMS)
193+
194+
expect(fetchMock).toHaveBeenCalledTimes(12)
195+
expect(result.output).toMatchObject({
196+
count: 12,
197+
hasMore: false,
198+
nextCursor: null,
199+
pages: 12,
200+
})
201+
})
202+
203+
it('stops at 10,000 conversations and preserves the cursor for resumption', async () => {
204+
fetchMock.mockImplementation(async (input) => {
205+
const cursor = new URL(String(input)).searchParams.get('cursor')
206+
const page = cursor ? Number(cursor.replace('cursor-', '')) : 1
207+
return slackResponse({
208+
ok: true,
209+
channels: Array.from({ length: 200 }, (_, index) => ({ id: `C${page}-${index}` })),
210+
response_metadata: { next_cursor: `cursor-${page + 1}` },
211+
})
212+
})
213+
214+
const result = await executeSlackListConversationsOperation({
215+
...BASE_PARAMS,
216+
limit: 200,
217+
})
218+
219+
expect(fetchMock).toHaveBeenCalledTimes(50)
220+
expect(result.output).toMatchObject({
221+
count: 10_000,
222+
hasMore: true,
223+
nextCursor: 'cursor-51',
224+
pages: 50,
225+
})
226+
})
227+
181228
it('fails fast on invalid pagination inputs before a provider request', async () => {
182229
await expect(
183230
executeSlackListConversationsOperation({ ...BASE_PARAMS, limit: 0 })
184231
).rejects.toThrow('Conversation page size must be an integer between 1 and 200')
185232
await expect(
186-
executeSlackListConversationsOperation({ ...BASE_PARAMS, maxPages: 11 })
187-
).rejects.toThrow('Maximum conversation pages must be an integer between 1 and 10')
233+
executeSlackListConversationsOperation({ ...BASE_PARAMS, maxPages: 201 })
234+
).rejects.toThrow('Maximum conversation pages must be an integer between 1 and 200')
188235
await expect(
189236
executeSlackListConversationsOperation({ ...BASE_PARAMS, cursor: ' ' })
190237
).rejects.toThrow('Pagination cursor is required')
@@ -229,5 +276,16 @@ describe('Slack list channels', () => {
229276
await expect(
230277
executeSlackListConversationsOperation({ ...BASE_PARAMS, cursor: 'same-cursor' })
231278
).rejects.toThrow('Slack returned a repeated conversation pagination cursor')
279+
280+
fetchMock.mockResolvedValueOnce(
281+
slackResponse({
282+
ok: true,
283+
channels: [{ id: 'C1' }, { id: 'C2' }],
284+
response_metadata: { next_cursor: '' },
285+
})
286+
)
287+
await expect(
288+
executeSlackListConversationsOperation({ ...BASE_PARAMS, limit: 1 })
289+
).rejects.toThrow('Slack returned more than the requested 1 conversations')
232290
})
233291
})

apps/sim/tools/slack/list_channels.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ export const DEFAULT_CONVERSATION_PAGE_LIMIT = 100
99
/** Slack's recommended upper bound for conversations.list page size. */
1010
export const MAX_CONVERSATION_PAGE_LIMIT = 200
1111

12-
/** Default and hard cap on Slack conversation pages fetched per invocation. */
13-
export const MAX_CONVERSATION_PAGES = 10
12+
/** Default and hard cap on Slack conversation provider pages fetched per invocation. */
13+
export const MAX_CONVERSATION_PAGES = 200
14+
15+
/** Hard cap on Slack conversations accumulated per invocation. */
16+
export const MAX_CONVERSATIONS = 10_000
1417

1518
export const slackListChannelsTool: InternalToolConfig<
1619
SlackListChannelsParams,
@@ -19,8 +22,8 @@ export const slackListChannelsTool: InternalToolConfig<
1922
id: 'slack_list_channels',
2023
name: 'Slack List Channels',
2124
description:
22-
'List accessible Slack conversations across multiple cursor pages. Credential-group user tokens also return one-to-one and group direct messages.',
23-
version: '1.2.0',
25+
'List up to 10,000 accessible Slack conversations across as many cursor pages as Slack supplies, capped at 200 provider pages. Credential-group user tokens also return one-to-one and group direct messages.',
26+
version: '1.3.0',
2427

2528
oauth: {
2629
required: true,
@@ -81,7 +84,7 @@ export const slackListChannelsTool: InternalToolConfig<
8184
type: 'number',
8285
required: false,
8386
visibility: 'user-or-llm',
84-
description: 'Maximum number of Slack pages to fetch (default: 10, max: 10)',
87+
description: 'Maximum number of Slack pages to fetch (default: 200, max: 200)',
8588
},
8689
},
8790

@@ -93,7 +96,7 @@ export const slackListChannelsTool: InternalToolConfig<
9396
channels: {
9497
type: 'array',
9598
description:
96-
'Accessible public and private channels, plus direct and group DMs for credential-group user tokens',
99+
'Up to 10,000 accessible public and private channels, plus direct and group DMs for credential-group user tokens',
97100
items: {
98101
type: 'object',
99102
properties: CONVERSATION_LIST_OUTPUT_PROPERTIES,
@@ -111,7 +114,7 @@ export const slackListChannelsTool: InternalToolConfig<
111114
},
112115
count: {
113116
type: 'number',
114-
description: 'Total number of conversations returned across all fetched pages',
117+
description: 'Total number of conversations returned across all fetched pages, up to 10,000',
115118
},
116119
hasMore: {
117120
type: 'boolean',

packages/deployment-config/src/integrations.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"updatedAt": "2026-09-06",
2+
"updatedAt": "2026-09-07",
33
"integrations": [
44
{
55
"type": "onepassword",
@@ -22205,7 +22205,7 @@
2220522205
},
2220622206
{
2220722207
"name": "List Channels",
22208-
"description": "List accessible Slack conversations across multiple cursor pages. Credential-group user tokens also return one-to-one and group direct messages."
22208+
"description": "List up to 10,000 accessible Slack conversations across as many cursor pages as Slack supplies, capped at 200 provider pages. Credential-group user tokens also return one-to-one and group direct messages."
2220922209
},
2221022210
{
2221122211
"name": "List Channel Members",

0 commit comments

Comments
 (0)