Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ function StepConfigure({
{allSelected && (
<p className='text-[var(--text-muted)] text-caption'>
Full access — the bot can read and send messages, react, upload files, and chat as an AI
assistant.
agent.
</p>
)}
</div>
Expand Down
24 changes: 13 additions & 11 deletions apps/sim/blocks/blocks/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
bgColor: '#611f69',
icon: SlackIcon,
triggerAllowed: true,
// Superseded by slack_v2, but stays discoverable until v2 GAs — hiding both
// would leave no Slack block in the toolbar while v2 is preview-gated. At v2
// GA this becomes `hideFromToolbar: true` (superseded-version paradigm).
// Superseded by slack_v2 (GA): hidden from discovery like other legacy _vN
// blocks; existing workflows keep executing it.
hideFromToolbar: true,
sunset: { status: 'legacy', replacedBy: 'slack_v2' },
subBlocks: [
{
id: 'operation',
Expand Down Expand Up @@ -2465,9 +2466,10 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te
team_id: { type: 'string', description: 'Slack workspace/team ID' },
event_id: { type: 'string', description: 'Unique event identifier for the trigger' },
},
// Trigger capabilities moved to slack_v2 so the trigger surfaces once.
// Legacy webhook trigger stays available while slack_v2 (which hosts the
// redesigned slack_oauth trigger) is preview-gated; drops at v2 GA.
// The legacy webhook trigger must STAY enabled: webhook-execution gates on
// blockConfig.triggers.enabled at runtime, so disabling it would break every
// deployed v1 Slack trigger workflow. New discovery surfaces only slack_v2
// (this block is hideFromToolbar).
triggers: {
enabled: true,
available: ['slack_webhook'],
Expand Down Expand Up @@ -2627,6 +2629,11 @@ export const SlackBlockMeta = {
],
} as const satisfies BlockMeta

export const SlackV2BlockMeta = {
tags: ['messaging', 'webhooks', 'automation'],
url: 'https://slack.com',
} as const satisfies BlockMeta

const SLACK_WEBHOOK_TRIGGER_SUBBLOCK_IDS = new Set(
getTrigger('slack_webhook').subBlocks.map((sb) => sb.id)
)
Expand Down Expand Up @@ -2677,11 +2684,6 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
...SlackBlock,
type: 'slack_v2',
hideFromToolbar: false,
// Preview-gated: hidden from every discovery surface until revealed via the
// block-visibility AppConfig (hosted) or PREVIEW_BLOCKS=slack_v2 (dev /
// self-host). At GA: drop this flag, add SlackV2BlockMeta + docs, and set
// hideFromToolbar on v1.
preview: true,
subBlocks: [
...SlackBlock.subBlocks.flatMap((sb) => {
// Drop the legacy paste-secret trigger config (v1 hosts slack_webhook)
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/blocks/registry-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ import { ShopifyBlock, ShopifyBlockMeta } from '@/blocks/blocks/shopify'
import { SimWorkspaceEventBlock } from '@/blocks/blocks/sim_workspace_event'
import { SimilarwebBlock, SimilarwebBlockMeta } from '@/blocks/blocks/similarweb'
import { SixtyfourBlock, SixtyfourBlockMeta } from '@/blocks/blocks/sixtyfour'
import { SlackBlock, SlackBlockMeta, SlackV2Block } from '@/blocks/blocks/slack'
import { SlackBlock, SlackBlockMeta, SlackV2Block, SlackV2BlockMeta } from '@/blocks/blocks/slack'
import { SmtpBlock, SmtpBlockMeta } from '@/blocks/blocks/smtp'
import { SportmonksBlock, SportmonksBlockMeta } from '@/blocks/blocks/sportmonks'
import { SpotifyBlock, SpotifyBlockMeta } from '@/blocks/blocks/spotify'
Expand Down Expand Up @@ -878,6 +878,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
similarweb: SimilarwebBlockMeta,
sixtyfour: SixtyfourBlockMeta,
slack: SlackBlockMeta,
slack_v2: SlackV2BlockMeta,
smtp: SmtpBlockMeta,
sportmonks: SportmonksBlockMeta,
spotify: SpotifyBlockMeta,
Expand Down
12 changes: 12 additions & 0 deletions apps/sim/lib/webhooks/providers/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ interface SlackTriggerEvent {
* submissions). Null for non-block_actions payloads.
*/
state: Record<string, unknown> | null
/**
* What the user is currently viewing, for `app_context_changed` (agent
* messaging experience): `{ entities: [{ type, value, team_id }] }`, ordered
* by relevance; an empty object when Slack has no entities. Null for every
* other event type.
*/
context: Record<string, unknown> | null
hasFiles: boolean
files: SlackDownloadedFile[]
}
Expand Down Expand Up @@ -151,6 +158,7 @@ function createSlackEvent(): SlackTriggerEvent {
view: null,
message: null,
state: null,
context: null,
hasFiles: false,
files: [],
}
Expand Down Expand Up @@ -605,6 +613,7 @@ export function resolveSlackEventKey(body: Record<string, unknown>): string | nu
case 'pin_removed':
case 'team_join':
case 'app_home_opened':
case 'app_context_changed':
case 'assistant_thread_started':
case 'assistant_thread_context_changed':
return type
Expand Down Expand Up @@ -977,6 +986,9 @@ export const slackHandler: WebhookProviderHandler = {
event.message_ts = messageTs
event.hasFiles = hasFiles
event.files = files
if (eventType === 'app_context_changed') {
event.context = (rawEvent?.context as Record<string, unknown> | undefined) ?? {}
}

return { input: { event } }
},
Expand Down
26 changes: 20 additions & 6 deletions apps/sim/triggers/slack/capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('buildSlackManifest - interactivity', () => {
})

describe('buildSlackManifest - description', () => {
it('emits display_information.description and reuses it as the assistant description', () => {
it('emits display_information.description and reuses it as the agent description', () => {
const manifest = buildSlackManifest(new Set(['action_assistant']), {
...opts,
description: 'Answers support questions.',
Expand All @@ -41,15 +41,29 @@ describe('buildSlackManifest - description', () => {
description: 'Answers support questions.',
})
const features = manifest.features as Record<string, Record<string, unknown>>
expect(features.assistant_view.assistant_description).toBe('Answers support questions.')
expect(features.agent_view.agent_description).toBe('Answers support questions.')
})

it('omits the description key and falls back for the assistant when absent', () => {
it('omits the description key and falls back for the agent when absent', () => {
const manifest = buildSlackManifest(new Set(['action_assistant']), opts)
expect(manifest.display_information).toEqual({ name: 'Test Bot' })
const features = manifest.features as Record<string, Record<string, unknown>>
expect(features.assistant_view.assistant_description).toBe(
'Test Bot — an AI assistant powered by Sim.'
)
expect(features.agent_view.agent_description).toBe('Test Bot — an AI agent powered by Sim.')
})

it('caps the agent description at the 300-char manifest limit', () => {
const manifest = buildSlackManifest(new Set(['action_assistant']), {
...opts,
description: 'x'.repeat(400),
})
const features = manifest.features as Record<string, Record<string, unknown>>
expect((features.agent_view.agent_description as string).length).toBe(300)
})

it('subscribes the agent-experience events, not the deprecated assistant_thread_* set', () => {
const manifest = buildSlackManifest(new Set(['action_assistant']), opts)
const settings = settingsOf(manifest)
const events = (settings.event_subscriptions as Record<string, unknown>).bot_events as string[]
expect(events).toEqual(['app_context_changed', 'app_home_opened', 'message.im'])
})
})
39 changes: 25 additions & 14 deletions apps/sim/triggers/slack/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ export interface SlackCapability {
scopes: readonly string[]
events: readonly string[]
/**
* Marks the AI Assistant capability. When enabled the manifest additionally
* declares the app as an Agents & AI app (`features.assistant_view`) and
* enables the App Home messages tab — required for assistant threads, the
* "thinking" status (`assistant.threads.setStatus`), and DM-style chat to work.
* Marks the AI agent capability. When enabled the manifest additionally
* declares the app as an Agents & AI app (`features.agent_view` — the current
* agent messaging experience; new Slack apps cannot use the deprecated
* `assistant_view`) and enables the App Home messages tab — required for the
* agent surface, the "thinking" status (`assistant.threads.setStatus`), and
* DM-style chat to work.
*/
assistant?: boolean
/**
Expand Down Expand Up @@ -166,13 +168,18 @@ export const SLACK_CAPABILITIES: readonly SlackCapability[] = [
},
{
id: 'action_assistant',
label: 'AI assistant',
label: 'AI agent',
description:
'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.*).',
'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.*).',
defaultChecked: true,
group: 'action',
scopes: ['assistant:write', 'im:history'],
events: ['assistant_thread_started', 'assistant_thread_context_changed', 'message.im'],
// The agent messaging experience's event set: app_home_opened (user opened
// the agent DM), app_context_changed (what the user is viewing; requires
// features.agent_view), message.im (incoming messages). The legacy
// assistant_thread_* events belong to the deprecated assistant_view
// experience and are not subscribable by new agent_view apps.
events: ['app_home_opened', 'app_context_changed', 'message.im'],
Comment thread
cursor[bot] marked this conversation as resolved.
assistant: true,
},
{
Expand Down Expand Up @@ -211,7 +218,7 @@ const WEBHOOK_URL_PLACEHOLDER = '<deploy workflow to generate webhook URL>'
export interface BuildManifestOptions {
appName: string
webhookUrl: string | null
/** Shown on the bot's Slack profile and as the assistant description. */
/** Shown on the bot's Slack profile and as the agent description. */
description?: string
}

Expand Down Expand Up @@ -241,12 +248,16 @@ export function buildSlackManifest(
bot_user: { display_name: displayName, always_online: true },
}
if (isAssistant) {
// Declares the app as an Agents & AI app; without this Slack won't surface
// the assistant thread UI or fire assistant_thread_* events. The messages
// tab must be enabled so users can chat the assistant.
features.assistant_view = {
assistant_description:
trimmedDescription || `${displayName} — an AI assistant powered by Sim.`,
// Declares the app as an Agents & AI app via the current agent messaging
// experience. New Slack apps can ONLY use agent_view (assistant_view is
// deprecated and rejected for new apps; switching an existing app to
// agent_view is irreversible). Without this Slack won't surface the agent
// UI or fire app_context_changed. The messages tab must be enabled so
// users can chat with the agent. agent_description caps at 300 chars.
features.agent_view = {
agent_description: (
trimmedDescription || `${displayName} — an AI agent powered by Sim.`
).slice(0, 300),
Comment thread
cursor[bot] marked this conversation as resolved.
}
features.app_home = {
home_tab_enabled: false,
Expand Down
11 changes: 11 additions & 0 deletions apps/sim/triggers/slack/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ export const SLACK_TRIGGER_OUTPUTS: Record<string, TriggerOutput> = {
description:
'Timestamp of the message the interaction originated from. Present for block_actions',
},
context: {
type: 'json',
description:
'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',
},
Comment thread
TheodoreSpeaks marked this conversation as resolved.
view: {
type: 'json',
description:
Expand Down Expand Up @@ -245,6 +250,12 @@ export const SLACK_EVENT_CATALOG: readonly SlackEventCatalogEntry[] = [
{ id: 'pin_removed', label: 'Pin removed', simSubscribed: false, filters: ['channels'] },
{ id: 'team_join', label: 'Member joined workspace', simSubscribed: false, filters: [] },
{ id: 'app_home_opened', label: 'App home opened', simSubscribed: false, filters: [] },
{
id: 'app_context_changed',
label: 'App context changed',
simSubscribed: false,
filters: [],
},
{ id: 'assistant_thread_started', label: 'Assistant opened', simSubscribed: true, filters: [] },
{
id: 'assistant_thread_context_changed',
Expand Down
Loading