fix(slack): win the trigger_id race — dispatch latency cuts + opt-in ingest loading modal - #7055
fix(slack): win the trigger_id race — dispatch latency cuts + opt-in ingest loading modal#7055icecrasher321 wants to merge 10 commits into
Conversation
…+ 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Latency / throughput: Webhook ingest overlaps path lookup with body parsing; the inline worker accepts a memory-only Slack UX: New 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 SummaryThis 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.
Confidence Score: 3/5This 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
|
| 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
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), |
There was a problem hiding this comment.
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:
| const prepared = await handler.prepareSyncDispatch({ | ||
| webhook: foundWebhook, | ||
| workflow: { | ||
| id: foundWorkflow.id, | ||
| userId: foundWorkflow.userId, | ||
| workspaceId: foundWorkflow.workspaceId, | ||
| }, | ||
| body, | ||
| requestId: options.requestId, | ||
| providerConfig, | ||
| }) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ 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 | ||
| ) | ||
|
|
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit b16c567. Configure here.
| error: toError(error).message, | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit b16c567. Configure here.


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).deploymentVersionId, credential resolution once and off the critical path, parallelized execution-core/logging/ingest reads, plus phase-level latency metrics (preprocessMs/loadsMs/providerConfigMs/formatInputMsand a new"Webhook executor started"line). All flags/closures are inert on Trigger.dev and recovery paths — those run unchanged. Admission semantics untouched.slack_oauthtrigger: ingest opens a minimal loading modal viaviews.openwhile the trigger_id is fresh (via a newprepareSyncDispatchprovider hook; failures never block dispatch), and the workflow updates it through the newloading_view_idtrigger output withslack_update_view(no TTL). Legacyslack_webhooktrigger unchanged.Note:
bun run type-checkcurrently fails on staging inlib/consent/scripts.ts(pre-existing from #7049); every file this PR touches checks clean, and the affected test battery (2,133 tests) +check:api-validationpass.🤖 Generated with Claude Code