Skip to content

fix(slack): win the trigger_id race — dispatch latency cuts + opt-in ingest loading modal - #7055

Closed
icecrasher321 wants to merge 10 commits into
stagingfrom
slack-trigger-id-expired
Closed

fix(slack): win the trigger_id race — dispatch latency cuts + opt-in ingest loading modal#7055
icecrasher321 wants to merge 10 commits into
stagingfrom
slack-trigger-id-expired

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

Slack modal opens fail with expired_trigger_id: workflows execute after the webhook ack, and dispatch eats the 3s TTL before the first block runs (measured in prod: p50 trigger age 2.5s at logging start; 19.5% of Slack runs already expired; ingest 834ms + worker 1,032ms of serial work).

  • Cuts the Sim-owned serial work: warm-context handoff from ingest to the inline worker (no re-fetch of workflow/webhook rows, no duplicate ban/subscription checks), cache-first deployed-state reads by deploymentVersionId, credential resolution once and off the critical path, parallelized execution-core/logging/ingest reads, plus phase-level latency metrics (preprocessMs/loadsMs/providerConfigMs/formatInputMs and a new "Webhook executor started" line). All flags/closures are inert on Trigger.dev and recovery paths — those run unchanged. Admission semantics untouched.
  • Adds an opt-in Open loading modal to the slack_oauth trigger: ingest opens a minimal loading modal via views.open while the trigger_id is fresh (via a new prepareSyncDispatch provider hook; failures never block dispatch), and the workflow updates it through the new loading_view_id trigger output with slack_update_view (no TTL). Legacy slack_webhook trigger unchanged.

Note: bun run type-check currently fails on staging in lib/consent/scripts.ts (pre-existing from #7049); every file this PR touches checks clean, and the affected test battery (2,133 tests) + check:api-validation pass.

🤖 Generated with Claude Code

icecrasher321 and others added 10 commits August 24, 2026 17:31
…+ executor-start metric

The "Webhook dispatch latency" line now carries preprocessMs/loadsMs/
providerConfigMs/formatInputMs, and a one-shot onBlockStart callback logs
"Webhook executor started" with the true trigger age when the first block
runs — the moment that decides a trigger_id-bound provider's 3s race,
which the existing metric (emitted before executeWorkflowCore) undercounts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…orker

preprocessExecution gains trustWorkflowRecord (skip the archived-state
re-read for a row fetched in the same request) and skipAccountChecks
(skip the ban + subscription re-reads; guarded to checkRateLimit: false).
Webhook ingest passes trust for the row findAllWebhooksForPath returned;
the inline runner closures hand the ingest-loaded workflow + webhook rows
to executeWebhookJob as memory-only warm context, so the worker drops the
third workflow fetch, the webhook re-select, and the duplicate account
checks. Trigger.dev and recovery jobs pass no warm context and are
byte-identical to before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oymentVersionId

loadWorkflowDeploymentVersionState consults the existing 5-min LRU before
the SELECT (the id is immutable, and entries now carry their workflowId so
a mismatched pair still falls through to the query). blockExistsInDeployment
routes through that loader when the webhook row's admitted version id is
known, instead of re-reading the entire state jsonb for one boolean — which
also warms the in-process cache the inline execution path reads moments
later. The raw active-version read remains the null-id fallback, and any
failure still answers false.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ritical path

The worker starts resolveCredentialAccountUserId before the state/webhook
loads and awaits it only where the owner id is first consumed, so its two
serial reads overlap the deployment-state load and provider-config
resolution. The resolved owner rides into formatInput as
credentialOwnerUserId (when the provider config names the same credential),
letting the Slack handler skip re-running the identical
resolveOAuthAccountId + account owner chain before refreshing the token.
The Slack token resolution is extracted into resolveSlackWebhookBotToken,
shared and behavior-identical when no owner id is provided.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PII-redaction row read (workspace ⋈ organization) joins the existing
state + environment Promise.all instead of running serially after logging
start, and eligibleOrgForWorkspace resolves its feature-flag and
enterprise-plan reads concurrently after the workspace lookup. Identical
results; fewer serial round trips for every execution path, including
Trigger.dev workers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ert concurrently

startWorkflowExecution's existing-log probe and
createSnapshotWithDeduplication have no data dependency; running them in
parallel cuts a serial round trip from every execution start. The
duplicate-executionId path still returns the prior log and snapshot — the
concurrent upsert is an idempotent no-op on an unchanged state hash, and a
changed-hash orphan is reclaimed by cleanupOrphanedSnapshots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
findAllWebhooksForPath depends only on the request path, so the ingest
route starts it before reading the body stream and awaits it after the
challenge short-circuit — one round trip off the pre-ack path. A challenge
response abandons a read-only query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Optional provider hook invoked after event filters, the deployment-block
check, and admission — immediately before the execution payload is
assembled — for work that must happen synchronously at ingest (e.g.
opening a Slack loading modal inside the 3-second trigger_id window). The
result rides the payload as syncInteraction (identifiers only, never token
material) and surfaces to formatInput. A hook failure or throw never
blocks dispatch. All three Slack ingest doors converge on this call site;
no route changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slack's trigger_id expires 3 seconds after the interaction, and webhook
workflows execute after the ack — so views.open from a workflow reliably
loses the race. When the slack_oauth trigger opts in, the ingest path now
opens a minimal Block Kit loading modal via views.open (2s timeout, any
failure logs and continues) for interactive payloads that carry a fresh
trigger_id and are not already inside a modal. The created view id rides
the payload into the trigger output as loading_view_id, which never
expires — the workflow updates it with Slack Update View.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the 'Open loading modal' switch plus title/text inputs to the
slack_oauth trigger (visible for block_actions; the ingest hook re-checks
payload shape), and declares the loading_view_id output shared by both
Slack triggers. buildProviderConfig copies subblock values generically, so
no deploy changes. The legacy slack_webhook trigger is deliberately left
without the option.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 25, 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 Aug 25, 2026 12:34am

Request Review

@cursor

cursor Bot commented Aug 25, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes admission-adjacent preprocessing (trustWorkflowRecord, skipAccountChecks) and inline-only warm handoff; misconfiguration could skip safety checks on the fast path, though queued/recovery jobs remain unchanged.

Overview
Targets Slack expired_trigger_id failures by shrinking Sim-owned work before the first block runs and by opening a loading modal while trigger_id is still valid.

Latency / throughput: Webhook ingest overlaps path lookup with body parsing; the inline worker accepts a memory-only WebhookWarmContext (workflow/webhook rows, trusted preprocessing, skipped ban/subscription re-reads when admission already ran). blockExistsInDeployment and deployment loads use cache-first deploymentVersionId reads; credential owner resolution overlaps state loads; execution logging runs snapshot creation in parallel with the duplicate-execution probe; execution core loads PII policy alongside workflow state. Webhook jobs add phase timings (preprocessMs, loadsMs, etc.) and a one-shot "Webhook executor started" log on first block start.

Slack UX: New prepareSyncDispatch hook runs after admission; Slack can views.open an opt-in loading modal and persist syncInteraction.loadingViewId on the job payload. formatInput exposes loading_view_id for slack_update_view. Failures never block dispatch.

Scope: Warm-context and skip flags apply only to same-process inline execution; Trigger.dev/recovery paths unchanged. Extensive tests cover warm context, sync dispatch, preprocessing trust flags, and deployment cache behavior.

Reviewed by Cursor Bugbot for commit b16c567. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reduces Slack webhook dispatch latency by handing ingress-loaded state to inline workers, parallelizing execution setup, and adding an opt-in loading modal whose view ID is exposed to workflows.

  • Adds warm workflow and webhook context for same-process execution and cache-first deployment-state loading.
  • Adds a synchronous provider preparation hook that can open a Slack loading modal before dispatch.
  • Extends Slack trigger output with loading_view_id and adds phase-level latency instrumentation.
  • Updates preprocessing, persistence, execution, provider, and webhook tests for the new paths.

Confidence Score: 3/5

This PR should not merge until warm execution revalidates workflow revocation and synchronous Slack modal preparation is bounded to preserve the acknowledgement deadline.

The inline worker can execute an immutable deployment from a stale workflow row after archive or undeploy, while loading-modal preparation can spend unbounded time resolving credentials before a two-second API call and delay Slack's required acknowledgement.

Files Needing Attention: apps/sim/background/webhook-execution.ts, apps/sim/lib/webhooks/processor.ts, apps/sim/lib/webhooks/providers/slack.ts

Important Files Changed

Filename Overview
apps/sim/lib/webhooks/processor.ts Adds warm-context dispatch and synchronous provider preparation, but the latter can delay Slack acknowledgement beyond its deadline.
apps/sim/background/webhook-execution.ts Reuses ingress-loaded rows and parallelizes setup, but trusting the warm workflow row permits execution after archive or undeploy.
apps/sim/lib/execution/preprocessing.ts Introduces explicit switches for trusting workflow state and skipping account checks; these materially weaken execution-time revalidation for warm workers.
apps/sim/lib/webhooks/providers/slack.ts Adds loading-modal creation and view-ID propagation, but only the final Slack fetch—not total synchronous preparation—is time-bounded.
apps/sim/lib/workflows/persistence/utils.ts Adds deployment-version-aware cache-first loading and block lookup while preserving immutable-version access.
apps/sim/triggers/slack/oauth.ts Adds opt-in loading-modal configuration and trigger output metadata consistently with the provider mapping.

Sequence Diagram

sequenceDiagram
    participant Slack
    participant Ingest as Webhook ingest
    participant API as Slack API
    participant Queue as Inline queue
    participant Worker as Webhook worker
    participant Core as Workflow core
    Slack->>Ingest: Interactive request + trigger_id
    Ingest->>Ingest: Verify, filter, and admit
    opt Open loading modal
        Ingest->>API: views.open
        API-->>Ingest: loading view ID
    end
    Ingest->>Queue: Enqueue payload + warm context
    Ingest-->>Slack: HTTP 200
    Queue-->>Worker: Start asynchronously
    Worker->>Worker: Preprocess warm workflow row
    Worker->>Core: Execute immutable deployment version
    Core-->>Worker: Result and logs
Loading

Reviews (1): Last reviewed commit: "feat(slack): loading-modal trigger confi..." | Re-trigger Greptile

executionType: 'async',
executionDeadlineAt: getExecutionDeadlineAt(timeoutController.signal)?.getTime(),
workflowRecord: warmWorkflowRecord,
trustWorkflowRecord: Boolean(warmWorkflowRecord),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Stale workflow state bypasses revocation

When a workflow is archived or undeployed after webhook ingress but before the asynchronous inline runner starts, trustWorkflowRecord suppresses the active-workflow reload and the immutable deployment remains loadable, causing blocks from the revoked workflow to execute.

Knowledge Base Used:

Comment on lines +736 to +746
const prepared = await handler.prepareSyncDispatch({
webhook: foundWebhook,
workflow: {
id: foundWorkflow.id,
userId: foundWorkflow.userId,
workspaceId: foundWorkflow.workspaceId,
},
body,
requestId: options.requestId,
providerConfig,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Modal preparation delays Slack acknowledgement

When loading-modal credential resolution or views.open is slow, this awaited hook delays enqueueing and the HTTP response beyond Slack's three-second acknowledgement window, causing failed interaction notices or retries while the modal's trigger_id expires.

Knowledge Base Used: Triggers and background automation

@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 high effort and found 2 potential issues.

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 b16c567. Configure here.

workflowId,
workflowState
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicate start depends on snapshot

Medium Severity

The duplicate-execution probe and snapshot upsert now run in Promise.all. If snapshot creation fails, the whole start fails even when an execution log already exists and the old path would have short-circuited after the probe. Idempotent retries can error instead of returning the existing log and snapshot.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b16c567. Configure here.

error: toError(error).message,
})
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fan-out burns shared trigger_id

Low Severity

dispatchSlackWebhooks awaits each matching webhook in series, and each admitted target with loading modal enabled calls views.open on the same trigger_id. Slack allows a single exchange, so the first success consumes it; later targets get exchanged_trigger_id, empty loading_view_id, and still run. Shared team/credential fan-out with multiple opted-in triggers only one modal wins.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b16c567. Configure here.

@icecrasher321
icecrasher321 marked this pull request as draft August 25, 2026 00:52
@waleedlatif1
waleedlatif1 deleted the slack-trigger-id-expired branch August 25, 2026 02:39
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