Skip to content

Commit 07233c2

Browse files
feat(slack): GA slack_v2 with agent_view custom-bot manifests
1 parent 49d3804 commit 07233c2

7 files changed

Lines changed: 84 additions & 33 deletions

File tree

apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ function StepConfigure({
310310
{allSelected && (
311311
<p className='text-[var(--text-muted)] text-caption'>
312312
Full access — the bot can read and send messages, react, upload files, and chat as an AI
313-
assistant.
313+
agent.
314314
</p>
315315
)}
316316
</div>

apps/sim/blocks/blocks/slack.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
2121
bgColor: '#611f69',
2222
icon: SlackIcon,
2323
triggerAllowed: true,
24-
// Superseded by slack_v2, but stays discoverable until v2 GAs — hiding both
25-
// would leave no Slack block in the toolbar while v2 is preview-gated. At v2
26-
// GA this becomes `hideFromToolbar: true` (superseded-version paradigm).
24+
// Superseded by slack_v2 (GA): hidden from discovery like other legacy _vN
25+
// blocks; existing workflows keep executing it.
26+
hideFromToolbar: true,
27+
sunset: { status: 'legacy', replacedBy: 'slack_v2' },
2728
subBlocks: [
2829
{
2930
id: 'operation',
@@ -2465,9 +2466,10 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
24652466
team_id: { type: 'string', description: 'Slack workspace/team ID' },
24662467
event_id: { type: 'string', description: 'Unique event identifier for the trigger' },
24672468
},
2468-
// Trigger capabilities moved to slack_v2 so the trigger surfaces once.
2469-
// Legacy webhook trigger stays available while slack_v2 (which hosts the
2470-
// redesigned slack_oauth trigger) is preview-gated; drops at v2 GA.
2469+
// The legacy webhook trigger must STAY enabled: webhook-execution gates on
2470+
// blockConfig.triggers.enabled at runtime, so disabling it would break every
2471+
// deployed v1 Slack trigger workflow. New discovery surfaces only slack_v2
2472+
// (this block is hideFromToolbar).
24712473
triggers: {
24722474
enabled: true,
24732475
available: ['slack_webhook'],
@@ -2627,6 +2629,11 @@ export const SlackBlockMeta = {
26272629
],
26282630
} as const satisfies BlockMeta
26292631

2632+
export const SlackV2BlockMeta = {
2633+
tags: ['messaging', 'webhooks', 'automation'],
2634+
url: 'https://slack.com',
2635+
} as const satisfies BlockMeta
2636+
26302637
const SLACK_WEBHOOK_TRIGGER_SUBBLOCK_IDS = new Set(
26312638
getTrigger('slack_webhook').subBlocks.map((sb) => sb.id)
26322639
)
@@ -2677,11 +2684,6 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
26772684
...SlackBlock,
26782685
type: 'slack_v2',
26792686
hideFromToolbar: false,
2680-
// Preview-gated: hidden from every discovery surface until revealed via the
2681-
// block-visibility AppConfig (hosted) or PREVIEW_BLOCKS=slack_v2 (dev /
2682-
// self-host). At GA: drop this flag, add SlackV2BlockMeta + docs, and set
2683-
// hideFromToolbar on v1.
2684-
preview: true,
26852687
subBlocks: [
26862688
...SlackBlock.subBlocks.flatMap((sb) => {
26872689
// Drop the legacy paste-secret trigger config (v1 hosts slack_webhook)

apps/sim/blocks/registry-maps.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ import { ShopifyBlock, ShopifyBlockMeta } from '@/blocks/blocks/shopify'
278278
import { SimWorkspaceEventBlock } from '@/blocks/blocks/sim_workspace_event'
279279
import { SimilarwebBlock, SimilarwebBlockMeta } from '@/blocks/blocks/similarweb'
280280
import { SixtyfourBlock, SixtyfourBlockMeta } from '@/blocks/blocks/sixtyfour'
281-
import { SlackBlock, SlackBlockMeta, SlackV2Block } from '@/blocks/blocks/slack'
281+
import { SlackBlock, SlackBlockMeta, SlackV2Block, SlackV2BlockMeta } from '@/blocks/blocks/slack'
282282
import { SmtpBlock, SmtpBlockMeta } from '@/blocks/blocks/smtp'
283283
import { SportmonksBlock, SportmonksBlockMeta } from '@/blocks/blocks/sportmonks'
284284
import { SpotifyBlock, SpotifyBlockMeta } from '@/blocks/blocks/spotify'
@@ -878,6 +878,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
878878
similarweb: SimilarwebBlockMeta,
879879
sixtyfour: SixtyfourBlockMeta,
880880
slack: SlackBlockMeta,
881+
slack_v2: SlackV2BlockMeta,
881882
smtp: SmtpBlockMeta,
882883
sportmonks: SportmonksBlockMeta,
883884
spotify: SpotifyBlockMeta,

apps/sim/lib/webhooks/providers/slack.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ interface SlackTriggerEvent {
117117
* submissions). Null for non-block_actions payloads.
118118
*/
119119
state: Record<string, unknown> | null
120+
/**
121+
* What the user is currently viewing, for `app_context_changed` (agent
122+
* messaging experience): `{ entities: [{ type, value, team_id }] }`, ordered
123+
* by relevance; an empty object when Slack has no entities. Null for every
124+
* other event type.
125+
*/
126+
context: Record<string, unknown> | null
120127
hasFiles: boolean
121128
files: SlackDownloadedFile[]
122129
}
@@ -151,6 +158,7 @@ function createSlackEvent(): SlackTriggerEvent {
151158
view: null,
152159
message: null,
153160
state: null,
161+
context: null,
154162
hasFiles: false,
155163
files: [],
156164
}
@@ -605,6 +613,7 @@ export function resolveSlackEventKey(body: Record<string, unknown>): string | nu
605613
case 'pin_removed':
606614
case 'team_join':
607615
case 'app_home_opened':
616+
case 'app_context_changed':
608617
case 'assistant_thread_started':
609618
case 'assistant_thread_context_changed':
610619
return type
@@ -977,6 +986,9 @@ export const slackHandler: WebhookProviderHandler = {
977986
event.message_ts = messageTs
978987
event.hasFiles = hasFiles
979988
event.files = files
989+
if (eventType === 'app_context_changed') {
990+
event.context = (rawEvent?.context as Record<string, unknown> | undefined) ?? {}
991+
}
980992

981993
return { input: { event } }
982994
},

apps/sim/triggers/slack/capabilities.test.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('buildSlackManifest - interactivity', () => {
3131
})
3232

3333
describe('buildSlackManifest - description', () => {
34-
it('emits display_information.description and reuses it as the assistant description', () => {
34+
it('emits display_information.description and reuses it as the agent description', () => {
3535
const manifest = buildSlackManifest(new Set(['action_assistant']), {
3636
...opts,
3737
description: 'Answers support questions.',
@@ -41,15 +41,29 @@ describe('buildSlackManifest - description', () => {
4141
description: 'Answers support questions.',
4242
})
4343
const features = manifest.features as Record<string, Record<string, unknown>>
44-
expect(features.assistant_view.assistant_description).toBe('Answers support questions.')
44+
expect(features.agent_view.agent_description).toBe('Answers support questions.')
4545
})
4646

47-
it('omits the description key and falls back for the assistant when absent', () => {
47+
it('omits the description key and falls back for the agent when absent', () => {
4848
const manifest = buildSlackManifest(new Set(['action_assistant']), opts)
4949
expect(manifest.display_information).toEqual({ name: 'Test Bot' })
5050
const features = manifest.features as Record<string, Record<string, unknown>>
51-
expect(features.assistant_view.assistant_description).toBe(
52-
'Test Bot — an AI assistant powered by Sim.'
53-
)
51+
expect(features.agent_view.agent_description).toBe('Test Bot — an AI agent powered by Sim.')
52+
})
53+
54+
it('caps the agent description at the 300-char manifest limit', () => {
55+
const manifest = buildSlackManifest(new Set(['action_assistant']), {
56+
...opts,
57+
description: 'x'.repeat(400),
58+
})
59+
const features = manifest.features as Record<string, Record<string, unknown>>
60+
expect((features.agent_view.agent_description as string).length).toBe(300)
61+
})
62+
63+
it('subscribes the agent-experience events, not the deprecated assistant_thread_* set', () => {
64+
const manifest = buildSlackManifest(new Set(['action_assistant']), opts)
65+
const settings = settingsOf(manifest)
66+
const events = (settings.event_subscriptions as Record<string, unknown>).bot_events as string[]
67+
expect(events).toEqual(['app_context_changed', 'app_home_opened', 'message.im'])
5468
})
5569
})

apps/sim/triggers/slack/capabilities.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ export interface SlackCapability {
2020
scopes: readonly string[]
2121
events: readonly string[]
2222
/**
23-
* Marks the AI Assistant capability. When enabled the manifest additionally
24-
* declares the app as an Agents & AI app (`features.assistant_view`) and
25-
* enables the App Home messages tab — required for assistant threads, the
26-
* "thinking" status (`assistant.threads.setStatus`), and DM-style chat to work.
23+
* Marks the AI agent capability. When enabled the manifest additionally
24+
* declares the app as an Agents & AI app (`features.agent_view` — the current
25+
* agent messaging experience; new Slack apps cannot use the deprecated
26+
* `assistant_view`) and enables the App Home messages tab — required for the
27+
* agent surface, the "thinking" status (`assistant.threads.setStatus`), and
28+
* DM-style chat to work.
2729
*/
2830
assistant?: boolean
2931
/**
@@ -166,13 +168,18 @@ export const SLACK_CAPABILITIES: readonly SlackCapability[] = [
166168
},
167169
{
168170
id: 'action_assistant',
169-
label: 'AI assistant',
171+
label: 'AI agent',
170172
description:
171-
'Register the bot as an AI assistant: users open an assistant thread, the bot shows a "thinking" status, and can set the thread title and suggested prompts (assistant.threads.*).',
173+
'Register the bot as an AI agent: users chat with it in the agent surface, the bot shows a "thinking" status, and can set the thread title and suggested prompts (assistant.threads.*).',
172174
defaultChecked: true,
173175
group: 'action',
174176
scopes: ['assistant:write', 'im:history'],
175-
events: ['assistant_thread_started', 'assistant_thread_context_changed', 'message.im'],
177+
// The agent messaging experience's event set: app_home_opened (user opened
178+
// the agent DM), app_context_changed (what the user is viewing; requires
179+
// features.agent_view), message.im (incoming messages). The legacy
180+
// assistant_thread_* events belong to the deprecated assistant_view
181+
// experience and are not subscribable by new agent_view apps.
182+
events: ['app_home_opened', 'app_context_changed', 'message.im'],
176183
assistant: true,
177184
},
178185
{
@@ -211,7 +218,7 @@ const WEBHOOK_URL_PLACEHOLDER = '<deploy workflow to generate webhook URL>'
211218
export interface BuildManifestOptions {
212219
appName: string
213220
webhookUrl: string | null
214-
/** Shown on the bot's Slack profile and as the assistant description. */
221+
/** Shown on the bot's Slack profile and as the agent description. */
215222
description?: string
216223
}
217224

@@ -241,12 +248,16 @@ export function buildSlackManifest(
241248
bot_user: { display_name: displayName, always_online: true },
242249
}
243250
if (isAssistant) {
244-
// Declares the app as an Agents & AI app; without this Slack won't surface
245-
// the assistant thread UI or fire assistant_thread_* events. The messages
246-
// tab must be enabled so users can chat the assistant.
247-
features.assistant_view = {
248-
assistant_description:
249-
trimmedDescription || `${displayName} — an AI assistant powered by Sim.`,
251+
// Declares the app as an Agents & AI app via the current agent messaging
252+
// experience. New Slack apps can ONLY use agent_view (assistant_view is
253+
// deprecated and rejected for new apps; switching an existing app to
254+
// agent_view is irreversible). Without this Slack won't surface the agent
255+
// UI or fire app_context_changed. The messages tab must be enabled so
256+
// users can chat with the agent. agent_description caps at 300 chars.
257+
features.agent_view = {
258+
agent_description: (
259+
trimmedDescription || `${displayName} — an AI agent powered by Sim.`
260+
).slice(0, 300),
250261
}
251262
features.app_home = {
252263
home_tab_enabled: false,

apps/sim/triggers/slack/shared.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ export const SLACK_TRIGGER_OUTPUTS: Record<string, TriggerOutput> = {
127127
description:
128128
'Timestamp of the message the interaction originated from. Present for block_actions',
129129
},
130+
context: {
131+
type: 'json',
132+
description:
133+
'What the user is currently viewing: an object with an entities array (each entry: type, value, team_id), e.g. context.entities[0].value. Present for app_context_changed; null otherwise',
134+
},
130135
view: {
131136
type: 'json',
132137
description:
@@ -245,6 +250,12 @@ export const SLACK_EVENT_CATALOG: readonly SlackEventCatalogEntry[] = [
245250
{ id: 'pin_removed', label: 'Pin removed', simSubscribed: false, filters: ['channels'] },
246251
{ id: 'team_join', label: 'Member joined workspace', simSubscribed: false, filters: [] },
247252
{ id: 'app_home_opened', label: 'App home opened', simSubscribed: false, filters: [] },
253+
{
254+
id: 'app_context_changed',
255+
label: 'App context changed',
256+
simSubscribed: false,
257+
filters: [],
258+
},
248259
{ id: 'assistant_thread_started', label: 'Assistant opened', simSubscribed: true, filters: [] },
249260
{
250261
id: 'assistant_thread_context_changed',

0 commit comments

Comments
 (0)