Skip to content

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

Draft
ericallam wants to merge 1 commit into
feat/hosted-webhook-ingressfrom
feat/hosted-webhooks-api
Draft

feat(sdk,slack): webhook sources, agent channels, and human-in-the-loop#4537
ericallam wants to merge 1 commit into
feat/hosted-webhook-ingressfrom
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: 1b6a78d

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/testcontainers Patch
@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

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

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ff85b0f7-83dd-4c0b-aa12-421a209296d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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. 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
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description explains the changes and merge plan but omits most required template sections, including testing and changelog details. Add the issue closure, checklist, testing steps, changelog, and screenshots sections, and complete each section or state why it does not apply.
✅ Passed checks (3 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main SDK, webhook, Slack channel, and human-in-the-loop changes.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • 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.

Comment thread packages/slack/src/index.ts Fixed
Comment thread packages/trigger-sdk/src/v3/webhooks.ts Fixed

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 14

🧹 Nitpick comments (9)
packages/slack/src/index.ts (3)

279-283: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Check the response_url response.

The code ignores the fetch result. If Slack rejects the replace (expired response_url, invalid blocks, or a non-2xx status), the buttons stay live and clickable, and no signal reaches the caller.

The connector contract treats a throw here as best-effort and logs it. Throw on failure so the outcome is visible.

♻️ Proposed change
-  await fetch(responseUrl, {
+  const res = await fetch(responseUrl, {
     method: "POST",
     headers: { "content-type": "application/json" },
     body: JSON.stringify({ replace_original: true, text: `${decision}${who}`, blocks }),
   });
+  if (!res.ok) {
+    throw new Error(`slack response_url replace failed: ${res.status}`);
+  }

356-368: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Handle Slack rate limiting in makeSlackSend.

The code retries only on an auth error and only when token is a function. Slack rate-limits chat.postMessage and chat.update per channel (about one message per second, with Retry-After). With delivery: "stream", debounced edits reach that limit quickly. Each ratelimited response then throws and fails the turn.

Add a bounded retry with a delay for ratelimited.

♻️ Proposed change
     let result = await post();
     // Re-resolve once on an auth error (token rotation) when a resolver was supplied.
     if (!result.ok && typeof token === "function" && isAuthError(result.error)) {
       botToken = await resolve();
       result = await post();
     }
+    // Slack rate limits chat.* per channel; retry a bounded number of times.
+    for (let attempt = 0; attempt < 3 && !result.ok && result.error === "ratelimited"; attempt++) {
+      await new Promise((r) => setTimeout(r, (result.retryAfterSeconds ?? 1) * 1000));
+      result = await post();
+    }

400-408: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Check the HTTP status before you parse the body.

slackApi calls res.json() for every response. Slack returns a non-JSON body for some non-2xx responses, for example a 429 or a 5xx from the edge. res.json() then rejects with a parse error, and the caller reports that instead of the real status. The retry-after header is also lost.

Return a structured error for a non-2xx response.

♻️ Proposed change
 async function slackApi(
   baseUrl: string,
   method: string,
   token: string,
   body: Record<string, unknown>
-): Promise<{ ok: boolean; ts?: string; error?: string }> {
+): Promise<{ ok: boolean; ts?: string; error?: string; retryAfterSeconds?: number }> {
   const res = await fetch(`${baseUrl}/${method}`, {
     method: "POST",
     headers: {
       "content-type": "application/json; charset=utf-8",
       authorization: `Bearer ${token}`,
     },
     body: JSON.stringify(body),
   });
+  if (!res.ok) {
+    const retryAfter = Number(res.headers?.get?.("retry-after"));
+    return {
+      ok: false,
+      error: res.status === 429 ? "ratelimited" : `http_${res.status}`,
+      retryAfterSeconds: Number.isFinite(retryAfter) ? retryAfter : undefined,
+    };
+  }
   return (await res.json()) as { ok: boolean; ts?: string; error?: string };
 }

Note: the test doubles in packages/slack/src/index.test.ts return objects with only a json method. Add ok: true (and headers) to those doubles if you apply this change.

packages/slack/package.json (1)

43-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider moving @trigger.dev/core to devDependencies.

packages/slack/src/index.ts imports from @trigger.dev/core/v3 with import type only. No runtime value comes from core. Keeping core as a runtime dependency lets a consumer install a second core copy next to the one that @trigger.dev/sdk already pulls in.

If no runtime import appears later, move it to devDependencies, or add it as a peer alongside @trigger.dev/sdk.

packages/slack/src/index.test.ts (2)

11-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore globals in afterEach.

Each test calls vi.unstubAllGlobals() as its last statement. If an assertion fails first, or an awaited call rejects, that statement never runs. The fetch stub then leaks into the following tests, and one failure cascades into unrelated failures.

Move the cleanup into an afterEach hook and remove the per-test calls.

♻️ Proposed change
-import { describe, expect, it, vi } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
 import { mentions, slack, toSlackMrkdwn, type SlackMessageEvent } from "./index.js";
@@
 describe("slack channel", () => {
+  afterEach(() => {
+    vi.unstubAllGlobals();
+  });
+

250-260: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the token-resolver retry path.

makeSlackSend in packages/slack/src/index.ts re-resolves the token and retries once when the token is a function and the first call returns an auth error (lines 356-361). No test covers that branch, and no test covers a function-valued token.

Add a case where token is a resolver, the first response is { ok: false, error: "invalid_auth" }, and the second succeeds. Assert two fetch calls and the second authorization header.

Do you want me to write that test?

packages/trigger-sdk/src/v3/webhooks.ts (1)

345-359: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the 14 repeated members with one typeof webhookSources.

Each member restates typeof webhookSources.X, and lines 368-381 restate the same 14 keys again. Any new producer requires three edits. An intersection keeps the list in one place.

♻️ Proposed refactor
   /** Header name used for webhook signatures */
   SIGNATURE_HEADER_NAME: string;
-  custom: typeof webhookSources.custom;
-  stripe: typeof webhookSources.stripe;
-  github: typeof webhookSources.github;
-  svix: typeof webhookSources.svix;
-  square: typeof webhookSources.square;
-  discord: typeof webhookSources.discord;
-  clerk: typeof webhookSources.clerk;
-  resend: typeof webhookSources.resend;
-  openai: typeof webhookSources.openai;
-  replicate: typeof webhookSources.replicate;
-  recallai: typeof webhookSources.recallai;
-  brex: typeof webhookSources.brex;
-  gitlab: typeof webhookSources.gitlab;
-  whatsapp: typeof webhookSources.whatsapp;
 }

Then declare the instance as Webhooks & ProviderProducers & typeof webhookSources and spread ...webhookSources in place of the 14 assignments.

packages/trigger-sdk/src/v3/ai.ts (1)

4793-4832: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Declare ChannelConnector as a type alias.

Every sibling in this block is a type. ChannelConnector is a data descriptor built by factory functions, not a behavioral contract that a class implements, so the repository rule applies.

Based on learnings, keep interface only for method-shape contracts that collaborators implement; this is a data shape.

As per coding guidelines: "Use types over interfaces for TypeScript".

♻️ Proposed change
-export interface ChannelConnector<TEvent = unknown> {
+export type ChannelConnector<TEvent = unknown> = {
   id: string;

Close with }; instead of }.

Sources: Coding guidelines, Learnings

packages/trigger-sdk/src/v3/channelReactions.test.ts (1)

9-15: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a case for a resolver that throws.

resolveReactionChoice awaits a user-supplied function and does not catch. The tests cover undefined, null, "", and [], but not a throw.

The behavior matters at the call sites. At packages/trigger-sdk/src/v3/ai.ts line 6991 the call sits inside the turn try, so a throw becomes a turn error. At line 8305 the call runs after the turn already completed, and at line 8578 it runs inside the error handler. A throwing reactions.done or reactions.error resolver escapes there.

Every other reaction step is best-effort: applyChannelReaction catches and logs. Make resolveReactionChoice match, then assert it here.

💚 Proposed test and matching guard
   it("skips when absent or empty", async () => {
     expect(await resolveReactionChoice(undefined, {})).toBeUndefined();
     expect(await resolveReactionChoice("", {})).toBeUndefined();
     expect(await resolveReactionChoice([], {})).toBeUndefined();
     expect(await resolveReactionChoice(() => undefined, {})).toBeUndefined();
     expect(await resolveReactionChoice(() => null, {})).toBeUndefined();
   });
+
+  it("skips when the resolver throws", async () => {
+    expect(
+      await resolveReactionChoice(() => {
+        throw new Error("boom");
+      }, {})
+    ).toBeUndefined();
+  });

In packages/trigger-sdk/src/v3/ai.ts:

 export async function resolveReactionChoice(
   choice: ChannelReactionChoice | undefined,
   event: unknown
 ): Promise<string | undefined> {
   if (choice == null) return undefined;
-  let value: string | string[] | null | undefined =
-    typeof choice === "function" ? await choice(event) : choice;
+  let value: string | string[] | null | undefined;
+  try {
+    value = typeof choice === "function" ? await choice(event) : choice;
+  } catch (error) {
+    logger.warn("chat.agent: reaction resolver threw; skipping reaction", { error });
+    return undefined;
+  }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 30f7bd73-ce46-4805-a3fb-8bb30dc30f3f

📥 Commits

Reviewing files that changed from the base of the PR and between c526528 and b4b9f89.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • .changeset/hosted-webhook-ingress.md
  • docs/ai-chat/backend.mdx
  • docs/ai-chat/reference.mdx
  • docs/docs.json
  • docs/webhooks/channels.mdx
  • docs/webhooks/connect.mdx
  • docs/webhooks/deliveries.mdx
  • docs/webhooks/filters.mdx
  • docs/webhooks/human-in-the-loop.mdx
  • docs/webhooks/overview.mdx
  • docs/webhooks/session-routing.mdx
  • docs/webhooks/sources.mdx
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/package.json
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/slack/tsconfig.json
  • packages/slack/tsconfig.src.json
  • packages/slack/vitest.config.ts
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 12)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp
🧰 Additional context used
📓 Path-based instructions (13)
**/tsconfig.json

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use strict mode in TypeScript configuration

Files:

  • packages/slack/tsconfig.json
docs/**/docs.json

📄 CodeRabbit inference engine (docs/CLAUDE.md)

docs/**/docs.json: Main documentation config must be defined in docs.json which includes navigation structure, theme, and metadata
Navigation structure in docs.json should be organized using navigation.dropdowns with groups and pages

Files:

  • docs/docs.json
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/CLAUDE.md)

docs/**/*.mdx: MDX documentation pages must include frontmatter with title (required), description (required), and sidebarTitle (optional) in YAML format
Use Mintlify components for structured content: , , , , , , /, /
Always import from @trigger.dev/sdk in code examples (never from @trigger.dev/sdk/v3)
Code examples must be complete and runnable where possible
Use language tags in code fences: typescript, bash, json

Documentation in docs/ uses MDX conventions defined by the documentation guidance.

Files:

  • docs/webhooks/connect.mdx
  • docs/webhooks/deliveries.mdx
  • docs/webhooks/human-in-the-loop.mdx
  • docs/webhooks/overview.mdx
  • docs/webhooks/filters.mdx
  • docs/ai-chat/reference.mdx
  • docs/webhooks/session-routing.mdx
  • docs/webhooks/channels.mdx
  • docs/webhooks/sources.mdx
  • docs/ai-chat/backend.mdx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: Prefer static imports over dynamic import(); use dynamic imports only for unresolvable circular dependencies, genuine performance code splitting, or conditional runtime loading.
Import Trigger.dev tasks from @trigger.dev/sdk; never use @trigger.dev/sdk/v3 or deprecated client.defineJob.
Add agentcrumbs while writing code using approved namespaces; mark lines with // @Crumbs or blocks with `// `#region` `@crumbs, and strip them before merging.

Files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/cli-v3/src/entryPoints/**/*

📄 CodeRabbit inference engine (packages/cli-v3/CLAUDE.md)

Code in src/entryPoints/ runs inside customer containers and is a different runtime environment from the CLI - changes affect deployed task execution directly

Files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
packages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

For public packages, use build for verification.

Files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/trigger-sdk/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/trigger-sdk/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (packages/trigger-sdk/CLAUDE.md)

Always import from @trigger.dev/sdk. Never use @trigger.dev/sdk/v3 (deprecated path alias)

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/cli-v3/src/dev/**/*

📄 CodeRabbit inference engine (packages/cli-v3/CLAUDE.md)

Dev mode code should be located in src/dev/ and runs tasks locally in the user's Node.js process without containers

Files:

  • packages/cli-v3/src/dev/devSupervisor.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use vitest for all tests in the Trigger.dev repository

**/*.{test,spec}.{ts,tsx}: Use Vitest exclusively and never mock dependencies; use Testcontainers for integration dependencies.
Place test files next to the source files they test.

Files:

  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/slack/src/index.test.ts
**/package.json

📄 CodeRabbit inference engine (AGENTS.md)

When adding Zod, use the exact repository-wide pinned version 3.25.76, never a different version or range.

Files:

  • packages/slack/package.json
🧠 Learnings (21)
📚 Learning: 2026-03-10T12:44:14.176Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3200
File: docs/config/config-file.mdx:353-368
Timestamp: 2026-03-10T12:44:14.176Z
Learning: In the trigger.dev repo, docs PRs are often companions to implementation PRs. When reviewing docs PRs (MDX files under docs/), check the PR description for any companion/related PR references and verify that the documented features exist in those companion PRs before flagging missing implementations. This ensures docs stay in sync with code changes across related PRs.

Applied to files:

  • docs/webhooks/connect.mdx
  • docs/webhooks/deliveries.mdx
  • docs/webhooks/human-in-the-loop.mdx
  • docs/webhooks/overview.mdx
  • docs/webhooks/filters.mdx
  • docs/ai-chat/reference.mdx
  • docs/webhooks/session-routing.mdx
  • docs/webhooks/channels.mdx
  • docs/webhooks/sources.mdx
  • docs/ai-chat/backend.mdx
📚 Learning: 2026-04-30T20:30:29.458Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3226
File: docs/ai-chat/quick-start.mdx:13-13
Timestamp: 2026-04-30T20:30:29.458Z
Learning: In this repo’s documentation MDX files (`docs/**/*.mdx`), use `ts` and `tsx` (not `typescript`) as the code-fence language tags for TypeScript/TSX snippets. Do not flag `ts`/`tsx` code-fence language tags as incorrect in any docs MDX file, since this is the site-wide Mintlify-compatible convention.

Applied to files:

  • docs/webhooks/connect.mdx
  • docs/webhooks/deliveries.mdx
  • docs/webhooks/human-in-the-loop.mdx
  • docs/webhooks/overview.mdx
  • docs/webhooks/filters.mdx
  • docs/ai-chat/reference.mdx
  • docs/webhooks/session-routing.mdx
  • docs/webhooks/channels.mdx
  • docs/webhooks/sources.mdx
  • docs/ai-chat/backend.mdx
📚 Learning: 2026-03-22T13:26:12.060Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: apps/webapp/app/components/code/TextEditor.tsx:81-86
Timestamp: 2026-03-22T13:26:12.060Z
Learning: In the triggerdotdev/trigger.dev codebase, do not flag `navigator.clipboard.writeText(...)` calls for `missing-await`/`unhandled-promise` issues. These clipboard writes are intentionally invoked without `await` and without `catch` handlers across the project; keep that behavior consistent when reviewing TypeScript/TSX files (e.g., usages like in `apps/webapp/app/components/code/TextEditor.tsx`).

Applied to files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-22T19:24:14.403Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3187
File: apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts:200-204
Timestamp: 2026-03-22T19:24:14.403Z
Learning: In the triggerdotdev/trigger.dev codebase, webhook URLs are not expected to contain embedded credentials/secrets (e.g., fields like `ProjectAlertWebhookProperties` should only hold credential-free webhook endpoints). During code review, if you see logging or inclusion of raw webhook URLs in error messages, do not automatically treat it as a credential-leak/secrets-in-logs issue by default—first verify the URL does not contain embedded credentials (for example, no username/password in the URL, no obvious secret/token query params or fragments). If the URL is credential-free per this project’s conventions, allow the logging.

Applied to files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-05-18T08:21:27.694Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3632
File: apps/webapp/sentry.server.ts:4-21
Timestamp: 2026-05-18T08:21:27.694Z
Learning: When handling Prisma error P1001 ("Can't reach database server") in TypeScript, don’t assume a single error shape. Prisma can surface P1001 via two different error classes/fields: `PrismaClientKnownRequestError` exposes it as `err.code === "P1001"` (common during mid-query connection drops), while `PrismaClientInitializationError` exposes it as `err.errorCode === "P1001"` (common on client startup failure). Therefore, predicates should use `err.code === "P1001" || err.errorCode === "P1001"`. Do not flag `err.code === "P1001"` as “unreachable/never matches,” as it is expected in production.

Applied to files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-05-18T08:21:27.694Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3632
File: apps/webapp/sentry.server.ts:4-21
Timestamp: 2026-05-18T08:21:27.694Z
Learning: When handling Prisma errors for P1001 ("Can't reach database server"), do not assume it only appears under a single property name. Prisma may surface P1001 via either `PrismaClientKnownRequestError` (`err.code === "P1001"`, e.g., mid-query connection drops) or `PrismaClientInitializationError` (`err.errorCode === "P1001"`, e.g., client startup connection failure). To reliably detect the condition, check `err.code === "P1001" || err.errorCode === "P1001"`, and avoid review rules that would incorrectly flag `err.code === "P1001"` as unreachable/never-matching.

Applied to files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-06-13T19:53:13.759Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3937
File: packages/trigger-sdk/skills/realtime-and-frontend/SKILL.md:258-260
Timestamp: 2026-06-13T19:53:13.759Z
Learning: When reviewing code that uses `trigger.dev/react-hooks`’s `useRealtimeRun`, preserve the call signature where the first argument is the full realtime handle object (not `handle.id`). This is intentional to maintain type-safety and is consistent with the official docs; do not suggest changing the first argument from the handle object to `handle.id`.

Applied to files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-06-17T17:13:49.929Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3948
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx:48-62
Timestamp: 2026-06-17T17:13:49.929Z
Learning: In triggerdotdev/trigger.dev, within `dashboardLoader`/`dashboardAction` (or similar context resolver code) whenever you resolve an organization ID from an organization slug for RBAC/enterprise authorization scope, always read from the primary Prisma client (`prisma`), not `$replica`. Using `$replica` can hit replica-lag and cause the RBAC lookup/authorization to run without the correct org scope (bypassing intended role enforcement). Implement the slug→org lookup with `prisma.organization.findFirst(...)` (or equivalent primary-client query) and add an inline comment documenting why the primary client is required (replica lag could lead to unscoped RBAC checks).

Applied to files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-06-23T13:04:21.413Z
Learnt from: carderne
Repo: triggerdotdev/trigger.dev PR: 4023
File: apps/webapp/app/services/upsertBranch.server.ts:14-18
Timestamp: 2026-06-23T13:04:21.413Z
Learning: In TypeScript, it’s valid to `import { type X }` and then use `typeof X` in a type-only position, e.g. `type Alias = z.infer<typeof X>`. The `type` modifier suppresses the runtime import, but the type checker still has the full exported type so `z.infer<typeof X>` can resolve correctly. In code reviews, don’t flag this as a TypeScript compile error as long as `typeof X` is used in a type context (e.g., with `z.infer`, `type` aliases, generics), not as a runtime value.

Applied to files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-06-04T18:16:35.386Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3836
File: apps/supervisor/src/backpressure/backpressureMonitor.ts:3-5
Timestamp: 2026-06-04T18:16:35.386Z
Learning: When reviewing TypeScript in this repo, apply the rule “prefer type aliases over interfaces” only to data/object shapes and union/intersection type modeling. If an interface is being used as a behavioral contract for collaborators to implement (e.g., method-shape interfaces that define required behavior, such as `BackpressureLogger` / `BackpressureSignalSource` in `apps/supervisor/src/backpressure/backpressureMonitor.ts`), keep it as an `interface` and do not flag it as a type-alias-vs-interface violation.

Applied to files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-06-09T17:58:04.699Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 3879
File: apps/webapp/app/models/vercelIntegration.server.ts:619-630
Timestamp: 2026-06-09T17:58:04.699Z
Learning: In this codebase, outbound raw `fetch` calls should typically rely on Node/undici’s default request timeout (about ~300s) rather than adding a per-call `AbortController` + `setTimeout` wrapper inside individual functions (e.g. in files like `apps/webapp/app/models/vercelIntegration.server.ts`). During code review, do not flag the absence of a per-call timeout on a single `fetch` as an issue; if per-call timeouts are needed, they should be implemented via a codebase-wide convention (e.g., a shared fetch wrapper or documented pattern) rather than ad-hoc per-function changes.

Applied to files:

  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/cli-v3/src/dev/devSupervisor.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/slack/vitest.config.ts
  • packages/slack/src/index.test.ts
  • packages/slack/src/index.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-31T21:37:27.212Z
Learnt from: isshaddad
Repo: triggerdotdev/trigger.dev PR: 3283
File: docs/migration-n8n.mdx:19-21
Timestamp: 2026-03-31T21:37:27.212Z
Learning: When reviewing code in `packages/trigger-sdk/src/v3`, treat `tasks.triggerAndWait()` and `tasks.batchTriggerAndWait()` as real exported APIs. They are defined in `shared.ts` and re-exported via the `tasks` object in `tasks.ts`, and they take the task ID string as their first argument (not a task instance). This is distinct from the instance methods `yourTask.triggerAndWait()` and `yourTask.batchTriggerAndWait()`. Do not flag calls to `tasks.triggerAndWait()` or `tasks.batchTriggerAndWait()` as non-existent or incorrectly invoked.

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-05-17T08:08:12.370Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3644
File: packages/trigger-sdk/src/v3/ai.ts:8695-8746
Timestamp: 2026-05-17T08:08:12.370Z
Learning: In the Trigger v3 session resume/streams logic, ensure session resumption uses sequence cursors rather than timestamps. Specifically: for each turn-complete control record written to `session.out`, include a `session-in-event-id` header whose value is the committed-consume cursor (`session.in.lastDispatchedSeqNum`). On boot/resume, scan `session.out` for the latest turn-complete record, read the `session-in-event-id` header, and seed the `sessionStreams` manager for `.in` using both `lastSeqNum` and `lastDispatchedSeqNum` so previously processed user messages are not replayed. Do not use `setMinTimestamp`/`lastOutTimestamp` for resume ordering in this flow.

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-05-18T14:19:56.437Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3655
File: packages/trigger-sdk/src/v3/ai.ts:8667-8731
Timestamp: 2026-05-18T14:19:56.437Z
Learning: In the Trigger SDK (v3) when making raw `fetch` calls to the Trigger API (including override paths such as `createChatStartSessionAction`), set the request headers to match `ApiClient`: `Content-Type`, `Authorization`, and `x-trigger-source: "sdk"`. Also forward the current preview branch by setting `x-trigger-branch` to `apiClientManager.branchName`. Prefer using the shared `overrideRequestHeaders(accessToken)` helper instead of manually constructing headers, so requests route correctly to preview environments.

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-05-19T22:37:47.286Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3671
File: packages/trigger-sdk/test/recovery-boot.test.ts:456-457
Timestamp: 2026-05-19T22:37:47.286Z
Learning: In `packages/trigger-sdk` (Trigger.dev SDK), `logger.warn` (and other SDK logger methods) should route to the Trigger.dev structured logger sink, not to `console.warn`. In SDK tests, `vi.spyOn(console, "warn")` (or similar console spies) should only be used to suppress stray console output; reviewers should not suggest asserting on `console.warn` spies to verify SDK-internal warning/fallback log behavior. Use the SDK’s structured-logger outputs/capture approach instead of console spies.

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/trigger-sdk/src/v3/webhooks.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-05-18T14:40:02.173Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3658
File: packages/core/src/v3/realtimeStreams/manager.test.ts:1-147
Timestamp: 2026-05-18T14:40:02.173Z
Learning: In this repo’s trigger.dev codebase, the “never mock — use testcontainers” guideline should only be applied to integration tests that talk to real external services (e.g., Redis, Postgres, S2). For unit tests that validate in-memory logic (e.g., deduplication/cache behavior in StandardRealtimeStreamsManager and similar module-boundary call counting), it is allowed to use Vitest mocks like `vi.fn()` and to stub/mock `ApiClient` objects to count calls or simulate in-process collaborators. Do not flag `vi.fn()`-based mocks as policy violations in these unit-test scenarios; reserve the rule for true external-service integration tests.

Applied to files:

  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/slack/src/index.test.ts
📚 Learning: 2026-05-18T14:40:02.173Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3658
File: packages/core/src/v3/realtimeStreams/manager.test.ts:1-147
Timestamp: 2026-05-18T14:40:02.173Z
Learning: In the triggerdotdev/trigger.dev repo, the policy “Never mock anything — use testcontainers instead” should only be enforced for integration tests that interact with real external services (e.g., Redis, Postgres) via actual infrastructure. For unit tests that exercise pure in-memory logic (e.g., cache semantics) it is OK to stub collaborators such as `ApiClient` using Vitest (`vi.fn()`) to assert call counts or control behavior. Do not flag `vi.fn()`-based `ApiClient` stubs in unit tests as violations of the testcontainers policy.

Applied to files:

  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/slack/src/index.test.ts
📚 Learning: 2026-06-16T09:19:47.637Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3960
File: apps/webapp/test/prismaInfrastructureErrorCapture.test.ts:0-0
Timestamp: 2026-06-16T09:19:47.637Z
Learning: In this repo’s Vitest setup, `vitest.config.ts` uses `globals: true`, so identifiers like `vi`, `describe`, `it`, and `expect` are available as globals in Vitest test files. During code review, do not flag missing `vi`/`describe`/`it`/`expect` imports as a runtime error or correctness issue when they’re used in `*.test.ts/tsx` or `*.spec.ts/tsx` files. Explicit imports are still preferred for consistency, but they’re not required for runtime behavior.

Applied to files:

  • packages/trigger-sdk/src/v3/channelReactions.test.ts
  • packages/slack/src/index.test.ts
📚 Learning: 2026-05-01T15:45:08.099Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3499
File: packages/plugins/tsup.config.ts:3-3
Timestamp: 2026-05-01T15:45:08.099Z
Learning: In build/tool configuration files (e.g., tsup.config.ts, vite.config.ts, vitest.config.ts), follow the tool’s documented export pattern and use `export default defineConfig(...)` (or the equivalent documented default export). The repo-wide guideline “use named exports instead of default exports” should apply only to application code (*.{ts,tsx,js,jsx}), not to these build/tool config files—so do not flag `export default defineConfig(...)` in these config files as a violation.

Applied to files:

  • packages/slack/vitest.config.ts
📚 Learning: 2026-06-16T13:14:09.440Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3964
File: docs/ai-chat/reference.mdx:482-482
Timestamp: 2026-06-16T13:14:09.440Z
Learning: When documenting or reviewing usage of `ChatTurn.complete(source?)` (in `packages/trigger-sdk/src/v3/ai.ts`), note that `source` is optional (`source?: UIMessageStreamable`). Calling `complete()` with no `source` is valid specifically for a final head-start handover (`handover.isFinal`), because the warm partial already contains the response. If examples or guidance omit `source`, ensure they are in this final-hand-over context so they remain correct.

Applied to files:

  • docs/ai-chat/reference.mdx
  • docs/ai-chat/backend.mdx
📚 Learning: 2026-06-16T13:14:14.382Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3964
File: docs/ai-chat/reference.mdx:478-478
Timestamp: 2026-06-16T13:14:14.382Z
Learning: When reviewing RC-gated `ai-chat` docs under `docs/ai-chat/`, don’t immediately flag missing SDK type fields or implementation details just because the field isn’t present on the docs branch yet. Instead, find and cross-check the companion implementation PR that’s intended to land alongside the docs PR, and only report missing/incorrect fields if they are also absent in the companion SDK/type changes.

Applied to files:

  • docs/ai-chat/reference.mdx
  • docs/ai-chat/backend.mdx
🪛 GitHub Actions: 📦 Preview packages (pkg.pr.new) / 0_Build and publish previews.txt
packages/trigger-sdk/src/v3/ai.ts

[error] 39-39: TypeScript build failed: Module '@trigger.dev/core/v3' has no exported member 'AnyChatEvent' (TS2305). Failed command: tshy.

🪛 GitHub Actions: 📦 Preview packages (pkg.pr.new) / Build and publish previews
packages/trigger-sdk/src/v3/ai.ts

[error] 39-39: TypeScript build failed in '@trigger.dev/sdk:build': Module '@trigger.dev/core/v3' has no exported member 'AnyChatEvent' (TS2305).

🪛 GitHub Check: code-quality / code-quality
packages/slack/src/index.test.ts

[warning] 97-97: eslint(no-unsafe-optional-chaining)
Unsafe usage of optional chaining


[warning] 48-48: eslint(no-unsafe-optional-chaining)
Unsafe usage of optional chaining

🪛 GitHub Check: CodeQL
packages/slack/src/index.ts

[failure] 306-307: Polynomial regular expression used on uncontrolled data
This regular expression that depends on library input may run slow on strings starting with '[' and with many repetitions of '[\'.
This regular expression that depends on library input may run slow on strings starting with '[\](http://' and with many repetitions of '[!](http://'.

packages/trigger-sdk/src/v3/webhooks.ts

[failure] 309-313: Polynomial regular expression used on uncontrolled data
This regular expression that depends on library input may run slow on strings starting with '{{' and with many repetitions of '{{|'.
This regular expression that depends on library input may run slow on strings starting with '{{' and with many repetitions of '{{|'.
This regular expression that depends on library input may run slow on strings starting with '{{' and with many repetitions of '{{|'.
This regular expression that depends on library input may run slow on strings starting with '{{' and with many repetitions of '{{|'.

🪛 LanguageTool
.changeset/hosted-webhook-ingress.md

[uncategorized] ~10-~10: The official name of this software platform is spelled with a capital “H”.
Context: ...rce with a preset (webhooks.stripe(), webhooks.github(), and others) or `webhooks.custom(...

(GITHUB)

docs/webhooks/sources.mdx

[uncategorized] ~53-~53: The official name of this software platform is spelled with a capital “H”.
Context: ... The available presets are stripe(), github(), svix(), square(), and `discord(...

(GITHUB)

🔇 Additional comments (36)
docs/webhooks/overview.mdx (1)

1-96: LGTM!

docs/webhooks/connect.mdx (1)

1-35: LGTM!

docs/webhooks/deliveries.mdx (1)

1-48: LGTM!

docs/webhooks/filters.mdx (1)

1-87: LGTM!

Also applies to: 97-99

docs/webhooks/session-routing.mdx (1)

1-96: LGTM!

docs/webhooks/channels.mdx (1)

20-28: LGTM!

Also applies to: 38-134

docs/webhooks/human-in-the-loop.mdx (1)

1-100: LGTM!

Also applies to: 121-143

docs/ai-chat/backend.mdx (1)

473-502: LGTM!

docs/ai-chat/reference.mdx (1)

50-51: LGTM!

Also applies to: 506-507, 537-572

docs/docs.json (1)

150-162: LGTM!

.changeset/hosted-webhook-ingress.md (1)

1-14: LGTM!

packages/cli-v3/src/dev/devSupervisor.ts (1)

35-35: LGTM!

packages/slack/src/index.ts (7)

39-83: LGTM!


125-133: LGTM!


141-189: LGTM!


237-247: LGTM!


286-297: LGTM!


328-333: LGTM!


377-392: LGTM!

packages/slack/tsconfig.json (1)

1-8: LGTM!

packages/slack/vitest.config.ts (1)

1-8: LGTM!

packages/slack/src/index.test.ts (1)

12-46: LGTM!

Also applies to: 115-248, 262-306

packages/slack/tsconfig.src.json (1)

5-11: 📐 Maintainability & Code Quality

No change needed for types: ["node"].

@types/node is available through the workspace dependency, and TypeScript type-only resolution does not require @types/node to be declared by each package that references it.

			> Likely an incorrect or invalid review comment.
packages/trigger-sdk/src/v3/webhooks.ts (4)

199-260: LGTM!


262-295: LGTM!


308-314: 🔒 Security & Privacy

The CodeQL ReDoS finding is a false positive at this call site.

[^}]+ is a negated class bounded by literal { and }. It has no ambiguous alternation, so the worst case is quadratic, not exponential, and only on input with many unclosed {.

The input is the key template that the developer writes in source and that runs once during indexing. It is not request data. Dismiss the alert or add a suppression comment so the check stops failing the pipeline.

Source: Linters/SAST tools


163-177: 🎯 Functional Correctness

No change needed.

gitlab can use the GitLab signing-token preset when configured, and whatsapp already matches the GitHub-style X-Hub-Signature-256 scheme.

packages/trigger-sdk/src/v3/ai.ts (7)

39-46: LGTM!

Also applies to: 61-61, 183-193


1266-1300: LGTM!


4840-4917: LGTM!

Also applies to: 4919-5047


5113-5132: LGTM!

Also applies to: 5235-5253, 5896-5920


6294-6297: LGTM!


8261-8315: LGTM!


8818-8863: LGTM!

Also applies to: 11399-11402, 11417-11418

packages/trigger-sdk/src/v3/chat.ts (1)

100-117: LGTM!

packages/trigger-sdk/src/v3/channelReactions.test.ts (1)

17-38: LGTM!

Comment on lines +29 to +37
<Step title="Create a Slack app">
Create an app at [api.slack.com/apps](https://api.slack.com/apps). Add the `chat:write` bot scope and install it to your workspace to get a bot token (`xoxb-...`).
</Step>
<Step title="Deploy the agent + connect the endpoint">
Deploying registers a hosted [endpoint](/webhooks/connect) for the channel. Set its signing secret to your Slack app's **Signing Secret**, and pass the bot token as `token`.
</Step>
<Step title="Subscribe to events">
In the app's **Event Subscriptions**, set the request URL to the endpoint's webhook URL. Slack sends a one-time `url_verification` handshake, which the endpoint answers automatically. Subscribe the bot to `message.channels`, then invite the bot to the channel (`/invite @yourapp`).
</Step>

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Slack API message.channels events required scopes channels:history

💡 Result:

To receive the message.channels event via the Slack Events API, your app must possess the channels:history OAuth scope [1][2]. This scope allows your app to view messages and other content in public channels that it has been added to [3][4]. When subscribing to message events, the specific event type dictates the required scope [1]: - message.channels: Requires channels:history [2]. - message.groups: Requires groups:history [1]. - message.im: Requires im:history [1]. - message.mpim: Requires mpim:history [1]. Please note that for your app to receive these events, it must be a member of the relevant channel [1][5]. If you intend to receive messages from private channels, groups, or direct messages, you must subscribe to the corresponding event type and request the appropriate history scope for that conversation type [1].

Citations:


🌐 Web query:

Slack API message.channels events scopes channels:history chat:write

💡 Result:

The message.channels event is an Event API type in Slack used to receive messages posted within public channels [1][2]. To use this event and perform related actions, your application requires specific OAuth scopes [3]. Required Scopes: channels:history: This scope is required to subscribe to the message.channels event [2][4]. It grants your app the permission to view messages and other content in public channels that the app has been added to [5]. chat:write: This scope is required to send messages on behalf of your app, for example, using the chat.postMessage API method [6][7]. It allows your app to post messages in the channels and contexts where it has been granted access [8]. In summary, subscribing to the message.channels event necessitates the channels:history scope to receive the data [4][9], while the chat:write scope is necessary if your application intends to respond to those events by sending new messages [10][7].

Citations:


Add the required Slack event-read scope.

message.channels requires channels:history; chat:write only enables replies. Add channels:history to Step “Create a Slack app” and tell readers to reinstall the app after changing scopes.

Comment thread docs/webhooks/sources.mdx
Comment on lines +29 to +48
```ts GitHub
export const githubWebhook = webhook({
id: "github",
source: webhooks.github(),
onEvent: async ({ event, headers }) => {
// GitHub puts the event type in a header
console.log(headers.get("x-github-event"));
},
});
```

```ts Svix
// Also covers Clerk, Resend, and other Svix-powered providers
export const svixWebhook = webhook({
id: "svix",
source: webhooks.svix(),
onEvent: async ({ event }) => {
// ...
},
});

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked MDX files around docs/webhooks:"
git ls-files docs/webhooks | sort

for f in docs/webhooks/sources.mdx docs/webhooks/filters.mdx docs/webhooks/channels.mdx docs/webhooks/human-in-the-loop.mdx; do
  if [ -f "$f" ]; then
    echo
    echo "===== $f ====="
    wc -l "$f"
    sed -n '1,150p' "$f"
  fi
done

echo
echo "Search imports/usages:"
python3 - <<'PY'
from pathlib import Path
import re

files = [
    Path('docs/webhooks/sources.mdx'),
    Path('docs/webhooks/filters.mdx'),
    Path('docs/webhooks/channels.mdx'),
    Path('docs/webhooks/human-in-the-loop.mdx'),
]
for path in files:
    text = path.read_text() if path.exists() else ''
    print(f"FILE {path}")
    for code in re.findall(r'```ts\s+([\s\S]*?)\n```', text):
        print("--- code block")
        print(code)
        imports = re.findall(r'import\s+([\s\S]*?)\s+from\s+([^\s;]+)', code)
        print("imports:", imports)
PY

Repository: triggerdotdev/trigger.dev

Length of output: 30269


Make the incomplete standalone code examples self-contained.

These blocks use symbols without importing them, so copied examples can fail type checking even where the surrounding docs already show compatible imports.

  • docs/webhooks/sources.mdx: Add import { webhook, webhooks } to the GitHub, Svix, and custom-provider examples.
  • docs/webhooks/filters.mdx#L88-L95: Add import { webhook, webhooks } to the GitHub filter example.
  • docs/webhooks/channels.mdx#L11-L19: Import streamText and anthropic together with the existing imports.
  • docs/webhooks/human-in-the-loop.mdx#L101-L120: Import chat.columns or use the existing chat/webhooks import pattern before chat.channels.custom(...).
📍 Affects 4 files
  • docs/webhooks/sources.mdx#L29-L48 (this comment)
  • docs/webhooks/sources.mdx#L59-L74
  • docs/webhooks/filters.mdx#L88-L95
  • docs/webhooks/channels.mdx#L11-L19
  • docs/webhooks/human-in-the-loop.mdx#L101-L120

Source: Coding guidelines

Comment on lines +198 to +199
webhooks: resourceCatalog.listWebhookManifests(),
unclaimedSessionWebhooks: resourceCatalog.listUnclaimedSessionWebhooks(),

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 \
  'INDEX_COMPLETE|indexerToWorkerMessages|listWebhookManifests|listUnclaimedSessionWebhooks|unclaimedSessionWebhooks|BuildManifest|CreateBackgroundWorkerRequestBody' \
  . --glob '*.{ts,tsx}'

Repository: triggerdotdev/trigger.dev

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^packages/core/v3/schemas|packages/core/v3/src/schemas|packages/cli-v3/src/.*schemas|packages/core/v3/src/resources|packages/cli-v3/src/.*/.*Resources|packages/cli-v3/src/utilities/buildManifest|packages/cli-v3/src/entryPoints/(dev-index-worker|managed-index-worker)|packages/cli-v3/src/dev/devSupervisor|apps/webapp/app/v3/services/createBackgroundWorker|packages/core/v3/isomorphic|packages/sdk)' | sed -n '1,220p'

echo
echo "== schema/type definitions for webhook fields =="
rg -n -C 12 'BuildManifest|WorkerManifest|unclaimedSessionWebhooks|webhooks|CreateBackgroundWorker' packages/core packages/cli-v3 apps/webapp/app/v3 --glob '*.ts' --max-count 80

echo
echo "== resource catalog list methods =="
rg -n -C 15 'listWebhookManifests|listUnclaimedSessionWebhooks|WebhookManifest|unclaimed session|unclaimedSession' packages/core packages/cli-v3 --glob '*.ts'

echo
echo "== indexed boundary files =="
sed -n '180,225p' packages/cli-v3/src/entryPoints/dev-index-worker.ts
sed -n '180,225p' packages/cli-v3/src/entryPoints/managed-index-worker.ts
sed -n '370,415p' packages/cli-v3/src/dev/devSupervisor.ts

Repository: triggerdotdev/trigger.dev

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== WorkerManifest exact shape =="
sed -n '103,132p' packages/core/src/v3/schemas/build.ts

echo
echo "== BuildManifest relevant shape =="
sed -n '33,100p' packages/core/src/v3/schemas/build.ts

echo
echo "== devSupervisor relevant section =="
sed -n '370,415p' packages/cli-v3/src/dev/devSupervisor.ts

echo
echo "== focused index endpoint producers =="
sed -n '186,200p' packages/cli-v3/src/entryPoints/dev-index-worker.ts
sed -n '186,200p' packages/cli-v3/src/entryPoints/managed-index-worker.ts

echo
echo "== focused schema/type mentions =="
rg -n -C 6 'unclaimedSessionWebhooks|listWebhookManifests|listUnclaimedSessionWebhooks|metadata: BackgroundWorkerMetadata|CreateBackgroundWorkerRequestBody|tasks: TaskManifest' \
  packages/core/src/v3/schemas/api.ts packages/core/src/v3/schemas/resources.ts packages/core/src/v3/schemas/build.ts packages/core/src/v3/schemas/webhooks.ts packages/core/v3 --glob '*.ts' --max-count 200

echo
echo "== resource catalog exact methods =="
rg -n -C 10 'listWebhookManifests|listUnclaimedSessionWebhooks|_webhooks|unclaimed|WebhookManifest' packages/core/src packages/cli-v3/src --glob '*.ts' --max-count 200

Repository: triggerdotdev/trigger.dev

Length of output: 8990


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== BackgroundWorkerMetadata schema =="
rg -n -C 16 'BackgroundWorkerMetadata|webhooks|prompts|queues|tasks' packages/core/src/v3/schemas/api.ts packages/core/src/v3/schemas/resources.ts --glob '*.ts'

echo
echo "== complete focused manifest/schema excerpts =="
sed -n '128,175p' packages/core/src/v3/schemas/api.ts
sed -n '100,145p' packages/core/src/v3/schemas/resources.ts
sed -n '220,270p' packages/core/src/v3/schemas/webhooks.ts
sed -n '385,410p' packages/cli-v3/src/dev/devSupervisor.ts

echo
echo "== resource catalog implementation excerpts =="
sed -n '280,380p' packages/core/src/v3/resource-catalog/standardResourceCatalog.ts
rg -n -C 8 'unclaimedSessionWebhooks|listWebhookManifests\(|registerWebhook|WebhookManifest' packages/core/src/v3 packages/cli-v3/src --glob '*.ts' --max-count 220

Repository: triggerdotdev/trigger.dev

Length of output: 25143


Add the webhook manifest fields to the shared WorkerManifest schema.

INDEX_COMPLETE sends workerManifest-typed data, but WorkerManifest currently only lists tasks, prompts, skills, and queues. Add optional webhooks and unclaimedSessionWebhooks there so dev-index-worker, managed-index-worker, and the dev manifest loader do not accept extra undeclared fields while also preserving them through the manifest boundary.

📍 Affects 3 files
  • packages/cli-v3/src/entryPoints/dev-index-worker.ts#L198-L199 (this comment)
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts#L194-L195
  • packages/cli-v3/src/dev/devSupervisor.ts#L385-L394
  • packages/cli-v3/src/dev/devSupervisor.ts#L404-L404

Comment on lines +34 to +55
"scripts": {
"clean": "rimraf dist .tshy .tshy-build .turbo",
"build": "tshy && pnpm run update-version",
"dev": "tshy --watch",
"typecheck": "tsc --noEmit -p tsconfig.src.json",
"test": "vitest",
"update-version": "tsx ../../scripts/updateVersion.ts",
"check-exports": "attw --pack ."
},
"dependencies": {
"@trigger.dev/core": "workspace:4.5.0-rc.7"
},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^4.5.0-rc.7"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.5",
"@trigger.dev/sdk": "workspace:4.5.0-rc.7",
"rimraf": "6.0.1",
"tshy": "^3.0.2",
"tsx": "4.17.0"
},

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify how sibling packages declare vitest and `@types/node`, and check hoisting settings.
set -euo pipefail
fd -t f 'package.json' packages --max-depth 2 --exec sh -c 'printf "== %s\n" "$1"; jq -r "{name, vitest: (.devDependencies.vitest), typesNode: (.devDependencies[\"`@types/node`\"]), test: .scripts.test}" "$1"' _ {}
echo "== .npmrc"; cat .npmrc 2>/dev/null || echo "no .npmrc"
echo "== pnpm-workspace"; cat pnpm-workspace.yaml 2>/dev/null || true

Repository: triggerdotdev/trigger.dev

Length of output: 2146


🏁 Script executed:

#!/bin/bash
# Inspect the Slack package manifest around dependencies/scripts and read the current tsconfig.
set -euo pipefail
printf "== slack package manifest lines 1-90\n"
sed -n '1,90p' packages/slack/package.json
printf "\n== slack tsconfig lines 1-80\n"
sed -n '1,80p' packages/slack/tsconfig.src.json

Repository: triggerdotdev/trigger.dev

Length of output: 2393


Declare the Slack package tooling explicitly. packages/slack/package.json is missing vitest, while test runs vitest, and the TypeScript config keeps types: ["node"] without declaring @types/node. Add vitest to devDependencies and add or remove @types/node to match the node type reference, so resolution does not depend on workspace hoisting.

📍 Affects 2 files
  • packages/slack/package.json#L34-L55 (this comment)
  • packages/slack/tsconfig.src.json#L5-L11

deliveryId: "d1",
}
);
const values = (msg?.blocks as any[]).flatMap((b) => b.elements ?? []).map((e: any) => e.value);

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Unsafe optional chaining in two assertions. Both sites pass an optionally-chained value straight into a type assertion. The cast hides a possible undefined, so a regression produces a TypeError instead of a readable assertion failure. ESLint reports no-unsafe-optional-chaining at both sites.

  • packages/slack/src/index.test.ts#L48-L48: assert msg?.blocks is defined before you call flatMap on the cast value.
  • packages/slack/src/index.test.ts#L97-L97: assert calls[0]?.body.blocks is defined before you call map on the cast value.
🧰 Tools
🪛 GitHub Check: code-quality / code-quality

[warning] 48-48: eslint(no-unsafe-optional-chaining)
Unsafe usage of optional chaining

📍 Affects 1 file
  • packages/slack/src/index.test.ts#L48-L48 (this comment)
  • packages/slack/src/index.test.ts#L97-L97

Source: Linters/SAST tools

Comment on lines +6944 to +7002
if (wireChannelEvent) {
channelConn = channels?.find((c) => c.id === wireChannelEvent.connectorId);
if (channelConn) {
const interaction = channelConn.onInteraction?.(wireChannelEvent.event) ?? null;
const resolutionMessage = interaction
? buildInteractionResolutionMessage(
interaction,
accumulatedUIMessages as UIMessage[]
)
: undefined;
if (resolutionMessage) {
effectiveIncomingMessage = resolutionMessage as typeof incomingMessage;
if (interaction && channelConn.finalizeInteraction) {
try {
await channelConn.finalizeInteraction(wireChannelEvent.event, interaction);
} catch (finalizeError) {
logger.warn("chat.agent: channel finalizeInteraction failed; continuing", {
error: finalizeError,
});
}
}
} else {
effectiveIncomingMessage = toUserUIMessage(
channelConn.inbound(wireChannelEvent.event),
currentWirePayload.messageId ?? wireChannelEvent.deliveryId
) as typeof incomingMessage;
if (channelConn.send && channelConn.ack) {
const recoveryPending = locals.get(chatChannelRecoveryPendingKey);
const recovered = recoveryPending?.value === true;
if (recovered) recoveryPending!.value = false;
const ackMessage = channelConn.ack(wireChannelEvent.event, { recovered });
if (ackMessage) {
try {
const ackResult = await channelConn.send(ackMessage, {
event: wireChannelEvent.event,
deliveryId: wireChannelEvent.deliveryId,
mode: channelConn.delivery,
final: false,
});
channelAckRef = ackResult?.ref;
} catch (ackError) {
logger.warn("chat.agent: channel ack post failed; continuing", {
error: ackError,
});
}
}
}
channelWorkingReaction = await resolveReactionChoice(
channelConn.reactions?.working,
wireChannelEvent.event
);
if (channelWorkingReaction) {
await applyChannelReaction(channelConn, wireChannelEvent, {
name: channelWorkingReaction,
});
}
}
}
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

A stale interaction callback becomes a spurious chat turn.

At line 6947 onInteraction can return a non-null resolution while buildInteractionResolutionMessage returns undefined, because no pending tool part matches the toolCallId. The docstring at line 4972 names this case a stale or duplicate callback.

Control then falls into the else at line 6965 and passes the raw callback event to channelConn.inbound(...). For Slack that maps a block_actions payload into a user message, so a repeated button click produces a bogus turn. The Slack connector filter is INTERACTIVITY_PASS || (...) (packages/slack/src/index.ts line 153), so every interaction callback reaches this code path.

finalizeInteraction is also skipped on this path, so the posted buttons stay clickable and the next click repeats the problem.

Handle three cases, not two: a null onInteraction result means treat as a message; a non-null result with a match means resume; a non-null result with no match means drop the delivery.

Separately, if no entry in channels matches wireChannelEvent.connectorId, line 6945 leaves channelConn undefined and the turn proceeds with no message and no log. Add a warning there.

🐛 Proposed fix
             if (wireChannelEvent) {
               channelConn = channels?.find((c) => c.id === wireChannelEvent.connectorId);
-              if (channelConn) {
+              if (!channelConn) {
+                logger.warn("chat.agent: no channel connector matched the delivery; ignoring", {
+                  connectorId: wireChannelEvent.connectorId,
+                });
+              } else {
                 const interaction = channelConn.onInteraction?.(wireChannelEvent.event) ?? null;
                 const resolutionMessage = interaction
                   ? buildInteractionResolutionMessage(
                       interaction,
                       accumulatedUIMessages as UIMessage[]
                     )
                   : undefined;
+                if (interaction && !resolutionMessage) {
+                  // A verified callback with no matching pending tool part: stale or duplicate.
+                  // Do not fall through to inbound() — that would fabricate a turn from a click.
+                  logger.warn("chat.agent: stale channel interaction callback; dropping", {
+                    toolCallId: interaction.toolCallId,
+                  });
+                  if (channelConn.finalizeInteraction) {
+                    try {
+                      await channelConn.finalizeInteraction(wireChannelEvent.event, interaction);
+                    } catch (finalizeError) {
+                      logger.warn("chat.agent: channel finalizeInteraction failed; continuing", {
+                        error: finalizeError,
+                      });
+                    }
+                  }
+                  continue;
+                }
                 if (resolutionMessage) {

Verify that continue is valid in the enclosing for (let turn ...) scope; if the surrounding try requires it, set a skip flag instead and branch on it before incomingMessages is built.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (wireChannelEvent) {
channelConn = channels?.find((c) => c.id === wireChannelEvent.connectorId);
if (channelConn) {
const interaction = channelConn.onInteraction?.(wireChannelEvent.event) ?? null;
const resolutionMessage = interaction
? buildInteractionResolutionMessage(
interaction,
accumulatedUIMessages as UIMessage[]
)
: undefined;
if (resolutionMessage) {
effectiveIncomingMessage = resolutionMessage as typeof incomingMessage;
if (interaction && channelConn.finalizeInteraction) {
try {
await channelConn.finalizeInteraction(wireChannelEvent.event, interaction);
} catch (finalizeError) {
logger.warn("chat.agent: channel finalizeInteraction failed; continuing", {
error: finalizeError,
});
}
}
} else {
effectiveIncomingMessage = toUserUIMessage(
channelConn.inbound(wireChannelEvent.event),
currentWirePayload.messageId ?? wireChannelEvent.deliveryId
) as typeof incomingMessage;
if (channelConn.send && channelConn.ack) {
const recoveryPending = locals.get(chatChannelRecoveryPendingKey);
const recovered = recoveryPending?.value === true;
if (recovered) recoveryPending!.value = false;
const ackMessage = channelConn.ack(wireChannelEvent.event, { recovered });
if (ackMessage) {
try {
const ackResult = await channelConn.send(ackMessage, {
event: wireChannelEvent.event,
deliveryId: wireChannelEvent.deliveryId,
mode: channelConn.delivery,
final: false,
});
channelAckRef = ackResult?.ref;
} catch (ackError) {
logger.warn("chat.agent: channel ack post failed; continuing", {
error: ackError,
});
}
}
}
channelWorkingReaction = await resolveReactionChoice(
channelConn.reactions?.working,
wireChannelEvent.event
);
if (channelWorkingReaction) {
await applyChannelReaction(channelConn, wireChannelEvent, {
name: channelWorkingReaction,
});
}
}
}
}
if (wireChannelEvent) {
channelConn = channels?.find((c) => c.id === wireChannelEvent.connectorId);
if (!channelConn) {
logger.warn("chat.agent: no channel connector matched the delivery; ignoring", {
connectorId: wireChannelEvent.connectorId,
});
} else {
const interaction = channelConn.onInteraction?.(wireChannelEvent.event) ?? null;
const resolutionMessage = interaction
? buildInteractionResolutionMessage(
interaction,
accumulatedUIMessages as UIMessage[]
)
: undefined;
if (interaction && !resolutionMessage) {
// A verified callback with no matching pending tool part: stale or duplicate.
// Do not fall through to inbound() — that would fabricate a turn from a click.
logger.warn("chat.agent: stale channel interaction callback; dropping", {
toolCallId: interaction.toolCallId,
});
if (channelConn.finalizeInteraction) {
try {
await channelConn.finalizeInteraction(wireChannelEvent.event, interaction);
} catch (finalizeError) {
logger.warn("chat.agent: channel finalizeInteraction failed; continuing", {
error: finalizeError,
});
}
}
continue;
}
if (resolutionMessage) {
effectiveIncomingMessage = resolutionMessage as typeof incomingMessage;
if (interaction && channelConn.finalizeInteraction) {
try {
await channelConn.finalizeInteraction(wireChannelEvent.event, interaction);
} catch (finalizeError) {
logger.warn("chat.agent: channel finalizeInteraction failed; continuing", {
error: finalizeError,
});
}
}
} else {
effectiveIncomingMessage = toUserUIMessage(
channelConn.inbound(wireChannelEvent.event),
currentWirePayload.messageId ?? wireChannelEvent.deliveryId
) as typeof incomingMessage;
if (channelConn.send && channelConn.ack) {
const recoveryPending = locals.get(chatChannelRecoveryPendingKey);
const recovered = recoveryPending?.value === true;
if (recovered) recoveryPending!.value = false;
const ackMessage = channelConn.ack(wireChannelEvent.event, { recovered });
if (ackMessage) {
try {
const ackResult = await channelConn.send(ackMessage, {
event: wireChannelEvent.event,
deliveryId: wireChannelEvent.deliveryId,
mode: channelConn.delivery,
final: false,
});
channelAckRef = ackResult?.ref;
} catch (ackError) {
logger.warn("chat.agent: channel ack post failed; continuing", {
error: ackError,
});
}
}
}
channelWorkingReaction = await resolveReactionChoice(
channelConn.reactions?.working,
wireChannelEvent.event
);
if (channelWorkingReaction) {
await applyChannelReaction(channelConn, wireChannelEvent, {
name: channelWorkingReaction,
});
}
}
}
}

Comment on lines +7163 to +7167
const isWebhookAction = currentWirePayload.actionSource === "webhook";
const parsedAction =
parseAction && !isWebhookAction
? await parseAction(currentWirePayload.action)
: currentWirePayload.action;

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where actionSource is produced and whether the append path sanitizes it.
rg -n -C 6 'actionSource' --glob '*.ts' --glob '!**/*.test.ts'

Repository: triggerdotdev/trigger.dev

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "tracked matches for actionSource:"
git ls-files -z '*.ts' | xargs -0 rg -n -C 6 'actionSource' || true

echo
echo "relevant ai.ts lines around 7140-7185:"
if [ -f packages/trigger-sdk/src/v3/ai.ts ]; then
  sed -n '7130,7195p' packages/trigger-sdk/src/v3/ai.ts | nl -ba -v7130
else
  fd -a 'ai\.ts$' . | sed -n '1,20p'
fi

echo
echo "search for ChatTaskWirePayload / ChatEventActions:"
rg -n -C 4 -i 'ChatTaskWirePayload|ChatEventActions|actionSchema|parseAction|webhook' packages/trigger-sdk/src/v3 --glob '*.ts' || true

Repository: triggerdotdev/trigger.dev

Length of output: 2712


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "files mentioning actionSource:"
git ls-files -z | awk 'BEGIN{RS="\0"} /actionSource/ {print}' | sed 's#^`#-` #'

echo
echo "chat.ts relevant payload docs:"
sed -n '80,125p' packages/trigger-sdk/src/v3/chat.ts

echo
echo "messages in ai.ts around currentWirePayload initialization:"
rg -n -C 8 'currentWirePayload|isAction|ChatTaskWirePayload|actionSource|parseAction' packages/trigger-sdk/src/v3/ai.ts

echo
echo "session.in / session.append actionSource occurrences:"
rg -n -C 8 'session\.(in|append)|append\(|actionSource|ChatTaskWirePayload' packages --glob '*.ts' || true

Repository: triggerdotdev/trigger.dev

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "files containing actionSource:"
git ls-files | awk '/actionSource/ {print}'

echo
echo "package file excerpts:"
sed -n '90,125p' packages/trigger-sdk/src/v3/chat.ts
sed -n '1480,1585p' packages/trigger-sdk/src/v3/ai.ts
sed -n '85,135p' packages/trigger-sdk/src/v3/ai-shared.ts | sed -n '1,120p'

echo
echo "append path references:"
git ls-files | awk 'tolower($0)|awk "tolower($0)/service|api|session|chat|v3|ingress|webhook|actions|action-source|actionSource/" {print}' | head -120
rg -n -C 5 'actionSource|session.in|append|in\.append|SessionStream|Session.*append|chat.*action|ChatTaskWirePayload|ChatInputChunk' packages --glob '*.ts' --glob '!**/*.test.ts' | head -240

Repository: triggerdotdev/trigger.dev

Length of output: 8648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "api and server candidates with websocket/session/in routing:"
git ls-files | awk '
/tsc-out|dist|lib/ { next }
/(\.js|\.ts|\.tsx)$|package.json$/ { print }
' | rg 'api|server|session|chat|webhook|ingress|auth|middleware' | head -200 || true

echo
echo "focused route/middleware search for session route action parsing:"
rg -n -C 6 'sessionStreams|readSessionStreamRecords|SessionStream|Session.*Streams|read\(|readRecords|actionSchema|parseAction|ChatInputChunk|ChatTaskWirePayload|actionSource' packages apps tools --glob '*.ts' --glob '!**/*.test.ts' | head -300 || true

echo
echo "append request bodies containing ChatInputChunk payload:"
rg -n -C 8 'JSON\.stringify\(\{\s*kind:\s*"message"|kind: "message"|actionSource|action.*=|actionSchema|validate.*action|parseAction' packages apps tools --glob '*.ts' --glob '!**/*.test.ts' | head -300 || true

Repository: triggerdotdev/trigger.dev

Length of output: 50381


Do not trust client-supplied actionSource: "webhook".

actionSource travels with ChatTaskWirePayload on session.in; a holder of session append authority can send a crafted action payload with this flag and bypass actionSchema. Set this field on hosted webhook ingress records and ignore or strip client values before using it to skip validation.

Comment on lines +7785 to 7813
let streamForPipe: typeof uiStream = uiStream;
if (
wireChannelEvent &&
channelConn?.send &&
channelConn.delivery === "stream" &&
channelAckRef &&
uiStream instanceof ReadableStream
) {
const editor = makeChannelStreamEditor(
channelConn,
wireChannelEvent,
channelAckRef
);
streamForPipe = uiStream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
editor.observe(chunk);
controller.enqueue(chunk);
},
flush() {
editor.stop();
},
})
);
}
await pipeChat(tapUIMessageChunks(streamForPipe, turnBufferedChunks), {
signal: combinedSignal,
spanName: "stream response",
});

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Stop the stream editor when the pipe aborts, not only on flush.

flush() runs only when the stream completes normally. If the user stops generation or the run cancels, pipeChat rejects and flush never runs, so editor.stop() is skipped and an armed timer survives.

That timer can fire after the turn and write the partial text to ackRef. The final egress edit at line 8264 writes the complete text. The two edits race, and the stale partial can win.

Hold the editor in a variable and stop it in a finally around the pipe.

🐛 Proposed fix
                       let streamForPipe: typeof uiStream = uiStream;
+                      let channelEditor: ReturnType<typeof makeChannelStreamEditor> | undefined;
                       if (
                         wireChannelEvent &&
                         channelConn?.send &&
                         channelConn.delivery === "stream" &&
                         channelAckRef &&
                         uiStream instanceof ReadableStream
                       ) {
-                        const editor = makeChannelStreamEditor(
+                        channelEditor = makeChannelStreamEditor(
                           channelConn,
                           wireChannelEvent,
                           channelAckRef
                         );
+                        const editor = channelEditor;
                         streamForPipe = uiStream.pipeThrough(
                           new TransformStream({
                             transform(chunk, controller) {
                               editor.observe(chunk);
                               controller.enqueue(chunk);
                             },
                             flush() {
                               editor.stop();
                             },
                           })
                         );
                       }
-                      await pipeChat(tapUIMessageChunks(streamForPipe, turnBufferedChunks), {
-                        signal: combinedSignal,
-                        spanName: "stream response",
-                      });
+                      try {
+                        await pipeChat(tapUIMessageChunks(streamForPipe, turnBufferedChunks), {
+                          signal: combinedSignal,
+                          spanName: "stream response",
+                        });
+                      } finally {
+                        channelEditor?.stop();
+                      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let streamForPipe: typeof uiStream = uiStream;
if (
wireChannelEvent &&
channelConn?.send &&
channelConn.delivery === "stream" &&
channelAckRef &&
uiStream instanceof ReadableStream
) {
const editor = makeChannelStreamEditor(
channelConn,
wireChannelEvent,
channelAckRef
);
streamForPipe = uiStream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
editor.observe(chunk);
controller.enqueue(chunk);
},
flush() {
editor.stop();
},
})
);
}
await pipeChat(tapUIMessageChunks(streamForPipe, turnBufferedChunks), {
signal: combinedSignal,
spanName: "stream response",
});
let streamForPipe: typeof uiStream = uiStream;
let channelEditor: ReturnType<typeof makeChannelStreamEditor> | undefined;
if (
wireChannelEvent &&
channelConn?.send &&
channelConn.delivery === "stream" &&
channelAckRef &&
uiStream instanceof ReadableStream
) {
channelEditor = makeChannelStreamEditor(
channelConn,
wireChannelEvent,
channelAckRef
);
const editor = channelEditor;
streamForPipe = uiStream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
editor.observe(chunk);
controller.enqueue(chunk);
},
flush() {
editor.stop();
},
})
);
}
try {
await pipeChat(tapUIMessageChunks(streamForPipe, turnBufferedChunks), {
signal: combinedSignal,
spanName: "stream response",
});
} finally {
channelEditor?.stop();
}

Comment on lines +8571 to +8586
if (channelWireEvent && channelConn?.react) {
if (channelWorkingReaction) {
await applyChannelReaction(channelConn, channelWireEvent, {
name: channelWorkingReaction,
remove: true,
});
}
const errorReaction = await resolveReactionChoice(
channelConn.reactions?.error,
channelWireEvent.event
);
if (errorReaction) {
await applyChannelReaction(channelConn, channelWireEvent, { name: errorReaction });
}
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The error path leaves the ack placeholder in the channel.

This block updates reactions but sends no channel message. The success path at line 8264 edits the ack into the answer. On a turn error the placeholder text, for example "on it...", stays in the thread forever.

The user sees a message that claims work is in progress and an error emoji, with no explanation. Add an egress edit here that reports the failure, gated on channelConn?.send && channelAckRef.

channelAckRef is currently declared inside the inner scope at line 6943. Promote it next to channelWireEvent at line 6927 so the catch block can read it.

Comment on lines +364 to +381
export const webhooks: Webhooks & ProviderProducers = {
...providerProducers,
constructEvent,
SIGNATURE_HEADER_NAME,
custom: webhookSources.custom,
stripe: webhookSources.stripe,
github: webhookSources.github,
svix: webhookSources.svix,
square: webhookSources.square,
discord: webhookSources.discord,
clerk: webhookSources.clerk,
resend: webhookSources.resend,
openai: webhookSources.openai,
replicate: webhookSources.replicate,
recallai: webhookSources.recallai,
brex: webhookSources.brex,
gitlab: webhookSources.gitlab,
whatsapp: webhookSources.whatsapp,

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List the provider ids in the core table and compare with the hand-written producers.
rg -n -A 40 'webhookProviderConfigs\s*=' packages/core/src --glob '*.ts'
rg -n 'WebhookProviderId' packages/core/src --glob '*.ts' -C 4

Repository: triggerdotdev/trigger.dev

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -u

echo "Find files named webhooks.ts and relevant paths:"
git ls-files | rg '(^|/)(webhooks\.ts|.*provider.*|.*webhook.*)$' || true

echo
echo "Locate webhookProviderConfigs across repository:"
rg -n 'webhookProviderConfigs|WebhookProviderId|webhookSources|ProviderProducers' . -g '*.ts' -g '*.tsx' || true

echo
echo "File sizes for likely candidates:"
fd -e ts 'webhooks' . -x sh -c 'wc -l "$1"' sh {}

Repository: triggerdotdev/trigger.dev

Length of output: 5806


🏁 Script executed:

#!/bin/bash
set -u

echo "packages/trigger-sdk/src/v3/webhooks.ts relevant sections:"
sed -n '1,220p' packages/trigger-sdk/src/v3/webhooks.ts
echo
sed -n '320,420p' packages/trigger-sdk/src/v3/webhooks.ts

echo
echo "packages/core/src/v3/schemas/webhooks.ts:"
sed -n '1,291p' packages/core/src/v3/schemas/webhooks.ts

echo
echo "Static check: generated provider ids present in webhooksSources keys"
python3 - <<'PY'
from pathlib import Path
p = Path("packages/trigger-sdk/src/v3/webhooks.ts").read_text()
start = p.index("export const webhookProviderConfigs")
end = p.index("export const webhookSources")
generated = p[start:end]
webhooks_start = p.index("export const webhookSources")
webhooks_end = p.index("\nexport const webhooks")
sources = p[webhooks_start:webhooks_end]
explicit_keys = []
for line in sources.splitlines():
    line=line.strip()
    if line.endswith(":",) or line.endswith(","):
        k=line.split(":",1)[0].strip()
        if k: explicit_keys.append(k)
keys=[]
for line in generated.splitlines():
    s=line.strip().strip(",")
    if ":" in s:
        if s.startswith("'") and "'" in s[1:]:
            keys.append(s.split(":",1)[0].strip().strip("'"))
        elif s.startswith('"') and '"' in s[1:]:
            keys.append(s.split(":",1)[0].strip().strip('"'))
        else:
            kv=s.split(",",1)[0]
            keys.append(kv.strip().strip("'").strip('"'))
overlap = [k for k in keys if k in explicit_keys]
print("generated:", len(keys), "explicit:", len(explicit_keys), "overlap:", overlap)
print("\nGenerated entries for overlap:")
for k in overlap:
    print(k, "=>", re.search(rf"^\s*'{re.escape(k)}'\s*:([^,\n]+)", generated,re.M).group(1) if False else generated[generated.find(k):].split("\n",3)[0])
PY

Repository: triggerdotdev/trigger.dev

Length of output: 19354


🏁 Script executed:

#!/bin/bash
set -u

python3 - <<'PY'
from pathlib import Path
import re

p = Path("packages/trigger-sdk/src/v3/webhooks.ts")
text = p.read_text()

# Locate the two ranges by matching the export declarations/imports directly
m = re.search(r'from ["' + "'" + r"]`@trigger`\.dev/core/webhooks";\n', text)
assert m, "could not find `@trigger.dev/core/webhooks` import"
start = m.end()

# webhookProviderConfigs block
m = re.search(r'export const webhookProviderConfigs\s*=.*?;/\*\*|\n[\s\S]*?\n,?\n', text[start:])
block = m.group(0)
end = start + m.end()

# webhookSources block starts after providerProducers
m = re.search(r'export const webhookSources\s*=.*?\}\s+as const;', text[end:])
sources = m.group(0)
sources_end = end + m.end()

# webhook() object starts after Webhooks interface / comment area
m = re.search(r'export const webhooks: Webhooks & ProviderProducers = \{', text[sources_end:])
webhooks_obj = text[source_end + m.start() : ]
webhooks_obj = re.match(r'export const webhooks: Webhooks & ProviderProducers = \{([\s\S]*?)\n\};', webhooks_obj).group(0)

print("imports", m.group(0)[:100], "...")
print("config block keys:")
for line in block.splitlines():
    s=line.strip().rstrip(',')
    if not s or s.startswith('/'):
        continue
    if ':' in s:
        k=s.split(':',1)[0].strip().strip("'").strip('"')
        print(k)
keys = []
for line in block.splitlines():
    s=line.strip().rstrip(',')
    if not s or s.startswith('/'):
        continue
    if ':' in s:
        k=s.split(':',1)[0].strip().strip("'").strip('"')
        keys.append(k)
print("explicit keys:")
for line in sources.splitlines():
    s=line.strip().rstrip(',')
    if ":" in s:
        k=s.split(":",1)[0].strip()
        if k:
            print(k)
explicit = [s.split(":",1)[0].strip() for s in sources.splitlines() if ":" in s and not s.strip().startswith("/")]
overlap = [k for k in keys if k in explicit]
print("overlap", overlap)
print("\nconfig entries in block:")
for k in overlap:
    m = re.search(rf'^\s*{re.escape(k)}\s*(:|$)', block, re.M)
    print(k, "=>", block[m.start():block.find("\n", m.start())].strip())

print("\nwebhooks object relevant lines:")
for line in webhooks_obj.splitlines():
    if line.strip().startswith(("...providerProducers", "custom:", "stripe:", "github:", "svix:", "square:", "discord:")):
        print(line)
PY

Repository: triggerdotdev/trigger.dev

Length of output: 366


🏁 Script executed:

#!/bin/bash
set -u
python3 - <<'PY'
from pathlib import Path
import re

p = Path("packages/trigger-sdk/src/v3/webhooks.ts")
text = p.read_text()
m = re.search(r"from [\"']`@trigger`\.dev/core/webhooks[\"'];\n", text)
assert m, "could not find `@trigger.dev/core/webhooks` import"
start = m.end()

# Collect keys from webhookProviderConfigs until webhooks export start.
config_end = text.index("export const webhooks", start)
block = text[start:config_end]

# Explicit keys in webhookSources.
webhooks_start = text.index("export const webhookSources", start)
sources_end = text.index("export const webhooks", start)
sources = text[webhooks_start:sources_end]

config_keys = []
for line in block.splitlines():
    s = line.strip().rstrip(",")
    if not s or s.startswith("/*") or s.startswith("/"):
        continue
    if ":" in s:
        config_keys.append(s.split(":", 1)[0].strip().strip("'").strip('"'))

explicit_keys = []
for line in sources.splitlines():
    s = line.strip().rstrip(",")
    if ":" in s and not s.startswith("/*"):
        explicit_keys.append(s.split(":", 1)[0].strip())

overlap = sorted(set(config_keys) & set(explicit_keys))
print("config_key_count", len(config_keys))
print("explicit_key_count", len(explicit_keys))
print("overlap", overlap)
for k in overlap:
    idx = block.find(k)
    end = block.find("\n", idx)
    print("overlap_entry", k, "=>", block[idx:end].strip() if end != -1 else block[idx:].strip())
print("\nwebhooks object relevant lines:")
m = re.search(r"export const webhooks: Webhooks & ProviderProducers = \{((?:.|\n)*?)\n\};", text)
assert m
for line in m.group(1).splitlines():
    if re.search(r"^\.\.\.providerProducers|^\s*(custom|stripe|github|svix|square|discord):", line):
        print(line)
PY

Repository: triggerdotdev/trigger.dev

Length of output: 5838


🏁 Script executed:

#!/bin/bash
set -u

echo "Find exported webhook config symbols:"
rg -n "webhookProviderConfigs|WebhookProviderId|stripeVerifierConfig|githubVerifierConfig|svixVerifierConfig|squareVerifierConfig|discordVerifierConfig" packages -g '*.ts' -g '*.tsx'

echo
echo "packages/trigger-sdk/src/v3/webhooks.ts import section:"
sed -n '1,35p' packages/trigger-sdk/src/v3/webhooks.ts

echo
echo "Search core webhooks exports:"
fd -i 'webhook|Webhook|webhooks' packages -x sh -c 'echo "--- $1"; rg -n "webhookProviderConfigs|WebhookProviderId|stripeVerifierConfig|githubVerifierConfig|svixVerifierConfig|squareVerifierConfig|discordVerifierConfig" "$1" -g "*.ts" -g "*.tsx"' sh {}

Repository: triggerdotdev/trigger.dev

Length of output: 5102


🏁 Script executed:

#!/bin/bash
set -u

echo "Core package manifests/packages files:"
git ls-files 'packages/*' | sed -n '1,120p'

echo
echo "Files under packages/core excluding tests:"
git ls-files 'packages/core/**' | rg '/src/|/index|package\.json$' | sed -n '1,200p'

echo
echo "Search for export `@trigger.dev/core/webhooks` in lockfiles/package files and core source:"
rg -n '"`@trigger`\.dev/core/webhooks"|`@trigger`\.dev/core/webhooks|webhooks' packages -g 'package.json' -g '*.ts' -g '*.tsx' | sed -n '1,200p'

Repository: triggerdotdev/trigger.dev

Length of output: 16863


🏁 Script executed:

#!/bin/bash
set -u

echo "packages/core/src/v3/schemas/index.ts:"
cat -n packages/core/src/v3/schemas/index.ts

echo
echo "packages/core/package.json exports main and webhooks references:"
python3 - <<'PY'
import json
from pathlib import Path
p=Path("packages/core/package.json")
data=json.loads(p.read_text())
print(json.dumps(data.get("exports"), indent=2, sort_keys=True))
PY

echo
echo "Top-level package files that mention core/webhooks:"
rg -n '"webhooks"|"`@trigger`\.dev/core/webhooks"|webhooks' package.json pnpm-lock.yaml -S | sed -n '1,160p'

echo
echo "Build/rollup/etc config mentions core/webhooks or webhookProducer:"
rg -n 'core/webhooks|webhookProviderConfigs|webhooks' . -g '*.json' -g '*.ts' -g '*.js' -g '*.mjs' -g '*.cjs' | rg -i '(build|rollup|vite|esbuild|exports|webhooks)' | sed -n '1,200p'

echo
echo "Trigger SDK package files:"
cat -n packages/trigger-sdk/package.json
git ls-files 'packages/trigger-sdk/src/v3/*.ts' | sed -n '1,120p'

Repository: triggerdotdev/trigger.dev

Length of output: 31814


🏁 Script executed:

#!/bin/bash
set -u

echo "Check for packages/src/webhooks source in monorepo:"
git ls-files | rg '(^|/)src/webhooks\.ts$|webhooks$' | sed -n '1,200p'

echo
echo "Search exact imports and exports containing core/webhooks:"
rg -n "`@trigger`\.dev/core/webhooks|webhooks'" packages/core packages/trigger-sdk packages -g '*.ts' -g '*.tsx' -g '*.json' | sed -n '1,200p'

Repository: triggerdotdev/trigger.dev

Length of output: 435


Resolve the @trigger.dev/core/webhooks reference.

packages/core does not export a webhooks entry, and packages/core/src/v3/schemas/webhooks.ts only exports alert/deployment webhook payload schemas, not provider configs. This import cannot be resolved from the current package structure, so import a concrete exported source package instead of adding a dependency on a non-existent @trigger.dev/core/webhooks.

@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@1b6a78d

trigger.dev

npm i https://pkg.pr.new/triggerdotdev/trigger.dev@1b6a78d

@trigger.dev/core

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/core@1b6a78d

@trigger.dev/python

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/python@1b6a78d

@trigger.dev/react-hooks

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

@trigger.dev/redis-worker

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

@trigger.dev/rsc

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/rsc@1b6a78d

@trigger.dev/schema-to-json

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

@trigger.dev/slack

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/slack@1b6a78d

@trigger.dev/sdk

npm i https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/sdk@1b6a78d

commit: 1b6a78d

@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch from 274c3ab to 9267911 Compare August 8, 2026 08:20
@ericallam
ericallam force-pushed the feat/hosted-webhooks-api branch from 9267911 to 1b6a78d Compare August 8, 2026 09:18
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