| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 38 | 51 | 0% |
| Commands | 0 | 25 | 4 | 0% |
| Section tags | 5 | 7 | 4 | 31% |
What each file covers
Sections
0 shared · 38 only in A · 51 only in B- − AGENTS.md
- − Build and Development Commands
- − pnpm run docker:full # Same + observability stack (Prometheus, Grafana, OTEL) and chaos tooling
- − Build packages (required before running)
- − 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
- + 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
Commands
0 shared · 25 only in A · 4 only in B- − pnpm run docker
- − pnpm run db:migrate
- − pnpm run db:seed
- − pnpm run build --filter webapp && pnpm run build --filter trigger.dev && pnpm run build --filter @trigger.dev/sdk
- − pnpm run dev --filter 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
- + task: firstScheduledTask.id,
- + npm add @trigger.dev/react-hooks
- + npx trigger.dev@latest init
- + npx trigger.dev@latest dev
Section tags
5 shared · 7 only in A · 4 only in B- − test
- − lint-format
- − code-style
- − testing-strategy
- − git-pr
- − dependencies
- − docs
- + types
- + security
- + database
- + deployment
- setup
- build
- architecture
- do-not
- agent-behaviour
Line diff
triggerdotdev/trigger.dev · AGENTS.md
@@ −1 @@
1# AGENTS.md
2
3This file provides guidance to Claude Code when working with this repository. Subdirectory CLAUDE.md files provide deeper context when you navigate into specific areas.
4
5## Build and Development Commands
6
7This is a pnpm 10.33.2 monorepo using Turborepo. Run commands from root with `pnpm run`.
8
9**Adding dependencies:** Edit `package.json` directly instead of using `pnpm add`, then run `pnpm i` from the repo root. See `.claude/rules/package-installation.md` for the full process.
10
11```bash
12pnpm run docker # Core dev services (Postgres, Redis, Electric, MinIO, ClickHouse, s2-lite)
13# pnpm run docker:full # Same + observability stack (Prometheus, Grafana, OTEL) and chaos tooling
14pnpm run db:migrate # Run database migrations
15pnpm run db:seed # Seed the database (required for reference projects)
16
17# Build packages (required before running)
18pnpm run build --filter webapp && pnpm run build --filter trigger.dev && pnpm run build --filter @trigger.dev/sdk
19
20pnpm run dev --filter webapp # Run webapp (http://localhost:3030)
21pnpm run dev --filter trigger.dev --filter "@trigger.dev/*" # Watch CLI and packages
22```
23
24### Verifying Changes
25
26The verification command depends on where the change lives:
27
28- **Apps and internal packages** (`apps/*`, `internal-packages/*`): Use `typecheck`. **Never use `build`** for these — building proves almost nothing about correctness.
29- **Public packages** (`packages/*`): Use `build`.
30
31```bash
32# Apps and internal packages — use typecheck
33pnpm run typecheck --filter webapp # ~1-2 minutes
34pnpm run typecheck --filter @internal/run-engine
35
36# Public packages — use build
37pnpm run build --filter @trigger.dev/sdk
38pnpm run build --filter @trigger.dev/core
39```
40
41Only run typecheck/build after major changes (new files, significant refactors, schema changes). For small edits, trust the types and let CI catch issues.
42
43## Testing
44
45We use vitest exclusively. **Never mock anything** - use testcontainers instead.
46
47```bash
48pnpm run test --filter webapp # All tests for a package
49cd internal-packages/run-engine
50pnpm run test ./src/engine/tests/ttl.test.ts --run # Single test file
51pnpm run build --filter @internal/run-engine # May need to build deps first
52```
53
54Test files go next to source files (e.g., `MyService.ts` -> `MyService.test.ts`).
55
56### Testcontainers for Redis/PostgreSQL
57
58```typescript
59import { redisTest, postgresTest, containerTest } from "@internal/testcontainers";
60
61redisTest("should use redis", async ({ redisOptions }) => {
62 /* ... */
63});
64postgresTest("should use postgres", async ({ prisma }) => {
65 /* ... */
66});
67containerTest("should use both", async ({ prisma, redisOptions }) => {
68 /* ... */
69});
70```
71
72## Code Style
73
74### Formatting and linting
75
76Format and lint are enforced by CI (`code-quality` check). Run before committing:
77
78```bash
79pnpm run format # oxfmt — auto-fixes formatting
80pnpm run lint:fix # oxlint — auto-fixes lint violations
81pnpm run lint # oxlint — check only (no fixes)
82```
83
84### Imports
85
86**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:
87- Circular dependencies cannot be resolved otherwise
88- Code splitting is genuinely needed for performance
89- The module must be loaded conditionally at runtime
90
91Dynamic imports add unnecessary overhead in hot paths and make code harder to analyze. If you find yourself using `await import()`, ask if a regular `import` statement would work instead.
92
93## Changesets and Server Changes
94
95When modifying any public package (`packages/*` or `integrations/*`), add a changeset:
96
97```bash
98pnpm run changeset:add
99```
100
101- Default to **patch** for bug fixes and minor changes
102- Confirm with maintainers before selecting **minor** (new features)
103- **Never** select major without explicit approval
104
105When modifying only server components (`apps/webapp/`, `apps/supervisor/`, etc.) with no package changes, add a `.server-changes/` file instead. See `.server-changes/README.md` for format and documentation.
106
107**Write the description for users, not maintainers.** Both changesets and `.server-changes/` notes ship verbatim in user-visible release notes. Lead with what changed *for the user* - one plain sentence describing behavior, not implementation, and never naming internal tools or infra. The full writing guidance in `.server-changes/README.md` applies to changesets too.
108
109## Dependency Pinning
110
111Zod is pinned to a single version across the entire monorepo (currently `3.25.76`). When adding zod to a new or existing package, use the **exact same version** as the rest of the repo - never a different version or a range. Mismatched zod versions cause runtime type incompatibilities (e.g., schemas from one package can't be used as body validators in another).
112
113## Architecture Overview
114
115### Request Flow
116
117User API call -> Webapp routes -> Services -> RunEngine -> Redis Queue -> Supervisor -> Container execution -> Results back through RunEngine -> ClickHouse (analytics) + PostgreSQL (state)
118
119### Apps
120
121- **apps/webapp**: Remix 2.17.4 app - main API, dashboard, orchestration. Uses Express server.
122- **apps/supervisor**: Manages task execution containers (Docker/Kubernetes).
123
124### Public Packages
125
126- **packages/trigger-sdk** (`@trigger.dev/sdk`): Main SDK for writing tasks
127- **packages/cli-v3** (`trigger.dev`): CLI - also bundles code that goes into customer task images
128- **packages/core** (`@trigger.dev/core`): Shared types. **Import subpaths only** (never root).
129- **packages/build** (`@trigger.dev/build`): Build extensions and types
130- **packages/react-hooks**: React hooks for realtime and triggering
131- **packages/redis-worker** (`@trigger.dev/redis-worker`): Redis-based background job system
132
133### Internal Packages
134
135- **internal-packages/database**: Prisma 6.14.0 client and schema (PostgreSQL)
136- **internal-packages/clickhouse**: ClickHouse client, schema migrations, analytics queries
137- **internal-packages/run-engine**: "Run Engine 2.0" - core run lifecycle management
138- **internal-packages/redis**: Redis client creation utilities (ioredis)
139- **internal-packages/testcontainers**: Test helpers for Redis/PostgreSQL containers
140- **internal-packages/schedule-engine**: Durable cron scheduling
141
142### v3 (engine V1) removed
143
144v3 (engine V1: MarQS + Graphile worker) is end-of-life and its execution code has been removed. The `apps/webapp/app/v3/` directory name is historical - everything there now serves V2 (Run Engine 2.0, `@internal/run-engine` + redis-worker). There is no V1 execution path: a `RunEngineVersion` `V1` branch only rejects or finalizes gracefully so v3 clients get a clean 4xx, never a 5xx. Do not reintroduce V1. See `apps/webapp/CLAUDE.md` and `.claude/rules/legacy-v3-code.md`.
145
146### Documentation
147
148Docs live in `docs/` as a Mintlify site (MDX format). See `docs/CLAUDE.md` for conventions.
149
150### Reference Projects
151
152Reference/example projects for testing SDK and platform features live in a separate repo: [`triggerdotdev/references`](https://github.com/triggerdotdev/references). Clone it alongside this repo and use its `projects/hello-world` to manually test changes before submitting PRs. See that repo's README for setup and linking to a local monorepo build.
153
154## Docker Image Guidelines
155
156When updating Docker image references:
157
158- **Always use multiplatform/index digests**, not architecture-specific digests
159- Architecture-specific digests cause CI failures on different build environments
160- Use the digest from the main Docker Hub page, not from a specific OS/ARCH variant
161
162## Writing Trigger.dev Tasks
163
164Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` or deprecated `client.defineJob`.
165
166```typescript
167import { task } from "@trigger.dev/sdk";
168
169export const myTask = task({
170 id: "my-task",
171 run: async (payload: { message: string }) => {
172 // Task logic
173 },
174});
175```
176
177### SDK Documentation Rules
178
179The `rules/` directory contains versioned SDK documentation distributed via the SDK installer. Current version: `rules/manifest.json`. Do NOT update `rules/` or `.claude/skills/trigger-dev-tasks/` unless explicitly asked - these are maintained in separate dedicated passes.
180
181## Testing with the hello-world Reference Project
182
183The reference projects live in the separate [`triggerdotdev/references`](https://github.com/triggerdotdev/references) repo - clone it alongside this repo.
184
185First-time setup:
186
1871. `pnpm run db:seed` to seed the database (creates the References org + hello-world project)
1882. Build the CLI/packages you want to test: `pnpm run build --filter trigger.dev`
1893. In your `references` clone, follow its README to link to your local monorepo build, then authorize: `cd projects/hello-world && pnpm exec trigger login -a http://localhost:3030`
190
191Running (from your `references` clone): `cd projects/hello-world && pnpm exec trigger dev`
192
193## Local Task Testing Workflow
194
195### Step 1: Start Webapp in Background
196
197```bash
198# Run from repo root with run_in_background: true
199pnpm run dev --filter webapp
200curl -s http://localhost:3030/healthcheck # Verify running
201```
202
203### Step 2: Start Trigger Dev in Background
204
205```bash
206# in your triggerdotdev/references clone
207cd projects/hello-world && pnpm exec trigger dev
208# Wait for "Local worker ready [node]"
209```
210
211### Step 3: Trigger and Monitor Tasks via MCP
212
213```
214mcp__trigger__get_current_worker(projectRef: "proj_rrkpdguyagvsoktglnod", environment: "dev")
215mcp__trigger__trigger_task(projectRef: "proj_rrkpdguyagvsoktglnod", environment: "dev", taskId: "hello-world", payload: {"message": "Hello"})
216mcp__trigger__list_runs(projectRef: "proj_rrkpdguyagvsoktglnod", environment: "dev", taskIdentifier: "hello-world", limit: 5)
217```
218
219Dashboard: http://localhost:3030/orgs/references-9dfd/projects/hello-world-97DT/env/dev/runs
220
221<!-- intent-skills:start -->
222
223# Skill mappings — when working in these areas, load the linked skill file into context.
224
225skills:
226
227- task: "Using agentcrumbs for debug tracing, adding crumbs, trails, markers, querying traces, or stripping debug code before merge"
228 load: "node_modules/agentcrumbs/skills/agentcrumbs/SKILL.md"
229- task: "Setting up agentcrumbs in the project, initializing namespace catalog, running crumbs init"
230 load: "node_modules/agentcrumbs/skills/agentcrumbs/init/SKILL.md"
231<!-- intent-skills:end -->
232
233## agentcrumbs
234
235Add crumbs as you write code — not just when debugging. Mark lines with
236`// @crumbs` or wrap blocks in `// #region @crumbs`. They stay on the
237branch throughout development and are stripped by `agentcrumbs strip`
238before merge.
239
240### Namespaces
241
242| Namespace | Description | Path |
243| ----------------- | ---------------------------------------------- | ----------------------------------- |
244| `webapp` | Main Remix app — API, dashboard, orchestration | `apps/webapp` |
245| `supervisor` | Task execution container manager | `apps/supervisor` |
246| `sdk` | Main SDK for writing tasks | `packages/trigger-sdk` |
247| `cli` | CLI and task image bundling | `packages/cli-v3` |
248| `core` | Shared types and subpath exports | `packages/core` |
249| `build` | Build extensions and types | `packages/build` |
250| `react-hooks` | React hooks for realtime/triggering | `packages/react-hooks` |
251| `redis-worker` | Redis-based background job system | `packages/redis-worker` |
252| `run-engine` | Run Engine 2.0 — core run lifecycle | `internal-packages/run-engine` |
253| `database` | Prisma client and schema | `internal-packages/database` |
254| `clickhouse` | ClickHouse client, migrations, analytics | `internal-packages/clickhouse` |
255| `schedule-engine` | Durable cron scheduling | `internal-packages/schedule-engine` |
256| `run-queue` | Run queue management | `internal-packages/run-queue` |
257
258Do not invent new namespaces — pick from this table or ask first.
259
260### For PR reviewers
261
262Do not flag `// @crumbs` markers, `#region @crumbs` blocks, or agentcrumbs
263imports in reviews. These are temporary debug instrumentation stripped
264before merge. Data logged in crumbs (IDs, names, values) never reaches
265production.
266
267### CLI
268
269```bash
270pnpm exec agentcrumbs collect # start collector (multi-service)
271pnpm exec agentcrumbs tail --app trigger # live tail
272pnpm exec agentcrumbs clear --app trigger # remove crumbs before merge
273```
274
275The preferred way to query for crumbs is to use `pnpm exec agentcrumbs query --app trigger` with the `--limit` option and cursor pagination, and clear existing crumbs before reproducing a bug via `pnpm exec agentcrumbs clear --app trigger`.
276
triggerdotdev/trigger.dev · .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
@@ −1 +1 @@
1−# AGENTS.md
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
27
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.
8+## Overview of writing a Trigger.dev task
49
5−## Build and Development Commands
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`.
614
7−This is a pnpm 10.33.2 monorepo using Turborepo. Run commands from root with `pnpm run`.
15+## Essential requirements when generating task code
816
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.
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.
1021
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)
22+## 🚨 CRITICAL INSTRUCTIONS FOR AI LANGUAGE MODELS 🚨
1623
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
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:
1925
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
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+});
2249 ```
2350
24−### Verifying Changes
51+Instead, you MUST ALWAYS generate ONLY this pattern:
2552
26−The verification command depends on where the change lives:
53+```ts
54+// ✅ ALWAYS GENERATE THIS EXACT PATTERN
2755
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`.
56+import { task } from "@trigger.dev/sdk";
3057
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
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+```
3568
36−# Public packages — use build
37−pnpm run build --filter @trigger.dev/sdk
38−pnpm run build --filter @trigger.dev/core
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+});
3982 ```
4083
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.
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
4288
43−## Testing
89+### Task configuration options
4490
45−We use vitest exclusively. **Never mock anything** - use testcontainers instead.
91+#### Retry options
4692
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
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+});
52109 ```
53110
54−Test files go next to source files (e.g., `MyService.ts` -> `MyService.test.ts`).
111+#### Queue options
55112
56−### Testcontainers for Redis/PostgreSQL
113+Control concurrency:
57114
58−```typescript
59−import { redisTest, postgresTest, containerTest } from "@internal/testcontainers";
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+```
60126
61−redisTest("should use redis", async ({ redisOptions }) => {
62− /* ... */
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+ },
63140 });
64−postgresTest("should use postgres", async ({ prisma }) => {
65− /* ... */
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+ },
66166 });
67−containerTest("should use both", async ({ prisma, redisOptions }) => {
68− /* ... */
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+ },
69186 });
70187 ```
71188
72−## Code Style
189+#### cleanup
73190
74−### Formatting and linting
191+Runs after each attempt, regardless of success/failure:
75192
76−Format and lint are enforced by CI (`code-quality` check). Run before committing:
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+```
77204
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)
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+});
82219 ```
83220
84−### Imports
221+#### onSuccess
85222
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
223+Runs when a task succeeds:
90224
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.
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+```
92236
93−## Changesets and Server Changes
237+#### onFailure
94238
95−When modifying any public package (`packages/*` or `integrations/*`), add a changeset:
239+Runs when a task fails after all retries:
96240
97−```bash
98−pnpm run changeset:add
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+});
99251 ```
100252
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
253+#### handleError
104254
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.
255+Controls error handling and retry behavior:
106256
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.
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+```
108268
109−## Dependency Pinning
269+Global lifecycle hooks can also be defined in `trigger.config.ts` to apply to all tasks.
110270
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).
271+## Correct Schedules task (cron) implementations
112272
113−## Architecture Overview
273+```ts
274+import { schedules } from "@trigger.dev/sdk";
114275
115−### Request Flow
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
116282
117−User API call -> Webapp routes -> Services -> RunEngine -> Redis Queue -> Supervisor -> Container execution -> Results back through RunEngine -> ClickHouse (analytics) + PostgreSQL (state)
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
118286
119−### Apps
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
120291
121−- **apps/webapp**: Remix 2.17.4 app - main API, dashboard, orchestration. Uses Express server.
122−- **apps/supervisor**: Manages task execution containers (Docker/Kubernetes).
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+ });
123296
124−### Public Packages
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
125300
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
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
132305
133−### Internal Packages
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+```
134311
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
312+### Attach a Declarative schedule
141313
142−### v3 (engine V1) removed
314+```ts
315+import { schedules } from "@trigger.dev/sdk";
143316
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`.
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+```
145327
146−### Documentation
328+```ts
329+import { schedules } from "@trigger.dev/sdk";
147330
148−Docs live in `docs/` as a Mintlify site (MDX format). See `docs/CLAUDE.md` for conventions.
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+```
149342
150−### Reference Projects
343+### Attach an Imperative schedule
151344
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.
345+Create schedules explicitly for tasks using the dashboard's "New schedule" button or the SDK.
153346
154−## Docker Image Guidelines
347+#### Benefits
348+- Dynamic creation (e.g., one schedule per user)
349+- Manage without code deployment:
350+ - Activate/disable
351+ - Edit
352+ - Delete
155353
156−When updating Docker image references:
354+#### Implementation
355+1. Define a task using `schedules.task()`
356+2. Attach one or more schedules via:
357+ - Dashboard
358+ - SDK
157359
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
360+#### Attach schedules with the SDK like this
161361
162−## Writing Trigger.dev Tasks
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+```
163372
164−Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` or deprecated `client.defineJob`.
373+## Correct Schema task implementations
165374
166−```typescript
167−import { task } from "@trigger.dev/sdk";
375+Schema tasks validate payloads against a schema before execution:
168376
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+
169461 export const myTask = task({
170462 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",
171628 run: async (payload: { message: string }) => {
172− // Task logic
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"
173635 },
174636 });
175637 ```
176638
177−### SDK Documentation Rules
639+### Update methods
178640
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.
641+Metadata can be updated as the run progresses:
180642
181−## Testing with the hello-world Reference Project
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()`
182652
183−The reference projects live in the separate [`triggerdotdev/references`](https://github.com/triggerdotdev/references) repo - clone it alongside this repo.
653+Updates can be chained with a fluent API:
184654
185−First-time setup:
655+```ts
656+metadata.set("progress", 0.1)
657+ .append("logs", "Step 1 complete")
658+ .increment("progress", 0.4);
659+```
186660
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`
661+### Parent & root updates
190662
191−Running (from your `references` clone): `cd projects/hello-world && pnpm exec trigger dev`
663+Child tasks can update parent task metadata:
192664
193−## Local Task Testing Workflow
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+```
194677
195−### Step 1: Start Webapp in Background
678+### Type safety
196679
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
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+}
201696 ```
202697
203−### Step 2: Start Trigger Dev in Background
698+### Important notes
204699
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+
205805 ```bash
206−# in your triggerdotdev/references clone
207−cd projects/hello-world && pnpm exec trigger dev
208−# Wait for "Local worker ready [node]"
806+npm add @trigger.dev/react-hooks
209807 ```
210808
211−### Step 3: Trigger and Monitor Tasks via MCP
809+### Authentication
212810
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+}
213822 ```
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)
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+}
217836 ```
218837
219−Dashboard: http://localhost:3030/orgs/references-9dfd/projects/hello-world-97DT/env/dev/runs
838+For Next.js App Router, wrap the provider in a client component:
220839
221−<!-- intent-skills:start -->
840+```ts
841+// components/TriggerProvider.tsx
842+"use client";
222843
223−# Skill mappings — when working in these areas, load the linked skill file into context.
844+import { TriggerAuthContext } from "@trigger.dev/react-hooks";
224845
225−skills:
846+export function TriggerProvider({ accessToken, children }) {
847+ return (
848+ <TriggerAuthContext.Provider value={{ accessToken }}>
849+ {children}
850+ </TriggerAuthContext.Provider>
851+ );
852+}
853+```
226854
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 -->
855+### Passing tokens to the frontend
232856
233−## agentcrumbs
857+Several approaches for Next.js App Router:
234858
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.
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+}
239867
240−### Namespaces
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+```
241878
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` |
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+```
257887
258−Do not invent new namespaces — pick from this table or ask first.
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+}
259899
260−### For PR reviewers
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+```
261912
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.
913+### Hook types
266914
267−### CLI
915+#### SWR hooks
268916
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
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+}
273932 ```
274933
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`.
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+
2761248
