RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Diff/triggerdotdev-trigger-dev-agents ↔ triggerdotdev-trigger-dev-cursor-rules-writing-tasks

Comparison

A · AGENTS.md · triggerdotdev/trigger.devB · Cursor rules · triggerdotdev/trigger.dev
What each file covers, counted
DimensionSharedOnly in AOnly in BOverlap
Sections038510%
Commands02540%
Section tags57431%

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

+1140 added−168 removed108 unchanged8.6% identical
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  
RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack