Skip to content

Commit 4e6ee86

Browse files
fix(slack): return one conversation page with next cursor (#7611)
* fix(slack): return one conversation page with next cursor * fix(slack): remove redundant conversation pages output
1 parent 44d25dd commit 4e6ee86

13 files changed

Lines changed: 168 additions & 216 deletions

File tree

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,7 @@ Rename the Slack agent session associated with a thread.
939939

940940
### Slack List Channels
941941

942-
List up to 10,000 accessible public and private Slack channels across as many cursor pages as Slack supplies, capped at 200 provider pages.
942+
List one page of accessible public and private Slack channels. Pass the returned nextCursor as cursor to fetch the next page.
943943

944944
#### Input
945945

@@ -951,13 +951,12 @@ List up to 10,000 accessible public and private Slack channels across as many cu
951951
| `excludeArchived` | boolean | No | Exclude archived channels \(default: true\) |
952952
| `limit` | number | No | Conversations to request per Slack page \(default: 100, max: 200\) |
953953
| `cursor` | string | No | Pagination cursor from a previous response.nextCursor to resume from |
954-
| `maxPages` | number | No | Maximum number of Slack pages to fetch \(default: 200, max: 200\) |
955954

956955
#### Output
957956

958957
| Parameter | Type | Description |
959958
| --------- | ---- | ----------- |
960-
| `channels` | array | Up to 10,000 accessible public and private channels |
959+
| `channels` | array | One page of accessible public and private channels |
961960
|`id` | string | Conversation ID \(for example, C123, D123, or G123\) |
962961
|`name` | string | Channel or group-DM name; omitted for one-to-one direct messages |
963962
|`is_channel` | boolean | Whether this is a channel |
@@ -983,10 +982,9 @@ List up to 10,000 accessible public and private Slack channels across as many cu
983982
|`priority` | number | Slack sidebar sort priority |
984983
| `ids` | array | Conversation IDs for every returned channel |
985984
| `names` | array | Names of returned channels |
986-
| `count` | number | Total number of conversations returned across all fetched pages, up to 10,000 |
987-
| `hasMore` | boolean | Whether more Slack conversation pages remain beyond the fetched window |
985+
| `count` | number | Number of conversations returned in this page |
986+
| `hasMore` | boolean | Whether a next cursor is available to fetch more Slack conversations |
988987
| `nextCursor` | string | Cursor to fetch the next page; null when there are no more pages |
989-
| `pages` | number | Number of Slack conversation pages fetched in this invocation |
990988

991989
### Slack List Channel Members
992990

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,12 @@ describe('Slack block release', () => {
190190
expect(selectTool(repurposedValues)).toBe('slack_set_suggested_prompts_v2')
191191
})
192192

193-
it('maps bounded cursor pagination for list channels', () => {
193+
it('maps a single page and cursor for list channels', () => {
194194
const values = { operation: 'list_channels' }
195195
expect(SlackV2Block.outputs.hasMore.description).toBe(
196196
'Whether more thread messages or provider pages remain beyond the fetched window'
197197
)
198-
expect(isSlackV2SubBlockVisible('channelMaxPages', values)).toBe(true)
198+
expect(SlackV2Block.subBlocks.some((subBlock) => subBlock.id === 'channelMaxPages')).toBe(false)
199199
expect(isSlackV2SubBlockVisible('paginationCursor', values)).toBe(true)
200200
expect(
201201
mapSlackV2Params({
@@ -206,15 +206,12 @@ describe('Slack block release', () => {
206206
})
207207
).toMatchObject({
208208
limit: 50,
209-
maxPages: 4,
210209
cursor: 'cursor-1',
211210
})
212211
expect(() => mapSlackV2Params({ ...values, channelLimit: '201' })).toThrow(
213212
'Conversations per page must be an integer between 1 and 200'
214213
)
215-
expect(() => mapSlackV2Params({ ...values, channelMaxPages: '201' })).toThrow(
216-
'Max pages must be an integer between 1 and 200'
217-
)
214+
expect(mapSlackV2Params({ ...values, channelMaxPages: '200' })).not.toHaveProperty('maxPages')
218215
expect(mapSlackV2Params({ ...values, channelLimit: null, channelMaxPages: ' ' })).toMatchObject(
219216
{ limit: 100 }
220217
)

apps/sim/blocks/blocks/slack.ts

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -771,17 +771,6 @@ Do not include any explanations, markdown formatting, or other text outside the
771771
},
772772
mode: 'advanced',
773773
},
774-
{
775-
id: 'channelMaxPages',
776-
title: 'Max Pages',
777-
type: 'short-input',
778-
placeholder: '200',
779-
condition: {
780-
field: 'operation',
781-
value: 'list_channels',
782-
},
783-
mode: 'advanced',
784-
},
785774
// List Members specific fields
786775
{
787776
id: 'memberLimit',
@@ -823,7 +812,7 @@ Do not include any explanations, markdown formatting, or other text outside the
823812
id: 'paginationCursor',
824813
title: 'Pagination Cursor',
825814
type: 'short-input',
826-
placeholder: 'next_cursor from a previous response',
815+
placeholder: 'nextCursor from a previous response',
827816
condition: {
828817
field: 'operation',
829818
value: ['list_channels', 'list_members', 'list_users'],
@@ -1923,7 +1912,6 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
19231912
emojiName,
19241913
includePrivate,
19251914
channelLimit,
1926-
channelMaxPages,
19271915
memberLimit,
19281916
includeDeleted,
19291917
userLimit,
@@ -2149,17 +2137,6 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
21492137
throw new Error('Conversations per page must be an integer between 1 and 200')
21502138
}
21512139
baseParams.limit = parsedLimit
2152-
const hasChannelMaxPages =
2153-
channelMaxPages !== undefined &&
2154-
channelMaxPages !== null &&
2155-
(typeof channelMaxPages !== 'string' || Boolean(channelMaxPages.trim()))
2156-
if (hasChannelMaxPages) {
2157-
const parsedMaxPages = Number(channelMaxPages)
2158-
if (!Number.isInteger(parsedMaxPages) || parsedMaxPages < 1 || parsedMaxPages > 200) {
2159-
throw new Error('Max pages must be an integer between 1 and 200')
2160-
}
2161-
baseParams.maxPages = parsedMaxPages
2162-
}
21632140
if (paginationCursor) {
21642141
baseParams.cursor = String(paginationCursor).trim()
21652142
}
@@ -2426,7 +2403,6 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
24262403
// List Channels inputs
24272404
includePrivate: { type: 'string', description: 'Include private channels (true/false)' },
24282405
channelLimit: { type: 'string', description: 'Conversations to request per Slack page' },
2429-
channelMaxPages: { type: 'string', description: 'Maximum Slack pages to fetch (max 200)' },
24302406
// List Members inputs
24312407
memberLimit: { type: 'string', description: 'Maximum number of members to return' },
24322408
// List Users inputs
@@ -2435,7 +2411,7 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
24352411
// Shared pagination input
24362412
paginationCursor: {
24372413
type: 'string',
2438-
description: 'Pagination cursor (next_cursor) for list_channels/list_members/list_users',
2414+
description: 'Pagination cursor (nextCursor) for list_channels/list_members/list_users',
24392415
},
24402416
// Ephemeral message inputs
24412417
ephemeralUser: { type: 'string', description: 'User ID who will see the ephemeral message' },
@@ -2657,7 +2633,7 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
26572633
channels: {
26582634
type: 'json',
26592635
description:
2660-
'Array of up to 10,000 accessible public and private channel objects, including conversation type and membership fields.',
2636+
'One page of accessible public and private channel objects, including conversation type and membership fields.',
26612637
},
26622638
count: {
26632639
type: 'number',

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const INPUTS = {
4545
},
4646
slack_delete_message: { accessToken: 'token', channel: 'C1', timestamp: '1.0' },
4747
slack_download: { accessToken: 'token', fileId: 'F1', fileName: 'report.pdf' },
48-
slack_list_channels: { accessToken: 'token', limit: 100, maxPages: 10 },
48+
slack_list_channels: { accessToken: 'token', limit: 100, cursor: 'cursor-1' },
4949
slack_ephemeral_message: {
5050
accessToken: 'token',
5151
channel: 'C1',

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

Lines changed: 25 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import type { InternalToolOperationImplementation } from '@/lib/internal/tool-op
55
import {
66
DEFAULT_CONVERSATION_PAGE_LIMIT,
77
MAX_CONVERSATION_PAGE_LIMIT,
8-
MAX_CONVERSATION_PAGES,
9-
MAX_CONVERSATIONS,
108
} from '@/tools/slack/list_channels'
119
import type { SlackListChannelsParams, SlackListChannelsResponse } from '@/tools/slack/types'
1210
import {
@@ -115,51 +113,33 @@ export const executeSlackListConversationsOperation: InternalToolOperationImplem
115113
'Conversation page size',
116114
MAX_CONVERSATION_PAGE_LIMIT
117115
)
118-
const maxPages = resolveBoundedInteger(
119-
params.maxPages,
120-
MAX_CONVERSATION_PAGES,
121-
'Maximum conversation pages',
122-
MAX_CONVERSATION_PAGES
123-
)
124-
let cursor =
116+
const cursor =
125117
params.cursor === undefined ? undefined : requireSlackString(params.cursor, 'Pagination cursor')
126-
const seenCursors = new Set(cursor ? [cursor] : [])
127-
const channels: SlackConversation[] = []
128-
let nextCursor: string | null = null
129-
let pages = 0
130-
131-
while (pages < maxPages && channels.length < MAX_CONVERSATIONS) {
132-
const pageLimit = Math.min(limit, MAX_CONVERSATIONS - channels.length)
133-
const { data } = await requestSlackApi({
134-
accessToken,
135-
method: 'conversations.list',
136-
httpMethod: 'GET',
137-
query: {
138-
types,
139-
exclude_archived: String(excludeArchived),
140-
limit: pageLimit,
141-
cursor,
142-
},
143-
signal,
144-
})
145-
const parsed = slackListConversationsResponseSchema.parse(data)
146-
assertSlackApiSuccess(parsed, 'Failed to list conversations from Slack')
147-
if (!parsed.channels) {
148-
throw new Error('Slack returned a malformed conversations list')
149-
}
150-
if (parsed.channels.length > pageLimit) {
151-
throw new Error(`Slack returned more than the requested ${pageLimit} conversations`)
152-
}
118+
const { data } = await requestSlackApi({
119+
accessToken,
120+
method: 'conversations.list',
121+
httpMethod: 'GET',
122+
query: {
123+
types,
124+
exclude_archived: String(excludeArchived),
125+
limit,
126+
cursor,
127+
},
128+
signal,
129+
})
130+
const parsed = slackListConversationsResponseSchema.parse(data)
131+
assertSlackApiSuccess(parsed, 'Failed to list conversations from Slack')
132+
if (!parsed.channels) {
133+
throw new Error('Slack returned a malformed conversations list')
134+
}
135+
if (parsed.channels.length > limit) {
136+
throw new Error(`Slack returned more than the requested ${limit} conversations`)
137+
}
153138

154-
channels.push(...parsed.channels.map(mapSlackConversation))
155-
pages += 1
156-
nextCursor = parsed.response_metadata?.next_cursor?.trim() || null
157-
if (!nextCursor) break
158-
if (seenCursors.has(nextCursor)) {
159-
throw new Error('Slack returned a repeated conversation pagination cursor')
160-
}
161-
seenCursors.add(nextCursor)
162-
cursor = nextCursor
139+
const channels = parsed.channels.map(mapSlackConversation)
140+
const nextCursor = parsed.response_metadata?.next_cursor?.trim() || null
141+
if (nextCursor && nextCursor === cursor) {
142+
throw new Error('Slack returned a repeated conversation pagination cursor')
163143
}
164144

165145
return {
@@ -173,7 +153,6 @@ export const executeSlackListConversationsOperation: InternalToolOperationImplem
173153
count: channels.length,
174154
hasMore: Boolean(nextCursor),
175155
nextCursor,
176-
pages,
177156
},
178157
}
179158
}

apps/sim/lib/workflows/migrations/subblock-migrations.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,34 @@ describe('migrateSubblockIds', () => {
177177
expect(blocks.b1.subBlocks.metrics).toBeUndefined()
178178
})
179179

180+
it.each(['slack', 'slack_v2'])(
181+
'removes the retired channel page cap from %s while preserving pagination inputs',
182+
(type) => {
183+
const input = {
184+
b1: makeBlock({
185+
type,
186+
subBlocks: {
187+
operation: { id: 'operation', type: 'dropdown', value: 'list_channels' },
188+
channelMaxPages: { id: 'channelMaxPages', type: 'short-input', value: '200' },
189+
channelLimit: { id: 'channelLimit', type: 'short-input', value: '50' },
190+
paginationCursor: { id: 'paginationCursor', type: 'short-input', value: 'cursor-2' },
191+
historyMaxPages: { id: 'historyMaxPages', type: 'short-input', value: '10' },
192+
},
193+
}),
194+
}
195+
196+
const { blocks, migrated } = migrateSubblockIds(input)
197+
198+
expect(migrated).toBe(true)
199+
expect(blocks.b1.subBlocks).not.toHaveProperty('channelMaxPages')
200+
expect(blocks.b1.subBlocks).not.toHaveProperty('_removed_channelMaxPages')
201+
expect(blocks.b1.subBlocks.channelLimit.value).toBe('50')
202+
expect(blocks.b1.subBlocks.paginationCursor.value).toBe('cursor-2')
203+
expect(blocks.b1.subBlocks.historyMaxPages.value).toBe('10')
204+
expect(migrateSubblockIds(blocks).migrated).toBe(false)
205+
}
206+
)
207+
180208
describe('snowflake block', () => {
181209
it('renames the object fields onto their advanced text inputs', () => {
182210
const input: Record<string, BlockState> = {

apps/sim/lib/workflows/migrations/subblock-migrations.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ function isFieldProjection(value: unknown): boolean {
111111
* secret onto a live subblock.
112112
*/
113113
export const SUBBLOCK_ID_MIGRATIONS: Record<string, readonly SubblockIdMigration[]> = {
114+
/** List Channels now returns one page and a cursor; automatic page limits are retired. */
115+
slack: [{ from: 'channelMaxPages', to: '_removed_channelMaxPages' }],
116+
slack_v2: [{ from: 'channelMaxPages', to: '_removed_channelMaxPages' }],
114117
instagram: [{ from: 'metrics', to: 'insightMetrics' }],
115118
knowledge: [{ from: 'knowledgeBaseId', to: 'knowledgeBaseSelector' }],
116119
/** Connected accounts resolve from the workspace; group selectors have no replacement. */

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.

0 commit comments

Comments
 (0)