| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 2 | 36 | 17 | 4% |
| Commands | 2 | 23 | 1 | 8% |
| Section tags | 5 | 7 | 4 | 31% |
What each file covers
Sections
2 shared · 36 only in A · 17 only in B- − AGENTS.md
- − Build and Development Commands
- − pnpm run docker:full # Same + observability stack (Prometheus, Grafana, OTEL) and chaos tooling
- − Build packages (required before running)
- − Apps and internal packages — use typecheck
- − Public packages — use build
- − Testing
- − Testcontainers for Redis/PostgreSQL
- − Code Style
- − Formatting and linting
- − Imports
- − Changesets and Server Changes
- − Dependency Pinning
- − Architecture Overview
- − Request Flow
- − Apps
- − Public Packages
- − Internal Packages
- − Documentation
- − Reference Projects
- − Docker Image Guidelines
- − Writing Trigger.dev Tasks
- − SDK Documentation Rules
- − Testing with the hello-world Reference Project
- − Local Task Testing Workflow
- − Step 1: Start Webapp in Background
- − Run from repo root with run_in_background: true
- − Step 2: Start Trigger Dev in Background
- − in your triggerdotdev/references clone
- − Wait for "Local worker ready [node]"
- − Step 3: Trigger and Monitor Tasks via MCP
- − Skill mappings — when working in these areas, load the linked skill file into context.
- − agentcrumbs
- − Namespaces
- − For PR reviewers
- − CLI
- + Webapp
- + Testing Dashboard Changes with Chrome DevTools MCP
- + Login
- + Navigating and Verifying
- + Tips
- + Key File Locations
- + Route Convention
- + Abort Signals
- + Environment Variables
- + Run Engine 2.0
- + Background Workers
- + Real-time
- + Performance: Trigger Hot Path
- + Prisma Query Patterns
- + Transactions
- + PAT-authenticated API routes
- + React Patterns
- Verifying Changes
- v3 (engine V1) removed
Commands
2 shared · 23 only in A · 1 only in B- − pnpm run docker
- − pnpm run db:migrate
- − pnpm run db:seed
- − pnpm run build --filter webapp && pnpm run build --filter trigger.dev && pnpm run build --filter @trigger.dev/sdk
- − pnpm run dev --filter trigger.dev --filter "@trigger.dev/*"
- − pnpm run typecheck --filter @internal/run-engine
- − pnpm run build --filter @trigger.dev/sdk
- − pnpm run build --filter @trigger.dev/core
- − pnpm run test --filter webapp
- − pnpm run test ./src/engine/tests/ttl.test.ts --run
- − pnpm run build --filter @internal/run-engine
- − pnpm run format
- − pnpm run lint:fix
- − pnpm run lint
- − pnpm run changeset:add
- − pnpm exec agentcrumbs collect
- − pnpm exec agentcrumbs tail --app trigger
- − pnpm exec agentcrumbs clear --app trigger
- − pnpm run
- − pnpm add
- − pnpm i
- − pnpm run build --filter trigger.dev
- − pnpm exec agentcrumbs query --app trigger
- + pnpm run build --filter webapp
- pnpm run dev --filter webapp
- pnpm run typecheck --filter webapp
Section tags
5 shared · 7 only in A · 4 only in B- − lint-format
- − architecture
- − testing-strategy
- − git-pr
- − dependencies
- − agent-behaviour
- − docs
- + security
- + database
- + api
- + performance
- setup
- build
- test
- code-style
- do-not
Line diff
triggerdotdev/trigger.dev · AGENTS.md
@@ −1 @@
1# AGENTS.md
2
3This file provides guidance to Claude Code when working with this repository. Subdirectory CLAUDE.md files provide deeper context when you navigate into specific areas.
4
5## Build and Development Commands
6
7This is a pnpm 10.33.2 monorepo using Turborepo. Run commands from root with `pnpm run`.
8
9**Adding dependencies:** Edit `package.json` directly instead of using `pnpm add`, then run `pnpm i` from the repo root. See `.claude/rules/package-installation.md` for the full process.
10
11```bash
12pnpm run docker # Core dev services (Postgres, Redis, Electric, MinIO, ClickHouse, s2-lite)
13# pnpm run docker:full # Same + observability stack (Prometheus, Grafana, OTEL) and chaos tooling
14pnpm run db:migrate # Run database migrations
15pnpm run db:seed # Seed the database (required for reference projects)
16
17# Build packages (required before running)
18pnpm run build --filter webapp && pnpm run build --filter trigger.dev && pnpm run build --filter @trigger.dev/sdk
19
20pnpm run dev --filter webapp # Run webapp (http://localhost:3030)
21pnpm run dev --filter trigger.dev --filter "@trigger.dev/*" # Watch CLI and packages
22```
23
24### Verifying Changes
25
26The verification command depends on where the change lives:
27
28- **Apps and internal packages** (`apps/*`, `internal-packages/*`): Use `typecheck`. **Never use `build`** for these — building proves almost nothing about correctness.
29- **Public packages** (`packages/*`): Use `build`.
30
31```bash
32# Apps and internal packages — use typecheck
33pnpm run typecheck --filter webapp # ~1-2 minutes
34pnpm run typecheck --filter @internal/run-engine
35
36# Public packages — use build
37pnpm run build --filter @trigger.dev/sdk
38pnpm run build --filter @trigger.dev/core
39```
40
41Only run typecheck/build after major changes (new files, significant refactors, schema changes). For small edits, trust the types and let CI catch issues.
42
43## Testing
44
45We use vitest exclusively. **Never mock anything** - use testcontainers instead.
46
47```bash
48pnpm run test --filter webapp # All tests for a package
49cd internal-packages/run-engine
50pnpm run test ./src/engine/tests/ttl.test.ts --run # Single test file
51pnpm run build --filter @internal/run-engine # May need to build deps first
52```
53
54Test files go next to source files (e.g., `MyService.ts` -> `MyService.test.ts`).
55
56### Testcontainers for Redis/PostgreSQL
57
58```typescript
59import { redisTest, postgresTest, containerTest } from "@internal/testcontainers";
60
61redisTest("should use redis", async ({ redisOptions }) => {
62 /* ... */
63});
64postgresTest("should use postgres", async ({ prisma }) => {
65 /* ... */
66});
67containerTest("should use both", async ({ prisma, redisOptions }) => {
68 /* ... */
69});
70```
71
72## Code Style
73
74### Formatting and linting
75
76Format and lint are enforced by CI (`code-quality` check). Run before committing:
77
78```bash
79pnpm run format # oxfmt — auto-fixes formatting
80pnpm run lint:fix # oxlint — auto-fixes lint violations
81pnpm run lint # oxlint — check only (no fixes)
82```
83
84### Imports
85
86**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:
87- Circular dependencies cannot be resolved otherwise
88- Code splitting is genuinely needed for performance
89- The module must be loaded conditionally at runtime
90
91Dynamic imports add unnecessary overhead in hot paths and make code harder to analyze. If you find yourself using `await import()`, ask if a regular `import` statement would work instead.
92
93## Changesets and Server Changes
94
95When modifying any public package (`packages/*` or `integrations/*`), add a changeset:
96
97```bash
98pnpm run changeset:add
99```
100
101- Default to **patch** for bug fixes and minor changes
102- Confirm with maintainers before selecting **minor** (new features)
103- **Never** select major without explicit approval
104
105When modifying only server components (`apps/webapp/`, `apps/supervisor/`, etc.) with no package changes, add a `.server-changes/` file instead. See `.server-changes/README.md` for format and documentation.
106
107**Write the description for users, not maintainers.** Both changesets and `.server-changes/` notes ship verbatim in user-visible release notes. Lead with what changed *for the user* - one plain sentence describing behavior, not implementation, and never naming internal tools or infra. The full writing guidance in `.server-changes/README.md` applies to changesets too.
108
109## Dependency Pinning
110
111Zod is pinned to a single version across the entire monorepo (currently `3.25.76`). When adding zod to a new or existing package, use the **exact same version** as the rest of the repo - never a different version or a range. Mismatched zod versions cause runtime type incompatibilities (e.g., schemas from one package can't be used as body validators in another).
112
113## Architecture Overview
114
115### Request Flow
116
117User API call -> Webapp routes -> Services -> RunEngine -> Redis Queue -> Supervisor -> Container execution -> Results back through RunEngine -> ClickHouse (analytics) + PostgreSQL (state)
118
119### Apps
120
121- **apps/webapp**: Remix 2.17.4 app - main API, dashboard, orchestration. Uses Express server.
122- **apps/supervisor**: Manages task execution containers (Docker/Kubernetes).
123
124### Public Packages
125
126- **packages/trigger-sdk** (`@trigger.dev/sdk`): Main SDK for writing tasks
127- **packages/cli-v3** (`trigger.dev`): CLI - also bundles code that goes into customer task images
128- **packages/core** (`@trigger.dev/core`): Shared types. **Import subpaths only** (never root).
129- **packages/build** (`@trigger.dev/build`): Build extensions and types
130- **packages/react-hooks**: React hooks for realtime and triggering
131- **packages/redis-worker** (`@trigger.dev/redis-worker`): Redis-based background job system
132
133### Internal Packages
134
135- **internal-packages/database**: Prisma 6.14.0 client and schema (PostgreSQL)
136- **internal-packages/clickhouse**: ClickHouse client, schema migrations, analytics queries
137- **internal-packages/run-engine**: "Run Engine 2.0" - core run lifecycle management
138- **internal-packages/redis**: Redis client creation utilities (ioredis)
139- **internal-packages/testcontainers**: Test helpers for Redis/PostgreSQL containers
140- **internal-packages/schedule-engine**: Durable cron scheduling
141
142### v3 (engine V1) removed
143
144v3 (engine V1: MarQS + Graphile worker) is end-of-life and its execution code has been removed. The `apps/webapp/app/v3/` directory name is historical - everything there now serves V2 (Run Engine 2.0, `@internal/run-engine` + redis-worker). There is no V1 execution path: a `RunEngineVersion` `V1` branch only rejects or finalizes gracefully so v3 clients get a clean 4xx, never a 5xx. Do not reintroduce V1. See `apps/webapp/CLAUDE.md` and `.claude/rules/legacy-v3-code.md`.
145
146### Documentation
147
148Docs live in `docs/` as a Mintlify site (MDX format). See `docs/CLAUDE.md` for conventions.
149
150### Reference Projects
151
152Reference/example projects for testing SDK and platform features live in a separate repo: [`triggerdotdev/references`](https://github.com/triggerdotdev/references). Clone it alongside this repo and use its `projects/hello-world` to manually test changes before submitting PRs. See that repo's README for setup and linking to a local monorepo build.
153
154## Docker Image Guidelines
155
156When updating Docker image references:
157
158- **Always use multiplatform/index digests**, not architecture-specific digests
159- Architecture-specific digests cause CI failures on different build environments
160- Use the digest from the main Docker Hub page, not from a specific OS/ARCH variant
161
162## Writing Trigger.dev Tasks
163
164Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` or deprecated `client.defineJob`.
165
166```typescript
167import { task } from "@trigger.dev/sdk";
168
169export const myTask = task({
170 id: "my-task",
171 run: async (payload: { message: string }) => {
172 // Task logic
173 },
174});
175```
176
177### SDK Documentation Rules
178
179The `rules/` directory contains versioned SDK documentation distributed via the SDK installer. Current version: `rules/manifest.json`. Do NOT update `rules/` or `.claude/skills/trigger-dev-tasks/` unless explicitly asked - these are maintained in separate dedicated passes.
180
181## Testing with the hello-world Reference Project
182
183The reference projects live in the separate [`triggerdotdev/references`](https://github.com/triggerdotdev/references) repo - clone it alongside this repo.
184
185First-time setup:
186
1871. `pnpm run db:seed` to seed the database (creates the References org + hello-world project)
1882. Build the CLI/packages you want to test: `pnpm run build --filter trigger.dev`
1893. In your `references` clone, follow its README to link to your local monorepo build, then authorize: `cd projects/hello-world && pnpm exec trigger login -a http://localhost:3030`
190
191Running (from your `references` clone): `cd projects/hello-world && pnpm exec trigger dev`
192
193## Local Task Testing Workflow
194
195### Step 1: Start Webapp in Background
196
197```bash
198# Run from repo root with run_in_background: true
199pnpm run dev --filter webapp
200curl -s http://localhost:3030/healthcheck # Verify running
201```
202
203### Step 2: Start Trigger Dev in Background
204
205```bash
206# in your triggerdotdev/references clone
207cd projects/hello-world && pnpm exec trigger dev
208# Wait for "Local worker ready [node]"
209```
210
211### Step 3: Trigger and Monitor Tasks via MCP
212
213```
214mcp__trigger__get_current_worker(projectRef: "proj_rrkpdguyagvsoktglnod", environment: "dev")
215mcp__trigger__trigger_task(projectRef: "proj_rrkpdguyagvsoktglnod", environment: "dev", taskId: "hello-world", payload: {"message": "Hello"})
216mcp__trigger__list_runs(projectRef: "proj_rrkpdguyagvsoktglnod", environment: "dev", taskIdentifier: "hello-world", limit: 5)
217```
218
219Dashboard: http://localhost:3030/orgs/references-9dfd/projects/hello-world-97DT/env/dev/runs
220
221<!-- intent-skills:start -->
222
223# Skill mappings — when working in these areas, load the linked skill file into context.
224
225skills:
226
227- task: "Using agentcrumbs for debug tracing, adding crumbs, trails, markers, querying traces, or stripping debug code before merge"
228 load: "node_modules/agentcrumbs/skills/agentcrumbs/SKILL.md"
229- task: "Setting up agentcrumbs in the project, initializing namespace catalog, running crumbs init"
230 load: "node_modules/agentcrumbs/skills/agentcrumbs/init/SKILL.md"
231<!-- intent-skills:end -->
232
233## agentcrumbs
234
235Add crumbs as you write code — not just when debugging. Mark lines with
236`// @crumbs` or wrap blocks in `// #region @crumbs`. They stay on the
237branch throughout development and are stripped by `agentcrumbs strip`
238before merge.
239
240### Namespaces
241
242| Namespace | Description | Path |
243| ----------------- | ---------------------------------------------- | ----------------------------------- |
244| `webapp` | Main Remix app — API, dashboard, orchestration | `apps/webapp` |
245| `supervisor` | Task execution container manager | `apps/supervisor` |
246| `sdk` | Main SDK for writing tasks | `packages/trigger-sdk` |
247| `cli` | CLI and task image bundling | `packages/cli-v3` |
248| `core` | Shared types and subpath exports | `packages/core` |
249| `build` | Build extensions and types | `packages/build` |
250| `react-hooks` | React hooks for realtime/triggering | `packages/react-hooks` |
251| `redis-worker` | Redis-based background job system | `packages/redis-worker` |
252| `run-engine` | Run Engine 2.0 — core run lifecycle | `internal-packages/run-engine` |
253| `database` | Prisma client and schema | `internal-packages/database` |
254| `clickhouse` | ClickHouse client, migrations, analytics | `internal-packages/clickhouse` |
255| `schedule-engine` | Durable cron scheduling | `internal-packages/schedule-engine` |
256| `run-queue` | Run queue management | `internal-packages/run-queue` |
257
258Do not invent new namespaces — pick from this table or ask first.
259
260### For PR reviewers
261
262Do not flag `// @crumbs` markers, `#region @crumbs` blocks, or agentcrumbs
263imports in reviews. These are temporary debug instrumentation stripped
264before merge. Data logged in crumbs (IDs, names, values) never reaches
265production.
266
267### CLI
268
269```bash
270pnpm exec agentcrumbs collect # start collector (multi-service)
271pnpm exec agentcrumbs tail --app trigger # live tail
272pnpm exec agentcrumbs clear --app trigger # remove crumbs before merge
273```
274
275The preferred way to query for crumbs is to use `pnpm exec agentcrumbs query --app trigger` with the `--limit` option and cursor pagination, and clear existing crumbs before reproducing a bug via `pnpm exec agentcrumbs clear --app trigger`.
276
triggerdotdev/trigger.dev · apps/webapp/CLAUDE.md
@@ +1 @@
1# Webapp
2
3Remix 2.17.4 app serving as the main API, dashboard, and orchestration engine. Uses an Express server (`server.ts`).
4
5## Verifying Changes
6
7**Never run `pnpm run build --filter webapp` to verify changes.** Building proves almost nothing about correctness. The webapp is an app, not a public package — use typecheck from the repo root:
8
9```bash
10pnpm run typecheck --filter webapp # ~1-2 minutes
11```
12
13Only run typecheck after major changes (new files, significant refactors, schema changes). For small edits, trust the types and let CI catch issues.
14
15Note: Public packages (`packages/*`) use `build` instead. See the root CLAUDE.md for details.
16
17## Testing Dashboard Changes with Chrome DevTools MCP
18
19Use the `chrome-devtools` MCP server to visually verify local dashboard changes. The webapp must be running (`pnpm run dev --filter webapp` from repo root).
20
21### Login
22
23```
241. mcp__chrome-devtools__new_page(url: "http://localhost:3030")
25 → Redirects to /login
262. mcp__chrome-devtools__click the "Continue with Email" link
273. mcp__chrome-devtools__fill the email field with "local@trigger.dev"
284. mcp__chrome-devtools__click "Send a magic link"
29 → Auto-logs in and redirects to the dashboard (no email verification needed locally)
30```
31
32### Navigating and Verifying
33
34- **take_snapshot**: Get an a11y tree of the page (text content, element UIDs for interaction). Prefer this over screenshots for understanding page structure.
35- **take_screenshot**: Capture what the page looks like visually. Use to verify styling, layout, and visual changes.
36- **navigate_page**: Go to specific URLs, e.g. `http://localhost:3030/orgs/references-bc08/projects/hello-world-SiWs/env/dev/runs`
37- **click / fill**: Interact with elements using UIDs from `take_snapshot`.
38- **evaluate_script**: Run JS in the browser console for debugging.
39- **list_console_messages**: Check for console errors after navigating.
40
41### Tips
42
43- Snapshots can be very large on complex pages (200K+ chars). Use `take_screenshot` first to orient, then `take_snapshot` only when you need element UIDs to interact.
44- The local seeded user email is `local@trigger.dev`.
45- Dashboard URL pattern: `http://localhost:3030/orgs/{orgSlug}/projects/{projectSlug}/env/{envSlug}/{section}`
46
47## Key File Locations
48
49- **Trigger API**: `app/routes/api.v1.tasks.$taskId.trigger.ts`
50- **Batch trigger**: `app/routes/api.v1.tasks.batch.ts`
51- **OTEL endpoints**: `app/routes/otel.v1.logs.ts`, `app/routes/otel.v1.traces.ts`
52- **Prisma setup**: `app/db.server.ts`
53- **Run engine config**: `app/v3/runEngine.server.ts`
54- **Services**: `app/v3/services/**/*.server.ts`
55- **Presenters**: `app/v3/presenters/**/*.server.ts`
56
57## Route Convention
58
59Routes use Remix flat-file convention with dot-separated segments:
60`api.v1.tasks.$taskId.trigger.ts` -> `/api/v1/tasks/:taskId/trigger`
61
62## Abort Signals
63
64**Never use `request.signal`** for detecting client disconnects. It is broken due to a Node.js bug ([nodejs/node#55428](https://github.com/nodejs/node/issues/55428)) where the AbortSignal chain is severed when Remix internally clones the Request object. Instead, use `getRequestAbortSignal()` from `app/services/httpAsyncStorage.server.ts`, which is wired directly to Express `res.on("close")` and fires reliably.
65
66```typescript
67import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
68
69// In route handlers, SSE streams, or any server-side code:
70const signal = getRequestAbortSignal();
71```
72
73## Environment Variables
74
75Access via `env` export from `app/env.server.ts`. **Never use `process.env` directly.**
76
77For testable code, **never import env.server.ts** in test files. Pass configuration as options instead:
78- `realtime/nativeRealtimeClient.server.ts` (testable service, takes config as constructor arg)
79- `realtime/nativeRealtimeClientInstance.server.ts` (creates singleton with env config)
80
81## Run Engine 2.0
82
83The webapp integrates `@internal/run-engine` via `app/v3/runEngine.server.ts`. This is the singleton engine instance. Services in `app/v3/services/` call engine methods for all run lifecycle operations (triggering, completing, cancelling, etc.).
84
85The `engineVersion.server.ts` file determines V1 vs V2 for a given environment. New code should always target V2.
86
87## Background Workers
88
89Background job workers use `@trigger.dev/redis-worker`:
90- `app/v3/commonWorker.server.ts`
91- `app/v3/alertsWorker.server.ts`
92- `app/v3/batchTriggerWorker.server.ts`
93
94## Real-time
95
96- Socket.io: `app/v3/handleSocketIo.server.ts`, `app/v3/handleWebsockets.server.ts`
97- Electric SQL: Powers real-time data sync for the dashboard
98
99## v3 (engine V1) removed
100
101v3 (engine V1: MarQS + Graphile worker) is end-of-life and its execution code is gone. The `app/v3/` directory name is historical; everything under it now serves V2. There is no V1 execution path: a `RunEngineVersion` `V1` branch (e.g. in `triggerTask.server.ts`, `cancelTaskRun.server.ts`) only rejects/finalizes gracefully so v3 clients get a clean 4xx, never a 5xx. Do not reintroduce V1. See `.claude/rules/legacy-v3-code.md` for the deprecation boundary.
102
103## Performance: Trigger Hot Path
104
105The `triggerTask.server.ts` service is the **highest-throughput code path** in the system. Every API trigger call goes through it. Keep it fast:
106
107- **Do NOT add database queries** to `triggerTask.server.ts` or `batchTriggerV3.server.ts`. Task defaults (TTL, etc.) are resolved via `backgroundWorkerTask.findFirst()` in the queue concern (`queues.server.ts`) - one query per request, in mutually exclusive branches depending on locked/non-locked path. Piggyback on the existing query instead of adding new ones.
108- **Two-stage resolution pattern**: Task metadata is resolved in two stages by design:
109 1. **Trigger time** (`triggerTask.server.ts`): Only TTL is resolved from task defaults. Everything else uses whatever the caller provides.
110 2. **Dequeue time** (`dequeueSystem.ts`): Full `BackgroundWorkerTask` is loaded and retry config, machine config, maxDuration, etc. are resolved against task defaults.
111- If you need to add a new task-level default, **add it to the existing `select` clause** in the `backgroundWorkerTask.findFirst()` query — do NOT add a second query. If the default doesn't need to be known at trigger time, resolve it at dequeue time instead.
112- Batch triggers (`batchTriggerV3.server.ts`) follow the same pattern — keep batch paths equally fast.
113
114## Prisma Query Patterns
115
116- **Always use `findFirst` instead of `findUnique`.** Prisma's `findUnique` has an implicit DataLoader that batches concurrent calls into a single `IN` query. This batching cannot be disabled and has active bugs even in Prisma 6.x: uppercase UUIDs returning null (#25484, confirmed 6.4.1), composite key SQL correctness issues (#22202), and 5-10x worse performance than manual DataLoader (#6573, open since 2021). `findFirst` is never batched and avoids this entire class of issues.
117
118## Transactions
119
120- **Always use the `$transaction` helper from `~/db.server`, never `prisma.$transaction` (or `$replica.$transaction`) directly.** The helper wraps the raw call with tracing (an OTEL span + an `isolation_level` attribute) and boundary logging for infrastructure errors (e.g. `PrismaClientInitializationError`) that the raw client swallows. Signature: `$transaction(prisma, name?, async (tx) => { ... }, options?)`.
121- Pass the isolation level via options as a string: `{ isolationLevel: "Serializable" }`. Reach for `Serializable` when a read-then-write must be atomic against concurrent transactions (e.g. a count-then-delete invariant); the loser of a race fails and can retry, which is the right trade for rare, correctness-critical paths.
122- The helper returns `R | undefined` — guard the result (`if (!result) throw ...`) when callers need a definite value.
123
124## PAT-authenticated API routes
125
126- **A PAT route must resolve its target org/project scoped to the caller's membership** (`members: { some: { userId } }`, or a helper like `findProjectByRef` / `resolveOrganizationForApiUser`). A PAT is user-scoped and can name any org/project by id/slug, and the OSS RBAC fallback ability is permissive — so `ability.can(...)` alone does NOT reject a non-member on self-hosted. The RBAC `authorization` gate enforces the *role*; the membership-scoped query is the *tenant* floor. Skipping it opens cross-org access on OSS.
127
128## React Patterns
129
130- Only use `useCallback`/`useMemo` for context provider values, expensive derived data that is a dependency elsewhere, or stable refs required by a dependency array. Don't wrap ordinary event handlers or trivial computations.
131- Use named constants for sentinel/placeholder values (e.g. `const UNSET_VALUE = "__unset__"`) instead of raw string literals scattered across comparisons.
132
@@ −1 +1 @@
1−# AGENTS.md
1+# Webapp
22
3−This file provides guidance to Claude Code when working with this repository. Subdirectory CLAUDE.md files provide deeper context when you navigate into specific areas.
3+Remix 2.17.4 app serving as the main API, dashboard, and orchestration engine. Uses an Express server (`server.ts`).
44
5−## Build and Development Commands
5+## Verifying Changes
66
7−This is a pnpm 10.33.2 monorepo using Turborepo. Run commands from root with `pnpm run`.
7+**Never run `pnpm run build --filter webapp` to verify changes.** Building proves almost nothing about correctness. The webapp is an app, not a public package — use typecheck from the repo root:
88
9−**Adding dependencies:** Edit `package.json` directly instead of using `pnpm add`, then run `pnpm i` from the repo root. See `.claude/rules/package-installation.md` for the full process.
10−
119 ```bash
12−pnpm run docker # Core dev services (Postgres, Redis, Electric, MinIO, ClickHouse, s2-lite)
13−# pnpm run docker:full # Same + observability stack (Prometheus, Grafana, OTEL) and chaos tooling
14−pnpm run db:migrate # Run database migrations
15−pnpm run db:seed # Seed the database (required for reference projects)
16−
17−# Build packages (required before running)
18−pnpm run build --filter webapp && pnpm run build --filter trigger.dev && pnpm run build --filter @trigger.dev/sdk
19−
20−pnpm run dev --filter webapp # Run webapp (http://localhost:3030)
21−pnpm run dev --filter trigger.dev --filter "@trigger.dev/*" # Watch CLI and packages
10+pnpm run typecheck --filter webapp # ~1-2 minutes
2211 ```
2312
24−### Verifying Changes
13+Only run typecheck after major changes (new files, significant refactors, schema changes). For small edits, trust the types and let CI catch issues.
2514
26−The verification command depends on where the change lives:
15+Note: Public packages (`packages/*`) use `build` instead. See the root CLAUDE.md for details.
2716
28−- **Apps and internal packages** (`apps/*`, `internal-packages/*`): Use `typecheck`. **Never use `build`** for these — building proves almost nothing about correctness.
29−- **Public packages** (`packages/*`): Use `build`.
17+## Testing Dashboard Changes with Chrome DevTools MCP
3018
31−```bash
32−# Apps and internal packages — use typecheck
33−pnpm run typecheck --filter webapp # ~1-2 minutes
34−pnpm run typecheck --filter @internal/run-engine
19+Use the `chrome-devtools` MCP server to visually verify local dashboard changes. The webapp must be running (`pnpm run dev --filter webapp` from repo root).
3520
36−# Public packages — use build
37−pnpm run build --filter @trigger.dev/sdk
38−pnpm run build --filter @trigger.dev/core
39−```
21+### Login
4022
41−Only run typecheck/build after major changes (new files, significant refactors, schema changes). For small edits, trust the types and let CI catch issues.
42−
43−## Testing
44−
45−We use vitest exclusively. **Never mock anything** - use testcontainers instead.
46−
47−```bash
48−pnpm run test --filter webapp # All tests for a package
49−cd internal-packages/run-engine
50−pnpm run test ./src/engine/tests/ttl.test.ts --run # Single test file
51−pnpm run build --filter @internal/run-engine # May need to build deps first
5223 ```
53−
54−Test files go next to source files (e.g., `MyService.ts` -> `MyService.test.ts`).
55−
56−### Testcontainers for Redis/PostgreSQL
57−
58−```typescript
59−import { redisTest, postgresTest, containerTest } from "@internal/testcontainers";
60−
61−redisTest("should use redis", async ({ redisOptions }) => {
62− /* ... */
63−});
64−postgresTest("should use postgres", async ({ prisma }) => {
65− /* ... */
66−});
67−containerTest("should use both", async ({ prisma, redisOptions }) => {
68− /* ... */
69−});
24+1. mcp__chrome-devtools__new_page(url: "http://localhost:3030")
25+ → Redirects to /login
26+2. mcp__chrome-devtools__click the "Continue with Email" link
27+3. mcp__chrome-devtools__fill the email field with "local@trigger.dev"
28+4. mcp__chrome-devtools__click "Send a magic link"
29+ → Auto-logs in and redirects to the dashboard (no email verification needed locally)
7030 ```
7131
72−## Code Style
32+### Navigating and Verifying
7333
74−### Formatting and linting
34+- **take_snapshot**: Get an a11y tree of the page (text content, element UIDs for interaction). Prefer this over screenshots for understanding page structure.
35+- **take_screenshot**: Capture what the page looks like visually. Use to verify styling, layout, and visual changes.
36+- **navigate_page**: Go to specific URLs, e.g. `http://localhost:3030/orgs/references-bc08/projects/hello-world-SiWs/env/dev/runs`
37+- **click / fill**: Interact with elements using UIDs from `take_snapshot`.
38+- **evaluate_script**: Run JS in the browser console for debugging.
39+- **list_console_messages**: Check for console errors after navigating.
7540
76−Format and lint are enforced by CI (`code-quality` check). Run before committing:
41+### Tips
7742
78−```bash
79−pnpm run format # oxfmt — auto-fixes formatting
80−pnpm run lint:fix # oxlint — auto-fixes lint violations
81−pnpm run lint # oxlint — check only (no fixes)
82−```
43+- Snapshots can be very large on complex pages (200K+ chars). Use `take_screenshot` first to orient, then `take_snapshot` only when you need element UIDs to interact.
44+- The local seeded user email is `local@trigger.dev`.
45+- Dashboard URL pattern: `http://localhost:3030/orgs/{orgSlug}/projects/{projectSlug}/env/{envSlug}/{section}`
8346
84−### Imports
47+## Key File Locations
8548
86−**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:
87−- Circular dependencies cannot be resolved otherwise
88−- Code splitting is genuinely needed for performance
89−- The module must be loaded conditionally at runtime
49+- **Trigger API**: `app/routes/api.v1.tasks.$taskId.trigger.ts`
50+- **Batch trigger**: `app/routes/api.v1.tasks.batch.ts`
51+- **OTEL endpoints**: `app/routes/otel.v1.logs.ts`, `app/routes/otel.v1.traces.ts`
52+- **Prisma setup**: `app/db.server.ts`
53+- **Run engine config**: `app/v3/runEngine.server.ts`
54+- **Services**: `app/v3/services/**/*.server.ts`
55+- **Presenters**: `app/v3/presenters/**/*.server.ts`
9056
91−Dynamic imports add unnecessary overhead in hot paths and make code harder to analyze. If you find yourself using `await import()`, ask if a regular `import` statement would work instead.
57+## Route Convention
9258
93−## Changesets and Server Changes
59+Routes use Remix flat-file convention with dot-separated segments:
60+`api.v1.tasks.$taskId.trigger.ts` -> `/api/v1/tasks/:taskId/trigger`
9461
95−When modifying any public package (`packages/*` or `integrations/*`), add a changeset:
62+## Abort Signals
9663
97−```bash
98−pnpm run changeset:add
99−```
64+**Never use `request.signal`** for detecting client disconnects. It is broken due to a Node.js bug ([nodejs/node#55428](https://github.com/nodejs/node/issues/55428)) where the AbortSignal chain is severed when Remix internally clones the Request object. Instead, use `getRequestAbortSignal()` from `app/services/httpAsyncStorage.server.ts`, which is wired directly to Express `res.on("close")` and fires reliably.
10065
101−- Default to **patch** for bug fixes and minor changes
102−- Confirm with maintainers before selecting **minor** (new features)
103−- **Never** select major without explicit approval
104−
105−When modifying only server components (`apps/webapp/`, `apps/supervisor/`, etc.) with no package changes, add a `.server-changes/` file instead. See `.server-changes/README.md` for format and documentation.
106−
107−**Write the description for users, not maintainers.** Both changesets and `.server-changes/` notes ship verbatim in user-visible release notes. Lead with what changed *for the user* - one plain sentence describing behavior, not implementation, and never naming internal tools or infra. The full writing guidance in `.server-changes/README.md` applies to changesets too.
108−
109−## Dependency Pinning
110−
111−Zod is pinned to a single version across the entire monorepo (currently `3.25.76`). When adding zod to a new or existing package, use the **exact same version** as the rest of the repo - never a different version or a range. Mismatched zod versions cause runtime type incompatibilities (e.g., schemas from one package can't be used as body validators in another).
112−
113−## Architecture Overview
114−
115−### Request Flow
116−
117−User API call -> Webapp routes -> Services -> RunEngine -> Redis Queue -> Supervisor -> Container execution -> Results back through RunEngine -> ClickHouse (analytics) + PostgreSQL (state)
118−
119−### Apps
120−
121−- **apps/webapp**: Remix 2.17.4 app - main API, dashboard, orchestration. Uses Express server.
122−- **apps/supervisor**: Manages task execution containers (Docker/Kubernetes).
123−
124−### Public Packages
125−
126−- **packages/trigger-sdk** (`@trigger.dev/sdk`): Main SDK for writing tasks
127−- **packages/cli-v3** (`trigger.dev`): CLI - also bundles code that goes into customer task images
128−- **packages/core** (`@trigger.dev/core`): Shared types. **Import subpaths only** (never root).
129−- **packages/build** (`@trigger.dev/build`): Build extensions and types
130−- **packages/react-hooks**: React hooks for realtime and triggering
131−- **packages/redis-worker** (`@trigger.dev/redis-worker`): Redis-based background job system
132−
133−### Internal Packages
134−
135−- **internal-packages/database**: Prisma 6.14.0 client and schema (PostgreSQL)
136−- **internal-packages/clickhouse**: ClickHouse client, schema migrations, analytics queries
137−- **internal-packages/run-engine**: "Run Engine 2.0" - core run lifecycle management
138−- **internal-packages/redis**: Redis client creation utilities (ioredis)
139−- **internal-packages/testcontainers**: Test helpers for Redis/PostgreSQL containers
140−- **internal-packages/schedule-engine**: Durable cron scheduling
141−
142−### v3 (engine V1) removed
143−
144−v3 (engine V1: MarQS + Graphile worker) is end-of-life and its execution code has been removed. The `apps/webapp/app/v3/` directory name is historical - everything there now serves V2 (Run Engine 2.0, `@internal/run-engine` + redis-worker). There is no V1 execution path: a `RunEngineVersion` `V1` branch only rejects or finalizes gracefully so v3 clients get a clean 4xx, never a 5xx. Do not reintroduce V1. See `apps/webapp/CLAUDE.md` and `.claude/rules/legacy-v3-code.md`.
145−
146−### Documentation
147−
148−Docs live in `docs/` as a Mintlify site (MDX format). See `docs/CLAUDE.md` for conventions.
149−
150−### Reference Projects
151−
152−Reference/example projects for testing SDK and platform features live in a separate repo: [`triggerdotdev/references`](https://github.com/triggerdotdev/references). Clone it alongside this repo and use its `projects/hello-world` to manually test changes before submitting PRs. See that repo's README for setup and linking to a local monorepo build.
153−
154−## Docker Image Guidelines
155−
156−When updating Docker image references:
157−
158−- **Always use multiplatform/index digests**, not architecture-specific digests
159−- Architecture-specific digests cause CI failures on different build environments
160−- Use the digest from the main Docker Hub page, not from a specific OS/ARCH variant
161−
162−## Writing Trigger.dev Tasks
163−
164−Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` or deprecated `client.defineJob`.
165−
16666 ```typescript
167−import { task } from "@trigger.dev/sdk";
67+import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
16868
169−export const myTask = task({
170− id: "my-task",
171− run: async (payload: { message: string }) => {
172− // Task logic
173− },
174−});
69+// In route handlers, SSE streams, or any server-side code:
70+const signal = getRequestAbortSignal();
17571 ```
17672
177−### SDK Documentation Rules
73+## Environment Variables
17874
179−The `rules/` directory contains versioned SDK documentation distributed via the SDK installer. Current version: `rules/manifest.json`. Do NOT update `rules/` or `.claude/skills/trigger-dev-tasks/` unless explicitly asked - these are maintained in separate dedicated passes.
75+Access via `env` export from `app/env.server.ts`. **Never use `process.env` directly.**
18076
181−## Testing with the hello-world Reference Project
77+For testable code, **never import env.server.ts** in test files. Pass configuration as options instead:
78+- `realtime/nativeRealtimeClient.server.ts` (testable service, takes config as constructor arg)
79+- `realtime/nativeRealtimeClientInstance.server.ts` (creates singleton with env config)
18280
183−The reference projects live in the separate [`triggerdotdev/references`](https://github.com/triggerdotdev/references) repo - clone it alongside this repo.
81+## Run Engine 2.0
18482
185−First-time setup:
83+The webapp integrates `@internal/run-engine` via `app/v3/runEngine.server.ts`. This is the singleton engine instance. Services in `app/v3/services/` call engine methods for all run lifecycle operations (triggering, completing, cancelling, etc.).
18684
187−1. `pnpm run db:seed` to seed the database (creates the References org + hello-world project)
188−2. Build the CLI/packages you want to test: `pnpm run build --filter trigger.dev`
189−3. In your `references` clone, follow its README to link to your local monorepo build, then authorize: `cd projects/hello-world && pnpm exec trigger login -a http://localhost:3030`
85+The `engineVersion.server.ts` file determines V1 vs V2 for a given environment. New code should always target V2.
19086
191−Running (from your `references` clone): `cd projects/hello-world && pnpm exec trigger dev`
87+## Background Workers
19288
193−## Local Task Testing Workflow
89+Background job workers use `@trigger.dev/redis-worker`:
90+- `app/v3/commonWorker.server.ts`
91+- `app/v3/alertsWorker.server.ts`
92+- `app/v3/batchTriggerWorker.server.ts`
19493
195−### Step 1: Start Webapp in Background
94+## Real-time
19695
197−```bash
198−# Run from repo root with run_in_background: true
199−pnpm run dev --filter webapp
200−curl -s http://localhost:3030/healthcheck # Verify running
201−```
96+- Socket.io: `app/v3/handleSocketIo.server.ts`, `app/v3/handleWebsockets.server.ts`
97+- Electric SQL: Powers real-time data sync for the dashboard
20298
203−### Step 2: Start Trigger Dev in Background
99+## v3 (engine V1) removed
204100
205−```bash
206−# in your triggerdotdev/references clone
207−cd projects/hello-world && pnpm exec trigger dev
208−# Wait for "Local worker ready [node]"
209−```
101+v3 (engine V1: MarQS + Graphile worker) is end-of-life and its execution code is gone. The `app/v3/` directory name is historical; everything under it now serves V2. There is no V1 execution path: a `RunEngineVersion` `V1` branch (e.g. in `triggerTask.server.ts`, `cancelTaskRun.server.ts`) only rejects/finalizes gracefully so v3 clients get a clean 4xx, never a 5xx. Do not reintroduce V1. See `.claude/rules/legacy-v3-code.md` for the deprecation boundary.
210102
211−### Step 3: Trigger and Monitor Tasks via MCP
103+## Performance: Trigger Hot Path
212104
213−```
214−mcp__trigger__get_current_worker(projectRef: "proj_rrkpdguyagvsoktglnod", environment: "dev")
215−mcp__trigger__trigger_task(projectRef: "proj_rrkpdguyagvsoktglnod", environment: "dev", taskId: "hello-world", payload: {"message": "Hello"})
216−mcp__trigger__list_runs(projectRef: "proj_rrkpdguyagvsoktglnod", environment: "dev", taskIdentifier: "hello-world", limit: 5)
217−```
105+The `triggerTask.server.ts` service is the **highest-throughput code path** in the system. Every API trigger call goes through it. Keep it fast:
218106
219−Dashboard: http://localhost:3030/orgs/references-9dfd/projects/hello-world-97DT/env/dev/runs
107+- **Do NOT add database queries** to `triggerTask.server.ts` or `batchTriggerV3.server.ts`. Task defaults (TTL, etc.) are resolved via `backgroundWorkerTask.findFirst()` in the queue concern (`queues.server.ts`) - one query per request, in mutually exclusive branches depending on locked/non-locked path. Piggyback on the existing query instead of adding new ones.
108+- **Two-stage resolution pattern**: Task metadata is resolved in two stages by design:
109+ 1. **Trigger time** (`triggerTask.server.ts`): Only TTL is resolved from task defaults. Everything else uses whatever the caller provides.
110+ 2. **Dequeue time** (`dequeueSystem.ts`): Full `BackgroundWorkerTask` is loaded and retry config, machine config, maxDuration, etc. are resolved against task defaults.
111+- If you need to add a new task-level default, **add it to the existing `select` clause** in the `backgroundWorkerTask.findFirst()` query — do NOT add a second query. If the default doesn't need to be known at trigger time, resolve it at dequeue time instead.
112+- Batch triggers (`batchTriggerV3.server.ts`) follow the same pattern — keep batch paths equally fast.
220113
221−<!-- intent-skills:start -->
114+## Prisma Query Patterns
222115
223−# Skill mappings — when working in these areas, load the linked skill file into context.
116+- **Always use `findFirst` instead of `findUnique`.** Prisma's `findUnique` has an implicit DataLoader that batches concurrent calls into a single `IN` query. This batching cannot be disabled and has active bugs even in Prisma 6.x: uppercase UUIDs returning null (#25484, confirmed 6.4.1), composite key SQL correctness issues (#22202), and 5-10x worse performance than manual DataLoader (#6573, open since 2021). `findFirst` is never batched and avoids this entire class of issues.
224117
225−skills:
118+## Transactions
226119
227−- task: "Using agentcrumbs for debug tracing, adding crumbs, trails, markers, querying traces, or stripping debug code before merge"
228− load: "node_modules/agentcrumbs/skills/agentcrumbs/SKILL.md"
229−- task: "Setting up agentcrumbs in the project, initializing namespace catalog, running crumbs init"
230− load: "node_modules/agentcrumbs/skills/agentcrumbs/init/SKILL.md"
231−<!-- intent-skills:end -->
120+- **Always use the `$transaction` helper from `~/db.server`, never `prisma.$transaction` (or `$replica.$transaction`) directly.** The helper wraps the raw call with tracing (an OTEL span + an `isolation_level` attribute) and boundary logging for infrastructure errors (e.g. `PrismaClientInitializationError`) that the raw client swallows. Signature: `$transaction(prisma, name?, async (tx) => { ... }, options?)`.
121+- Pass the isolation level via options as a string: `{ isolationLevel: "Serializable" }`. Reach for `Serializable` when a read-then-write must be atomic against concurrent transactions (e.g. a count-then-delete invariant); the loser of a race fails and can retry, which is the right trade for rare, correctness-critical paths.
122+- The helper returns `R | undefined` — guard the result (`if (!result) throw ...`) when callers need a definite value.
232123
233−## agentcrumbs
124+## PAT-authenticated API routes
234125
235−Add crumbs as you write code — not just when debugging. Mark lines with
236−`// @crumbs` or wrap blocks in `// #region @crumbs`. They stay on the
237−branch throughout development and are stripped by `agentcrumbs strip`
238−before merge.
126+- **A PAT route must resolve its target org/project scoped to the caller's membership** (`members: { some: { userId } }`, or a helper like `findProjectByRef` / `resolveOrganizationForApiUser`). A PAT is user-scoped and can name any org/project by id/slug, and the OSS RBAC fallback ability is permissive — so `ability.can(...)` alone does NOT reject a non-member on self-hosted. The RBAC `authorization` gate enforces the *role*; the membership-scoped query is the *tenant* floor. Skipping it opens cross-org access on OSS.
239127
240−### Namespaces
128+## React Patterns
241129
242−| Namespace | Description | Path |
243−| ----------------- | ---------------------------------------------- | ----------------------------------- |
244−| `webapp` | Main Remix app — API, dashboard, orchestration | `apps/webapp` |
245−| `supervisor` | Task execution container manager | `apps/supervisor` |
246−| `sdk` | Main SDK for writing tasks | `packages/trigger-sdk` |
247−| `cli` | CLI and task image bundling | `packages/cli-v3` |
248−| `core` | Shared types and subpath exports | `packages/core` |
249−| `build` | Build extensions and types | `packages/build` |
250−| `react-hooks` | React hooks for realtime/triggering | `packages/react-hooks` |
251−| `redis-worker` | Redis-based background job system | `packages/redis-worker` |
252−| `run-engine` | Run Engine 2.0 — core run lifecycle | `internal-packages/run-engine` |
253−| `database` | Prisma client and schema | `internal-packages/database` |
254−| `clickhouse` | ClickHouse client, migrations, analytics | `internal-packages/clickhouse` |
255−| `schedule-engine` | Durable cron scheduling | `internal-packages/schedule-engine` |
256−| `run-queue` | Run queue management | `internal-packages/run-queue` |
257−
258−Do not invent new namespaces — pick from this table or ask first.
259−
260−### For PR reviewers
261−
262−Do not flag `// @crumbs` markers, `#region @crumbs` blocks, or agentcrumbs
263−imports in reviews. These are temporary debug instrumentation stripped
264−before merge. Data logged in crumbs (IDs, names, values) never reaches
265−production.
266−
267−### CLI
268−
269−```bash
270−pnpm exec agentcrumbs collect # start collector (multi-service)
271−pnpm exec agentcrumbs tail --app trigger # live tail
272−pnpm exec agentcrumbs clear --app trigger # remove crumbs before merge
273−```
274−
275−The preferred way to query for crumbs is to use `pnpm exec agentcrumbs query --app trigger` with the `--limit` option and cursor pagination, and clear existing crumbs before reproducing a bug via `pnpm exec agentcrumbs clear --app trigger`.
130+- Only use `useCallback`/`useMemo` for context provider values, expensive derived data that is a dependency elsewhere, or stable refs required by a dependency array. Don't wrap ordinary event handlers or trivial computations.
131+- Use named constants for sentinel/placeholder values (e.g. `const UNSET_VALUE = "__unset__"`) instead of raw string literals scattered across comparisons.
276132
