feat(sdk,slack): webhook sources, agent channels, and human-in-the-loop - #4537
feat(sdk,slack): webhook sources, agent channels, and human-in-the-loop#4537ericallam wants to merge 3 commits into
Conversation
🦋 Changeset detectedLatest commit: 481f6c1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 30 packages
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 |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughAdded typed webhook sources, provider verification, webhook tasks, and durable 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (9)
packages/slack/src/index.ts (3)
279-283: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winCheck the
response_urlresponse.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 winHandle Slack rate limiting in
makeSlackSend.The code retries only on an auth error and only when
tokenis a function. Slack rate-limitschat.postMessageandchat.updateper channel (about one message per second, withRetry-After). Withdelivery: "stream", debounced edits reach that limit quickly. Eachratelimitedresponse 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 winCheck the HTTP status before you parse the body.
slackApicallsres.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. Theretry-afterheader 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.tsreturn objects with only ajsonmethod. Addok: true(andheaders) to those doubles if you apply this change.packages/slack/package.json (1)
43-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider moving
@trigger.dev/coretodevDependencies.
packages/slack/src/index.tsimports from@trigger.dev/core/v3withimport typeonly. 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/sdkalready 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 winRestore 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. Thefetchstub then leaks into the following tests, and one failure cascades into unrelated failures.Move the cleanup into an
afterEachhook 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 winAdd a test for the token-resolver retry path.
makeSlackSendinpackages/slack/src/index.tsre-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-valuedtoken.Add a case where
tokenis a resolver, the first response is{ ok: false, error: "invalid_auth" }, and the second succeeds. Assert two fetch calls and the secondauthorizationheader.Do you want me to write that test?
packages/trigger-sdk/src/v3/webhooks.ts (1)
345-359: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace 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 webhookSourcesand spread...webhookSourcesin place of the 14 assignments.packages/trigger-sdk/src/v3/ai.ts (1)
4793-4832: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDeclare
ChannelConnectoras a type alias.Every sibling in this block is a
type.ChannelConnectoris a data descriptor built by factory functions, not a behavioral contract that a class implements, so the repository rule applies.Based on learnings, keep
interfaceonly 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 winAdd a case for a resolver that throws.
resolveReactionChoiceawaits a user-supplied function and does not catch. The tests coverundefined,null,"", and[], but not a throw.The behavior matters at the call sites. At
packages/trigger-sdk/src/v3/ai.tsline 6991 the call sits inside the turntry, 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 throwingreactions.doneorreactions.errorresolver escapes there.Every other reaction step is best-effort:
applyChannelReactioncatches and logs. MakeresolveReactionChoicematch, 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (25)
.changeset/hosted-webhook-ingress.mddocs/ai-chat/backend.mdxdocs/ai-chat/reference.mdxdocs/docs.jsondocs/webhooks/channels.mdxdocs/webhooks/connect.mdxdocs/webhooks/deliveries.mdxdocs/webhooks/filters.mdxdocs/webhooks/human-in-the-loop.mdxdocs/webhooks/overview.mdxdocs/webhooks/session-routing.mdxdocs/webhooks/sources.mdxpackages/cli-v3/src/dev/devSupervisor.tspackages/cli-v3/src/entryPoints/dev-index-worker.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/package.jsonpackages/slack/src/index.test.tspackages/slack/src/index.tspackages/slack/tsconfig.jsonpackages/slack/tsconfig.src.jsonpackages/slack/vitest.config.tspackages/trigger-sdk/src/v3/ai.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/trigger-sdk/src/v3/chat.tspackages/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 indocs.jsonwhich includes navigation structure, theme, and metadata
Navigation structure indocs.jsonshould be organized usingnavigation.dropdownswith 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/sdkin 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,jsonDocumentation in
docs/uses MDX conventions defined by the documentation guidance.
Files:
docs/webhooks/connect.mdxdocs/webhooks/deliveries.mdxdocs/webhooks/human-in-the-loop.mdxdocs/webhooks/overview.mdxdocs/webhooks/filters.mdxdocs/ai-chat/reference.mdxdocs/webhooks/session-routing.mdxdocs/webhooks/channels.mdxdocs/webhooks/sources.mdxdocs/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 dynamicimport(); 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/v3or deprecatedclient.defineJob.
Add agentcrumbs while writing code using approved namespaces; mark lines with//@Crumbsor blocks with `// `#region` `@crumbs, and strip them before merging.
Files:
packages/cli-v3/src/entryPoints/dev-index-worker.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/cli-v3/src/entryPoints/managed-index-worker.ts
packages/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
For public packages, use
buildfor verification.
Files:
packages/cli-v3/src/entryPoints/dev-index-worker.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/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.mdxdocs/webhooks/deliveries.mdxdocs/webhooks/human-in-the-loop.mdxdocs/webhooks/overview.mdxdocs/webhooks/filters.mdxdocs/ai-chat/reference.mdxdocs/webhooks/session-routing.mdxdocs/webhooks/channels.mdxdocs/webhooks/sources.mdxdocs/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.mdxdocs/webhooks/deliveries.mdxdocs/webhooks/human-in-the-loop.mdxdocs/webhooks/overview.mdxdocs/webhooks/filters.mdxdocs/ai-chat/reference.mdxdocs/webhooks/session-routing.mdxdocs/webhooks/channels.mdxdocs/webhooks/sources.mdxdocs/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/chat.tspackages/cli-v3/src/dev/devSupervisor.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/cli-v3/src/entryPoints/managed-index-worker.tspackages/slack/vitest.config.tspackages/slack/src/index.test.tspackages/slack/src/index.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/trigger-sdk/src/v3/channelReactions.test.tspackages/trigger-sdk/src/v3/webhooks.tspackages/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.tspackages/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.tspackages/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.tspackages/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.mdxdocs/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.mdxdocs/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 QualityNo change needed for
types: ["node"].
@types/nodeis available through the workspace dependency, and TypeScript type-only resolution does not require@types/nodeto 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 & PrivacyThe 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
keytemplate 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 CorrectnessNo change needed.
gitlabcan use the GitLab signing-token preset when configured, andX-Hub-Signature-256scheme.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!
| <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> |
There was a problem hiding this comment.
🎯 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:
- 1: https://docs.slack.dev/reference/events/message.md
- 2: https://docs.slack.dev/reference/events/message.channels.md
- 3: https://docs.slack.dev/reference/scopes/channels.history
- 4: https://docs.slack.dev/reference/scopes/channels.history.md
- 5: https://docs.slack.dev/apis/events-api/
🌐 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:
- 1: https://docs.slack.dev/reference/events/message.channels
- 2: https://docs.slack.dev/reference/events/message.channels.md
- 3: https://docs.slack.dev/apis/events-api/
- 4: https://docs.slack.dev/reference/scopes/channels.history.md
- 5: https://docs.slack.dev/reference/scopes/channels.history
- 6: https://docs.slack.dev/reference/scopes/chat.write
- 7: https://docs.slack.dev/reference/scopes/chat.write.md
- 8: https://docs.slack.dev/changelog/2018-01-the-right-chat-write-for-workspace-token-apps/
- 9: https://docs.slack.dev/reference/events/message.md
- 10: https://docs.slack.dev/reference/methods/chat.postmessage.md
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.
| ```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 }) => { | ||
| // ... | ||
| }, | ||
| }); |
There was a problem hiding this comment.
📐 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)
PYRepository: 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: Addimport { webhook, webhooks }to the GitHub, Svix, and custom-provider examples.docs/webhooks/filters.mdx#L88-L95: Addimport { webhook, webhooks }to the GitHub filter example.docs/webhooks/channels.mdx#L11-L19: ImportstreamTextandanthropictogether with the existing imports.docs/webhooks/human-in-the-loop.mdx#L101-L120: Importchat.columnsor use the existingchat/webhooksimport pattern beforechat.channels.custom(...).
📍 Affects 4 files
docs/webhooks/sources.mdx#L29-L48(this comment)docs/webhooks/sources.mdx#L59-L74docs/webhooks/filters.mdx#L88-L95docs/webhooks/channels.mdx#L11-L19docs/webhooks/human-in-the-loop.mdx#L101-L120
Source: Coding guidelines
| webhooks: resourceCatalog.listWebhookManifests(), | ||
| unclaimedSessionWebhooks: resourceCatalog.listUnclaimedSessionWebhooks(), |
There was a problem hiding this comment.
🗄️ 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.tsRepository: 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 200Repository: 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 220Repository: 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-L195packages/cli-v3/src/dev/devSupervisor.ts#L385-L394packages/cli-v3/src/dev/devSupervisor.ts#L404-L404
| "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" | ||
| }, |
There was a problem hiding this comment.
📐 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 || trueRepository: 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.jsonRepository: 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); |
There was a problem hiding this comment.
📐 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: assertmsg?.blocksis defined before you callflatMapon the cast value.packages/slack/src/index.test.ts#L97-L97: assertcalls[0]?.body.blocksis defined before you callmapon 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
| 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, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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, | |
| }); | |
| } | |
| } | |
| } | |
| } |
| const isWebhookAction = currentWirePayload.actionSource === "webhook"; | ||
| const parsedAction = | ||
| parseAction && !isWebhookAction | ||
| ? await parseAction(currentWirePayload.action) | ||
| : currentWirePayload.action; |
There was a problem hiding this comment.
🔒 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' || trueRepository: 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' || trueRepository: 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 -240Repository: 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 || trueRepository: 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.
| 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", | ||
| }); |
There was a problem hiding this comment.
🩺 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.
| 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(); | |
| } |
| 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 }); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 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.
| 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, |
There was a problem hiding this comment.
🗄️ 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 4Repository: 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])
PYRepository: 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)
PYRepository: 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)
PYRepository: 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.
b4b9f89 to
274c3ab
Compare
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/slack
@trigger.dev/sdk
commit: |
34c70fc to
0430f92
Compare
The deploy path now forwards declared webhooks to the server the same way dev does, so hosted webhook endpoints are created and stay active on deploy instead of only working under trigger dev.
Two webhook() declarations sharing an id used to silently overwrite each other in the worker manifest. Indexing now fails with the colliding ids and their file paths, matching how duplicate task ids are already handled.
0430f92 to
481f6c1
Compare
Summary
The public SDK and docs half of hosted webhooks:
webhook()with typed provider sources (webhooks.stripe(),webhooks.github(),webhooks.svix(), and more, pluswebhooks.custom<T>()),chat.eventandchat.channelsfor agent channels, human-in-the-loop tool approvals, the new@trigger.dev/slackconnector, and the webhooks docs section.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/coreschemas 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, andtrigger.devtogether, 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
mainautomatically.