Skip to content

feat(sdk,slack): webhook sources, agent channels, and human-in-the-loop - #4537

Open
ericallam wants to merge 31 commits into
mainfrom
feat/hosted-webhooks-api
Open

feat(sdk,slack): webhook sources, agent channels, and human-in-the-loop#4537
ericallam wants to merge 31 commits into
mainfrom
feat/hosted-webhooks-api

Conversation

@ericallam

@ericallam ericallam commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

The public SDK and docs half of hosted webhooks: webhook() with typed provider sources (webhooks.stripe(), webhooks.github(), webhooks.svix(), and more, plus webhooks.custom<T>()), chat.event and chat.channels for agent channels, human-in-the-loop tool approvals, the new @trigger.dev/slack connector, and the webhooks docs section.

import { webhook, webhooks } from "@trigger.dev/sdk";

export const stripeWebhook = webhook({
  id: "stripe-webhook",
  source: webhooks.stripe(),
  onEvent: async ({ event, headers, ctx }) => {
    // verified, typed Stripe event
  },
});

Stacked on the server PR

This is the top of a stack. Its base is #4344 (the server half: ingress, delivery pipeline, dashboard, and the shared @trigger.dev/core schemas this SDK builds on), so the diff here is API-only and it builds against a base that already has core.

The single changeset in this PR bumps @trigger.dev/core, @trigger.dev/sdk, @trigger.dev/slack, and trigger.dev together, so core (whose code lands via #4344) is published alongside the SDK.

Merge order

Merges after #4344. The plan: land and deploy the server behind its flag, cut prerelease (rc) packages for early users to test against the live environment, then merge this and cut the real release once the feature is live. When #4344 merges, GitHub retargets this PR's base to main automatically.

@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 693c646

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
@trigger.dev/core Minor
@trigger.dev/sdk Minor
@trigger.dev/slack Minor
trigger.dev Minor
@trigger.dev/build Minor
@trigger.dev/python Minor
@trigger.dev/redis-worker Minor
@trigger.dev/schema-to-json Minor
@internal/cache Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/metrics-pipeline Patch
@trigger.dev/rbac Minor
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@trigger.dev/sso Minor
@internal/tracing Patch
@internal/tsql Patch
@internal/webhook-engine Patch
@internal/webhook-sources Patch
@internal/dashboard-agent Patch
@internal/sdk-compat-tests Patch
@trigger.dev/react-hooks Minor
@trigger.dev/rsc Minor
@trigger.dev/database Minor
@trigger.dev/otlp-importer Minor
@internal/testcontainers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 93ce36fc-9cfa-44e2-a447-8bd239b94bd0

📥 Commits

Reviewing files that changed from the base of the PR and between e75a7ca and f86d3cd.

📒 Files selected for processing (1)
  • packages/trigger-sdk/src/v3/ai.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/trigger-sdk/src/v3/ai.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 8 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (37)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 12)
  • GitHub Check: sdk-compat / Node.js 22.23 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Node.js 20.20 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Node.js 26.4 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 12)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Node.js 24.18 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 12)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-ubuntu-latest-x64-4x - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-ubuntu-latest-x64-4x - npm)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 12)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (1, 3)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 12)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - npm)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 12)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - pnpm)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: typecheck / typecheck
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: audit
  • GitHub Check: audit
  • GitHub Check: 🔍 What moved
  • GitHub Check: code-quality / code-quality
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build and publish previews

Walkthrough

Added typed webhook sources, provider verification, webhook tasks, and durable chat.event routing. Extended chat agents with channel connectors, streaming replies, reactions, recovery handling, and human-in-the-loop controls. Added the Slack connector package with API operations and tests. Propagated webhook metadata through CLI manifests and added duplicate webhook ID reporting. Added documentation for webhook setup, filtering, deliveries, session routing, channels, and approvals.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the changes and merge context but omits the required issue reference, checklist, testing, changelog, and screenshots sections. Add the template sections, complete the checklist, describe testing steps, add a short changelog entry, include screenshots or state that they are not applicable, and reference the issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 63.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main SDK, Slack, webhook, agent channel, and human-in-the-loop changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hosted-webhooks-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

github-advanced-security[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch from b4b9f89 to 274c3ab Compare August 8, 2026 08:06
@ericallam
ericallam changed the base branch from main to feat/hosted-webhook-ingress August 8, 2026 08:06
@pkg-pr-new

pkg-pr-new Bot commented Aug 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/build@693c646

trigger.dev

npm i https://pkg.pr.new/triggerdotdev/trigger.dev@693c646

@trigger.dev/core

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/core@693c646

@trigger.dev/python

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/python@693c646

@trigger.dev/react-hooks

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/react-hooks@693c646

@trigger.dev/redis-worker

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/redis-worker@693c646

@trigger.dev/rsc

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/rsc@693c646

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/schema-to-json@693c646

@trigger.dev/slack

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/slack@693c646

@trigger.dev/sdk

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/sdk@693c646

commit: 693c646

@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch 11 times, most recently from 8f0664d to 0a23edf Compare August 9, 2026 18:23
@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch 4 times, most recently from 38468bb to 2718f32 Compare August 9, 2026 22:26
@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch 4 times, most recently from 39c0585 to 0debb64 Compare August 11, 2026 05:23
coderabbitai[bot]

This comment was marked as resolved.

…fter one decision

defaultSlackFinalizeInteraction re-rendered the message by dropping every actions
block, so once one tool call was approved or denied the other pending tool calls
in the same message lost their buttons and their turn could not resume. Drop only
the resolved tool call's actions block (matched by the toolCallId:: button value
prefix) and keep the rest.
…rl-secret warning

The typed-event example now passes webhooks.stripe<Stripe.Event>() to webhook()
so it actually demonstrates typed access to event. The verification section no
longer claims every idempotency key comes from a provider event id (it documents
the raw-body/timestamp/signature fallback), and adds a warning that url-secret
exposes the secret in the URL.
…st harness

sendChannelEvent/deliverChannelEvent default connectorId to DEFAULT_TEST_CONNECTOR_ID,
not to a sole configured connector's id as the previous doc implied. Describe the
actual default (which lines up with recordingChannelConnector's own default id).
@ericallam
ericallam marked this pull request as ready for review August 16, 2026 20:58
devin-ai-integration[bot]

This comment was marked as resolved.

…channel interaction

A duplicate or late channel interaction (e.g. a repeated Slack approval
click) whose toolCallId matches no pending tool call was dropped without
detaching the per-turn session.in listener. That listener marks inbound
records CONSUMED, so after the drop the next user message was swallowed
into pendingWireMessages (already drained before the wait) and the turn
idled until the timeout, ending the run and losing the message.

Mirror the action path: detach msgSub and decrement turn (a dropped
interaction is not a turn) before falling through to the wait.
devin-ai-integration[bot]

This comment was marked as resolved.

…l error callbacks

Two channel-connector fixes in the chat.agent turn loop:

- Under a pendingMessages-enabled agent, the mid-turn session.in handler
  returned early for any record without a steerable `.message`. Channel
  deliveries carry `channelEvent` and no `.message`, so an inbound Slack
  message or block_actions approval click arriving mid-turn was neither
  steered nor buffered, and never became a turn. Channel records now fall
  through to pendingWireMessages so they dispatch as the next turn.

- In the turn error handler, resolveReactionChoice (which invokes the
  user's reactions.error callback) and channelConn.outbound ran unguarded.
  A throw escaped the catch, skipping the error chunk, turn-complete, and
  wait-for-next-message, killing the whole conversation. Both are now
  wrapped so a bad user callback degrades to a warn.
devin-ai-integration[bot]

This comment was marked as resolved.

…eMessages agents

A channel interaction (e.g. a Slack approval click) is resolved at the
top of the turn by matching its toolCallId against the in-memory
accumulator. For agents that register hydrateMessages, the accumulator is
empty on a fresh continuation / OOM-retry boot (those agents own
persistence, so the boot seeding block is skipped and the hydrate hook
only runs later in the turn). The click therefore matched nothing, was
classified as a stale interaction, and was dropped, losing the human's
decision and leaving the paused turn unresolved.

When an interaction arrives and hydrateMessages is registered but the
accumulator is empty, load the persisted chain via the hydrate hook first
so the interaction can find its pending tool call. The per-turn hydrate
later in the turn still runs to persist the synthesized resolution
message. Guarded on an empty accumulator so warm turns don't re-hydrate.
devin-ai-integration[bot]

This comment was marked as resolved.

…l error egress

- buildInteractionResolutionMessage matched a tool part by toolCallId
  without checking it was still input-available, so a duplicate or
  retried approval/deny click on an already-answered tool call built a
  fresh resolution and ran the decision a second time. Gate on
  state === 'input-available' so a resolved call resolves to a dropped
  stale interaction, matching the function's documented contract.

- The turn-error channel egress only edited the start-of-turn ack
  placeholder, so a connector configured with ack: null posted nothing on
  a failed turn (the user saw silence). Send a fresh final message when
  there is no placeholder ref. The error text is now run through the
  agent's onError option (same sanitizer the browser stream uses) instead
  of forwarding turnError.message verbatim to the channel surface.
…'t hit a TDZ

The channel-interaction pre-hydrate added for hydrateMessages agents
referenced clientData, but clientData was declared with const later in the
same turn block. The early reference hit the temporal dead zone and threw
ReferenceError, which the local try/catch swallowed, so hydration silently
never ran and the button click was still dropped as a stale interaction.
Parse clientData once above the channel-event block so both the interaction
hydrate and the rest of the turn share the same value.
devin-ai-integration[bot]

This comment was marked as resolved.

…acing, don't clobber posted answers

- channelReplyText selected parts with 'text' in p, which also matches AI
  SDK reasoning parts ({ type: 'reasoning', text }). With sendReasoning
  defaulting to true, a reasoning-capable model's private chain-of-thought
  was concatenated into the text posted to Slack. Narrowed the selection to
  parts whose type is exactly 'text'.

- normalizeKeyString namespaced only the whole placeholder, so a fallback
  key like {a || b} (the Slack connector's DEFAULT_KEY form) left every
  alternative after the first bare, diverging from ValidatedWebhookKey which
  validates each || side and defaults bare sides to the body. It now splits
  on ||, trims, and namespaces each alternative. Added test cases.

- On the turn-error path the error text was sent with previousRef pointing
  at the ack placeholder. If a hook after the final answer was posted threw
  (e.g. onTurnComplete), the delivered answer was edited into the error
  string. Track whether the final answer was posted and, if so, send the
  error as a fresh follow-up instead of editing over the answer.
devin-ai-integration[bot]

This comment was marked as resolved.

…ll is absent; unspace Slack guard in-list

- The channel-interaction pre-hydrate was gated on an empty accumulator, so
  a hydrate-backed agent whose in-memory accumulator was non-empty but no
  longer held the pending tool part (after a chat.history mutation or a
  compaction trim) would skip the hydrate and drop the click as stale. Gate
  instead on the pending toolCallId being absent from the accumulator, so
  the persisted chain is reloaded whenever the call can't be found (a
  duplicate click on an already-resolved call still short-circuits without a
  needless hydrate, since the call is present).

- Unspace the Slack SELF_MESSAGE_GUARD in-list to match the documented
  filter style. The runtime parser already skips whitespace so this is
  cosmetic, but it keeps the guard consistent with the docs example.
devin-ai-integration[bot]

This comment was marked as resolved.

…, skip unroutable channel turns

- webhooks.gitlab used the Svix preset (svix-id/svix-timestamp/svix-signature,
  HMAC over {id}.{timestamp}.{body}), but GitLab doesn't sign bodies: it echoes
  the configured token in X-Gitlab-Token. Every GitLab delivery failed
  verification. Switched it to the shared-secret scheme (header, x-gitlab-token).

- The Slack connector declared secretProvisioning 'integrator', so the Connect
  panel offered a Generate-secret button, but Slack mints its own Signing Secret
  that must be pasted (core already classifies slack as 'provider'). Changed to
  'provider'.

- A verified channel delivery naming a connector the running agent doesn't have
  fell through to a normal turn with an empty incoming message, so the agent
  answered nothing and burned tokens. Handle the unresolved-connector case like a
  stale interaction: warn with the connectorId/deliveryId and skip the turn
  without consuming a turn.
devin-ai-integration[bot]

This comment was marked as resolved.

…ocks

A text-only reply issued chat.update with text alone, so Slack kept whatever
blocks were already on the message (e.g. approval controls previously rendered
into the same ts). Send blocks: [] on edits when the outgoing message has no
blocks, so an edit is authoritative. postMessage still omits blocks when empty.
…ai.ts formatting

The SELF_MESSAGE_GUARD in-list was unspaced earlier to match the documented
filter style, but its unit test still asserted the spaced substring. Update the
assertion. Also apply oxfmt to a wrapped line in the channel error-reaction
block that exceeded the width limit.
devin-ai-integration[bot]

This comment was marked as resolved.

…n't fail a delivered turn

The turn-start 'working' resolver and the success-path 'done' resolver both
awaited user-supplied reactions.working / reactions.done callbacks unguarded. A
rejection propagated into the turn catch, which then posted an error to the
channel and skipped onTurnComplete / the snapshot write, turning an
already-delivered answer into a reported failure. Wrapped both in try/catch that
degrade to a warn, matching the error-path reaction handling.
devin-ai-integration[bot]

This comment was marked as resolved.

The generic webhooks.slack() SDK source was auto-generated from the core provider
config, which carries only the HMAC verifier. Slack additionally needs the
one-time url_verification handshake (before an Event Subscriptions URL can be
saved) and form-encoded interactivity parsing, so an endpoint built from
webhooks.slack() could never be connected.

Drop slack from the generated SDK producers (runtime + type) and export
webhookSource() from @trigger.dev/slack, which carries the handshake + formPayload
the connector already uses. Users get a plain Slack webhook source from
webhookSource(), or the full chat channel from slack(). Core's slack config stays
for server-side verification.
devin-ai-integration[bot]

This comment was marked as resolved.

…ress

In stream delivery the debounce editor's stop() cleared the pending timer but
returned immediately even with an edit request already in flight. The turn then
issued the authoritative final edit to the same message ref, so a slower
in-flight partial-text edit could land after it and leave truncated text as the
last write. stop() now awaits any in-flight edit (and makeChannelStreamTap's
flush/cancel and the turn finally await stop()), so the final edit is always the
last write.
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.

2 participants