feat(slack): native OAuth trigger#5323
Conversation
Add assistant:write, app_mentions:read, and im:history to the Slack bot OAuth scopes so the Set Assistant Status / Title / Suggested Prompts tools (assistant.threads.*) work with users' existing Slack credentials — no new app or credentials required. Restore the action_assistant trigger capability (scope assistant:write) in the manifest generator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpeT8J5yVCrrNQB9Hzm9uS
…e, add channel-id input
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview Adds Trigger UX is redesigned around one event per block ( Also expands Slack OAuth scopes, wires Reviewed by Cursor Bugbot for commit 9de9952. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThis PR redesigns the native Slack OAuth trigger from a multi-event model to a single-event-per-block model with a centralized
Confidence Score: 5/5Safe to merge — the core routing, filtering, and token-resolution logic is well-structured with no regressions to existing webhook paths. The shared filter function correctly short-circuits for legacy webhooks, the canonical pair resolution addresses the stale-channel concern from a prior review, bot-token resolution for reaction text and file downloads is now credential-owner–based rather than actor-based, and the database migration uses CONCURRENTLY to avoid lock contention. The only newly introduced gap is that file_shared events omit the uploader's user ID in the formatted output, which is a minor output-schema issue with no impact on trigger routing or execution safety. apps/sim/lib/webhooks/providers/slack.ts — event.user is empty for file_shared events; also carries the still-unresolved channel_rename channel-filter issue from a previous review cycle. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Slack
participant SlackRoute as /api/webhooks/slack (native)
participant WebhookRoute as /api/webhooks/[path] (custom)
participant Processor as processor.ts
participant Filter as shouldSkipSlackTriggerEvent
participant Handler as slackHandler.formatInput
participant Queue as Execution Queue
Slack->>SlackRoute: POST event (HMAC-signed, api_app_id)
SlackRoute->>SlackRoute: verifySlackRequestSignature(SLACK_SIGNING_SECRET)
SlackRoute->>Processor: findWebhooksByRoutingKey(team_id)
Processor-->>SlackRoute: matching slack_app webhooks
loop each webhook
SlackRoute->>Filter: shouldSkipSlackTriggerEvent(payload, providerConfig)
Filter-->>SlackRoute: "skip=true/false"
SlackRoute->>Queue: queueWebhookExecution (if not skipped)
end
Slack->>WebhookRoute: POST event (custom app, per-workflow path)
WebhookRoute->>Handler: slackHandler.verifyAuth (signing secret from providerConfig)
WebhookRoute->>Filter: "shouldSkipEvent → shouldSkipSlackTriggerEvent (if triggerId=slack_oauth)"
Filter-->>WebhookRoute: "skip=true/false"
WebhookRoute->>Queue: queueWebhookExecution (if not skipped)
Queue->>Handler: formatInput (resolves botToken via credentialId owner lookup)
Handler-->>Queue: "{ event: SlackTriggerEvent }"
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Slack
participant SlackRoute as /api/webhooks/slack (native)
participant WebhookRoute as /api/webhooks/[path] (custom)
participant Processor as processor.ts
participant Filter as shouldSkipSlackTriggerEvent
participant Handler as slackHandler.formatInput
participant Queue as Execution Queue
Slack->>SlackRoute: POST event (HMAC-signed, api_app_id)
SlackRoute->>SlackRoute: verifySlackRequestSignature(SLACK_SIGNING_SECRET)
SlackRoute->>Processor: findWebhooksByRoutingKey(team_id)
Processor-->>SlackRoute: matching slack_app webhooks
loop each webhook
SlackRoute->>Filter: shouldSkipSlackTriggerEvent(payload, providerConfig)
Filter-->>SlackRoute: "skip=true/false"
SlackRoute->>Queue: queueWebhookExecution (if not skipped)
end
Slack->>WebhookRoute: POST event (custom app, per-workflow path)
WebhookRoute->>Handler: slackHandler.verifyAuth (signing secret from providerConfig)
WebhookRoute->>Filter: "shouldSkipEvent → shouldSkipSlackTriggerEvent (if triggerId=slack_oauth)"
Filter-->>WebhookRoute: "skip=true/false"
WebhookRoute->>Queue: queueWebhookExecution (if not skipped)
Queue->>Handler: formatInput (resolves botToken via credentialId owner lookup)
Handler-->>Queue: "{ event: SlackTriggerEvent }"
Reviews (6): Last reviewed commit: "fix(slack-trigger): apply event/channel/..." | Re-trigger Greptile |
# Conflicts: # apps/sim/blocks/blocks/slack.ts # apps/sim/lib/webhooks/deploy.ts # packages/db/migrations/meta/0253_snapshot.json # packages/db/migrations/meta/_journal.json # scripts/check-api-validation-contracts.ts
|
@greptile review |
…oken for reaction text + file downloads
|
@greptile review — addressed the three findings: (1) unmapped/unselected events now drop instead of bypassing the Operations filter, (2) formatInput resolves the OAuth bot token for slack_app webhooks so reaction-message text works, (3) re-added the includeFiles toggle + token resolution so native file downloads work. |
…token via credential owner not execution actor
|
@greptile review — addressed both: empty operation selection now fires nothing (no match-all), and the OAuth token is resolved via the credential owner (not the execution actor) so it works in shared workspaces. |
…r channel picker over stale manual ids
| workflowId, | ||
| blockId: block.id, | ||
| provider, | ||
| triggerPath, |
There was a problem hiding this comment.
Redeploy leaves stale Slack routing
High Severity
After the first deploy, changing the linked Slack account, Operations, channel filters, or Sim/Custom app type can leave the existing slack_app webhook row unchanged. routingKey, provider, path, and providerConfig are only written on insert, while shouldRecreateExternalWebhookSubscription returns false for slack/slack_app, so events may route to the wrong workspace or not at all.
Reviewed by Cursor Bugbot for commit 310061d. Configure here.
…ull event catalog
|
@greptile review |
| error: { message: 'Select a Slack account for the trigger.', status: 400 }, | ||
| } | ||
| } | ||
| const botToken = await refreshAccessTokenIfNeeded(credentialId, userId, requestId) |
There was a problem hiding this comment.
Deploy uses deployer not owner
High Severity
Sim-mode Slack deploy resolves team_id via refreshAccessTokenIfNeeded using the deploying user’s id, but that helper only loads OAuth tokens when the account row belongs to that user. In shared workspaces, a teammate deploying a trigger wired to someone else’s Slack credential gets a failed deploy even though runtime formatInput already resolves the credential owner for the same case.
Reviewed by Cursor Bugbot for commit 65c1415. Configure here.
|
Fixed: custom (bring-your-own-app) Slack deliveries now apply the same event/source/threads/emoji/channel/bot filtering as the native app. Extracted the filter into a shared @greptile review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9de9952. Configure here.
| if (supports('source')) { | ||
| const sources = normalizeSelection(providerConfig.source) | ||
| if (sources.length > 0 && (!channelType || !sources.includes(channelType))) return true | ||
| } |
There was a problem hiding this comment.
Source filter drops edit events
Medium Severity
For message_edited and message_deleted, the Source filter reads channel_type on the outer event. Slack often omits that field on message_changed / message_deleted payloads, so any non-empty Source selection causes shouldSkipSlackTriggerEvent to drop every matching event.
Reviewed by Cursor Bugbot for commit 9de9952. Configure here.


Summary
Redesigns the native Slack trigger around a single event per block with contextual filters, replacing the multi-select "Operations" model.
eventType); compose complex behavior with multiple blocks instead of a downstream condition blockSLACK_EVENT_CATALOG) as the single source of truth — message, app mentioned, reactions, message edited/deleted, file shared, member/channel lifecycle, pins, workspace join, app home, assistant — driving UI gating and route filteringcapabilities.ts). Deploy rejects an unsupported event on Sim modeauthorizations[]; message edit/delete subtypes mapped; DMs bypass the channel filter;pins:readaddedtrigger-advancedfields (generalizes the block-level advanced section; zero blast radius — every existing such field is canonical-paired)Type of Change
Testing
route.test.ts): source/threads/emoji/channel gating, self-drop, subtype mapping, legacy back-compat, DM-bypasses-channel-filter — all passingbunx tsc --noEmitclean,bun run lint:checkclean,bun run check:api-validation:strictpassedChecklist