diff --git a/apps/docs/content/docs/integrations/slack.mdx b/apps/docs/content/docs/integrations/slack.mdx index 56af83e283d..3bb636602ae 100644 --- a/apps/docs/content/docs/integrations/slack.mdx +++ b/apps/docs/content/docs/integrations/slack.mdx @@ -1,6 +1,6 @@ --- title: Slack -description: Send, update, delete messages, manage views and modals, add or remove reactions, manage canvases, get channel info and user presence in Slack +description: Send and manage Slack messages, Agent Sessions, streamed replies, views, reactions, conversations, and canvases --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -24,9 +24,23 @@ With the Slack integration in Sim, you can: - **Read messages**: Retrieve recent messages from channels or DMs, with filtering by time range - **Manage channels and users**: List channels, members, and users in your Slack workspace - **Download files**: Retrieve files shared in Slack channels for processing within a workflow +- **Build Slack agents**: Manage Agent Sessions, stream replies, handle stop/title/context events, and configure Agent View actions and suggested prompts for custom bots In Sim, the Slack integration enables your agents to programmatically interact with Slack as part of their workflows. This allows for automation scenarios such as sending notifications with dynamic updates, managing conversational flows with editable status messages, acknowledging important messages with reactions, and maintaining clean channels by removing outdated bot messages. The integration can also be used in trigger mode to start a workflow when a message is sent to a channel. +## Stream Trigger Responses + +Custom-bot Slack triggers can stream workflow outputs directly back into the conversation that started a run. Enable **Stream response to Slack** on a Message, App Mention, or Assistant Thread Started trigger, then select the outputs to deliver. + +- A selected Agent output streams immediately as it is generated. If the Agent later calls a tool, any pre-tool commentary already streamed remains visible. +- A selected non-streaming block output is sent when that block invocation completes. +- Loop and parallel invocations each create their own Slack response. +- The response status label defaults to `Running` and can be customized in the trigger's advanced settings. +- Optional thinking and tool-call updates appear as Slack tasks in a timeline or plan. +- Slack Agent Sessions remain in processing state for the run, return to active when it finishes, and the native Slack stop button cancels active workflow executions. + +Automatic trigger responses require a custom bot created by the Slack setup wizard. They are not available with the shared Sim Slack app. + ## AI-Generated Content Sim workflows may use AI models to generate messages and responses sent to Slack. AI-generated content may be inaccurate or contain errors. Always review automated outputs, especially for critical communications. @@ -39,7 +53,7 @@ If you encounter issues with the Slack integration, contact us at [help@sim.ai]( ## Usage Instructions -Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, open/update/push modal views, publish Home tab views, create canvases, read messages, and add or remove reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel. +Integrate Slack messaging and administration into a workflow. Custom Slack bots can manage Agent Sessions, stream incremental Markdown or structured chunks, react to Agent Session events, and configure Agent View suggested prompts. Standard messaging and management operations support both the Sim app and custom bot credentials. @@ -802,9 +816,9 @@ Get a stable permalink URL to a specific Slack message. | `channel` | string | Channel ID containing the message | | `permalink` | string | The permalink URL to the message | -### Slack Set Assistant Status +### Slack Set Suggested Prompts -Set or clear the assistant thread status indicator (the loading shimmer) on a Slack AI app thread. Pass an empty status to clear it. +Set the clickable suggested prompts shown in a Slack assistant thread (the prompt chips in an AI app). #### Input @@ -814,61 +828,86 @@ Set or clear the assistant thread status indicator (the loading shimmer) on a Sl | `botToken` | string | No | Bot token for Custom Bot | | `channel` | string | Yes | Channel ID containing the assistant thread \(e.g., C1234567890 or D1234567890\) | | `threadTs` | string | Yes | Thread timestamp \(thread_ts\) of the assistant thread \(e.g., 1405894322.002768\) | -| `status` | string | No | Status text to display, e.g. 'Working on it…'. Omit or pass an empty string to clear the status. | -| `loadingMessages` | json | No | Optional list of messages to rotate through as an animated loading indicator \(max 10\). | +| `prompts` | json | Yes | Array of prompts, each with a "title" \(shown on the chip\) and a "message" \(sent when clicked\). Max 4. | +| `promptsTitle` | string | No | Optional heading for the prompt list, e.g. 'Suggested Prompts' | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ok` | boolean | Whether the status was set successfully | -| `channel` | string | Channel ID the status was set on | -| `threadTs` | string | Thread timestamp the status was set on | +| `ok` | boolean | Whether the suggested prompts were set successfully | +| `channel` | string | Channel ID the prompts were set on | +| `threadTs` | string | Thread timestamp the prompts were set on | -### Slack Set Assistant Title +### Slack Set Agent Suggested Prompts -Set the title of a Slack assistant thread (shown in the AI app thread header). +Set suggested prompts in Slack Agent View, optionally scoped to a specific thread. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authMethod` | string | No | Authentication method: oauth or bot_token | -| `botToken` | string | No | Bot token for Custom Bot | -| `channel` | string | Yes | Channel ID containing the assistant thread \(e.g., C1234567890 or D1234567890\) | -| `threadTs` | string | Yes | Thread timestamp \(thread_ts\) of the assistant thread \(e.g., 1405894322.002768\) | -| `title` | string | Yes | The title to display for the assistant thread | +| `authMethod` | string | No | Slack authentication method | +| `botToken` | string | No | Custom Slack bot token | +| `channel` | string | Yes | Agent direct-message channel ID | +| `threadTs` | string | No | Optional thread timestamp for legacy thread-scoped prompts | +| `prompts` | json | Yes | One to four prompt objects with title and message fields | +| `promptsTitle` | string | No | Optional heading displayed above the prompt chips | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ok` | boolean | Whether the title was set successfully | -| `channel` | string | Channel ID the title was set on | -| `threadTs` | string | Thread timestamp the title was set on | +| `ok` | boolean | Whether Slack updated the suggested prompts | -### Slack Set Suggested Prompts +### Slack Set Agent Session Status -Set the clickable suggested prompts shown in a Slack assistant thread (the prompt chips in an AI app). +Create or update the state of a Slack agent session associated with a thread. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authMethod` | string | No | Authentication method: oauth or bot_token | -| `botToken` | string | No | Bot token for Custom Bot | -| `channel` | string | Yes | Channel ID containing the assistant thread \(e.g., C1234567890 or D1234567890\) | -| `threadTs` | string | Yes | Thread timestamp \(thread_ts\) of the assistant thread \(e.g., 1405894322.002768\) | -| `prompts` | json | Yes | Array of prompts, each with a "title" \(shown on the chip\) and a "message" \(sent when clicked\). Max 4. | -| `promptsTitle` | string | No | Optional heading for the prompt list, e.g. 'Suggested Prompts' | +| `authMethod` | string | No | Slack authentication method | +| `botToken` | string | No | Custom Slack bot token | +| `channel` | string | Yes | Channel ID containing the agent session thread | +| `threadTs` | string | Yes | Timestamp of the thread associated with the agent session | +| `status` | string | Yes | Agent session state: active, processing, suspended, or closed | +| `title` | string | No | Title used when creating the agent session, up to 200 characters | +| `initiatorUserId` | string | No | Slack user ID that initiated the session | +| `iconEmoji` | string | No | Emoji used to customize the agent identity | +| `iconUrl` | string | No | Image URL used to customize the agent identity | +| `username` | string | No | Display name used to customize the agent identity | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ok` | boolean | Whether the suggested prompts were set successfully | -| `channel` | string | Channel ID the prompts were set on | -| `threadTs` | string | Thread timestamp the prompts were set on | +| `ok` | boolean | Whether Slack updated the agent session | +| `status` | string | Requested agent session status | +| `agentStatus` | string | Agent status recorded by Slack | +| `title` | string | Current agent session title, or null when the session has no title | + +### Slack Rename Agent Session + +Rename the Slack agent session associated with a thread. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Slack authentication method | +| `botToken` | string | No | Custom Slack bot token | +| `channel` | string | Yes | Channel ID containing the agent session thread | +| `threadTs` | string | Yes | Timestamp of the thread associated with the agent session | +| `title` | string | Yes | New agent session title, from 1 to 200 characters | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ok` | boolean | Whether Slack renamed the agent session | +| `title` | string | Updated agent session title | ### Slack List Channels @@ -1873,7 +1912,7 @@ A **Trigger** is a block that starts a workflow when an event happens in this se ### Slack -Trigger from Slack events (mentions, messages, reactions) +Trigger from Slack events, interactions, and slash commands #### Configuration @@ -1886,9 +1925,16 @@ Trigger from Slack events (mentions, messages, reactions) | `channelFilter` | channel-selector | No | Restrict to specific channels. Leave empty to trigger on any channel the bot has been added to. | | `manualChannelFilter` | string | No | Comma-separated channel IDs to restrict to. Set IDs directly here. | | `threads` | string | No | Include thread replies, exclude them \(top-level only\), or fire only on thread replies. | +| `streamResponse` | boolean | No | Create a Slack agent session and stream selected workflow outputs into the conversation that started this run. Custom bots only. | +| `streamOutputs` | workflow-output-selector | No | Each selected block invocation creates its own Slack response. Agent outputs stream live; other outputs are sent when the block completes. | +| `streamTaskTitle` | string | No | Optional status Slack shows while each selected response is being produced. Leave empty to use Running. | +| `streamTaskDisplayMode` | string | No | Choose how Slack displays thinking and tool progress. | +| `streamIncludeThinking` | boolean | No | Show agent thinking as Slack task updates while the response is generated. | +| `streamIncludeToolCalls` | boolean | No | Show tool execution lifecycle as Slack task updates. | | `emoji` | string | No | Comma-separated emoji names to restrict to. Leave empty to match any emoji. | | `nameContains` | string | No | Only fire when the created channel name contains this text. | | `interactionFilter` | string | No | Comma-separated action_ids \(buttons/selects\) or callback_ids \(modals\) to restrict to. Leave empty to fire on any interaction. | +| `commandFilter` | string | No | Restrict this trigger to one slash command. Leave empty to fire for every command configured on the bot. | | `filterBotMessages` | boolean | No | Ignore messages sent by other bots. This app's own output is always ignored. | | `includeOwnMessages` | boolean | No | Also fire on this app's own messages and reactions. Can cause loops — use with care. | | `includeFiles` | boolean | No | Download and include file attachments from messages. Requires files:read. | @@ -1909,7 +1955,14 @@ Trigger from Slack events (mentions, messages, reactions) | ↳ `text` | string | Message text content. For slash commands, the text after the command. For interactivity, the source message text \(falls back to the triggering action value\) | | ↳ `timestamp` | string | Message timestamp from the triggering event | | ↳ `thread_ts` | string | Parent thread timestamp \(if message is in a thread\) | +| ↳ `streaming_message_ts` | array | Message timestamps streamed during a stopped agent session | +| ↳ `title` | string | Current agent session title | +| ↳ `previous_title` | string | Previous agent session title | +| ↳ `tab` | string | App Home tab that was opened, including messages for Agent View | +| ↳ `context` | json | Current Agent View context. Normalized from context on app_context_changed/app_home_opened or app_context on message.im | | ↳ `team_id` | string | Slack workspace/team ID | +| ↳ `user_team_id` | string | Slack workspace/team ID of the user who triggered the event. Used for Slack Connect response streaming. | +| ↳ `enterprise_id` | string | Slack Enterprise Grid organization ID | | ↳ `event_id` | string | Unique event identifier | | ↳ `reaction` | string | Emoji reaction name \(e.g., thumbsup\). Present for reaction_added/reaction_removed events | | ↳ `item_user` | string | User ID of the original message author. Present for reaction_added/reaction_removed events | diff --git a/apps/sim/app/api/webhooks/slack/custom/[credentialId]/route.ts b/apps/sim/app/api/webhooks/slack/custom/[credentialId]/route.ts index 4570994e060..af68ba1c6a5 100644 --- a/apps/sim/app/api/webhooks/slack/custom/[credentialId]/route.ts +++ b/apps/sim/app/api/webhooks/slack/custom/[credentialId]/route.ts @@ -6,6 +6,7 @@ import { parseWebhookBody } from '@/lib/webhooks/processor' import { handleSlackChallenge } from '@/lib/webhooks/providers/slack' import { dispatchSlackCustomBotCredential, + handleSlackAgentSessionStopped, verifySlackCustomBotCredentialRequest, } from '@/lib/webhooks/slack-custom-ingress' import { getSlackDispatchResponse } from '@/lib/webhooks/slack-dispatch' @@ -68,12 +69,15 @@ async function handleSlackCustomBotWebhook( return authError } - const dispatchResults = await dispatchSlackCustomBotCredential({ - credentialId, - body, - request, - requestId, - receivedAt, - }) + const [, dispatchResults] = await Promise.all([ + handleSlackAgentSessionStopped(credentialId, body), + dispatchSlackCustomBotCredential({ + credentialId, + body, + request, + requestId, + receivedAt, + }), + ]) return getSlackDispatchResponse(dispatchResults) } diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx index cf0a76bcac1..fc42525d229 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx @@ -3,6 +3,7 @@ import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button, + Chip, ChipDropdown, type ChipDropdownOption, ChipInput, @@ -12,7 +13,7 @@ import { SecretInput, Wizard, } from '@sim/emcn' -import { Loader } from '@sim/emcn/icons' +import { Loader, Plus, Trash } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' @@ -28,6 +29,7 @@ import { getSlackManagedUserAuthorizationManifestConfig, SLACK_CAPABILITIES, SLACK_MANAGED_USER_AUTHORIZATION_CAPABILITY, + type SlackSlashCommand, } from '@/triggers/slack/capabilities' import { buildSlackCustomBotRequestUrl } from '@/triggers/webhook-url' @@ -49,6 +51,38 @@ const CAPABILITY_OPTIONS: ChipDropdownOption[] = CUSTOM_BOT_CAPABILITIES.map((ca label: capability.label, })) +interface SlackSlashCommandDraft extends SlackSlashCommand { + id: string +} + +function getSlashCommandsError(commands: readonly SlackSlashCommandDraft[]): string | null { + if (commands.some((entry) => !entry.command.trim() || !entry.description.trim())) { + return 'Every slash command needs a command and description.' + } + if ( + commands.some((entry) => { + const command = entry.command.trim() + return !command.startsWith('/') || command.length === 1 || /\s/.test(command) + }) + ) { + return 'Slash commands must be one word beginning with /.' + } + if (commands.some((entry) => entry.command.trim().length > 32)) { + return 'Slash commands must be 32 characters or fewer.' + } + const normalizedCommands = commands.map((entry) => entry.command.trim()) + if (new Set(normalizedCommands).size !== normalizedCommands.length) { + return 'Each slash command must be unique.' + } + return null +} + +function getAgentDescriptionError(description: string): string | null { + return description.trim().length > 300 + ? 'Slack Agent View descriptions must be 300 characters or fewer.' + : null +} + interface ConnectSlackBotModalProps { open: boolean onOpenChange: (open: boolean) => void @@ -91,6 +125,7 @@ export function ConnectSlackBotModal({ const [appName, setAppName] = useState(initialDisplayName ?? '') const [appDescription, setAppDescription] = useState(initialDescription ?? '') const [selected, setSelected] = useState>(() => new Set(ALL_CAPABILITIES)) + const [slashCommands, setSlashCommands] = useState([]) const [signingSecret, setSigningSecret] = useState('') const [botToken, setBotToken] = useState('') const [createError, setCreateError] = useState(null) @@ -105,6 +140,7 @@ export function ConnectSlackBotModal({ setAppName(initialDisplayName ?? '') setAppDescription(initialDescription ?? '') setSelected(new Set(ALL_CAPABILITIES)) + setSlashCommands([]) setSigningSecret('') setBotToken('') setCreateError(null) @@ -124,7 +160,12 @@ export function ConnectSlackBotModal({ // window.location.origin) so Slack's servers can reach it. const requestUrl = useMemo(() => buildSlackCustomBotRequestUrl(credentialId), [credentialId]) + const descriptionError = getAgentDescriptionError(appDescription) + const slashCommandsError = getSlashCommandsError(slashCommands) + const manifestConfigurationError = descriptionError ?? slashCommandsError + const manifestJson = useMemo(() => { + if (manifestConfigurationError) return '' const managedUserAuthorization = selected.has(SLACK_MANAGED_USER_AUTHORIZATION_CAPABILITY.id) ? getSlackManagedUserAuthorizationManifestConfig(getBaseUrl()) : undefined @@ -132,10 +173,15 @@ export function ConnectSlackBotModal({ appName: appName.trim() || DEFAULT_APP_NAME, webhookUrl: requestUrl, description: appDescription, + slashCommands: slashCommands.map(({ command, description, usageHint }) => ({ + command, + description, + usageHint, + })), ...(managedUserAuthorization ? { managedUserAuthorization } : {}), }) return JSON.stringify(manifest, null, 2) - }, [selected, appName, appDescription, requestUrl]) + }, [manifestConfigurationError, selected, appName, appDescription, slashCommands, requestUrl]) const capabilityIds = useMemo(() => [...selected], [selected]) const setCapabilityIds = useCallback((next: string[]) => setSelected(new Set(next)), []) @@ -214,12 +260,19 @@ export function ConnectSlackBotModal({ {/* Bot name is required so the credential name, the manifest app name, and uniqueness all use the user's choice — never the shared Slack team name fallback, which collides for a second bot in the same workspace. */} - 0}> + 0 && !descriptionError && !slashCommandsError} + > @@ -269,6 +322,10 @@ interface StepConfigureProps { onAppNameChange: (next: string) => void appDescription: string onAppDescriptionChange: (next: string) => void + descriptionError: string | null + slashCommands: readonly SlackSlashCommandDraft[] + onSlashCommandsChange: (commands: SlackSlashCommandDraft[]) => void + slashCommandsError: string | null capabilityIds: string[] onCapabilityIdsChange: (next: string[]) => void } @@ -277,6 +334,10 @@ function StepConfigure({ onAppNameChange, appDescription, onAppDescriptionChange, + descriptionError, + slashCommands, + onSlashCommandsChange, + slashCommandsError, capabilityIds, onCapabilityIdsChange, }: StepConfigureProps) { @@ -305,26 +366,117 @@ function StepConfigure({ onChange={(e) => onAppDescriptionChange(e.target.value)} placeholder="Optional — shown on the bot's Slack profile" maxLength={140} + error={Boolean(descriptionError)} /> + {descriptionError && ( +

{descriptionError}

+ )}
- + {allSelected && (

- Full access — the bot can read and send messages, react, upload files, and chat as an AI - assistant, and people can authorize it through Credential Groups. + All additional permissions enabled — the bot can read messages, react, access files and + users, and people can authorize it through Credential Groups.

)}
+ + + ) +} + +interface SlashCommandsEditorProps { + commands: readonly SlackSlashCommandDraft[] + onChange: (commands: SlackSlashCommandDraft[]) => void + error: string | null +} + +function SlashCommandsEditor({ commands, onChange, error }: SlashCommandsEditorProps) { + const addCommand = () => { + onChange([...commands, { id: generateId(), command: '', description: '', usageHint: '' }]) + } + const updateCommand = ( + id: string, + field: keyof Pick, + value: string + ) => { + onChange(commands.map((entry) => (entry.id === id ? { ...entry, [field]: value } : entry))) + } + const removeCommand = (id: string) => { + onChange(commands.filter((entry) => entry.id !== id)) + } + + return ( +
+
+ + = 50} + > + Add + +
+ {commands.length > 0 && ( +
+ {commands.map((entry, index) => ( +
+
+ updateCommand(entry.id, 'command', event.target.value)} + placeholder='/ask-sim' + maxLength={32} + inputClassName='font-mono' + aria-label={`Slash command ${index + 1}`} + /> + updateCommand(entry.id, 'description', event.target.value)} + placeholder='Short description shown in Slack' + maxLength={2000} + aria-label={`Slash command ${index + 1} description`} + /> + updateCommand(entry.id, 'usageHint', event.target.value)} + placeholder='Usage hint (optional), e.g. question or task' + maxLength={1000} + aria-label={`Slash command ${index + 1} usage hint`} + /> +
+ +
+ ))} +
+ )} + {error &&

{error}

}
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts index 981ba63f9b1..362fc0c79d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts @@ -38,4 +38,5 @@ export { Text } from './text' export { TimeInput } from './time-input' export { ToolInput } from './tool-input' export { VariablesInput } from './variables-input' +export { WorkflowOutputSelector } from './workflow-output-selector' export { WorkflowSelectorInput } from './workflow-selector' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx index c30d2b70382..bc68f4d7ff6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx @@ -213,10 +213,6 @@ function StepConfigure({ }: StepConfigureProps) { return (
-

- Pick a name and choose what events should trigger your workflow and what actions your bot - can take. -