Skip to content

Commit 8d6199b

Browse files
feat(slack): stream trigger responses to agent sessions
1 parent 6c26a7f commit 8d6199b

27 files changed

Lines changed: 1740 additions & 35 deletions

File tree

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ With the Slack integration in Sim, you can:
2828

2929
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.
3030

31+
## Stream Trigger Responses
32+
33+
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.
34+
35+
- A selected Agent output streams as it is generated. Intermediate pre-tool turns are held back so only the final answer is appended.
36+
- A selected non-streaming block output is sent when that block invocation completes.
37+
- Loop and parallel invocations each create their own Slack response.
38+
- Optional thinking and tool-call updates appear as Slack tasks in a timeline or plan.
39+
- 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.
40+
41+
Automatic trigger responses require a custom bot created by the Slack setup wizard. They are not available with the shared Sim Slack app.
42+
3143
## AI-Generated Content
3244

3345
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.
@@ -1992,6 +2004,11 @@ Trigger from Slack events (mentions, messages, reactions)
19922004
| `eventType` | string | Yes | The single Slack event this trigger fires on. Add another trigger block for another event. |
19932005
| `customBotCredential` | string | Yes | Choose a custom Slack bot you set up once and reuse across triggers. |
19942006
| `manualBotCredential` | string | Yes | Set the custom bot credential ID directly. |
2007+
| `streamResponse` | boolean | No | Stream selected workflow outputs into the Slack conversation that started this run. Custom bots only. |
2008+
| `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. |
2009+
| `streamIncludeThinking` | boolean | No | Show agent thinking as Slack task updates while the response is generated. |
2010+
| `streamIncludeToolCalls` | boolean | No | Show tool execution lifecycle as Slack task updates. |
2011+
| `streamTaskDisplayMode` | string | No | Choose how Slack displays thinking and tool progress. |
19952012
| `source` | string | No | Restrict to direct messages, public channels, or private channels. Leave empty to match any. |
19962013
| `channelFilter` | channel-selector | No | Restrict to specific channels. Leave empty to trigger on any channel the bot has been added to. |
19972014
| `manualChannelFilter` | string | No | Comma-separated channel IDs to restrict to. Set IDs directly here. |
@@ -2025,6 +2042,7 @@ Trigger from Slack events (mentions, messages, reactions)
20252042
|`tab` | string | App Home tab that was opened, including messages for Agent View |
20262043
|`context` | json | Current Agent View context. Normalized from context on app_context_changed/app_home_opened or app_context on message.im |
20272044
|`team_id` | string | Slack workspace/team ID |
2045+
|`user_team_id` | string | Slack workspace/team ID of the user who triggered the event. Used for Slack Connect response streaming. |
20282046
|`enterprise_id` | string | Slack Enterprise Grid organization ID |
20292047
|`event_id` | string | Unique event identifier |
20302048
|`reaction` | string | Emoji reaction name \(e.g., thumbsup\). Present for reaction_added/reaction_removed events |

apps/sim/app/api/webhooks/slack/custom/[credentialId]/route.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { parseWebhookBody } from '@/lib/webhooks/processor'
66
import { handleSlackChallenge } from '@/lib/webhooks/providers/slack'
77
import {
88
dispatchSlackCustomBotCredential,
9+
handleSlackAgentSessionStopped,
910
verifySlackCustomBotCredentialRequest,
1011
} from '@/lib/webhooks/slack-custom-ingress'
1112
import { getSlackDispatchResponse } from '@/lib/webhooks/slack-dispatch'
@@ -68,12 +69,15 @@ async function handleSlackCustomBotWebhook(
6869
return authError
6970
}
7071

71-
const dispatchResults = await dispatchSlackCustomBotCredential({
72-
credentialId,
73-
body,
74-
request,
75-
requestId,
76-
receivedAt,
77-
})
72+
const [, dispatchResults] = await Promise.all([
73+
handleSlackAgentSessionStopped(credentialId, body),
74+
dispatchSlackCustomBotCredential({
75+
credentialId,
76+
body,
77+
request,
78+
requestId,
79+
receivedAt,
80+
}),
81+
])
7882
return getSlackDispatchResponse(dispatchResults)
7983
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ export { Text } from './text'
3838
export { TimeInput } from './time-input'
3939
export { ToolInput } from './tool-input'
4040
export { VariablesInput } from './variables-input'
41+
export { WorkflowOutputSelector } from './workflow-output-selector'
4142
export { WorkflowSelectorInput } from './workflow-selector'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { WorkflowOutputSelector } from './workflow-output-selector'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { OutputSelect } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select'
2+
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
3+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
4+
5+
const EMPTY_OUTPUTS: string[] = []
6+
7+
interface WorkflowOutputSelectorProps {
8+
blockId: string
9+
subBlockId: string
10+
isPreview?: boolean
11+
previewValue?: string[] | null
12+
disabled?: boolean
13+
placeholder?: string
14+
}
15+
16+
export function WorkflowOutputSelector({
17+
blockId,
18+
subBlockId,
19+
isPreview = false,
20+
previewValue,
21+
disabled = false,
22+
placeholder,
23+
}: WorkflowOutputSelectorProps) {
24+
const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
25+
const [storedValue, setStoredValue] = useSubBlockValue<string[]>(blockId, subBlockId)
26+
const selectedOutputs = isPreview
27+
? (previewValue ?? EMPTY_OUTPUTS)
28+
: (storedValue ?? EMPTY_OUTPUTS)
29+
30+
return (
31+
<OutputSelect
32+
workflowId={workflowId}
33+
selectedOutputs={selectedOutputs}
34+
onOutputSelect={setStoredValue}
35+
disabled={disabled || isPreview}
36+
placeholder={placeholder}
37+
size='md'
38+
className='w-full'
39+
/>
40+
)
41+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
TimeInput,
4848
ToolInput,
4949
VariablesInput,
50+
WorkflowOutputSelector,
5051
WorkflowSelectorInput,
5152
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components'
5253
import { MODAL_REGISTRY } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/modal-registry'
@@ -1097,6 +1098,18 @@ function SubBlockComponent({
10971098
/>
10981099
)
10991100

1101+
case 'workflow-output-selector':
1102+
return (
1103+
<WorkflowOutputSelector
1104+
blockId={blockId}
1105+
subBlockId={config.id}
1106+
isPreview={isPreview}
1107+
previewValue={previewValue as string[] | null | undefined}
1108+
disabled={isDisabled}
1109+
placeholder={config.placeholder}
1110+
/>
1111+
)
1112+
11001113
case 'mcp-server-selector':
11011114
return (
11021115
<McpServerSelector

apps/sim/background/webhook-execution.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ import {
6666
createWebhookExecutionPrincipal,
6767
} from '@/lib/webhooks/execution-principal'
6868
import { getProviderHandler } from '@/lib/webhooks/providers'
69+
import { SlackExecutionStreamController } from '@/lib/webhooks/slack-execution-stream'
70+
import { readSlackStreamResponseConfig } from '@/lib/webhooks/slack-stream-config'
6971
import {
7072
executeWorkflowCore,
7173
wasExecutionFinalizedByCore,
@@ -1054,25 +1056,72 @@ async function executeWebhookJobInternal(
10541056
})
10551057
}
10561058

1059+
const persistedProviderConfig = isRecordLike(resolvedWebhookRecord.providerConfig)
1060+
? resolvedWebhookRecord.providerConfig
1061+
: {}
1062+
const slackStreamConfig =
1063+
payload.provider === 'slack' || payload.provider === 'slack_app'
1064+
? readSlackStreamResponseConfig(persistedProviderConfig)
1065+
: null
1066+
if (slackStreamConfig && payload.provider !== 'slack') {
1067+
throw new Error('Slack trigger response streaming is only supported for custom bots')
1068+
}
1069+
const slackStreamCredentialId =
1070+
typeof persistedProviderConfig.credentialId === 'string'
1071+
? persistedProviderConfig.credentialId
1072+
: null
1073+
if (slackStreamConfig && !slackStreamCredentialId) {
1074+
throw new Error('Slack stream configuration is missing its custom bot credential')
1075+
}
1076+
const slackStreamController = slackStreamConfig
1077+
? await SlackExecutionStreamController.create({
1078+
credentialId: slackStreamCredentialId!,
1079+
workspaceId,
1080+
workflowId: payload.workflowId,
1081+
executionId,
1082+
userId: actorUserId,
1083+
triggerInput,
1084+
config: slackStreamConfig,
1085+
loggingSession,
1086+
abortSignal: timeoutController.signal,
1087+
})
1088+
: null
1089+
10571090
const snapshot = new ExecutionSnapshot(
10581091
metadata,
10591092
workflowRecord,
10601093
triggerInput,
10611094
workflowVariables,
1062-
[]
1095+
slackStreamController?.selectedOutputs ?? []
10631096
)
10641097

10651098
workflowCoreStarted = true
1066-
const executionResult = await executeWorkflowCore({
1067-
snapshot,
1068-
callbacks: {},
1069-
loggingSession,
1070-
trustedInitialResolvedSecretTraceProvenance:
1071-
resolvedSecretTraceRegistry.exportProvenanceForValue(triggerInput),
1072-
includeFileBase64: false,
1073-
base64MaxBytes: undefined,
1074-
abortSignal: timeoutController.signal,
1075-
})
1099+
let executionResult: ExecutionResult
1100+
try {
1101+
executionResult = await executeWorkflowCore({
1102+
snapshot,
1103+
callbacks: slackStreamController?.callbacks ?? {},
1104+
loggingSession,
1105+
trustedInitialResolvedSecretTraceProvenance:
1106+
resolvedSecretTraceRegistry.exportProvenanceForValue(triggerInput),
1107+
includeFileBase64: false,
1108+
base64MaxBytes: undefined,
1109+
abortSignal: timeoutController.signal,
1110+
})
1111+
} catch (error) {
1112+
if (slackStreamController) {
1113+
await slackStreamController.finalize({
1114+
success: false,
1115+
output: {},
1116+
error: toError(error).message,
1117+
})
1118+
}
1119+
throw error
1120+
}
1121+
if (slackStreamController) {
1122+
await slackStreamController.finalize(executionResult)
1123+
slackStreamController.assertSucceeded()
1124+
}
10761125

10771126
await handleExecutionResult(executionResult, {
10781127
loggingSession,

apps/sim/blocks/blocks.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ describe.concurrent('Blocks Module', () => {
582582
'variables-input',
583583
'messages-input',
584584
'workflow-selector',
585+
'workflow-output-selector',
585586
'workflow-input-mapper',
586587
'text',
587588
'router-input',

apps/sim/blocks/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,9 @@ export interface SubBlockConfig {
417417
* `watchFields` is treated as a credential ID and fetched via the credentials
418418
* API. The subblock is hidden unless `credential.type` matches `requiredType`.
419419
*
420-
* Only one subblock per block may use this. The serializer ignores it —
421-
* the field is always serialized when it has a value.
420+
* Every reactive subblock on a block must watch the same credential fields.
421+
* The serializer ignores this — the field is always serialized when it has
422+
* a value, so server-side validation must reject unsupported credentials.
422423
*/
423424
reactiveCondition?: {
424425
watchFields: string[]

apps/sim/executor/execution/block-executor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,8 @@ export class BlockExecutor {
292292
block,
293293
streamingExec,
294294
resolvedInputs,
295-
normalizeStringArray(blockCtx.selectedOutputs)
295+
normalizeStringArray(blockCtx.selectedOutputs),
296+
blockLog?.executionOrder
296297
)
297298
} catch (streamError) {
298299
const resultRegistry = blockCtx.resolvedSecretTraceRegistry
@@ -1150,7 +1151,8 @@ export class BlockExecutor {
11501151
block: SerializedBlock,
11511152
streamingExec: StreamingExecution,
11521153
resolvedInputs: Record<string, any>,
1153-
selectedOutputs: string[]
1154+
selectedOutputs: string[],
1155+
executionOrder?: number
11541156
): Promise<void> {
11551157
const blockId = node.id
11561158
const piiEnabled = Boolean(ctx.piiBlockOutputRedaction?.enabled)
@@ -1202,6 +1204,8 @@ export class BlockExecutor {
12021204
onStreamPromise = ctx
12031205
.onStream({
12041206
...streamingExecutionForConsumer,
1207+
blockId,
1208+
...(executionOrder !== undefined ? { executionOrder } : {}),
12051209
stream: processedClientStream,
12061210
streamFormat: 'text',
12071211
subscribe: pump.subscribe,

0 commit comments

Comments
 (0)