Skip to content

feat(slack): native OAuth trigger#5323

Open
TheodoreSpeaks wants to merge 10 commits into
stagingfrom
improve/slack
Open

feat(slack): native OAuth trigger#5323
TheodoreSpeaks wants to merge 10 commits into
stagingfrom
improve/slack

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Redesigns the native Slack trigger around a single event per block with contextual filters, replacing the multi-select "Operations" model.

  • One trigger = one event (single-select eventType); compose complex behavior with multiple blocks instead of a downstream condition block
  • Full event catalog (SLACK_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 filtering
  • Manifest gating per app type: Sim mode offers only events the official app subscribes to; Custom mode offers all and generates the manifest (capabilities.ts). Deploy rejects an unsupported event on Sim mode
  • Contextual filters, each gated by event: Source (multiselect: DM / public / private, empty = any), Channels (picker + manual IDs), three-way Threads (messages+replies / messages only / replies only), Emoji, Name-contains
  • Correctness: always-on self-drop (app_id for messages, stored bot_user_id for reactions) with an advanced opt-in; "Ignore bot messages" = other bots only; Slack Connect routing via authorizations[]; message edit/delete subtypes mapped; DMs bypass the channel filter; pins:read added
  • Advanced-toggle support for standalone trigger-advanced fields (generalizes the block-level advanced section; zero blast radius — every existing such field is canonical-paired)
  • Adds Slack tools to the minimal dev registry so the action block runs end-to-end locally

Type of Change

  • New feature

Testing

  • Route filter unit tests (route.test.ts): source/threads/emoji/channel gating, self-drop, subtype mapping, legacy back-compat, DM-bypasses-channel-filter — all passing
  • bunx tsc --noEmit clean, bun run lint:check clean, bun run check:api-validation:strict passed
  • Manually verified end-to-end against a live workspace (event routed → filtered → action fired)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

TheodoreSpeaks and others added 4 commits June 30, 2026 19:59
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
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 7, 2026 12:48am

Request Review

@cursor

cursor Bot commented Jul 1, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches webhook ingress (signature verification, multi-workflow fan-out), deploy-time token use, and a schema migration on webhook; misconfiguration could drop or misroute Slack events across tenants.

Overview
Introduces a unified slack_oauth trigger (replacing slack_webhook on the Slack block) with Sim vs Custom app type: Sim uses OAuth on the official app and routes by workspace team_id; Custom keeps per-workflow URLs and signing secrets.

Adds /api/webhooks/slack as the shared Events API ingest (HMAC via SLACK_SIGNING_SECRET), webhook.routing_key (nullable path), deploy-time team_id from auth.test, findWebhooksByRoutingKey, Slack Connect authorizations[] fan-out with dedup, and deactivation of slack_app webhooks when credentials are deleted.

Trigger UX is redesigned around one event per block (SLACK_EVENT_CATALOG) with contextual filters (source, channels, threads, emoji, name) applied via shared shouldSkipSlackTriggerEvent on both ingest paths; legacy events[] configs still work. Editor support adds trigger-advanced fields under the block advanced toggle via isStandaloneAdvancedMode.

Also expands Slack OAuth scopes, wires slack_app in the provider registry, registers Slack in minimal dev block/tool registries, and adds route + filter unit tests.

Reviewed by Cursor Bugbot for commit 9de9952. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR redesigns the native Slack OAuth trigger from a multi-event model to a single-event-per-block model with a centralized SLACK_EVENT_CATALOG, introducing a shared shouldSkipSlackTriggerEvent filter function that applies uniformly to both the official Sim app path (/api/webhooks/slack, routing by team_id) and the custom bring-your-own-app path. It also adds a new routingKey column to the webhook table, resolves the bot token for reaction text and file downloads via credential-owner lookup, and deactivates native webhooks on credential deletion.

  • New ingest route (/api/webhooks/slack): receives all Sim Slack app events, routes by team_id/authorizations, runs shouldSkipSlackTriggerEvent per webhook, and queues execution — replacing the per-workflow path model for native installs.
  • Shared filter (shouldSkipSlackTriggerEvent): single source of truth for event-type, source, threads, emoji, name-contains, channel, self-drop, and bot filters; used by both the native route and slackHandler.shouldSkipEvent (gated to triggerId === 'slack_oauth' so legacy webhooks are unaffected).
  • Bot token resolution fix (formatInput): for native triggers carrying only a credentialId, the owner is looked up via DB and the token is refreshed — fixing reaction text and file download on the native path.

Confidence Score: 5/5

Safe 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

Filename Overview
apps/sim/app/api/webhooks/slack/route.ts New shared-app ingest route: HMAC-verified, routes by team_id and authorizations, deduplicates across Slack Connect fan-out, delegates filtering to shouldSkipSlackTriggerEvent. Logic is clean.
apps/sim/lib/webhooks/providers/slack.ts Adds shouldSkipSlackTriggerEvent (shared filter), fetchSlackTeamId, and resolveSlackEventKey. Bot-token resolution via credential-owner DB lookup is correct. file_shared events map user_id at the wrong key.
apps/sim/triggers/slack/shared.ts Well-structured catalog, option arrays, and filter helpers as single source of truth. slackEventsSupportingFilter is used correctly in both UI gating and route logic.
apps/sim/triggers/slack/oauth.ts Unified trigger config with proper sub-block conditions, canonical param IDs, and advanced-mode fields. appType gating correctly gates UI options and deploy validation.
apps/sim/lib/webhooks/deploy.ts slack_oauth correctly dispatches to slack_app (sim) vs slack (custom), fetches team_id via auth.test, stores routingKey, and handles null triggerPath throughout. effectiveProvider is threaded correctly into all downstream calls.
packages/db/schema.ts path made nullable, routingKey column added with a partial index on (routing_key, provider) WHERE archived_at IS NULL. Migration uses CONCURRENTLY to avoid table lock.
apps/sim/lib/credentials/deletion.ts Adds deactivateSlackAppWebhooks: deactivates native Slack webhooks when a credential is deleted, using a JSON arrow query to match credentialId in providerConfig. Parameterized correctly via Drizzle sql template.
apps/sim/lib/webhooks/processor.ts Adds findWebhooksByRoutingKey for multi-workflow fan-out lookup by team_id; joins with active deployment version check. Path null-coalescion to empty string for legacy compatibility is correct.
apps/sim/triggers/slack/capabilities.ts Adds file_shared, member channel, channel lifecycle, pin, team_join, app_home, and assistant capabilities. Removes trigger_group_dm and re-adds action_assistant. assistant:write scope is now included.
apps/sim/app/api/webhooks/slack/route.test.ts Tests verify queue/skip behavior, Slack Connect dedup, and missing team_id early return. Coverage is adequate for the new route's core paths.

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 }"
Loading
%%{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 }"
Loading

Reviews (6): Last reviewed commit: "fix(slack-trigger): apply event/channel/..." | Re-trigger Greptile

Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
Comment thread apps/sim/lib/webhooks/deploy.ts
Comment thread apps/sim/triggers/slack/oauth.ts
# 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
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@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.

Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
Comment thread apps/sim/lib/webhooks/providers/slack.ts Outdated
Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
…token via credential owner not execution actor
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@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.

Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
workflowId,
blockId: block.id,
provider,
triggerPath,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 310061d. Configure here.

Comment thread apps/sim/app/api/webhooks/slack/route.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

error: { message: 'Select a Slack account for the trigger.', status: 400 },
}
}
const botToken = await refreshAccessTokenIfNeeded(credentialId, userId, requestId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 65c1415. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

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 shouldSkipSlackTriggerEvent used by both /api/webhooks/slack and the path route via slackHandler.shouldSkipEvent (guarded to slack_oauth so the legacy slack_webhook trigger stays unfiltered).

@greptile review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Fix All in Cursor

❌ 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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9de9952. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant