Skip to content
Merged
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
10 changes: 4 additions & 6 deletions apps/docs/content/docs/integrations/slack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,7 @@ Rename the Slack agent session associated with a thread.

### Slack List Channels

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

#### Input

Expand All @@ -951,13 +951,12 @@ List up to 10,000 accessible public and private Slack channels across as many cu
| `excludeArchived` | boolean | No | Exclude archived channels \(default: true\) |
| `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: 200, max: 200\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `channels` | array | Up to 10,000 accessible public and private channels |
| `channels` | array | One page of accessible public and private channels |
| ↳ `id` | string | Conversation ID \(for example, C123, D123, or G123\) |
| ↳ `name` | string | Channel or group-DM name; omitted for one-to-one direct messages |
| ↳ `is_channel` | boolean | Whether this is a channel |
Expand All @@ -983,10 +982,9 @@ List up to 10,000 accessible public and private Slack channels across as many cu
| ↳ `priority` | number | Slack sidebar sort priority |
| `ids` | array | Conversation IDs for every returned channel |
| `names` | array | Names of returned channels |
| `count` | number | Total number of conversations returned across all fetched pages, up to 10,000 |
| `hasMore` | boolean | Whether more Slack conversation pages remain beyond the fetched window |
| `count` | number | Number of conversations returned in this page |
| `hasMore` | boolean | Whether a next cursor is available to fetch more Slack conversations |
| `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
9 changes: 3 additions & 6 deletions apps/sim/blocks/blocks/slack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,12 @@ describe('Slack block release', () => {
expect(selectTool(repurposedValues)).toBe('slack_set_suggested_prompts_v2')
})

it('maps bounded cursor pagination for list channels', () => {
it('maps a single page and cursor 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(SlackV2Block.subBlocks.some((subBlock) => subBlock.id === 'channelMaxPages')).toBe(false)
expect(isSlackV2SubBlockVisible('paginationCursor', values)).toBe(true)
expect(
mapSlackV2Params({
Expand All @@ -206,15 +206,12 @@ describe('Slack block release', () => {
})
).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: '201' })).toThrow(
'Max pages must be an integer between 1 and 200'
)
expect(mapSlackV2Params({ ...values, channelMaxPages: '200' })).not.toHaveProperty('maxPages')
expect(mapSlackV2Params({ ...values, channelLimit: null, channelMaxPages: ' ' })).toMatchObject(
{ limit: 100 }
)
Expand Down
30 changes: 3 additions & 27 deletions apps/sim/blocks/blocks/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,17 +771,6 @@ Do not include any explanations, markdown formatting, or other text outside the
},
mode: 'advanced',
},
{
id: 'channelMaxPages',
title: 'Max Pages',
type: 'short-input',
placeholder: '200',
condition: {
field: 'operation',
value: 'list_channels',
},
mode: 'advanced',
},
// List Members specific fields
{
id: 'memberLimit',
Expand Down Expand Up @@ -823,7 +812,7 @@ Do not include any explanations, markdown formatting, or other text outside the
id: 'paginationCursor',
title: 'Pagination Cursor',
type: 'short-input',
placeholder: 'next_cursor from a previous response',
placeholder: 'nextCursor from a previous response',
condition: {
field: 'operation',
value: ['list_channels', 'list_members', 'list_users'],
Expand Down Expand Up @@ -1923,7 +1912,6 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
emojiName,
includePrivate,
channelLimit,
channelMaxPages,
memberLimit,
includeDeleted,
userLimit,
Expand Down Expand Up @@ -2149,17 +2137,6 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
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 > 200) {
throw new Error('Max pages must be an integer between 1 and 200')
}
baseParams.maxPages = parsedMaxPages
}
if (paginationCursor) {
baseParams.cursor = String(paginationCursor).trim()
}
Expand Down Expand Up @@ -2426,7 +2403,6 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
// List Channels inputs
includePrivate: { type: 'string', description: 'Include private channels (true/false)' },
channelLimit: { type: 'string', description: 'Conversations to request per Slack page' },
channelMaxPages: { type: 'string', description: 'Maximum Slack pages to fetch (max 200)' },
// List Members inputs
memberLimit: { type: 'string', description: 'Maximum number of members to return' },
// List Users inputs
Expand All @@ -2435,7 +2411,7 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
// Shared pagination input
paginationCursor: {
type: 'string',
description: 'Pagination cursor (next_cursor) for list_channels/list_members/list_users',
description: 'Pagination cursor (nextCursor) for list_channels/list_members/list_users',
},
// Ephemeral message inputs
ephemeralUser: { type: 'string', description: 'User ID who will see the ephemeral message' },
Expand Down Expand Up @@ -2657,7 +2633,7 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
channels: {
type: 'json',
description:
'Array of up to 10,000 accessible public and private channel objects, including conversation type and membership fields.',
'One page of accessible public and private channel objects, including conversation type and membership fields.',
},
count: {
type: 'number',
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/internal/slack/execute-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +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_list_channels: { accessToken: 'token', limit: 100, cursor: 'cursor-1' },
slack_ephemeral_message: {
accessToken: 'token',
channel: 'C1',
Expand Down
71 changes: 25 additions & 46 deletions apps/sim/lib/internal/slack/operations/list-conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import type { InternalToolOperationImplementation } from '@/lib/internal/tool-op
import {
DEFAULT_CONVERSATION_PAGE_LIMIT,
MAX_CONVERSATION_PAGE_LIMIT,
MAX_CONVERSATION_PAGES,
MAX_CONVERSATIONS,
} from '@/tools/slack/list_channels'
import type { SlackListChannelsParams, SlackListChannelsResponse } from '@/tools/slack/types'
import {
Expand Down Expand Up @@ -115,51 +113,33 @@ export const executeSlackListConversationsOperation: InternalToolOperationImplem
'Conversation page size',
MAX_CONVERSATION_PAGE_LIMIT
)
const maxPages = resolveBoundedInteger(
params.maxPages,
MAX_CONVERSATION_PAGES,
'Maximum conversation pages',
MAX_CONVERSATION_PAGES
)
let cursor =
const cursor =
params.cursor === undefined ? undefined : requireSlackString(params.cursor, 'Pagination cursor')
const seenCursors = new Set(cursor ? [cursor] : [])
const channels: SlackConversation[] = []
let nextCursor: string | null = null
let pages = 0

while (pages < maxPages && channels.length < MAX_CONVERSATIONS) {
const pageLimit = Math.min(limit, MAX_CONVERSATIONS - channels.length)
const { data } = await requestSlackApi({
accessToken,
method: 'conversations.list',
httpMethod: 'GET',
query: {
types,
exclude_archived: String(excludeArchived),
limit: pageLimit,
cursor,
},
signal,
})
const parsed = slackListConversationsResponseSchema.parse(data)
assertSlackApiSuccess(parsed, 'Failed to list conversations from Slack')
if (!parsed.channels) {
throw new Error('Slack returned a malformed conversations list')
}
if (parsed.channels.length > pageLimit) {
throw new Error(`Slack returned more than the requested ${pageLimit} conversations`)
}
const { data } = await requestSlackApi({
accessToken,
method: 'conversations.list',
httpMethod: 'GET',
query: {
types,
exclude_archived: String(excludeArchived),
limit,
cursor,
},
signal,
})
const parsed = slackListConversationsResponseSchema.parse(data)
assertSlackApiSuccess(parsed, 'Failed to list conversations from Slack')
if (!parsed.channels) {
throw new Error('Slack returned a malformed conversations list')
}
if (parsed.channels.length > limit) {
throw new Error(`Slack returned more than the requested ${limit} conversations`)
}

channels.push(...parsed.channels.map(mapSlackConversation))
pages += 1
nextCursor = parsed.response_metadata?.next_cursor?.trim() || null
if (!nextCursor) break
if (seenCursors.has(nextCursor)) {
throw new Error('Slack returned a repeated conversation pagination cursor')
}
seenCursors.add(nextCursor)
cursor = nextCursor
const channels = parsed.channels.map(mapSlackConversation)
const nextCursor = parsed.response_metadata?.next_cursor?.trim() || null
if (nextCursor && nextCursor === cursor) {
throw new Error('Slack returned a repeated conversation pagination cursor')
}

return {
Expand All @@ -173,7 +153,6 @@ export const executeSlackListConversationsOperation: InternalToolOperationImplem
count: channels.length,
hasMore: Boolean(nextCursor),
nextCursor,
pages,
},
}
}
28 changes: 28 additions & 0 deletions apps/sim/lib/workflows/migrations/subblock-migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,34 @@ describe('migrateSubblockIds', () => {
expect(blocks.b1.subBlocks.metrics).toBeUndefined()
})

it.each(['slack', 'slack_v2'])(
'removes the retired channel page cap from %s while preserving pagination inputs',
(type) => {
const input = {
b1: makeBlock({
type,
subBlocks: {
operation: { id: 'operation', type: 'dropdown', value: 'list_channels' },
channelMaxPages: { id: 'channelMaxPages', type: 'short-input', value: '200' },
channelLimit: { id: 'channelLimit', type: 'short-input', value: '50' },
paginationCursor: { id: 'paginationCursor', type: 'short-input', value: 'cursor-2' },
historyMaxPages: { id: 'historyMaxPages', type: 'short-input', value: '10' },
},
}),
}

const { blocks, migrated } = migrateSubblockIds(input)

expect(migrated).toBe(true)
expect(blocks.b1.subBlocks).not.toHaveProperty('channelMaxPages')
expect(blocks.b1.subBlocks).not.toHaveProperty('_removed_channelMaxPages')
expect(blocks.b1.subBlocks.channelLimit.value).toBe('50')
expect(blocks.b1.subBlocks.paginationCursor.value).toBe('cursor-2')
expect(blocks.b1.subBlocks.historyMaxPages.value).toBe('10')
expect(migrateSubblockIds(blocks).migrated).toBe(false)
}
)

describe('snowflake block', () => {
it('renames the object fields onto their advanced text inputs', () => {
const input: Record<string, BlockState> = {
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/lib/workflows/migrations/subblock-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ function isFieldProjection(value: unknown): boolean {
* secret onto a live subblock.
*/
export const SUBBLOCK_ID_MIGRATIONS: Record<string, readonly SubblockIdMigration[]> = {
/** List Channels now returns one page and a cursor; automatic page limits are retired. */
slack: [{ from: 'channelMaxPages', to: '_removed_channelMaxPages' }],
slack_v2: [{ from: 'channelMaxPages', to: '_removed_channelMaxPages' }],
instagram: [{ from: 'metrics', to: 'insightMetrics' }],
knowledge: [{ from: 'knowledgeBaseId', to: 'knowledgeBaseSelector' }],
/** Connected accounts resolve from the workspace; group selectors have no replacement. */
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/tools/generated/tool-metadata.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/sim/tools/generated/tool-outputs.ts

Large diffs are not rendered by default.

Loading
Loading