Cursor rule
.cursor/rules/writing-tasks.mdcGuidelines for writing Trigger.dev tasks
Cursor rules
Quality
64/100
Scores the file, not the repository.Length
4,289 words
80 headings · 63 code blocksRepository
16k
— · pushed 0 days agoLast changed
3 days ago
First indexed 3 days ago.123456# How to write Trigger.dev tasks78## Overview of writing a Trigger.dev task9101. 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`.1415## Essential requirements when generating task code16171. 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 subtasks204. If you are able to generate an example payload for a task, do so.2122## 🚨 CRITICAL INSTRUCTIONS FOR AI LANGUAGE MODELS 🚨2324As an AI language model, you MUST NOT generate any of the following code patterns, as they are DEPRECATED and will BREAK the application:2526```ts27// ❌ NEVER GENERATE THIS CODE - IT WILL BREAK THE APPLICATION2829client.defineJob({ // ❌ BREAKS APPLICATION30 id: "job-id", // ❌ BREAKS APPLICATION31 name: "job-name", // ❌ BREAKS APPLICATION32 version: "0.0.1", // ❌ BREAKS APPLICATION33 trigger: eventTrigger({ // ❌ BREAKS APPLICATION34 name: "job.trigger", // ❌ BREAKS APPLICATION35 schema: z.object({ // ❌ BREAKS APPLICATION36 // Input schema here // ❌ BREAKS APPLICATION37 }), // ❌ BREAKS APPLICATION38 }), // ❌ BREAKS APPLICATION39 integrations: { // ❌ BREAKS APPLICATION40 // Integrations here // ❌ BREAKS APPLICATION41 }, // ❌ BREAKS APPLICATION42 run: async (payload, io) => { // ❌ BREAKS APPLICATION43 // Job logic goes here // ❌ BREAKS APPLICATION44 return { // ❌ BREAKS APPLICATION45 // Return job results // ❌ BREAKS APPLICATION46 }; // ❌ BREAKS APPLICATION47 }, // ❌ BREAKS APPLICATION48});49```5051Instead, you MUST ALWAYS generate ONLY this pattern:5253```ts54// ✅ ALWAYS GENERATE THIS EXACT PATTERN5556import { task } from "@trigger.dev/sdk";5758//1. You need to export each task, even if it's a subtask59export const helloWorld = task({60 //2. Use a unique id for each task61 id: "hello-world",62 //3. The run function is the main function of the task63 run: async (payload: { message: string }) => {64 //4. Write your task code here. Code here runs for a long time, there are no timeouts65 },66});67```6869## Correct Task implementations7071A task is a function that can run for a long time with resilience to failure:7273```ts74import { task } from "@trigger.dev/sdk";7576export const helloWorld = task({77 id: "hello-world",78 run: async (payload: { message: string }) => {79 console.log(payload.message);80 },81});82```8384Key points:85- Tasks must be exported, even subtasks in the same file86- Each task needs a unique ID within your project87- The `run` function contains your task logic8889### Task configuration options9091#### Retry options9293Control retry behavior when errors occur:9495```ts96export 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 logic107 },108});109```110111#### Queue options112113Control concurrency:114115```ts116export const oneAtATime = task({117 id: "one-at-a-time",118 queue: {119 concurrencyLimit: 1,120 },121 run: async (payload) => {122 // Task logic123 },124});125```126127#### Machine options128129Specify CPU/RAM requirements:130131```ts132export const heavyTask = task({133 id: "heavy-task",134 machine: {135 preset: "large-1x", // 4 vCPU, 8 GB RAM136 },137 run: async (payload) => {138 // Task logic139 },140});141```142143Machine configuration options:144145| 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 |154155#### Max Duration156157Limit how long a task can run:158159```ts160export const longTask = task({161 id: "long-task",162 maxDuration: 300, // 5 minutes163 run: async (payload) => {164 // Task logic165 },166});167```168169### Lifecycle functions170171Tasks support several lifecycle hooks:172173#### init174175Runs before each attempt, can return data for other functions:176177```ts178export 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```188189#### cleanup190191Runs after each attempt, regardless of success/failure:192193```ts194export const taskWithCleanup = task({195 id: "task-with-cleanup",196 cleanup: async (payload, { ctx }) => {197 // Cleanup resources198 },199 run: async (payload, { ctx }) => {200 // Task logic201 },202});203```204205#### onStart206207Runs once when a task starts (not on retries):208209```ts210export 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 logic217 },218});219```220221#### onSuccess222223Runs when a task succeeds:224225```ts226export const taskWithOnSuccess = task({227 id: "task-with-on-success",228 onSuccess: async (payload, output, { ctx }) => {229 // Handle success230 },231 run: async (payload, { ctx }) => {232 // Task logic233 },234});235```236237#### onFailure238239Runs when a task fails after all retries:240241```ts242export const taskWithOnFailure = task({243 id: "task-with-on-failure",244 onFailure: async (payload, error, { ctx }) => {245 // Handle failure246 },247 run: async (payload, { ctx }) => {248 // Task logic249 },250});251```252253#### handleError254255Controls error handling and retry behavior:256257```ts258export const taskWithErrorHandling = task({259 id: "task-with-error-handling",260 handleError: async (error, { ctx }) => {261 // Custom error handling262 },263 run: async (payload, { ctx }) => {264 // Task logic265 },266});267```268269Global lifecycle hooks can also be defined in `trigger.config.ts` to apply to all tasks.270271## Correct Schedules task (cron) implementations272273```ts274import { schedules } from "@trigger.dev/sdk";275276export const firstScheduledTask = schedules.task({277 id: "first-scheduled-task",278 run: async (payload) => {279 //when the task was scheduled to run280 //note this will be slightly different from new Date() because it takes a few ms to run the task281 console.log(payload.timestamp); //is a Date object282283 //when the task was last run284 //this can be undefined if it's never been run285 console.log(payload.lastTimestamp); //is a Date object or undefined286287 //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/timezones290 console.log(payload.timezone); //is a string291292 //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 });296297 //the schedule id (you can have many schedules for the same task)298 //using this you can remove the schedule, update it, etc299 console.log(payload.scheduleId); //is a string300301 //you can optionally provide an external id when creating the schedule302 //usually you would set this to a userId or some other unique identifier303 //this can be undefined if you didn't provide one304 console.log(payload.externalId); //is a string or undefined305306 //the next 5 dates this task is scheduled to run307 console.log(payload.upcoming); //is an array of Date objects308 },309});310```311312### Attach a Declarative schedule313314```ts315import { schedules } from "@trigger.dev/sdk";316317// 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 something324 },325});326```327328```ts329import { schedules } from "@trigger.dev/sdk";330331// Specify a specific timezone like this:332export const secondScheduledTask = schedules.task({333 id: "second-scheduled-task",334 cron: {335 //5am every day Tokyo time336 pattern: "0 5 * * *",337 timezone: "Asia/Tokyo",338 },339 run: async (payload) => {},340});341```342343### Attach an Imperative schedule344345Create schedules explicitly for tasks using the dashboard's "New schedule" button or the SDK.346347#### Benefits348- Dynamic creation (e.g., one schedule per user)349- Manage without code deployment:350 - Activate/disable351 - Edit352 - Delete353354#### Implementation3551. Define a task using `schedules.task()`3562. Attach one or more schedules via:357 - Dashboard358 - SDK359360#### Attach schedules with the SDK like this361362```ts363const 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```372373## Correct Schema task implementations374375Schema tasks validate payloads against a schema before execution:376377```ts378import { schemaTask } from "@trigger.dev/sdk";379import { z } from "zod";380381const 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 validated389 console.log(payload.name, payload.age);390 },391});392```393394## Correct implementations for triggering a task from your backend395396When 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.397398### tasks.trigger()399400Triggers a single run of a task with specified payload and options without importing the task. Use type-only imports for full type checking.401402```ts403import { tasks } from "@trigger.dev/sdk";404import type { emailSequence } from "~/trigger/emails";405406export 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```415416### tasks.batchTrigger()417418Triggers multiple runs of a single task with different payloads without importing the task.419420```ts421import { tasks } from "@trigger.dev/sdk";422import type { emailSequence } from "~/trigger/emails";423424export 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```433434### batch.trigger()435436Triggers multiple runs of different tasks at once, useful when you need to execute multiple tasks simultaneously.437438```ts439import { batch } from "@trigger.dev/sdk";440import type { myTask1, myTask2 } from "~/trigger/myTasks";441442export 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```451452## Correct implementations for triggering a task from inside another task453454### yourTask.trigger()455456Triggers a single run of a task with specified payload and options.457458```ts459import { myOtherTask, runs } from "~/trigger/my-other-task";460461export const myTask = task({462 id: "my-task",463 run: async (payload: string) => {464 const handle = await myOtherTask.trigger({ foo: "some data" });465466 const run = await runs.retrieve(handle);467 // Do something with the run468 },469});470```471472If 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.473474### yourTask.batchTrigger()475476Triggers multiple runs of a single task with different payloads.477478```ts479import { myOtherTask, batch } from "~/trigger/my-other-task";480481export const myTask = task({482 id: "my-task",483 run: async (payload: string) => {484 const batchHandle = await myOtherTask.batchTrigger([{ payload: "some data" }]);485486 //...do other stuff487 const batch = await batch.retrieve(batchHandle.id);488 },489});490```491492### yourTask.triggerAndWait()493494Triggers a task and waits for the result, useful when you need to call a different task and use its result.495496```ts497export 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);502503 //...do stuff with the result504 },505});506```507508The 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.509510### yourTask.batchTriggerAndWait()511512Batch triggers a task and waits for all results, useful for fan-out patterns.513514```ts515export 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);524525 //...do stuff with the result526 },527});528```529530You can handle run failures by inspecting individual run results and implementing custom error handling strategies. This method should only be used inside a task.531532### batch.triggerAndWait()533534Batch triggers multiple different tasks and waits for all results.535536```ts537export 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 ]);544545 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```560561### batch.triggerByTask()562563Batch triggers multiple tasks by passing task instances, useful for static task sets.564565```ts566export 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 ]);573574 const run1 = await runs.retrieve(results.runs[0]);575 const run2 = await runs.retrieve(results.runs[1]);576 },577});578```579580### batch.triggerByTaskAndWait()581582Batch triggers multiple tasks by passing task instances and waits for all results.583584```ts585export 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 ]);592593 if (runs[0].ok) {594 console.log("Child task 1 output", runs[0].output);595 }596597 if (runs[1].ok) {598 console.log("Child task 2 output", runs[1].output);599 }600 },601});602```603604## Correct Metadata implementation605606### Overview607608Metadata 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.609610### Basic Usage611612Add metadata when triggering a task:613614```ts615const handle = await myTask.trigger(616 { message: "hello world" },617 { metadata: { user: { name: "Eric", id: "user_1234" } } }618);619```620621Access metadata inside a run:622623```ts624import { task, metadata } from "@trigger.dev/sdk";625626export const myTask = task({627 id: "my-task",628 run: async (payload: { message: string }) => {629 // Get the whole metadata object630 const currentMetadata = metadata.current();631632 // Get a specific key633 const user = metadata.get("user");634 console.log(user.name); // "Eric"635 },636});637```638639### Update methods640641Metadata can be updated as the run progresses:642643- **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()`652653Updates can be chained with a fluent API:654655```ts656metadata.set("progress", 0.1)657 .append("logs", "Step 1 complete")658 .increment("progress", 0.4);659```660661### Parent & root updates662663Child tasks can update parent task metadata:664665```ts666export const childTask = task({667 id: "child-task",668 run: async (payload: { message: string }) => {669 // Update parent task's metadata670 metadata.parent.set("progress", 0.5);671672 // Update root task's metadata673 metadata.root.set("status", "processing");674 },675});676```677678### Type safety679680Metadata accepts any JSON-serializable object. For type safety, consider wrapping with Zod:681682```ts683import { z } from "zod";684685const Metadata = z.object({686 user: z.object({687 name: z.string(),688 id: z.string(),689 }),690 date: z.coerce.date(),691});692693function getMetadata() {694 return Metadata.parse(metadata.current());695}696```697698### Important notes699700- Metadata methods only work inside run functions or task lifecycle hooks701- Metadata is NOT automatically propagated to child tasks702- Maximum size is 256KB (configurable if self-hosting)703- Objects like Dates are serialized to strings and must be deserialized when retrieved704705## Correct Realtime implementation706707### Overview708709Trigger.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.710711### Basic usage712713Subscribe to a run after triggering a task:714715```ts716import { runs, tasks } from "@trigger.dev/sdk";717718async function myBackend() {719 const handle = await tasks.trigger("my-task", { some: "data" });720721 for await (const run of runs.subscribeToRun(handle.id)) {722 console.log(run); // Logs the run every time it changes723 }724}725```726727### Subscription methods728729- **subscribeToRun**: Subscribe to changes for a specific run730- **subscribeToRunsWithTag**: Subscribe to changes for all runs with a specific tag731- **subscribeToBatch**: Subscribe to changes for all runs in a batch732733### Type safety734735You can infer types of run's payload and output by passing the task type:736737```ts738import { runs } from "@trigger.dev/sdk";739import type { myTask } from "./trigger/my-task";740741for await (const run of runs.subscribeToRun<typeof myTask>(handle.id)) {742 console.log(run.payload.some); // Type-safe access to payload743744 if (run.output) {745 console.log(run.output.result); // Type-safe access to output746 }747}748```749750### Realtime Streams751752Stream data in realtime from inside your tasks using the metadata system:753754```ts755import { task, metadata } from "@trigger.dev/sdk";756import OpenAI from "openai";757758export type STREAMS = {759 openai: OpenAI.ChatCompletionChunk;760};761762export 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 });770771 // Register the stream with the key "openai"772 const stream = await metadata.stream("openai", completion);773774 let text = "";775 for await (const chunk of stream) {776 text += chunk.choices.map((choice) => choice.delta?.content).join("");777 }778779 return { text };780 },781});782```783784Subscribe to streams using `withStreams`:785786```ts787for 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```800801## Realtime hooks802803### Installation804805```bash806npm add @trigger.dev/react-hooks807```808809### Authentication810811All hooks require a Public Access Token. You can provide it directly to each hook:812813```ts814import { useRealtimeRun } from "@trigger.dev/react-hooks";815816function MyComponent({ runId, publicAccessToken }) {817 const { run, error } = useRealtimeRun(runId, {818 accessToken: publicAccessToken,819 baseURL: "https://your-trigger-dev-instance.com", // Optional for self-hosting820 });821}822```823824Or use the `TriggerAuthContext` provider:825826```ts827import { TriggerAuthContext } from "@trigger.dev/react-hooks";828829function SetupTrigger({ publicAccessToken }) {830 return (831 <TriggerAuthContext.Provider value={{ accessToken: publicAccessToken }}>832 <MyComponent />833 </TriggerAuthContext.Provider>834 );835}836```837838For Next.js App Router, wrap the provider in a client component:839840```ts841// components/TriggerProvider.tsx842"use client";843844import { TriggerAuthContext } from "@trigger.dev/react-hooks";845846export function TriggerProvider({ accessToken, children }) {847 return (848 <TriggerAuthContext.Provider value={{ accessToken }}>849 {children}850 </TriggerAuthContext.Provider>851 );852}853```854855### Passing tokens to the frontend856857Several approaches for Next.js App Router:8588591. **Using cookies**:860```ts861// Server action862export 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}867868// Page component869export 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```8788792. **Using query parameters**:880```ts881// Server action882export async function startRun() {883 const handle = await tasks.trigger<typeof exampleTask>("example", { foo: "bar" });884 redirect(`/runs/${handle.id}?publicAccessToken=${handle.publicAccessToken}`);885}886```8878883. **Server-side token generation**:889```ts890// Page component891export 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}899900// Token generation function901export async function generatePublicAccessToken(runId: string) {902 return auth.createPublicToken({903 scopes: {904 read: {905 runs: [runId],906 },907 },908 expirationTime: "1h",909 });910}911```912913### Hook types914915#### SWR hooks916917Data fetching hooks that use SWR for caching:918919```ts920"use client";921import { useRun } from "@trigger.dev/react-hooks";922import type { myTask } from "@/trigger/myTask";923924function MyComponent({ runId }) {925 const { run, error, isLoading } = useRun<typeof myTask>(runId);926927 if (isLoading) return <div>Loading...</div>;928 if (error) return <div>Error: {error.message}</div>;929930 return <div>Run: {run.id}</div>;931}932```933934Common options:935- `revalidateOnFocus`: Revalidate when window regains focus936- `revalidateOnReconnect`: Revalidate when network reconnects937- `refreshInterval`: Polling interval in milliseconds938939#### Realtime hooks940941Hooks that use Trigger.dev's realtime API for live updates (recommended over polling).942943For most use cases, Realtime hooks are preferred over SWR hooks with polling due to better performance and lower API usage.944945### Authentication946947For client-side usage, generate a public access token with appropriate scopes:948949```ts950import { auth } from "@trigger.dev/sdk";951952const publicToken = await auth.createPublicToken({953 scopes: {954 read: {955 runs: ["run_1234"],956 },957 },958});959```960961## Correct Idempotency implementation962963Idempotency ensures that an operation produces the same result when called multiple times. Trigger.dev supports idempotency at the task level through the `idempotencyKey` option.964965### Using idempotencyKey966967Provide an `idempotencyKey` when triggering a task to ensure it runs only once with that key:968969```ts970import { idempotencyKeys, task } from "@trigger.dev/sdk";971972export 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 run979 const idempotencyKey = await idempotencyKeys.create("my-task-key");980981 // Child task will only be triggered once across all retries982 await childTask.trigger({ foo: "bar" }, { idempotencyKey });983984 // This may throw an error and cause retries985 throw new Error("Something went wrong");986 },987});988```989990### Scoping Idempotency Keys991992By default, keys are scoped to the current run. You can create globally unique keys:993994```ts995const idempotencyKey = await idempotencyKeys.create("my-task-key", { scope: "global" });996```997998When triggering from backend code:9991000```ts1001const idempotencyKey = await idempotencyKeys.create([myUser.id, "my-task"]);1002await tasks.trigger("my-task", { some: "data" }, { idempotencyKey });1003```10041005You can also pass a string directly:10061007```ts1008await myTask.trigger({ some: "data" }, { idempotencyKey: myUser.id });1009```10101011### Time-To-Live (TTL)10121013The `idempotencyKeyTTL` option defines a time window during which duplicate triggers return the original run:10141015```ts1016await childTask.trigger(1017 { foo: "bar" },1018 { idempotencyKey, idempotencyKeyTTL: "60s" }1019);10201021await wait.for({ seconds: 61 });10221023// Key expired, will trigger a new run1024await childTask.trigger({ foo: "bar" }, { idempotencyKey });1025```10261027Supported 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`)10321033### Payload-Based Idempotency10341035While not directly supported, you can implement payload-based idempotency by hashing the payload:10361037```ts1038import { createHash } from "node:crypto";10391040const idempotencyKey = await idempotencyKeys.create(hash(payload));1041await tasks.trigger("child-task", payload, { idempotencyKey });10421043function hash(payload: any): string {1044 const hash = createHash("sha256");1045 hash.update(JSON.stringify(payload));1046 return hash.digest("hex");1047}1048```10491050### Important Notes10511052- Idempotency keys are scoped to the task and environment1053- Different tasks with the same key will still both run1054- Default TTL is 30 days1055- Not available with `triggerAndWait` or `batchTriggerAndWait` in v3.3.0+ due to a bug10561057## Correct Logs implementation10581059```ts1060// onFailure executes after all retries are exhausted; use for notifications, logging, or side effects on final failure:1061import { task, logger } from "@trigger.dev/sdk";10621063export 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```10751076## Correct `trigger.config.ts` implementation10771078The `trigger.config.ts` file configures your Trigger.dev project, specifying task locations, retry settings, telemetry, and build options.10791080```ts1081import { defineConfig } from "@trigger.dev/sdk";10821083export 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```10981099### Key configuration options11001101#### Dirs11021103Specify where your tasks are located:11041105```ts1106dirs: ["./trigger"],1107```11081109Files with `.test` or `.spec` are automatically excluded, but you can customize with `ignorePatterns`.11101111#### Lifecycle functions11121113Add global hooks for all tasks:11141115```ts1116onStart: 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```11261127#### Telemetry instrumentations11281129Add OpenTelemetry instrumentations for enhanced logging:11301131```ts1132telemetry: {1133 instrumentations: [1134 new PrismaInstrumentation(),1135 new OpenAIInstrumentation()1136 ],1137 exporters: [axiomExporter], // Optional custom exporters1138},1139```11401141#### Runtime11421143Specify the runtime environment:11441145```ts1146runtime: "node", // or "bun" (experimental)1147```11481149#### Machine settings11501151Set default machine for all tasks:11521153```ts1154defaultMachine: "large-1x",1155```11561157#### Log level11581159Configure logging verbosity:11601161```ts1162logLevel: "debug", // Controls logger API logs1163```11641165#### Max Duration11661167Set default maximum runtime for all tasks:11681169```ts1170maxDuration: 60, // 60 seconds1171```11721173### Build configuration11741175Customize the build process:11761177```ts1178build: {1179 external: ["header-generator"], // Don't bundle these packages1180 jsx: {1181 fragment: "Fragment",1182 factory: "h",1183 automatic: false,1184 },1185 conditions: ["react-server"], // Import conditions1186 extensions: [1187 // Build extensions1188 additionalFiles({ files: ["./assets/**", "./fonts/**"] }),1189 additionalPackages({ packages: ["wrangler"] }),1190 aptGet({ packages: ["ffmpeg"] }),1191 ],1192}1193```11941195### Build Extensions11961197Trigger.dev provides several built-in extensions:11981199- **additionalFiles**: Copy files to the build directory1200- **additionalPackages**: Include extra packages in the build1201- **aptGet**: Install system packages in the deployed image1202- **emitDecoratorMetadata**: Enable TypeScript decorator metadata1203- **prismaExtension**: Support for Prisma ORM1204- **syncEnvVars**: Sync environment variables1205- **puppeteer**: Add Puppeteer support1206- **ffmpeg**: Add FFmpeg support1207- **esbuildPlugin**: Add custom esbuild plugins12081209You can also create custom build extensions with hooks like `onBuildStart`, `onBuildComplete`, and `externalsForTarget`.12101211#### Key points:12121213- Validates payload at trigger time and before execution1214- Supports Zod, Yup, Superstruct, ArkType, Effect/schema, runtypes, valibot, typebox1215- Can use input/output schemas (e.g., defaults, type coercion)1216- Custom parser functions supported1217- Invalid payloads throw errors and prevent task execution12181219#### Trigger with:12201221```ts1222await myTask.trigger({ name: "Alice", age: 30 });1223```12241225## AI model verification steps12261227Before generating any code, you MUST verify:122812291. 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.12321233## Consequences of incorrect implementations12341235If you generate code that fails the verification steps above, your implementation will:123612371. Break in production12382. Fail to deploy to the Trigger.dev servers12393. Fail to run in a local Dev environment12401241## AI model response template12421243When asked about Trigger.dev task implementation, you MUST:12441. FIRST use code patterns from this guide12452. NEVER suggest deprecated approaches12463. VERIFY your response against the patterns shown here12471248
Also in triggerdotdev/trigger.dev
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| triggerdotdev/trigger.devpackages/cli-v3/CLAUDE.md · 16k | CLAUDE.md | deploymentdo-notdocs | 51/100 | 3 days ago | |
| triggerdotdev/trigger.dev.cursor/rules/executing-commands.mdc · 16k | Cursor rules | test | 49/100 | 3 days ago | |
| triggerdotdev/trigger.dev.cursor/rules/otel-metrics.mdc · 16k | Cursor rules | styledo-not | 61/100 | 3 days ago | |
| triggerdotdev/trigger.dev.cursor/rules/webapp.mdc · 16k | Cursor rules | setuptestsecurity | 55/100 | 3 days ago | |
| triggerdotdev/trigger.dev.github/copilot-instructions.md · 16k | Copilot instructions | teststyletypes | 32/100 | 3 days ago | |
| triggerdotdev/trigger.devAGENTS.md · 16k | AGENTS.md | setupbuildtestlint-format+8 | 88/100 | 3 days ago | |
| triggerdotdev/trigger.devapps/supervisor/CLAUDE.md · 16k | CLAUDE.md | no sections | 25/100 | 3 days ago | |
| triggerdotdev/trigger.devapps/webapp/CLAUDE.md · 16k | CLAUDE.md | setupbuildteststyle+5 | 88/100 | 3 days ago | |
| triggerdotdev/trigger.devinternal-packages/clickhouse/CLAUDE.md · 16k | CLAUDE.md | styletypesdo-not | 61/100 | 3 days ago | |
| triggerdotdev/trigger.devinternal-packages/database/CLAUDE.md · 16k | CLAUDE.md | typesdatabasedo-not | 65/100 | 3 days ago | |
| triggerdotdev/trigger.devinternal-packages/run-engine/CLAUDE.md · 16k | CLAUDE.md | buildteststyle | 70/100 | 3 days ago | |
| triggerdotdev/trigger.devpackages/core/CLAUDE.md · 16k | CLAUDE.md | no sections | 31/100 | 3 days ago | |
| triggerdotdev/trigger.devpackages/redis-worker/CLAUDE.md · 16k | CLAUDE.md | test | 29/100 | 3 days ago | |
| triggerdotdev/trigger.devpackages/trigger-sdk/CLAUDE.md · 16k | CLAUDE.md | do-not | 54/100 | 3 days ago |
Diff against packages/cli-v3/CLAUDE.md Diff against .cursor/rules/executing-commands.mdc Diff against .cursor/rules/otel-metrics.mdc Diff against .cursor/rules/webapp.mdc Diff against .github/copilot-instructions.md Diff against AGENTS.md Diff against apps/supervisor/CLAUDE.md Diff against apps/webapp/CLAUDE.md Diff against internal-packages/clickhouse/CLAUDE.md Diff against internal-packages/database/CLAUDE.md Diff against internal-packages/run-engine/CLAUDE.md Diff against packages/core/CLAUDE.md Diff against packages/redis-worker/CLAUDE.md Diff against packages/trigger-sdk/CLAUDE.md
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| hiromaily/go-crypto-wallet.cursor/rules/typescript.mdc · 126 | Cursor rules | setupbuildtestlint-format+6 | 100/100 | 3 days ago | |
| TechSquidTV/Hermes.cursor/rules/10-hermes-api.mdc · 45 | Cursor rules | testlint-formatstylearch+5 | 100/100 | 3 days ago | |
| dodgecfr/combatfilms-webapp.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| deifos/clipmira-subtitles.cursor/rules/frontend.mdc · 1 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| markstev/mark-starter.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+6 | 99/100 | 3 days ago | |
| Allymahmoud/case-intake-platform.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| langflow-ai/langflow.cursor/rules/docs_development.mdc · 153k | Cursor rules | setupbuildtestlint-format+7 | 97/100 | 3 days ago | |
| TechSquidTV/Hermes.cursor/rules/20-hermes-api-tests.mdc · 45 | Cursor rules | teststyletesting-strategysecurity+3 | 97/100 | 3 days ago |
