| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 51 | 38 | 0% |
| Commands | 0 | 4 | 25 | 0% |
| Section tags | 5 | 4 | 7 | 31% |
What each file covers
Sections
0 shared · 51 only in A · 38 only in B- − How to write Trigger.dev tasks
- − Overview of writing a Trigger.dev task
- − Essential requirements when generating task code
- − 🚨 CRITICAL INSTRUCTIONS FOR AI LANGUAGE MODELS 🚨
- − Correct Task implementations
- − Task configuration options
- − Lifecycle functions
- − Correct Schedules task (cron) implementations
- − Attach a Declarative schedule
- − Attach an Imperative schedule
- − Correct Schema task implementations
- − Correct implementations for triggering a task from your backend
- − tasks.trigger()
- − tasks.batchTrigger()
- − batch.trigger()
- − Correct implementations for triggering a task from inside another task
- − yourTask.trigger()
- − yourTask.batchTrigger()
- − yourTask.triggerAndWait()
- − yourTask.batchTriggerAndWait()
- − batch.triggerAndWait()
- − batch.triggerByTask()
- − batch.triggerByTaskAndWait()
- − Correct Metadata implementation
- − Overview
- − Basic usage
- − Update methods
- − Parent & root updates
- − Type safety
- − Important Notes
- − Correct Realtime implementation
- − Subscription methods
- − Realtime Streams
- − Realtime hooks
- − Installation
- − Authentication
- − Passing tokens to the frontend
- − Hook types
- − Correct Idempotency implementation
- − Using idempotencyKey
- − Scoping Idempotency Keys
- − Time-To-Live (TTL)
- − Payload-Based Idempotency
- − Correct Logs implementation
- − Correct `trigger.config.ts` implementation
- − Key configuration options
- − Build configuration
- − Build Extensions
- − AI model verification steps
- − Consequences of incorrect implementations
- − AI model response template
- + AGENTS.md
- + Build and Development Commands
- + pnpm run docker:full # Same + observability stack (Prometheus, Grafana, OTEL) and chaos tooling
- + Build packages (required before running)
- + Verifying Changes
- + 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
- + v3 (engine V1) removed
- + 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
Commands
0 shared · 4 only in A · 25 only in B- − task: firstScheduledTask.id,
- − npm add @trigger.dev/react-hooks
- − npx trigger.dev@latest init
- − npx trigger.dev@latest dev
- + 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 webapp
- + pnpm run dev --filter trigger.dev --filter "@trigger.dev/*"
- + pnpm run typecheck --filter webapp
- + 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
Section tags
5 shared · 4 only in A · 7 only in B- − types
- − security
- − database
- − deployment
- + test
- + lint-format
- + code-style
- + testing-strategy
- + git-pr
- + dependencies
- + docs
- setup
- build
- architecture
- do-not
- agent-behaviour
Line diff
triggerdotdev/trigger.dev · .cursor/rules/writing-tasks.mdc
@@ −1 @@
1---
2globs: **/trigger/**/*.ts, **/trigger/**/*.tsx,**/trigger/**/*.js,**/trigger/**/*.jsx
3description: Guidelines for writing Trigger.dev tasks
4alwaysApply: false
5---
6# How to write Trigger.dev tasks
7
8## Overview of writing a Trigger.dev task
9
101. Run the CLI `init` command: `npx trigger.dev@latest init`.
112. Create a Trigger.dev task.
123. Set up any environment variables.
134. Run the Trigger.dev dev command: `npx trigger.dev@latest dev`.
14
15## Essential requirements when generating task code
16
171. You MUST import from `@trigger.dev/sdk` (NEVER `@trigger.dev/sdk/v3`)
182. You MUST NEVER use `client.defineJob`
193. YOU MUST `export` every task, including subtasks
204. If you are able to generate an example payload for a task, do so.
21
22## 🚨 CRITICAL INSTRUCTIONS FOR AI LANGUAGE MODELS 🚨
23
24As an AI language model, you MUST NOT generate any of the following code patterns, as they are DEPRECATED and will BREAK the application:
25
26```ts
27// ❌ NEVER GENERATE THIS CODE - IT WILL BREAK THE APPLICATION
28
29client.defineJob({ // ❌ BREAKS APPLICATION
30 id: "job-id", // ❌ BREAKS APPLICATION
31 name: "job-name", // ❌ BREAKS APPLICATION
32 version: "0.0.1", // ❌ BREAKS APPLICATION
33 trigger: eventTrigger({ // ❌ BREAKS APPLICATION
34 name: "job.trigger", // ❌ BREAKS APPLICATION
35 schema: z.object({ // ❌ BREAKS APPLICATION
36 // Input schema here // ❌ BREAKS APPLICATION
37 }), // ❌ BREAKS APPLICATION
38 }), // ❌ BREAKS APPLICATION
39 integrations: { // ❌ BREAKS APPLICATION
40 // Integrations here // ❌ BREAKS APPLICATION
41 }, // ❌ BREAKS APPLICATION
42 run: async (payload, io) => { // ❌ BREAKS APPLICATION
43 // Job logic goes here // ❌ BREAKS APPLICATION
44 return { // ❌ BREAKS APPLICATION
45 // Return job results // ❌ BREAKS APPLICATION
46 }; // ❌ BREAKS APPLICATION
47 }, // ❌ BREAKS APPLICATION
48});
49```
50
51Instead, you MUST ALWAYS generate ONLY this pattern:
52
53```ts
54// ✅ ALWAYS GENERATE THIS EXACT PATTERN
55
56import { task } from "@trigger.dev/sdk";
57
58//1. You need to export each task, even if it's a subtask
59export const helloWorld = task({
60 //2. Use a unique id for each task
61 id: "hello-world",
62 //3. The run function is the main function of the task
63 run: async (payload: { message: string }) => {
64 //4. Write your task code here. Code here runs for a long time, there are no timeouts
65 },
66});
67```
68
69## Correct Task implementations
70
71A task is a function that can run for a long time with resilience to failure:
72
73```ts
74import { task } from "@trigger.dev/sdk";
75
76export const helloWorld = task({
77 id: "hello-world",
78 run: async (payload: { message: string }) => {
79 console.log(payload.message);
80 },
81});
82```
83
84Key points:
85- Tasks must be exported, even subtasks in the same file
86- Each task needs a unique ID within your project
87- The `run` function contains your task logic
88
89### Task configuration options
90
91#### Retry options
92
93Control retry behavior when errors occur:
94
95```ts
96export const taskWithRetries = task({
97 id: "task-with-retries",
98 retry: {
99 maxAttempts: 10,
100 factor: 1.8,
101 minTimeoutInMs: 500,
102 maxTimeoutInMs: 30_000,
103 randomize: false,
104 },
105 run: async (payload) => {
106 // Task logic
107 },
108});
109```
110
111#### Queue options
112
113Control concurrency:
114
115```ts
116export const oneAtATime = task({
117 id: "one-at-a-time",
118 queue: {
119 concurrencyLimit: 1,
120 },
121 run: async (payload) => {
122 // Task logic
123 },
124});
125```
126
127#### Machine options
128
129Specify CPU/RAM requirements:
130
131```ts
132export const heavyTask = task({
133 id: "heavy-task",
134 machine: {
135 preset: "large-1x", // 4 vCPU, 8 GB RAM
136 },
137 run: async (payload) => {
138 // Task logic
139 },
140});
141```
142
143Machine configuration options:
144
145| Machine name | vCPU | Memory | Disk space |
146| ------------------- | ---- | ------ | ---------- |
147| micro | 0.25 | 0.25 | 10GB |
148| small-1x (default) | 0.5 | 0.5 | 10GB |
149| small-2x | 1 | 1 | 10GB |
150| medium-1x | 1 | 2 | 10GB |
151| medium-2x | 2 | 4 | 10GB |
152| large-1x | 4 | 8 | 10GB |
153| large-2x | 8 | 16 | 10GB |
154
155#### Max Duration
156
157Limit how long a task can run:
158
159```ts
160export const longTask = task({
161 id: "long-task",
162 maxDuration: 300, // 5 minutes
163 run: async (payload) => {
164 // Task logic
165 },
166});
167```
168
169### Lifecycle functions
170
171Tasks support several lifecycle hooks:
172
173#### init
174
175Runs before each attempt, can return data for other functions:
176
177```ts
178export const taskWithInit = task({
179 id: "task-with-init",
180 init: async (payload, { ctx }) => {
181 return { someData: "someValue" };
182 },
183 run: async (payload, { ctx, init }) => {
184 console.log(init.someData); // "someValue"
185 },
186});
187```
188
189#### cleanup
190
191Runs after each attempt, regardless of success/failure:
192
193```ts
194export const taskWithCleanup = task({
195 id: "task-with-cleanup",
196 cleanup: async (payload, { ctx }) => {
197 // Cleanup resources
198 },
199 run: async (payload, { ctx }) => {
200 // Task logic
201 },
202});
203```
204
205#### onStart
206
207Runs once when a task starts (not on retries):
208
209```ts
210export const taskWithOnStart = task({
211 id: "task-with-on-start",
212 onStart: async (payload, { ctx }) => {
213 // Send notification, log, etc.
214 },
215 run: async (payload, { ctx }) => {
216 // Task logic
217 },
218});
219```
220
221#### onSuccess
222
223Runs when a task succeeds:
224
225```ts
226export const taskWithOnSuccess = task({
227 id: "task-with-on-success",
228 onSuccess: async (payload, output, { ctx }) => {
229 // Handle success
230 },
231 run: async (payload, { ctx }) => {
232 // Task logic
233 },
234});
235```
236
237#### onFailure
238
239Runs when a task fails after all retries:
240
241```ts
242export const taskWithOnFailure = task({
243 id: "task-with-on-failure",
244 onFailure: async (payload, error, { ctx }) => {
245 // Handle failure
246 },
247 run: async (payload, { ctx }) => {
248 // Task logic
249 },
250});
251```
252
253#### handleError
254
255Controls error handling and retry behavior:
256
257```ts
258export const taskWithErrorHandling = task({
259 id: "task-with-error-handling",
260 handleError: async (error, { ctx }) => {
261 // Custom error handling
262 },
263 run: async (payload, { ctx }) => {
264 // Task logic
265 },
266});
267```
268
269Global lifecycle hooks can also be defined in `trigger.config.ts` to apply to all tasks.
270
271## Correct Schedules task (cron) implementations
272
273```ts
274import { schedules } from "@trigger.dev/sdk";
275
276export const firstScheduledTask = schedules.task({
277 id: "first-scheduled-task",
278 run: async (payload) => {
279 //when the task was scheduled to run
280 //note this will be slightly different from new Date() because it takes a few ms to run the task
281 console.log(payload.timestamp); //is a Date object
282
283 //when the task was last run
284 //this can be undefined if it's never been run
285 console.log(payload.lastTimestamp); //is a Date object or undefined
286
287 //the timezone the schedule was registered with, defaults to "UTC"
288 //this is in IANA format, e.g. "America/New_York"
289 //See the full list here: https://cloud.trigger.dev/timezones
290 console.log(payload.timezone); //is a string
291
292 //If you want to output the time in the user's timezone do this:
293 const formatted = payload.timestamp.toLocaleString("en-US", {
294 timeZone: payload.timezone,
295 });
296
297 //the schedule id (you can have many schedules for the same task)
298 //using this you can remove the schedule, update it, etc
299 console.log(payload.scheduleId); //is a string
300
301 //you can optionally provide an external id when creating the schedule
302 //usually you would set this to a userId or some other unique identifier
303 //this can be undefined if you didn't provide one
304 console.log(payload.externalId); //is a string or undefined
305
306 //the next 5 dates this task is scheduled to run
307 console.log(payload.upcoming); //is an array of Date objects
308 },
309});
310```
311
312### Attach a Declarative schedule
313
314```ts
315import { schedules } from "@trigger.dev/sdk";
316
317// Sepcify a cron pattern (UTC)
318export const firstScheduledTask = schedules.task({
319 id: "first-scheduled-task",
320 //every two hours (UTC timezone)
321 cron: "0 */2 * * *",
322 run: async (payload, { ctx }) => {
323 //do something
324 },
325});
326```
327
328```ts
329import { schedules } from "@trigger.dev/sdk";
330
331// Specify a specific timezone like this:
332export const secondScheduledTask = schedules.task({
333 id: "second-scheduled-task",
334 cron: {
335 //5am every day Tokyo time
336 pattern: "0 5 * * *",
337 timezone: "Asia/Tokyo",
338 },
339 run: async (payload) => {},
340});
341```
342
343### Attach an Imperative schedule
344
345Create schedules explicitly for tasks using the dashboard's "New schedule" button or the SDK.
346
347#### Benefits
348- Dynamic creation (e.g., one schedule per user)
349- Manage without code deployment:
350 - Activate/disable
351 - Edit
352 - Delete
353
354#### Implementation
3551. Define a task using `schedules.task()`
3562. Attach one or more schedules via:
357 - Dashboard
358 - SDK
359
360#### Attach schedules with the SDK like this
361
362```ts
363const createdSchedule = await schedules.create({
364 //The id of the scheduled task you want to attach to.
365 task: firstScheduledTask.id,
366 //The schedule in cron format.
367 cron: "0 0 * * *",
368 //this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists.
369 deduplicationKey: "my-deduplication-key",
370});
371```
372
373## Correct Schema task implementations
374
375Schema tasks validate payloads against a schema before execution:
376
377```ts
378import { schemaTask } from "@trigger.dev/sdk";
379import { z } from "zod";
380
381const myTask = schemaTask({
382 id: "my-task",
383 schema: z.object({
384 name: z.string(),
385 age: z.number(),
386 }),
387 run: async (payload) => {
388 // Payload is typed and validated
389 console.log(payload.name, payload.age);
390 },
391});
392```
393
394## Correct implementations for triggering a task from your backend
395
396When you trigger a task from your backend code, you need to set the `TRIGGER_SECRET_KEY` environment variable. You can find the value on the API keys page in the Trigger.dev dashboard.
397
398### tasks.trigger()
399
400Triggers a single run of a task with specified payload and options without importing the task. Use type-only imports for full type checking.
401
402```ts
403import { tasks } from "@trigger.dev/sdk";
404import type { emailSequence } from "~/trigger/emails";
405
406export async function POST(request: Request) {
407 const data = await request.json();
408 const handle = await tasks.trigger<typeof emailSequence>("email-sequence", {
409 to: data.email,
410 name: data.name,
411 });
412 return Response.json(handle);
413}
414```
415
416### tasks.batchTrigger()
417
418Triggers multiple runs of a single task with different payloads without importing the task.
419
420```ts
421import { tasks } from "@trigger.dev/sdk";
422import type { emailSequence } from "~/trigger/emails";
423
424export async function POST(request: Request) {
425 const data = await request.json();
426 const batchHandle = await tasks.batchTrigger<typeof emailSequence>(
427 "email-sequence",
428 data.users.map((u) => ({ payload: { to: u.email, name: u.name } }))
429 );
430 return Response.json(batchHandle);
431}
432```
433
434### batch.trigger()
435
436Triggers multiple runs of different tasks at once, useful when you need to execute multiple tasks simultaneously.
437
438```ts
439import { batch } from "@trigger.dev/sdk";
440import type { myTask1, myTask2 } from "~/trigger/myTasks";
441
442export async function POST(request: Request) {
443 const data = await request.json();
444 const result = await batch.trigger<typeof myTask1 | typeof myTask2>([
445 { id: "my-task-1", payload: { some: data.some } },
446 { id: "my-task-2", payload: { other: data.other } },
447 ]);
448 return Response.json(result);
449}
450```
451
452## Correct implementations for triggering a task from inside another task
453
454### yourTask.trigger()
455
456Triggers a single run of a task with specified payload and options.
457
458```ts
459import { myOtherTask, runs } from "~/trigger/my-other-task";
460
461export const myTask = task({
462 id: "my-task",
463 run: async (payload: string) => {
464 const handle = await myOtherTask.trigger({ foo: "some data" });
465
466 const run = await runs.retrieve(handle);
467 // Do something with the run
468 },
469});
470```
471
472If you need to call `trigger()` on a task in a loop, use `batchTrigger()` instead which can trigger up to 500 runs in a single call.
473
474### yourTask.batchTrigger()
475
476Triggers multiple runs of a single task with different payloads.
477
478```ts
479import { myOtherTask, batch } from "~/trigger/my-other-task";
480
481export const myTask = task({
482 id: "my-task",
483 run: async (payload: string) => {
484 const batchHandle = await myOtherTask.batchTrigger([{ payload: "some data" }]);
485
486 //...do other stuff
487 const batch = await batch.retrieve(batchHandle.id);
488 },
489});
490```
491
492### yourTask.triggerAndWait()
493
494Triggers a task and waits for the result, useful when you need to call a different task and use its result.
495
496```ts
497export const parentTask = task({
498 id: "parent-task",
499 run: async (payload: string) => {
500 const result = await childTask.triggerAndWait("some-data");
501 console.log("Result", result);
502
503 //...do stuff with the result
504 },
505});
506```
507
508The result object needs to be checked to see if the child task run was successful. You can also use the `unwrap` method to get the output directly or handle errors with `SubtaskUnwrapError`. This method should only be used inside a task.
509
510### yourTask.batchTriggerAndWait()
511
512Batch triggers a task and waits for all results, useful for fan-out patterns.
513
514```ts
515export const batchParentTask = task({
516 id: "parent-task",
517 run: async (payload: string) => {
518 const results = await childTask.batchTriggerAndWait([
519 { payload: "item4" },
520 { payload: "item5" },
521 { payload: "item6" },
522 ]);
523 console.log("Results", results);
524
525 //...do stuff with the result
526 },
527});
528```
529
530You can handle run failures by inspecting individual run results and implementing custom error handling strategies. This method should only be used inside a task.
531
532### batch.triggerAndWait()
533
534Batch triggers multiple different tasks and waits for all results.
535
536```ts
537export const parentTask = task({
538 id: "parent-task",
539 run: async (payload: string) => {
540 const results = await batch.triggerAndWait<typeof childTask1 | typeof childTask2>([
541 { id: "child-task-1", payload: { foo: "World" } },
542 { id: "child-task-2", payload: { bar: 42 } },
543 ]);
544
545 for (const result of results) {
546 if (result.ok) {
547 switch (result.taskIdentifier) {
548 case "child-task-1":
549 console.log("Child task 1 output", result.output);
550 break;
551 case "child-task-2":
552 console.log("Child task 2 output", result.output);
553 break;
554 }
555 }
556 }
557 },
558});
559```
560
561### batch.triggerByTask()
562
563Batch triggers multiple tasks by passing task instances, useful for static task sets.
564
565```ts
566export const parentTask = task({
567 id: "parent-task",
568 run: async (payload: string) => {
569 const results = await batch.triggerByTask([
570 { task: childTask1, payload: { foo: "World" } },
571 { task: childTask2, payload: { bar: 42 } },
572 ]);
573
574 const run1 = await runs.retrieve(results.runs[0]);
575 const run2 = await runs.retrieve(results.runs[1]);
576 },
577});
578```
579
580### batch.triggerByTaskAndWait()
581
582Batch triggers multiple tasks by passing task instances and waits for all results.
583
584```ts
585export const parentTask = task({
586 id: "parent-task",
587 run: async (payload: string) => {
588 const { runs } = await batch.triggerByTaskAndWait([
589 { task: childTask1, payload: { foo: "World" } },
590 { task: childTask2, payload: { bar: 42 } },
591 ]);
592
593 if (runs[0].ok) {
594 console.log("Child task 1 output", runs[0].output);
595 }
596
597 if (runs[1].ok) {
598 console.log("Child task 2 output", runs[1].output);
599 }
600 },
601});
602```
603
604## Correct Metadata implementation
605
606### Overview
607
608Metadata allows attaching up to 256KB of structured data to a run, which can be accessed during execution, via API, Realtime, and in the dashboard. Useful for storing user information, tracking progress, or saving intermediate results.
609
610### Basic Usage
611
612Add metadata when triggering a task:
613
614```ts
615const handle = await myTask.trigger(
616 { message: "hello world" },
617 { metadata: { user: { name: "Eric", id: "user_1234" } } }
618);
619```
620
621Access metadata inside a run:
622
623```ts
624import { task, metadata } from "@trigger.dev/sdk";
625
626export const myTask = task({
627 id: "my-task",
628 run: async (payload: { message: string }) => {
629 // Get the whole metadata object
630 const currentMetadata = metadata.current();
631
632 // Get a specific key
633 const user = metadata.get("user");
634 console.log(user.name); // "Eric"
635 },
636});
637```
638
639### Update methods
640
641Metadata can be updated as the run progresses:
642
643- **set**: `metadata.set("progress", 0.5)`
644- **del**: `metadata.del("progress")`
645- **replace**: `metadata.replace({ user: { name: "Eric" } })`
646- **append**: `metadata.append("logs", "Step 1 complete")`
647- **remove**: `metadata.remove("logs", "Step 1 complete")`
648- **increment**: `metadata.increment("progress", 0.4)`
649- **decrement**: `metadata.decrement("progress", 0.4)`
650- **stream**: `await metadata.stream("logs", readableStream)`
651- **flush**: `await metadata.flush()`
652
653Updates can be chained with a fluent API:
654
655```ts
656metadata.set("progress", 0.1)
657 .append("logs", "Step 1 complete")
658 .increment("progress", 0.4);
659```
660
661### Parent & root updates
662
663Child tasks can update parent task metadata:
664
665```ts
666export const childTask = task({
667 id: "child-task",
668 run: async (payload: { message: string }) => {
669 // Update parent task's metadata
670 metadata.parent.set("progress", 0.5);
671
672 // Update root task's metadata
673 metadata.root.set("status", "processing");
674 },
675});
676```
677
678### Type safety
679
680Metadata accepts any JSON-serializable object. For type safety, consider wrapping with Zod:
681
682```ts
683import { z } from "zod";
684
685const Metadata = z.object({
686 user: z.object({
687 name: z.string(),
688 id: z.string(),
689 }),
690 date: z.coerce.date(),
691});
692
693function getMetadata() {
694 return Metadata.parse(metadata.current());
695}
696```
697
698### Important notes
699
700- Metadata methods only work inside run functions or task lifecycle hooks
701- Metadata is NOT automatically propagated to child tasks
702- Maximum size is 256KB (configurable if self-hosting)
703- Objects like Dates are serialized to strings and must be deserialized when retrieved
704
705## Correct Realtime implementation
706
707### Overview
708
709Trigger.dev Realtime enables subscribing to runs for real-time updates on run status, useful for monitoring tasks, updating UIs, and building realtime dashboards. It's built on Electric SQL, a PostgreSQL syncing engine.
710
711### Basic usage
712
713Subscribe to a run after triggering a task:
714
715```ts
716import { runs, tasks } from "@trigger.dev/sdk";
717
718async function myBackend() {
719 const handle = await tasks.trigger("my-task", { some: "data" });
720
721 for await (const run of runs.subscribeToRun(handle.id)) {
722 console.log(run); // Logs the run every time it changes
723 }
724}
725```
726
727### Subscription methods
728
729- **subscribeToRun**: Subscribe to changes for a specific run
730- **subscribeToRunsWithTag**: Subscribe to changes for all runs with a specific tag
731- **subscribeToBatch**: Subscribe to changes for all runs in a batch
732
733### Type safety
734
735You can infer types of run's payload and output by passing the task type:
736
737```ts
738import { runs } from "@trigger.dev/sdk";
739import type { myTask } from "./trigger/my-task";
740
741for await (const run of runs.subscribeToRun<typeof myTask>(handle.id)) {
742 console.log(run.payload.some); // Type-safe access to payload
743
744 if (run.output) {
745 console.log(run.output.result); // Type-safe access to output
746 }
747}
748```
749
750### Realtime Streams
751
752Stream data in realtime from inside your tasks using the metadata system:
753
754```ts
755import { task, metadata } from "@trigger.dev/sdk";
756import OpenAI from "openai";
757
758export type STREAMS = {
759 openai: OpenAI.ChatCompletionChunk;
760};
761
762export const myTask = task({
763 id: "my-task",
764 run: async (payload: { prompt: string }) => {
765 const completion = await openai.chat.completions.create({
766 messages: [{ role: "user", content: payload.prompt }],
767 model: "gpt-3.5-turbo",
768 stream: true,
769 });
770
771 // Register the stream with the key "openai"
772 const stream = await metadata.stream("openai", completion);
773
774 let text = "";
775 for await (const chunk of stream) {
776 text += chunk.choices.map((choice) => choice.delta?.content).join("");
777 }
778
779 return { text };
780 },
781});
782```
783
784Subscribe to streams using `withStreams`:
785
786```ts
787for await (const part of runs.subscribeToRun<typeof myTask>(runId).withStreams<STREAMS>()) {
788 switch (part.type) {
789 case "run": {
790 console.log("Received run", part.run);
791 break;
792 }
793 case "openai": {
794 console.log("Received OpenAI chunk", part.chunk);
795 break;
796 }
797 }
798}
799```
800
801## Realtime hooks
802
803### Installation
804
805```bash
806npm add @trigger.dev/react-hooks
807```
808
809### Authentication
810
811All hooks require a Public Access Token. You can provide it directly to each hook:
812
813```ts
814import { useRealtimeRun } from "@trigger.dev/react-hooks";
815
816function MyComponent({ runId, publicAccessToken }) {
817 const { run, error } = useRealtimeRun(runId, {
818 accessToken: publicAccessToken,
819 baseURL: "https://your-trigger-dev-instance.com", // Optional for self-hosting
820 });
821}
822```
823
824Or use the `TriggerAuthContext` provider:
825
826```ts
827import { TriggerAuthContext } from "@trigger.dev/react-hooks";
828
829function SetupTrigger({ publicAccessToken }) {
830 return (
831 <TriggerAuthContext.Provider value={{ accessToken: publicAccessToken }}>
832 <MyComponent />
833 </TriggerAuthContext.Provider>
834 );
835}
836```
837
838For Next.js App Router, wrap the provider in a client component:
839
840```ts
841// components/TriggerProvider.tsx
842"use client";
843
844import { TriggerAuthContext } from "@trigger.dev/react-hooks";
845
846export function TriggerProvider({ accessToken, children }) {
847 return (
848 <TriggerAuthContext.Provider value={{ accessToken }}>
849 {children}
850 </TriggerAuthContext.Provider>
851 );
852}
853```
854
855### Passing tokens to the frontend
856
857Several approaches for Next.js App Router:
858
8591. **Using cookies**:
860```ts
861// Server action
862export async function startRun() {
863 const handle = await tasks.trigger<typeof exampleTask>("example", { foo: "bar" });
864 cookies().set("publicAccessToken", handle.publicAccessToken);
865 redirect(`/runs/${handle.id}`);
866}
867
868// Page component
869export default function RunPage({ params }) {
870 const publicAccessToken = cookies().get("publicAccessToken");
871 return (
872 <TriggerProvider accessToken={publicAccessToken}>
873 <RunDetails id={params.id} />
874 </TriggerProvider>
875 );
876}
877```
878
8792. **Using query parameters**:
880```ts
881// Server action
882export async function startRun() {
883 const handle = await tasks.trigger<typeof exampleTask>("example", { foo: "bar" });
884 redirect(`/runs/${handle.id}?publicAccessToken=${handle.publicAccessToken}`);
885}
886```
887
8883. **Server-side token generation**:
889```ts
890// Page component
891export default async function RunPage({ params }) {
892 const publicAccessToken = await generatePublicAccessToken(params.id);
893 return (
894 <TriggerProvider accessToken={publicAccessToken}>
895 <RunDetails id={params.id} />
896 </TriggerProvider>
897 );
898}
899
900// Token generation function
901export async function generatePublicAccessToken(runId: string) {
902 return auth.createPublicToken({
903 scopes: {
904 read: {
905 runs: [runId],
906 },
907 },
908 expirationTime: "1h",
909 });
910}
911```
912
913### Hook types
914
915#### SWR hooks
916
917Data fetching hooks that use SWR for caching:
918
919```ts
920"use client";
921import { useRun } from "@trigger.dev/react-hooks";
922import type { myTask } from "@/trigger/myTask";
923
924function MyComponent({ runId }) {
925 const { run, error, isLoading } = useRun<typeof myTask>(runId);
926
927 if (isLoading) return <div>Loading...</div>;
928 if (error) return <div>Error: {error.message}</div>;
929
930 return <div>Run: {run.id}</div>;
931}
932```
933
934Common options:
935- `revalidateOnFocus`: Revalidate when window regains focus
936- `revalidateOnReconnect`: Revalidate when network reconnects
937- `refreshInterval`: Polling interval in milliseconds
938
939#### Realtime hooks
940
941Hooks that use Trigger.dev's realtime API for live updates (recommended over polling).
942
943For most use cases, Realtime hooks are preferred over SWR hooks with polling due to better performance and lower API usage.
944
945### Authentication
946
947For client-side usage, generate a public access token with appropriate scopes:
948
949```ts
950import { auth } from "@trigger.dev/sdk";
951
952const publicToken = await auth.createPublicToken({
953 scopes: {
954 read: {
955 runs: ["run_1234"],
956 },
957 },
958});
959```
960
961## Correct Idempotency implementation
962
963Idempotency ensures that an operation produces the same result when called multiple times. Trigger.dev supports idempotency at the task level through the `idempotencyKey` option.
964
965### Using idempotencyKey
966
967Provide an `idempotencyKey` when triggering a task to ensure it runs only once with that key:
968
969```ts
970import { idempotencyKeys, task } from "@trigger.dev/sdk";
971
972export const myTask = task({
973 id: "my-task",
974 retry: {
975 maxAttempts: 4,
976 },
977 run: async (payload: any) => {
978 // Create a key unique to this task run
979 const idempotencyKey = await idempotencyKeys.create("my-task-key");
980
981 // Child task will only be triggered once across all retries
982 await childTask.trigger({ foo: "bar" }, { idempotencyKey });
983
984 // This may throw an error and cause retries
985 throw new Error("Something went wrong");
986 },
987});
988```
989
990### Scoping Idempotency Keys
991
992By default, keys are scoped to the current run. You can create globally unique keys:
993
994```ts
995const idempotencyKey = await idempotencyKeys.create("my-task-key", { scope: "global" });
996```
997
998When triggering from backend code:
999
1000```ts
1001const idempotencyKey = await idempotencyKeys.create([myUser.id, "my-task"]);
1002await tasks.trigger("my-task", { some: "data" }, { idempotencyKey });
1003```
1004
1005You can also pass a string directly:
1006
1007```ts
1008await myTask.trigger({ some: "data" }, { idempotencyKey: myUser.id });
1009```
1010
1011### Time-To-Live (TTL)
1012
1013The `idempotencyKeyTTL` option defines a time window during which duplicate triggers return the original run:
1014
1015```ts
1016await childTask.trigger(
1017 { foo: "bar" },
1018 { idempotencyKey, idempotencyKeyTTL: "60s" }
1019);
1020
1021await wait.for({ seconds: 61 });
1022
1023// Key expired, will trigger a new run
1024await childTask.trigger({ foo: "bar" }, { idempotencyKey });
1025```
1026
1027Supported time units:
1028- `s` for seconds (e.g., `60s`)
1029- `m` for minutes (e.g., `5m`)
1030- `h` for hours (e.g., `2h`)
1031- `d` for days (e.g., `3d`)
1032
1033### Payload-Based Idempotency
1034
1035While not directly supported, you can implement payload-based idempotency by hashing the payload:
1036
1037```ts
1038import { createHash } from "node:crypto";
1039
1040const idempotencyKey = await idempotencyKeys.create(hash(payload));
1041await tasks.trigger("child-task", payload, { idempotencyKey });
1042
1043function hash(payload: any): string {
1044 const hash = createHash("sha256");
1045 hash.update(JSON.stringify(payload));
1046 return hash.digest("hex");
1047}
1048```
1049
1050### Important Notes
1051
1052- Idempotency keys are scoped to the task and environment
1053- Different tasks with the same key will still both run
1054- Default TTL is 30 days
1055- Not available with `triggerAndWait` or `batchTriggerAndWait` in v3.3.0+ due to a bug
1056
1057## Correct Logs implementation
1058
1059```ts
1060// onFailure executes after all retries are exhausted; use for notifications, logging, or side effects on final failure:
1061import { task, logger } from "@trigger.dev/sdk";
1062
1063export const loggingExample = task({
1064 id: "logging-example",
1065 run: async (payload: { data: Record<string, string> }) => {
1066 //the first parameter is the message, the second parameter must be a key-value object (Record<string, unknown>)
1067 logger.debug("Debug message", payload.data);
1068 logger.log("Log message", payload.data);
1069 logger.info("Info message", payload.data);
1070 logger.warn("You've been warned", payload.data);
1071 logger.error("Error message", payload.data);
1072 },
1073});
1074```
1075
1076## Correct `trigger.config.ts` implementation
1077
1078The `trigger.config.ts` file configures your Trigger.dev project, specifying task locations, retry settings, telemetry, and build options.
1079
1080```ts
1081import { defineConfig } from "@trigger.dev/sdk";
1082
1083export default defineConfig({
1084 project: "<project ref>",
1085 dirs: ["./trigger"],
1086 retries: {
1087 enabledInDev: false,
1088 default: {
1089 maxAttempts: 3,
1090 minTimeoutInMs: 1000,
1091 maxTimeoutInMs: 10000,
1092 factor: 2,
1093 randomize: true,
1094 },
1095 },
1096});
1097```
1098
1099### Key configuration options
1100
1101#### Dirs
1102
1103Specify where your tasks are located:
1104
1105```ts
1106dirs: ["./trigger"],
1107```
1108
1109Files with `.test` or `.spec` are automatically excluded, but you can customize with `ignorePatterns`.
1110
1111#### Lifecycle functions
1112
1113Add global hooks for all tasks:
1114
1115```ts
1116onStart: async (payload, { ctx }) => {
1117 console.log("Task started", ctx.task.id);
1118},
1119onSuccess: async (payload, output, { ctx }) => {
1120 console.log("Task succeeded", ctx.task.id);
1121},
1122onFailure: async (payload, error, { ctx }) => {
1123 console.log("Task failed", ctx.task.id);
1124},
1125```
1126
1127#### Telemetry instrumentations
1128
1129Add OpenTelemetry instrumentations for enhanced logging:
1130
1131```ts
1132telemetry: {
1133 instrumentations: [
1134 new PrismaInstrumentation(),
1135 new OpenAIInstrumentation()
1136 ],
1137 exporters: [axiomExporter], // Optional custom exporters
1138},
1139```
1140
1141#### Runtime
1142
1143Specify the runtime environment:
1144
1145```ts
1146runtime: "node", // or "bun" (experimental)
1147```
1148
1149#### Machine settings
1150
1151Set default machine for all tasks:
1152
1153```ts
1154defaultMachine: "large-1x",
1155```
1156
1157#### Log level
1158
1159Configure logging verbosity:
1160
1161```ts
1162logLevel: "debug", // Controls logger API logs
1163```
1164
1165#### Max Duration
1166
1167Set default maximum runtime for all tasks:
1168
1169```ts
1170maxDuration: 60, // 60 seconds
1171```
1172
1173### Build configuration
1174
1175Customize the build process:
1176
1177```ts
1178build: {
1179 external: ["header-generator"], // Don't bundle these packages
1180 jsx: {
1181 fragment: "Fragment",
1182 factory: "h",
1183 automatic: false,
1184 },
1185 conditions: ["react-server"], // Import conditions
1186 extensions: [
1187 // Build extensions
1188 additionalFiles({ files: ["./assets/**", "./fonts/**"] }),
1189 additionalPackages({ packages: ["wrangler"] }),
1190 aptGet({ packages: ["ffmpeg"] }),
1191 ],
1192}
1193```
1194
1195### Build Extensions
1196
1197Trigger.dev provides several built-in extensions:
1198
1199- **additionalFiles**: Copy files to the build directory
1200- **additionalPackages**: Include extra packages in the build
1201- **aptGet**: Install system packages in the deployed image
1202- **emitDecoratorMetadata**: Enable TypeScript decorator metadata
1203- **prismaExtension**: Support for Prisma ORM
1204- **syncEnvVars**: Sync environment variables
1205- **puppeteer**: Add Puppeteer support
1206- **ffmpeg**: Add FFmpeg support
1207- **esbuildPlugin**: Add custom esbuild plugins
1208
1209You can also create custom build extensions with hooks like `onBuildStart`, `onBuildComplete`, and `externalsForTarget`.
1210
1211#### Key points:
1212
1213- Validates payload at trigger time and before execution
1214- Supports Zod, Yup, Superstruct, ArkType, Effect/schema, runtypes, valibot, typebox
1215- Can use input/output schemas (e.g., defaults, type coercion)
1216- Custom parser functions supported
1217- Invalid payloads throw errors and prevent task execution
1218
1219#### Trigger with:
1220
1221```ts
1222await myTask.trigger({ name: "Alice", age: 30 });
1223```
1224
1225## AI model verification steps
1226
1227Before generating any code, you MUST verify:
1228
12291. Are you importing from `@trigger.dev/sdk` (NOT `@trigger.dev/sdk/v3`)? If not, STOP and FIX.
12302. Have you exported every task? If not, STOP and FIX.
12313. Have you generated any DEPRECATED code patterns? If yes, STOP and FIX.
1232
1233## Consequences of incorrect implementations
1234
1235If you generate code that fails the verification steps above, your implementation will:
1236
12371. Break in production
12382. Fail to deploy to the Trigger.dev servers
12393. Fail to run in a local Dev environment
1240
1241## AI model response template
1242
1243When asked about Trigger.dev task implementation, you MUST:
12441. FIRST use code patterns from this guide
12452. NEVER suggest deprecated approaches
12463. VERIFY your response against the patterns shown here
1247
1248
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
@@ −1 +1 @@
1−---
2−globs: **/trigger/**/*.ts, **/trigger/**/*.tsx,**/trigger/**/*.js,**/trigger/**/*.jsx
3−description: Guidelines for writing Trigger.dev tasks
4−alwaysApply: false
5−---
6−# How to write Trigger.dev tasks
1+# AGENTS.md
72
8−## Overview of writing a Trigger.dev task
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.
94
10−1. Run the CLI `init` command: `npx trigger.dev@latest init`.
11−2. Create a Trigger.dev task.
12−3. Set up any environment variables.
13−4. Run the Trigger.dev dev command: `npx trigger.dev@latest dev`.
5+## Build and Development Commands
146
15−## Essential requirements when generating task code
7+This is a pnpm 10.33.2 monorepo using Turborepo. Run commands from root with `pnpm run`.
168
17−1. You MUST import from `@trigger.dev/sdk` (NEVER `@trigger.dev/sdk/v3`)
18−2. You MUST NEVER use `client.defineJob`
19−3. YOU MUST `export` every task, including subtasks
20−4. If you are able to generate an example payload for a task, do so.
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.
2110
22−## 🚨 CRITICAL INSTRUCTIONS FOR AI LANGUAGE MODELS 🚨
11+```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)
2316
24−As an AI language model, you MUST NOT generate any of the following code patterns, as they are DEPRECATED and will BREAK the application:
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
2519
26−```ts
27−// ❌ NEVER GENERATE THIS CODE - IT WILL BREAK THE APPLICATION
28−
29−client.defineJob({ // ❌ BREAKS APPLICATION
30− id: "job-id", // ❌ BREAKS APPLICATION
31− name: "job-name", // ❌ BREAKS APPLICATION
32− version: "0.0.1", // ❌ BREAKS APPLICATION
33− trigger: eventTrigger({ // ❌ BREAKS APPLICATION
34− name: "job.trigger", // ❌ BREAKS APPLICATION
35− schema: z.object({ // ❌ BREAKS APPLICATION
36− // Input schema here // ❌ BREAKS APPLICATION
37− }), // ❌ BREAKS APPLICATION
38− }), // ❌ BREAKS APPLICATION
39− integrations: { // ❌ BREAKS APPLICATION
40− // Integrations here // ❌ BREAKS APPLICATION
41− }, // ❌ BREAKS APPLICATION
42− run: async (payload, io) => { // ❌ BREAKS APPLICATION
43− // Job logic goes here // ❌ BREAKS APPLICATION
44− return { // ❌ BREAKS APPLICATION
45− // Return job results // ❌ BREAKS APPLICATION
46− }; // ❌ BREAKS APPLICATION
47− }, // ❌ BREAKS APPLICATION
48−});
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
4922 ```
5023
51−Instead, you MUST ALWAYS generate ONLY this pattern:
24+### Verifying Changes
5225
53−```ts
54−// ✅ ALWAYS GENERATE THIS EXACT PATTERN
26+The verification command depends on where the change lives:
5527
56−import { task } from "@trigger.dev/sdk";
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`.
5730
58−//1. You need to export each task, even if it's a subtask
59−export const helloWorld = task({
60− //2. Use a unique id for each task
61− id: "hello-world",
62− //3. The run function is the main function of the task
63− run: async (payload: { message: string }) => {
64− //4. Write your task code here. Code here runs for a long time, there are no timeouts
65− },
66−});
67−```
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
6835
69−## Correct Task implementations
70−
71−A task is a function that can run for a long time with resilience to failure:
72−
73−```ts
74−import { task } from "@trigger.dev/sdk";
75−
76−export const helloWorld = task({
77− id: "hello-world",
78− run: async (payload: { message: string }) => {
79− console.log(payload.message);
80− },
81−});
36+# Public packages — use build
37+pnpm run build --filter @trigger.dev/sdk
38+pnpm run build --filter @trigger.dev/core
8239 ```
8340
84−Key points:
85−- Tasks must be exported, even subtasks in the same file
86−- Each task needs a unique ID within your project
87−- The `run` function contains your task logic
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.
8842
89−### Task configuration options
43+## Testing
9044
91−#### Retry options
45+We use vitest exclusively. **Never mock anything** - use testcontainers instead.
9246
93−Control retry behavior when errors occur:
94−
95−```ts
96−export const taskWithRetries = task({
97− id: "task-with-retries",
98− retry: {
99− maxAttempts: 10,
100− factor: 1.8,
101− minTimeoutInMs: 500,
102− maxTimeoutInMs: 30_000,
103− randomize: false,
104− },
105− run: async (payload) => {
106− // Task logic
107− },
108−});
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
10952 ```
11053
111−#### Queue options
54+Test files go next to source files (e.g., `MyService.ts` -> `MyService.test.ts`).
11255
113−Control concurrency:
56+### Testcontainers for Redis/PostgreSQL
11457
115−```ts
116−export const oneAtATime = task({
117− id: "one-at-a-time",
118− queue: {
119− concurrencyLimit: 1,
120− },
121− run: async (payload) => {
122− // Task logic
123− },
124−});
125−```
58+```typescript
59+import { redisTest, postgresTest, containerTest } from "@internal/testcontainers";
12660
127−#### Machine options
128−
129−Specify CPU/RAM requirements:
130−
131−```ts
132−export const heavyTask = task({
133− id: "heavy-task",
134− machine: {
135− preset: "large-1x", // 4 vCPU, 8 GB RAM
136− },
137− run: async (payload) => {
138− // Task logic
139− },
61+redisTest("should use redis", async ({ redisOptions }) => {
62+ /* ... */
14063 });
141−```
142−
143−Machine configuration options:
144−
145−| Machine name | vCPU | Memory | Disk space |
146−| ------------------- | ---- | ------ | ---------- |
147−| micro | 0.25 | 0.25 | 10GB |
148−| small-1x (default) | 0.5 | 0.5 | 10GB |
149−| small-2x | 1 | 1 | 10GB |
150−| medium-1x | 1 | 2 | 10GB |
151−| medium-2x | 2 | 4 | 10GB |
152−| large-1x | 4 | 8 | 10GB |
153−| large-2x | 8 | 16 | 10GB |
154−
155−#### Max Duration
156−
157−Limit how long a task can run:
158−
159−```ts
160−export const longTask = task({
161− id: "long-task",
162− maxDuration: 300, // 5 minutes
163− run: async (payload) => {
164− // Task logic
165− },
64+postgresTest("should use postgres", async ({ prisma }) => {
65+ /* ... */
16666 });
167−```
168−
169−### Lifecycle functions
170−
171−Tasks support several lifecycle hooks:
172−
173−#### init
174−
175−Runs before each attempt, can return data for other functions:
176−
177−```ts
178−export const taskWithInit = task({
179− id: "task-with-init",
180− init: async (payload, { ctx }) => {
181− return { someData: "someValue" };
182− },
183− run: async (payload, { ctx, init }) => {
184− console.log(init.someData); // "someValue"
185− },
67+containerTest("should use both", async ({ prisma, redisOptions }) => {
68+ /* ... */
18669 });
18770 ```
18871
189−#### cleanup
72+## Code Style
19073
191−Runs after each attempt, regardless of success/failure:
74+### Formatting and linting
19275
193−```ts
194−export const taskWithCleanup = task({
195− id: "task-with-cleanup",
196− cleanup: async (payload, { ctx }) => {
197− // Cleanup resources
198− },
199− run: async (payload, { ctx }) => {
200− // Task logic
201− },
202−});
203−```
76+Format and lint are enforced by CI (`code-quality` check). Run before committing:
20477
205−#### onStart
206−
207−Runs once when a task starts (not on retries):
208−
209−```ts
210−export const taskWithOnStart = task({
211− id: "task-with-on-start",
212− onStart: async (payload, { ctx }) => {
213− // Send notification, log, etc.
214− },
215− run: async (payload, { ctx }) => {
216− // Task logic
217− },
218−});
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)
21982 ```
22083
221−#### onSuccess
84+### Imports
22285
223−Runs when a task succeeds:
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
22490
225−```ts
226−export const taskWithOnSuccess = task({
227− id: "task-with-on-success",
228− onSuccess: async (payload, output, { ctx }) => {
229− // Handle success
230− },
231− run: async (payload, { ctx }) => {
232− // Task logic
233− },
234−});
235−```
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.
23692
237−#### onFailure
93+## Changesets and Server Changes
23894
239−Runs when a task fails after all retries:
95+When modifying any public package (`packages/*` or `integrations/*`), add a changeset:
24096
241−```ts
242−export const taskWithOnFailure = task({
243− id: "task-with-on-failure",
244− onFailure: async (payload, error, { ctx }) => {
245− // Handle failure
246− },
247− run: async (payload, { ctx }) => {
248− // Task logic
249− },
250−});
97+```bash
98+pnpm run changeset:add
25199 ```
252100
253−#### handleError
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
254104
255−Controls error handling and retry behavior:
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.
256106
257−```ts
258−export const taskWithErrorHandling = task({
259− id: "task-with-error-handling",
260− handleError: async (error, { ctx }) => {
261− // Custom error handling
262− },
263− run: async (payload, { ctx }) => {
264− // Task logic
265− },
266−});
267−```
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.
268108
269−Global lifecycle hooks can also be defined in `trigger.config.ts` to apply to all tasks.
109+## Dependency Pinning
270110
271−## Correct Schedules task (cron) implementations
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).
272112
273−```ts
274−import { schedules } from "@trigger.dev/sdk";
113+## Architecture Overview
275114
276−export const firstScheduledTask = schedules.task({
277− id: "first-scheduled-task",
278− run: async (payload) => {
279− //when the task was scheduled to run
280− //note this will be slightly different from new Date() because it takes a few ms to run the task
281− console.log(payload.timestamp); //is a Date object
115+### Request Flow
282116
283− //when the task was last run
284− //this can be undefined if it's never been run
285− console.log(payload.lastTimestamp); //is a Date object or undefined
117+User API call -> Webapp routes -> Services -> RunEngine -> Redis Queue -> Supervisor -> Container execution -> Results back through RunEngine -> ClickHouse (analytics) + PostgreSQL (state)
286118
287− //the timezone the schedule was registered with, defaults to "UTC"
288− //this is in IANA format, e.g. "America/New_York"
289− //See the full list here: https://cloud.trigger.dev/timezones
290− console.log(payload.timezone); //is a string
119+### Apps
291120
292− //If you want to output the time in the user's timezone do this:
293− const formatted = payload.timestamp.toLocaleString("en-US", {
294− timeZone: payload.timezone,
295− });
121+- **apps/webapp**: Remix 2.17.4 app - main API, dashboard, orchestration. Uses Express server.
122+- **apps/supervisor**: Manages task execution containers (Docker/Kubernetes).
296123
297− //the schedule id (you can have many schedules for the same task)
298− //using this you can remove the schedule, update it, etc
299− console.log(payload.scheduleId); //is a string
124+### Public Packages
300125
301− //you can optionally provide an external id when creating the schedule
302− //usually you would set this to a userId or some other unique identifier
303− //this can be undefined if you didn't provide one
304− console.log(payload.externalId); //is a string or undefined
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
305132
306− //the next 5 dates this task is scheduled to run
307− console.log(payload.upcoming); //is an array of Date objects
308− },
309−});
310−```
133+### Internal Packages
311134
312−### Attach a Declarative schedule
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
313141
314−```ts
315−import { schedules } from "@trigger.dev/sdk";
142+### v3 (engine V1) removed
316143
317−// Sepcify a cron pattern (UTC)
318−export const firstScheduledTask = schedules.task({
319− id: "first-scheduled-task",
320− //every two hours (UTC timezone)
321− cron: "0 */2 * * *",
322− run: async (payload, { ctx }) => {
323− //do something
324− },
325−});
326−```
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`.
327145
328−```ts
329−import { schedules } from "@trigger.dev/sdk";
146+### Documentation
330147
331−// Specify a specific timezone like this:
332−export const secondScheduledTask = schedules.task({
333− id: "second-scheduled-task",
334− cron: {
335− //5am every day Tokyo time
336− pattern: "0 5 * * *",
337− timezone: "Asia/Tokyo",
338− },
339− run: async (payload) => {},
340−});
341−```
148+Docs live in `docs/` as a Mintlify site (MDX format). See `docs/CLAUDE.md` for conventions.
342149
343−### Attach an Imperative schedule
150+### Reference Projects
344151
345−Create schedules explicitly for tasks using the dashboard's "New schedule" button or the SDK.
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.
346153
347−#### Benefits
348−- Dynamic creation (e.g., one schedule per user)
349−- Manage without code deployment:
350− - Activate/disable
351− - Edit
352− - Delete
154+## Docker Image Guidelines
353155
354−#### Implementation
355−1. Define a task using `schedules.task()`
356−2. Attach one or more schedules via:
357− - Dashboard
358− - SDK
156+When updating Docker image references:
359157
360−#### Attach schedules with the SDK like this
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
361161
362−```ts
363−const createdSchedule = await schedules.create({
364− //The id of the scheduled task you want to attach to.
365− task: firstScheduledTask.id,
366− //The schedule in cron format.
367− cron: "0 0 * * *",
368− //this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists.
369− deduplicationKey: "my-deduplication-key",
370−});
371−```
162+## Writing Trigger.dev Tasks
372163
373−## Correct Schema task implementations
164+Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` or deprecated `client.defineJob`.
374165
375−Schema tasks validate payloads against a schema before execution:
166+```typescript
167+import { task } from "@trigger.dev/sdk";
376168
377−```ts
378−import { schemaTask } from "@trigger.dev/sdk";
379−import { z } from "zod";
380−
381−const myTask = schemaTask({
382− id: "my-task",
383− schema: z.object({
384− name: z.string(),
385− age: z.number(),
386− }),
387− run: async (payload) => {
388− // Payload is typed and validated
389− console.log(payload.name, payload.age);
390− },
391−});
392−```
393−
394−## Correct implementations for triggering a task from your backend
395−
396−When you trigger a task from your backend code, you need to set the `TRIGGER_SECRET_KEY` environment variable. You can find the value on the API keys page in the Trigger.dev dashboard.
397−
398−### tasks.trigger()
399−
400−Triggers a single run of a task with specified payload and options without importing the task. Use type-only imports for full type checking.
401−
402−```ts
403−import { tasks } from "@trigger.dev/sdk";
404−import type { emailSequence } from "~/trigger/emails";
405−
406−export async function POST(request: Request) {
407− const data = await request.json();
408− const handle = await tasks.trigger<typeof emailSequence>("email-sequence", {
409− to: data.email,
410− name: data.name,
411− });
412− return Response.json(handle);
413−}
414−```
415−
416−### tasks.batchTrigger()
417−
418−Triggers multiple runs of a single task with different payloads without importing the task.
419−
420−```ts
421−import { tasks } from "@trigger.dev/sdk";
422−import type { emailSequence } from "~/trigger/emails";
423−
424−export async function POST(request: Request) {
425− const data = await request.json();
426− const batchHandle = await tasks.batchTrigger<typeof emailSequence>(
427− "email-sequence",
428− data.users.map((u) => ({ payload: { to: u.email, name: u.name } }))
429− );
430− return Response.json(batchHandle);
431−}
432−```
433−
434−### batch.trigger()
435−
436−Triggers multiple runs of different tasks at once, useful when you need to execute multiple tasks simultaneously.
437−
438−```ts
439−import { batch } from "@trigger.dev/sdk";
440−import type { myTask1, myTask2 } from "~/trigger/myTasks";
441−
442−export async function POST(request: Request) {
443− const data = await request.json();
444− const result = await batch.trigger<typeof myTask1 | typeof myTask2>([
445− { id: "my-task-1", payload: { some: data.some } },
446− { id: "my-task-2", payload: { other: data.other } },
447− ]);
448− return Response.json(result);
449−}
450−```
451−
452−## Correct implementations for triggering a task from inside another task
453−
454−### yourTask.trigger()
455−
456−Triggers a single run of a task with specified payload and options.
457−
458−```ts
459−import { myOtherTask, runs } from "~/trigger/my-other-task";
460−
461169 export const myTask = task({
462170 id: "my-task",
463− run: async (payload: string) => {
464− const handle = await myOtherTask.trigger({ foo: "some data" });
465−
466− const run = await runs.retrieve(handle);
467− // Do something with the run
468− },
469−});
470−```
471−
472−If you need to call `trigger()` on a task in a loop, use `batchTrigger()` instead which can trigger up to 500 runs in a single call.
473−
474−### yourTask.batchTrigger()
475−
476−Triggers multiple runs of a single task with different payloads.
477−
478−```ts
479−import { myOtherTask, batch } from "~/trigger/my-other-task";
480−
481−export const myTask = task({
482− id: "my-task",
483− run: async (payload: string) => {
484− const batchHandle = await myOtherTask.batchTrigger([{ payload: "some data" }]);
485−
486− //...do other stuff
487− const batch = await batch.retrieve(batchHandle.id);
488− },
489−});
490−```
491−
492−### yourTask.triggerAndWait()
493−
494−Triggers a task and waits for the result, useful when you need to call a different task and use its result.
495−
496−```ts
497−export const parentTask = task({
498− id: "parent-task",
499− run: async (payload: string) => {
500− const result = await childTask.triggerAndWait("some-data");
501− console.log("Result", result);
502−
503− //...do stuff with the result
504− },
505−});
506−```
507−
508−The result object needs to be checked to see if the child task run was successful. You can also use the `unwrap` method to get the output directly or handle errors with `SubtaskUnwrapError`. This method should only be used inside a task.
509−
510−### yourTask.batchTriggerAndWait()
511−
512−Batch triggers a task and waits for all results, useful for fan-out patterns.
513−
514−```ts
515−export const batchParentTask = task({
516− id: "parent-task",
517− run: async (payload: string) => {
518− const results = await childTask.batchTriggerAndWait([
519− { payload: "item4" },
520− { payload: "item5" },
521− { payload: "item6" },
522− ]);
523− console.log("Results", results);
524−
525− //...do stuff with the result
526− },
527−});
528−```
529−
530−You can handle run failures by inspecting individual run results and implementing custom error handling strategies. This method should only be used inside a task.
531−
532−### batch.triggerAndWait()
533−
534−Batch triggers multiple different tasks and waits for all results.
535−
536−```ts
537−export const parentTask = task({
538− id: "parent-task",
539− run: async (payload: string) => {
540− const results = await batch.triggerAndWait<typeof childTask1 | typeof childTask2>([
541− { id: "child-task-1", payload: { foo: "World" } },
542− { id: "child-task-2", payload: { bar: 42 } },
543− ]);
544−
545− for (const result of results) {
546− if (result.ok) {
547− switch (result.taskIdentifier) {
548− case "child-task-1":
549− console.log("Child task 1 output", result.output);
550− break;
551− case "child-task-2":
552− console.log("Child task 2 output", result.output);
553− break;
554− }
555− }
556− }
557− },
558−});
559−```
560−
561−### batch.triggerByTask()
562−
563−Batch triggers multiple tasks by passing task instances, useful for static task sets.
564−
565−```ts
566−export const parentTask = task({
567− id: "parent-task",
568− run: async (payload: string) => {
569− const results = await batch.triggerByTask([
570− { task: childTask1, payload: { foo: "World" } },
571− { task: childTask2, payload: { bar: 42 } },
572− ]);
573−
574− const run1 = await runs.retrieve(results.runs[0]);
575− const run2 = await runs.retrieve(results.runs[1]);
576− },
577−});
578−```
579−
580−### batch.triggerByTaskAndWait()
581−
582−Batch triggers multiple tasks by passing task instances and waits for all results.
583−
584−```ts
585−export const parentTask = task({
586− id: "parent-task",
587− run: async (payload: string) => {
588− const { runs } = await batch.triggerByTaskAndWait([
589− { task: childTask1, payload: { foo: "World" } },
590− { task: childTask2, payload: { bar: 42 } },
591− ]);
592−
593− if (runs[0].ok) {
594− console.log("Child task 1 output", runs[0].output);
595− }
596−
597− if (runs[1].ok) {
598− console.log("Child task 2 output", runs[1].output);
599− }
600− },
601−});
602−```
603−
604−## Correct Metadata implementation
605−
606−### Overview
607−
608−Metadata allows attaching up to 256KB of structured data to a run, which can be accessed during execution, via API, Realtime, and in the dashboard. Useful for storing user information, tracking progress, or saving intermediate results.
609−
610−### Basic Usage
611−
612−Add metadata when triggering a task:
613−
614−```ts
615−const handle = await myTask.trigger(
616− { message: "hello world" },
617− { metadata: { user: { name: "Eric", id: "user_1234" } } }
618−);
619−```
620−
621−Access metadata inside a run:
622−
623−```ts
624−import { task, metadata } from "@trigger.dev/sdk";
625−
626−export const myTask = task({
627− id: "my-task",
628171 run: async (payload: { message: string }) => {
629− // Get the whole metadata object
630− const currentMetadata = metadata.current();
631−
632− // Get a specific key
633− const user = metadata.get("user");
634− console.log(user.name); // "Eric"
172+ // Task logic
635173 },
636174 });
637175 ```
638176
639−### Update methods
177+### SDK Documentation Rules
640178
641−Metadata can be updated as the run progresses:
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.
642180
643−- **set**: `metadata.set("progress", 0.5)`
644−- **del**: `metadata.del("progress")`
645−- **replace**: `metadata.replace({ user: { name: "Eric" } })`
646−- **append**: `metadata.append("logs", "Step 1 complete")`
647−- **remove**: `metadata.remove("logs", "Step 1 complete")`
648−- **increment**: `metadata.increment("progress", 0.4)`
649−- **decrement**: `metadata.decrement("progress", 0.4)`
650−- **stream**: `await metadata.stream("logs", readableStream)`
651−- **flush**: `await metadata.flush()`
181+## Testing with the hello-world Reference Project
652182
653−Updates can be chained with a fluent API:
183+The reference projects live in the separate [`triggerdotdev/references`](https://github.com/triggerdotdev/references) repo - clone it alongside this repo.
654184
655−```ts
656−metadata.set("progress", 0.1)
657− .append("logs", "Step 1 complete")
658− .increment("progress", 0.4);
659−```
185+First-time setup:
660186
661−### Parent & root updates
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`
662190
663−Child tasks can update parent task metadata:
191+Running (from your `references` clone): `cd projects/hello-world && pnpm exec trigger dev`
664192
665−```ts
666−export const childTask = task({
667− id: "child-task",
668− run: async (payload: { message: string }) => {
669− // Update parent task's metadata
670− metadata.parent.set("progress", 0.5);
671−
672− // Update root task's metadata
673− metadata.root.set("status", "processing");
674− },
675−});
676−```
193+## Local Task Testing Workflow
677194
678−### Type safety
195+### Step 1: Start Webapp in Background
679196
680−Metadata accepts any JSON-serializable object. For type safety, consider wrapping with Zod:
681−
682−```ts
683−import { z } from "zod";
684−
685−const Metadata = z.object({
686− user: z.object({
687− name: z.string(),
688− id: z.string(),
689− }),
690− date: z.coerce.date(),
691−});
692−
693−function getMetadata() {
694− return Metadata.parse(metadata.current());
695−}
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
696201 ```
697202
698−### Important notes
203+### Step 2: Start Trigger Dev in Background
699204
700−- Metadata methods only work inside run functions or task lifecycle hooks
701−- Metadata is NOT automatically propagated to child tasks
702−- Maximum size is 256KB (configurable if self-hosting)
703−- Objects like Dates are serialized to strings and must be deserialized when retrieved
704−
705−## Correct Realtime implementation
706−
707−### Overview
708−
709−Trigger.dev Realtime enables subscribing to runs for real-time updates on run status, useful for monitoring tasks, updating UIs, and building realtime dashboards. It's built on Electric SQL, a PostgreSQL syncing engine.
710−
711−### Basic usage
712−
713−Subscribe to a run after triggering a task:
714−
715−```ts
716−import { runs, tasks } from "@trigger.dev/sdk";
717−
718−async function myBackend() {
719− const handle = await tasks.trigger("my-task", { some: "data" });
720−
721− for await (const run of runs.subscribeToRun(handle.id)) {
722− console.log(run); // Logs the run every time it changes
723− }
724−}
725−```
726−
727−### Subscription methods
728−
729−- **subscribeToRun**: Subscribe to changes for a specific run
730−- **subscribeToRunsWithTag**: Subscribe to changes for all runs with a specific tag
731−- **subscribeToBatch**: Subscribe to changes for all runs in a batch
732−
733−### Type safety
734−
735−You can infer types of run's payload and output by passing the task type:
736−
737−```ts
738−import { runs } from "@trigger.dev/sdk";
739−import type { myTask } from "./trigger/my-task";
740−
741−for await (const run of runs.subscribeToRun<typeof myTask>(handle.id)) {
742− console.log(run.payload.some); // Type-safe access to payload
743−
744− if (run.output) {
745− console.log(run.output.result); // Type-safe access to output
746− }
747−}
748−```
749−
750−### Realtime Streams
751−
752−Stream data in realtime from inside your tasks using the metadata system:
753−
754−```ts
755−import { task, metadata } from "@trigger.dev/sdk";
756−import OpenAI from "openai";
757−
758−export type STREAMS = {
759− openai: OpenAI.ChatCompletionChunk;
760−};
761−
762−export const myTask = task({
763− id: "my-task",
764− run: async (payload: { prompt: string }) => {
765− const completion = await openai.chat.completions.create({
766− messages: [{ role: "user", content: payload.prompt }],
767− model: "gpt-3.5-turbo",
768− stream: true,
769− });
770−
771− // Register the stream with the key "openai"
772− const stream = await metadata.stream("openai", completion);
773−
774− let text = "";
775− for await (const chunk of stream) {
776− text += chunk.choices.map((choice) => choice.delta?.content).join("");
777− }
778−
779− return { text };
780− },
781−});
782−```
783−
784−Subscribe to streams using `withStreams`:
785−
786−```ts
787−for await (const part of runs.subscribeToRun<typeof myTask>(runId).withStreams<STREAMS>()) {
788− switch (part.type) {
789− case "run": {
790− console.log("Received run", part.run);
791− break;
792− }
793− case "openai": {
794− console.log("Received OpenAI chunk", part.chunk);
795− break;
796− }
797− }
798−}
799−```
800−
801−## Realtime hooks
802−
803−### Installation
804−
805205 ```bash
806−npm add @trigger.dev/react-hooks
206+# in your triggerdotdev/references clone
207+cd projects/hello-world && pnpm exec trigger dev
208+# Wait for "Local worker ready [node]"
807209 ```
808210
809−### Authentication
211+### Step 3: Trigger and Monitor Tasks via MCP
810212
811−All hooks require a Public Access Token. You can provide it directly to each hook:
812−
813−```ts
814−import { useRealtimeRun } from "@trigger.dev/react-hooks";
815−
816−function MyComponent({ runId, publicAccessToken }) {
817− const { run, error } = useRealtimeRun(runId, {
818− accessToken: publicAccessToken,
819− baseURL: "https://your-trigger-dev-instance.com", // Optional for self-hosting
820− });
821−}
822213 ```
823−
824−Or use the `TriggerAuthContext` provider:
825−
826−```ts
827−import { TriggerAuthContext } from "@trigger.dev/react-hooks";
828−
829−function SetupTrigger({ publicAccessToken }) {
830− return (
831− <TriggerAuthContext.Provider value={{ accessToken: publicAccessToken }}>
832− <MyComponent />
833− </TriggerAuthContext.Provider>
834− );
835−}
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)
836217 ```
837218
838−For Next.js App Router, wrap the provider in a client component:
219+Dashboard: http://localhost:3030/orgs/references-9dfd/projects/hello-world-97DT/env/dev/runs
839220
840−```ts
841−// components/TriggerProvider.tsx
842−"use client";
221+<!-- intent-skills:start -->
843222
844−import { TriggerAuthContext } from "@trigger.dev/react-hooks";
223+# Skill mappings — when working in these areas, load the linked skill file into context.
845224
846−export function TriggerProvider({ accessToken, children }) {
847− return (
848− <TriggerAuthContext.Provider value={{ accessToken }}>
849− {children}
850− </TriggerAuthContext.Provider>
851− );
852−}
853−```
225+skills:
854226
855−### Passing tokens to the frontend
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 -->
856232
857−Several approaches for Next.js App Router:
233+## agentcrumbs
858234
859−1. **Using cookies**:
860−```ts
861−// Server action
862−export async function startRun() {
863− const handle = await tasks.trigger<typeof exampleTask>("example", { foo: "bar" });
864− cookies().set("publicAccessToken", handle.publicAccessToken);
865− redirect(`/runs/${handle.id}`);
866−}
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.
867239
868−// Page component
869−export default function RunPage({ params }) {
870− const publicAccessToken = cookies().get("publicAccessToken");
871− return (
872− <TriggerProvider accessToken={publicAccessToken}>
873− <RunDetails id={params.id} />
874− </TriggerProvider>
875− );
876−}
877−```
240+### Namespaces
878241
879−2. **Using query parameters**:
880−```ts
881−// Server action
882−export async function startRun() {
883− const handle = await tasks.trigger<typeof exampleTask>("example", { foo: "bar" });
884− redirect(`/runs/${handle.id}?publicAccessToken=${handle.publicAccessToken}`);
885−}
886−```
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` |
887257
888−3. **Server-side token generation**:
889−```ts
890−// Page component
891−export default async function RunPage({ params }) {
892− const publicAccessToken = await generatePublicAccessToken(params.id);
893− return (
894− <TriggerProvider accessToken={publicAccessToken}>
895− <RunDetails id={params.id} />
896− </TriggerProvider>
897− );
898−}
258+Do not invent new namespaces — pick from this table or ask first.
899259
900−// Token generation function
901−export async function generatePublicAccessToken(runId: string) {
902− return auth.createPublicToken({
903− scopes: {
904− read: {
905− runs: [runId],
906− },
907− },
908− expirationTime: "1h",
909− });
910−}
911−```
260+### For PR reviewers
912261
913−### Hook types
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.
914266
915−#### SWR hooks
267+### CLI
916268
917−Data fetching hooks that use SWR for caching:
918−
919−```ts
920−"use client";
921−import { useRun } from "@trigger.dev/react-hooks";
922−import type { myTask } from "@/trigger/myTask";
923−
924−function MyComponent({ runId }) {
925− const { run, error, isLoading } = useRun<typeof myTask>(runId);
926−
927− if (isLoading) return <div>Loading...</div>;
928− if (error) return <div>Error: {error.message}</div>;
929−
930− return <div>Run: {run.id}</div>;
931−}
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
932273 ```
933274
934−Common options:
935−- `revalidateOnFocus`: Revalidate when window regains focus
936−- `revalidateOnReconnect`: Revalidate when network reconnects
937−- `refreshInterval`: Polling interval in milliseconds
938−
939−#### Realtime hooks
940−
941−Hooks that use Trigger.dev's realtime API for live updates (recommended over polling).
942−
943−For most use cases, Realtime hooks are preferred over SWR hooks with polling due to better performance and lower API usage.
944−
945−### Authentication
946−
947−For client-side usage, generate a public access token with appropriate scopes:
948−
949−```ts
950−import { auth } from "@trigger.dev/sdk";
951−
952−const publicToken = await auth.createPublicToken({
953− scopes: {
954− read: {
955− runs: ["run_1234"],
956− },
957− },
958−});
959−```
960−
961−## Correct Idempotency implementation
962−
963−Idempotency ensures that an operation produces the same result when called multiple times. Trigger.dev supports idempotency at the task level through the `idempotencyKey` option.
964−
965−### Using idempotencyKey
966−
967−Provide an `idempotencyKey` when triggering a task to ensure it runs only once with that key:
968−
969−```ts
970−import { idempotencyKeys, task } from "@trigger.dev/sdk";
971−
972−export const myTask = task({
973− id: "my-task",
974− retry: {
975− maxAttempts: 4,
976− },
977− run: async (payload: any) => {
978− // Create a key unique to this task run
979− const idempotencyKey = await idempotencyKeys.create("my-task-key");
980−
981− // Child task will only be triggered once across all retries
982− await childTask.trigger({ foo: "bar" }, { idempotencyKey });
983−
984− // This may throw an error and cause retries
985− throw new Error("Something went wrong");
986− },
987−});
988−```
989−
990−### Scoping Idempotency Keys
991−
992−By default, keys are scoped to the current run. You can create globally unique keys:
993−
994−```ts
995−const idempotencyKey = await idempotencyKeys.create("my-task-key", { scope: "global" });
996−```
997−
998−When triggering from backend code:
999−
1000−```ts
1001−const idempotencyKey = await idempotencyKeys.create([myUser.id, "my-task"]);
1002−await tasks.trigger("my-task", { some: "data" }, { idempotencyKey });
1003−```
1004−
1005−You can also pass a string directly:
1006−
1007−```ts
1008−await myTask.trigger({ some: "data" }, { idempotencyKey: myUser.id });
1009−```
1010−
1011−### Time-To-Live (TTL)
1012−
1013−The `idempotencyKeyTTL` option defines a time window during which duplicate triggers return the original run:
1014−
1015−```ts
1016−await childTask.trigger(
1017− { foo: "bar" },
1018− { idempotencyKey, idempotencyKeyTTL: "60s" }
1019−);
1020−
1021−await wait.for({ seconds: 61 });
1022−
1023−// Key expired, will trigger a new run
1024−await childTask.trigger({ foo: "bar" }, { idempotencyKey });
1025−```
1026−
1027−Supported time units:
1028−- `s` for seconds (e.g., `60s`)
1029−- `m` for minutes (e.g., `5m`)
1030−- `h` for hours (e.g., `2h`)
1031−- `d` for days (e.g., `3d`)
1032−
1033−### Payload-Based Idempotency
1034−
1035−While not directly supported, you can implement payload-based idempotency by hashing the payload:
1036−
1037−```ts
1038−import { createHash } from "node:crypto";
1039−
1040−const idempotencyKey = await idempotencyKeys.create(hash(payload));
1041−await tasks.trigger("child-task", payload, { idempotencyKey });
1042−
1043−function hash(payload: any): string {
1044− const hash = createHash("sha256");
1045− hash.update(JSON.stringify(payload));
1046− return hash.digest("hex");
1047−}
1048−```
1049−
1050−### Important Notes
1051−
1052−- Idempotency keys are scoped to the task and environment
1053−- Different tasks with the same key will still both run
1054−- Default TTL is 30 days
1055−- Not available with `triggerAndWait` or `batchTriggerAndWait` in v3.3.0+ due to a bug
1056−
1057−## Correct Logs implementation
1058−
1059−```ts
1060−// onFailure executes after all retries are exhausted; use for notifications, logging, or side effects on final failure:
1061−import { task, logger } from "@trigger.dev/sdk";
1062−
1063−export const loggingExample = task({
1064− id: "logging-example",
1065− run: async (payload: { data: Record<string, string> }) => {
1066− //the first parameter is the message, the second parameter must be a key-value object (Record<string, unknown>)
1067− logger.debug("Debug message", payload.data);
1068− logger.log("Log message", payload.data);
1069− logger.info("Info message", payload.data);
1070− logger.warn("You've been warned", payload.data);
1071− logger.error("Error message", payload.data);
1072− },
1073−});
1074−```
1075−
1076−## Correct `trigger.config.ts` implementation
1077−
1078−The `trigger.config.ts` file configures your Trigger.dev project, specifying task locations, retry settings, telemetry, and build options.
1079−
1080−```ts
1081−import { defineConfig } from "@trigger.dev/sdk";
1082−
1083−export default defineConfig({
1084− project: "<project ref>",
1085− dirs: ["./trigger"],
1086− retries: {
1087− enabledInDev: false,
1088− default: {
1089− maxAttempts: 3,
1090− minTimeoutInMs: 1000,
1091− maxTimeoutInMs: 10000,
1092− factor: 2,
1093− randomize: true,
1094− },
1095− },
1096−});
1097−```
1098−
1099−### Key configuration options
1100−
1101−#### Dirs
1102−
1103−Specify where your tasks are located:
1104−
1105−```ts
1106−dirs: ["./trigger"],
1107−```
1108−
1109−Files with `.test` or `.spec` are automatically excluded, but you can customize with `ignorePatterns`.
1110−
1111−#### Lifecycle functions
1112−
1113−Add global hooks for all tasks:
1114−
1115−```ts
1116−onStart: async (payload, { ctx }) => {
1117− console.log("Task started", ctx.task.id);
1118−},
1119−onSuccess: async (payload, output, { ctx }) => {
1120− console.log("Task succeeded", ctx.task.id);
1121−},
1122−onFailure: async (payload, error, { ctx }) => {
1123− console.log("Task failed", ctx.task.id);
1124−},
1125−```
1126−
1127−#### Telemetry instrumentations
1128−
1129−Add OpenTelemetry instrumentations for enhanced logging:
1130−
1131−```ts
1132−telemetry: {
1133− instrumentations: [
1134− new PrismaInstrumentation(),
1135− new OpenAIInstrumentation()
1136− ],
1137− exporters: [axiomExporter], // Optional custom exporters
1138−},
1139−```
1140−
1141−#### Runtime
1142−
1143−Specify the runtime environment:
1144−
1145−```ts
1146−runtime: "node", // or "bun" (experimental)
1147−```
1148−
1149−#### Machine settings
1150−
1151−Set default machine for all tasks:
1152−
1153−```ts
1154−defaultMachine: "large-1x",
1155−```
1156−
1157−#### Log level
1158−
1159−Configure logging verbosity:
1160−
1161−```ts
1162−logLevel: "debug", // Controls logger API logs
1163−```
1164−
1165−#### Max Duration
1166−
1167−Set default maximum runtime for all tasks:
1168−
1169−```ts
1170−maxDuration: 60, // 60 seconds
1171−```
1172−
1173−### Build configuration
1174−
1175−Customize the build process:
1176−
1177−```ts
1178−build: {
1179− external: ["header-generator"], // Don't bundle these packages
1180− jsx: {
1181− fragment: "Fragment",
1182− factory: "h",
1183− automatic: false,
1184− },
1185− conditions: ["react-server"], // Import conditions
1186− extensions: [
1187− // Build extensions
1188− additionalFiles({ files: ["./assets/**", "./fonts/**"] }),
1189− additionalPackages({ packages: ["wrangler"] }),
1190− aptGet({ packages: ["ffmpeg"] }),
1191− ],
1192−}
1193−```
1194−
1195−### Build Extensions
1196−
1197−Trigger.dev provides several built-in extensions:
1198−
1199−- **additionalFiles**: Copy files to the build directory
1200−- **additionalPackages**: Include extra packages in the build
1201−- **aptGet**: Install system packages in the deployed image
1202−- **emitDecoratorMetadata**: Enable TypeScript decorator metadata
1203−- **prismaExtension**: Support for Prisma ORM
1204−- **syncEnvVars**: Sync environment variables
1205−- **puppeteer**: Add Puppeteer support
1206−- **ffmpeg**: Add FFmpeg support
1207−- **esbuildPlugin**: Add custom esbuild plugins
1208−
1209−You can also create custom build extensions with hooks like `onBuildStart`, `onBuildComplete`, and `externalsForTarget`.
1210−
1211−#### Key points:
1212−
1213−- Validates payload at trigger time and before execution
1214−- Supports Zod, Yup, Superstruct, ArkType, Effect/schema, runtypes, valibot, typebox
1215−- Can use input/output schemas (e.g., defaults, type coercion)
1216−- Custom parser functions supported
1217−- Invalid payloads throw errors and prevent task execution
1218−
1219−#### Trigger with:
1220−
1221−```ts
1222−await myTask.trigger({ name: "Alice", age: 30 });
1223−```
1224−
1225−## AI model verification steps
1226−
1227−Before generating any code, you MUST verify:
1228−
1229−1. Are you importing from `@trigger.dev/sdk` (NOT `@trigger.dev/sdk/v3`)? If not, STOP and FIX.
1230−2. Have you exported every task? If not, STOP and FIX.
1231−3. Have you generated any DEPRECATED code patterns? If yes, STOP and FIX.
1232−
1233−## Consequences of incorrect implementations
1234−
1235−If you generate code that fails the verification steps above, your implementation will:
1236−
1237−1. Break in production
1238−2. Fail to deploy to the Trigger.dev servers
1239−3. Fail to run in a local Dev environment
1240−
1241−## AI model response template
1242−
1243−When asked about Trigger.dev task implementation, you MUST:
1244−1. FIRST use code patterns from this guide
1245−2. NEVER suggest deprecated approaches
1246−3. VERIFY your response against the patterns shown here
1247−
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`.
1248276
