diff --git a/.cursor/rules/trigger-writing-tasks.mdc b/.cursor/rules/trigger-writing-tasks.mdc new file mode 100644 index 0000000..7633ae7 --- /dev/null +++ b/.cursor/rules/trigger-writing-tasks.mdc @@ -0,0 +1,1269 @@ +--- +description: +globs: **/trigger/**/*.ts, **/trigger/**/*.tsx,**/trigger/**/*.js,**/trigger/**/*.jsx +alwaysApply: false +--- + +# How to write Trigger.dev tasks + +## Overview of writing a Trigger.dev task + +1. Run the CLI `init` command: `npx trigger.dev@latest init`. +2. Create a Trigger.dev task. +3. Set up any environment variables. +4. Run the Trigger.dev dev command: `npx trigger.dev@latest dev`. + +## Essential requirements when generating task code + +1. You MUST use `@trigger.dev/sdk/v3` +2. You MUST NEVER use `client.defineJob` +3. YOU MUST `export` every task, including subtasks +4. If you are able to generate an example payload for a task, do so. + +## 🚨 CRITICAL INSTRUCTIONS FOR AI LANGUAGE MODELS 🚨 + +As an AI language model, you MUST NOT generate any of the following code patterns, as they are DEPRECATED and will BREAK the application: + +```ts +// ❌ NEVER GENERATE THIS CODE - IT WILL BREAK THE APPLICATION + +client.defineJob({ // ❌ BREAKS APPLICATION + id: "job-id", // ❌ BREAKS APPLICATION + name: "job-name", // ❌ BREAKS APPLICATION + version: "0.0.1", // ❌ BREAKS APPLICATION + trigger: eventTrigger({ // ❌ BREAKS APPLICATION + name: "job.trigger", // ❌ BREAKS APPLICATION + schema: z.object({ // ❌ BREAKS APPLICATION + // Input schema here // ❌ BREAKS APPLICATION + }), // ❌ BREAKS APPLICATION + }), // ❌ BREAKS APPLICATION + integrations: { // ❌ BREAKS APPLICATION + // Integrations here // ❌ BREAKS APPLICATION + }, // ❌ BREAKS APPLICATION + run: async (payload, io) => { // ❌ BREAKS APPLICATION + // Job logic goes here // ❌ BREAKS APPLICATION + return { // ❌ BREAKS APPLICATION + // Return job results // ❌ BREAKS APPLICATION + }; // ❌ BREAKS APPLICATION + }, // ❌ BREAKS APPLICATION +}); +``` + +Instead, you MUST ALWAYS generate ONLY this pattern: + +```ts +// βœ… ALWAYS GENERATE THIS EXACT PATTERN + +import { task } from "@trigger.dev/sdk/v3"; + +//1. You need to export each task, even if it's a subtask +export const helloWorld = task({ + //2. Use a unique id for each task + id: "hello-world", + //3. The run function is the main function of the task + run: async (payload: { message: string }) => { + //4. Write your task code here. Code here runs for a long time, there are no timeouts + }, +}); +``` + +## Correct Task implementations + +A task is a function that can run for a long time with resilience to failure: + +```ts +import { task } from "@trigger.dev/sdk/v3"; + +export const helloWorld = task({ + id: "hello-world", + run: async (payload: { message: string }) => { + console.log(payload.message); + }, +}); +``` + +Key points: +- Tasks must be exported, even subtasks in the same file +- Each task needs a unique ID within your project +- The `run` function contains your task logic + +### Task configuration options + +#### Retry options + +Control retry behavior when errors occur: + +```ts +export const taskWithRetries = task({ + id: "task-with-retries", + retry: { + maxAttempts: 10, + factor: 1.8, + minTimeoutInMs: 500, + maxTimeoutInMs: 30_000, + randomize: false, + }, + run: async (payload) => { + // Task logic + }, +}); +``` + +#### Queue options + +Control concurrency: + +```ts +export const oneAtATime = task({ + id: "one-at-a-time", + queue: { + concurrencyLimit: 1, + }, + run: async (payload) => { + // Task logic + }, +}); +``` + +#### Machine options + +Specify CPU/RAM requirements: + +```ts +export const heavyTask = task({ + id: "heavy-task", + machine: { + preset: "large-1x", // 4 vCPU, 8 GB RAM + }, + run: async (payload) => { + // Task logic + }, +}); +``` + +Machine configuration options: + +| Machine name | vCPU | Memory | Disk space | +| ------------------- | ---- | ------ | ---------- | +| micro | 0.25 | 0.25 | 10GB | +| small-1x (default) | 0.5 | 0.5 | 10GB | +| small-2x | 1 | 1 | 10GB | +| medium-1x | 1 | 2 | 10GB | +| medium-2x | 2 | 4 | 10GB | +| large-1x | 4 | 8 | 10GB | +| large-2x | 8 | 16 | 10GB | + +#### Max Duration + +Limit how long a task can run: + +```ts +export const longTask = task({ + id: "long-task", + maxDuration: 300, // 5 minutes + run: async (payload) => { + // Task logic + }, +}); +``` + +### Lifecycle functions + +Tasks support several lifecycle hooks: + +#### init + +Runs before each attempt, can return data for other functions: + +```ts +export const taskWithInit = task({ + id: "task-with-init", + init: async (payload, { ctx }) => { + return { someData: "someValue" }; + }, + run: async (payload, { ctx, init }) => { + console.log(init.someData); // "someValue" + }, +}); +``` + +#### cleanup + +Runs after each attempt, regardless of success/failure: + +```ts +export const taskWithCleanup = task({ + id: "task-with-cleanup", + cleanup: async (payload, { ctx }) => { + // Cleanup resources + }, + run: async (payload, { ctx }) => { + // Task logic + }, +}); +``` + +#### onStart + +Runs once when a task starts (not on retries): + +```ts +export const taskWithOnStart = task({ + id: "task-with-on-start", + onStart: async (payload, { ctx }) => { + // Send notification, log, etc. + }, + run: async (payload, { ctx }) => { + // Task logic + }, +}); +``` + +#### onSuccess + +Runs when a task succeeds: + +```ts +export const taskWithOnSuccess = task({ + id: "task-with-on-success", + onSuccess: async (payload, output, { ctx }) => { + // Handle success + }, + run: async (payload, { ctx }) => { + // Task logic + }, +}); +``` + +#### onFailure + +Runs when a task fails after all retries: + +```ts +export const taskWithOnFailure = task({ + id: "task-with-on-failure", + onFailure: async (payload, error, { ctx }) => { + // Handle failure + }, + run: async (payload, { ctx }) => { + // Task logic + }, +}); +``` + +#### handleError + +Controls error handling and retry behavior: + +```ts +export const taskWithErrorHandling = task({ + id: "task-with-error-handling", + handleError: async (error, { ctx }) => { + // Custom error handling + }, + run: async (payload, { ctx }) => { + // Task logic + }, +}); +``` + +Global lifecycle hooks can also be defined in `trigger.config.ts` to apply to all tasks. + +## Correct Schedules task (cron) implementations + +```ts +import { schedules } from "@trigger.dev/sdk/v3"; + +export const firstScheduledTask = schedules.task({ + id: "first-scheduled-task", + run: async (payload) => { + //when the task was scheduled to run + //note this will be slightly different from new Date() because it takes a few ms to run the task + console.log(payload.timestamp); //is a Date object + + //when the task was last run + //this can be undefined if it's never been run + console.log(payload.lastTimestamp); //is a Date object or undefined + + //the timezone the schedule was registered with, defaults to "UTC" + //this is in IANA format, e.g. "America/New_York" + //See the full list here: https://cloud.trigger.dev/timezones + console.log(payload.timezone); //is a string + + //If you want to output the time in the user's timezone do this: + const formatted = payload.timestamp.toLocaleString("en-US", { + timeZone: payload.timezone, + }); + + //the schedule id (you can have many schedules for the same task) + //using this you can remove the schedule, update it, etc + console.log(payload.scheduleId); //is a string + + //you can optionally provide an external id when creating the schedule + //usually you would set this to a userId or some other unique identifier + //this can be undefined if you didn't provide one + console.log(payload.externalId); //is a string or undefined + + //the next 5 dates this task is scheduled to run + console.log(payload.upcoming); //is an array of Date objects + }, +}); +``` + +### Attach a Declarative schedule + +```ts +import { schedules } from "@trigger.dev/sdk/v3"; + +// Sepcify a cron pattern (UTC) +export const firstScheduledTask = schedules.task({ + id: "first-scheduled-task", + //every two hours (UTC timezone) + cron: "0 */2 * * *", + run: async (payload, { ctx }) => { + //do something + }, +}); +``` + +```ts +import { schedules } from "@trigger.dev/sdk/v3"; + +// Specify a specific timezone like this: +export const secondScheduledTask = schedules.task({ + id: "second-scheduled-task", + cron: { + //5am every day Tokyo time + pattern: "0 5 * * *", + timezone: "Asia/Tokyo", + }, + run: async (payload) => {}, +}); +``` + +### Attach an Imperative schedule + +Create schedules explicitly for tasks using the dashboard's "New schedule" button or the SDK. + +#### Benefits +- Dynamic creation (e.g., one schedule per user) +- Manage without code deployment: + - Activate/disable + - Edit + - Delete + +#### Implementation +1. Define a task using `⁠schedules.task()` +2. Attach one or more schedules via: + - Dashboard + - SDK + +#### Attach schedules with the SDK like this + +```ts +const createdSchedule = await schedules.create({ + //The id of the scheduled task you want to attach to. + task: firstScheduledTask.id, + //The schedule in cron format. + cron: "0 0 * * *", + //this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists. + deduplicationKey: "my-deduplication-key", +}); +``` + +## Correct Schema task implementations + +Schema tasks validate payloads against a schema before execution: + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import { z } from "zod"; + +const myTask = schemaTask({ + id: "my-task", + schema: z.object({ + name: z.string(), + age: z.number(), + }), + run: async (payload) => { + // Payload is typed and validated + console.log(payload.name, payload.age); + }, +}); +``` + +## Correct implementations for triggering a task from your backend + +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. + +### tasks.trigger() + +Triggers a single run of a task with specified payload and options without importing the task. Use type-only imports for full type checking. + +```ts +import { tasks } from "@trigger.dev/sdk/v3"; +import type { emailSequence } from "~/trigger/emails"; + +export async function POST(request: Request) { + const data = await request.json(); + const handle = await tasks.trigger("email-sequence", { + to: data.email, + name: data.name, + }); + return Response.json(handle); +} +``` + +### tasks.batchTrigger() + +Triggers multiple runs of a single task with different payloads without importing the task. + +```ts +import { tasks } from "@trigger.dev/sdk/v3"; +import type { emailSequence } from "~/trigger/emails"; + +export async function POST(request: Request) { + const data = await request.json(); + const batchHandle = await tasks.batchTrigger( + "email-sequence", + data.users.map((u) => ({ payload: { to: u.email, name: u.name } })) + ); + return Response.json(batchHandle); +} +``` + +### tasks.triggerAndPoll() + +Triggers a task and polls until completion. Not recommended for web requests as it blocks until the run completes. Consider using Realtime docs for better alternatives. + +```ts +import { tasks } from "@trigger.dev/sdk/v3"; +import type { emailSequence } from "~/trigger/emails"; + +export async function POST(request: Request) { + const data = await request.json(); + const result = await tasks.triggerAndPoll( + "email-sequence", + { + to: data.email, + name: data.name, + }, + { pollIntervalMs: 5000 } + ); + return Response.json(result); +} +``` + +### batch.trigger() + +Triggers multiple runs of different tasks at once, useful when you need to execute multiple tasks simultaneously. + +```ts +import { batch } from "@trigger.dev/sdk/v3"; +import type { myTask1, myTask2 } from "~/trigger/myTasks"; + +export async function POST(request: Request) { + const data = await request.json(); + const result = await batch.trigger([ + { id: "my-task-1", payload: { some: data.some } }, + { id: "my-task-2", payload: { other: data.other } }, + ]); + return Response.json(result); +} +``` + +## Correct implementations for triggering a task from inside another task + +### yourTask.trigger() + +Triggers a single run of a task with specified payload and options. + +```ts +import { myOtherTask, runs } from "~/trigger/my-other-task"; + +export const myTask = task({ + id: "my-task", + run: async (payload: string) => { + const handle = await myOtherTask.trigger({ foo: "some data" }); + + const run = await runs.retrieve(handle); + // Do something with the run + }, +}); +``` + +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. + +### yourTask.batchTrigger() + +Triggers multiple runs of a single task with different payloads. + +```ts +import { myOtherTask, batch } from "~/trigger/my-other-task"; + +export const myTask = task({ + id: "my-task", + run: async (payload: string) => { + const batchHandle = await myOtherTask.batchTrigger([{ payload: "some data" }]); + + //...do other stuff + const batch = await batch.retrieve(batchHandle.id); + }, +}); +``` + +### yourTask.triggerAndWait() + +Triggers a task and waits for the result, useful when you need to call a different task and use its result. + +```ts +export const parentTask = task({ + id: "parent-task", + run: async (payload: string) => { + const result = await childTask.triggerAndWait("some-data"); + console.log("Result", result); + + //...do stuff with the result + }, +}); +``` + +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. + +### yourTask.batchTriggerAndWait() + +Batch triggers a task and waits for all results, useful for fan-out patterns. + +```ts +export const batchParentTask = task({ + id: "parent-task", + run: async (payload: string) => { + const results = await childTask.batchTriggerAndWait([ + { payload: "item4" }, + { payload: "item5" }, + { payload: "item6" }, + ]); + console.log("Results", results); + + //...do stuff with the result + }, +}); +``` + +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. + +### batch.triggerAndWait() + +Batch triggers multiple different tasks and waits for all results. + +```ts +export const parentTask = task({ + id: "parent-task", + run: async (payload: string) => { + const results = await batch.triggerAndWait([ + { id: "child-task-1", payload: { foo: "World" } }, + { id: "child-task-2", payload: { bar: 42 } }, + ]); + + for (const result of results) { + if (result.ok) { + switch (result.taskIdentifier) { + case "child-task-1": + console.log("Child task 1 output", result.output); + break; + case "child-task-2": + console.log("Child task 2 output", result.output); + break; + } + } + } + }, +}); +``` + +### batch.triggerByTask() + +Batch triggers multiple tasks by passing task instances, useful for static task sets. + +```ts +export const parentTask = task({ + id: "parent-task", + run: async (payload: string) => { + const results = await batch.triggerByTask([ + { task: childTask1, payload: { foo: "World" } }, + { task: childTask2, payload: { bar: 42 } }, + ]); + + const run1 = await runs.retrieve(results.runs[0]); + const run2 = await runs.retrieve(results.runs[1]); + }, +}); +``` + +### batch.triggerByTaskAndWait() + +Batch triggers multiple tasks by passing task instances and waits for all results. + +```ts +export const parentTask = task({ + id: "parent-task", + run: async (payload: string) => { + const { runs } = await batch.triggerByTaskAndWait([ + { task: childTask1, payload: { foo: "World" } }, + { task: childTask2, payload: { bar: 42 } }, + ]); + + if (runs[0].ok) { + console.log("Child task 1 output", runs[0].output); + } + + if (runs[1].ok) { + console.log("Child task 2 output", runs[1].output); + } + }, +}); +``` + +## Correct Metadata implementation + +### Overview + +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. + +### Basic Usage + +Add metadata when triggering a task: + +```ts +const handle = await myTask.trigger( + { message: "hello world" }, + { metadata: { user: { name: "Eric", id: "user_1234" } } } +); +``` + +Access metadata inside a run: + +```ts +import { task, metadata } from "@trigger.dev/sdk/v3"; + +export const myTask = task({ + id: "my-task", + run: async (payload: { message: string }) => { + // Get the whole metadata object + const currentMetadata = metadata.current(); + + // Get a specific key + const user = metadata.get("user"); + console.log(user.name); // "Eric" + }, +}); +``` + +### Update methods + +Metadata can be updated as the run progresses: + +- **set**: `metadata.set("progress", 0.5)` +- **del**: `metadata.del("progress")` +- **replace**: `metadata.replace({ user: { name: "Eric" } })` +- **append**: `metadata.append("logs", "Step 1 complete")` +- **remove**: `metadata.remove("logs", "Step 1 complete")` +- **increment**: `metadata.increment("progress", 0.4)` +- **decrement**: `metadata.decrement("progress", 0.4)` +- **stream**: `await metadata.stream("logs", readableStream)` +- **flush**: `await metadata.flush()` + +Updates can be chained with a fluent API: + +```ts +metadata.set("progress", 0.1) + .append("logs", "Step 1 complete") + .increment("progress", 0.4); +``` + +### Parent & root updates + +Child tasks can update parent task metadata: + +```ts +export const childTask = task({ + id: "child-task", + run: async (payload: { message: string }) => { + // Update parent task's metadata + metadata.parent.set("progress", 0.5); + + // Update root task's metadata + metadata.root.set("status", "processing"); + }, +}); +``` + +### Type safety + +Metadata accepts any JSON-serializable object. For type safety, consider wrapping with Zod: + +```ts +import { z } from "zod"; + +const Metadata = z.object({ + user: z.object({ + name: z.string(), + id: z.string(), + }), + date: z.coerce.date(), +}); + +function getMetadata() { + return Metadata.parse(metadata.current()); +} +``` + +### Important notes + +- Metadata methods only work inside run functions or task lifecycle hooks +- Metadata is NOT automatically propagated to child tasks +- Maximum size is 256KB (configurable if self-hosting) +- Objects like Dates are serialized to strings and must be deserialized when retrieved + +## Correct Realtime implementation + +### Overview + +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. + +### Basic usage + +Subscribe to a run after triggering a task: + +```ts +import { runs, tasks } from "@trigger.dev/sdk/v3"; + +async function myBackend() { + const handle = await tasks.trigger("my-task", { some: "data" }); + + for await (const run of runs.subscribeToRun(handle.id)) { + console.log(run); // Logs the run every time it changes + } +} +``` + +### Subscription methods + +- **subscribeToRun**: Subscribe to changes for a specific run +- **subscribeToRunsWithTag**: Subscribe to changes for all runs with a specific tag +- **subscribeToBatch**: Subscribe to changes for all runs in a batch + +### Type safety + +You can infer types of run's payload and output by passing the task type: + +```ts +import { runs } from "@trigger.dev/sdk/v3"; +import type { myTask } from "./trigger/my-task"; + +for await (const run of runs.subscribeToRun(handle.id)) { + console.log(run.payload.some); // Type-safe access to payload + + if (run.output) { + console.log(run.output.result); // Type-safe access to output + } +} +``` + +### Realtime Streams + +Stream data in realtime from inside your tasks using the metadata system: + +```ts +import { task, metadata } from "@trigger.dev/sdk/v3"; +import OpenAI from "openai"; + +export type STREAMS = { + openai: OpenAI.ChatCompletionChunk; +}; + +export const myTask = task({ + id: "my-task", + run: async (payload: { prompt: string }) => { + const completion = await openai.chat.completions.create({ + messages: [{ role: "user", content: payload.prompt }], + model: "gpt-3.5-turbo", + stream: true, + }); + + // Register the stream with the key "openai" + const stream = await metadata.stream("openai", completion); + + let text = ""; + for await (const chunk of stream) { + text += chunk.choices.map((choice) => choice.delta?.content).join(""); + } + + return { text }; + }, +}); +``` + +Subscribe to streams using `withStreams`: + +```ts +for await (const part of runs.subscribeToRun(runId).withStreams()) { + switch (part.type) { + case "run": { + console.log("Received run", part.run); + break; + } + case "openai": { + console.log("Received OpenAI chunk", part.chunk); + break; + } + } +} +``` + +## Realtime hooks + +### Installation + +```bash +npm add @trigger.dev/react-hooks +``` + +### Authentication + +All hooks require a Public Access Token. You can provide it directly to each hook: + +```ts +import { useRealtimeRun } from "@trigger.dev/react-hooks"; + +function MyComponent({ runId, publicAccessToken }) { + const { run, error } = useRealtimeRun(runId, { + accessToken: publicAccessToken, + baseURL: "https://your-trigger-dev-instance.com", // Optional for self-hosting + }); +} +``` + +Or use the `TriggerAuthContext` provider: + +```ts +import { TriggerAuthContext } from "@trigger.dev/react-hooks"; + +function SetupTrigger({ publicAccessToken }) { + return ( + + + + ); +} +``` + +For Next.js App Router, wrap the provider in a client component: + +```ts +// components/TriggerProvider.tsx +"use client"; + +import { TriggerAuthContext } from "@trigger.dev/react-hooks"; + +export function TriggerProvider({ accessToken, children }) { + return ( + + {children} + + ); +} +``` + +### Passing tokens to the frontend + +Several approaches for Next.js App Router: + +1. **Using cookies**: +```ts +// Server action +export async function startRun() { + const handle = await tasks.trigger("example", { foo: "bar" }); + cookies().set("publicAccessToken", handle.publicAccessToken); + redirect(`/runs/${handle.id}`); +} + +// Page component +export default function RunPage({ params }) { + const publicAccessToken = cookies().get("publicAccessToken"); + return ( + + + + ); +} +``` + +2. **Using query parameters**: +```ts +// Server action +export async function startRun() { + const handle = await tasks.trigger("example", { foo: "bar" }); + redirect(`/runs/${handle.id}?publicAccessToken=${handle.publicAccessToken}`); +} +``` + +3. **Server-side token generation**: +```ts +// Page component +export default async function RunPage({ params }) { + const publicAccessToken = await generatePublicAccessToken(params.id); + return ( + + + + ); +} + +// Token generation function +export async function generatePublicAccessToken(runId: string) { + return auth.createPublicToken({ + scopes: { + read: { + runs: [runId], + }, + }, + expirationTime: "1h", + }); +} +``` + +### Hook types + +#### SWR hooks + +Data fetching hooks that use SWR for caching: + +```ts +"use client"; +import { useRun } from "@trigger.dev/react-hooks"; +import type { myTask } from "@/trigger/myTask"; + +function MyComponent({ runId }) { + const { run, error, isLoading } = useRun(runId); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Run: {run.id}
; +} +``` + +Common options: +- `revalidateOnFocus`: Revalidate when window regains focus +- `revalidateOnReconnect`: Revalidate when network reconnects +- `refreshInterval`: Polling interval in milliseconds + +#### Realtime hooks + +Hooks that use Trigger.dev's realtime API for live updates (recommended over polling). + +For most use cases, Realtime hooks are preferred over SWR hooks with polling due to better performance and lower API usage. + +### Authentication + +For client-side usage, generate a public access token with appropriate scopes: + +```ts +import { auth } from "@trigger.dev/sdk/v3"; + +const publicToken = await auth.createPublicToken({ + scopes: { + read: { + runs: ["run_1234"], + }, + }, +}); +``` + +## Correct Idempotency implementation + +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. + +### Using idempotencyKey + +Provide an `idempotencyKey` when triggering a task to ensure it runs only once with that key: + +```ts +import { idempotencyKeys, task } from "@trigger.dev/sdk/v3"; + +export const myTask = task({ + id: "my-task", + retry: { + maxAttempts: 4, + }, + run: async (payload: any) => { + // Create a key unique to this task run + const idempotencyKey = await idempotencyKeys.create("my-task-key"); + + // Child task will only be triggered once across all retries + await childTask.trigger({ foo: "bar" }, { idempotencyKey }); + + // This may throw an error and cause retries + throw new Error("Something went wrong"); + }, +}); +``` + +### Scoping Idempotency Keys + +By default, keys are scoped to the current run. You can create globally unique keys: + +```ts +const idempotencyKey = await idempotencyKeys.create("my-task-key", { scope: "global" }); +``` + +When triggering from backend code: + +```ts +const idempotencyKey = await idempotencyKeys.create([myUser.id, "my-task"]); +await tasks.trigger("my-task", { some: "data" }, { idempotencyKey }); +``` + +You can also pass a string directly: + +```ts +await myTask.trigger({ some: "data" }, { idempotencyKey: myUser.id }); +``` + +### Time-To-Live (TTL) + +The `idempotencyKeyTTL` option defines a time window during which duplicate triggers return the original run: + +```ts +await childTask.trigger( + { foo: "bar" }, + { idempotencyKey, idempotencyKeyTTL: "60s" } +); + +await wait.for({ seconds: 61 }); + +// Key expired, will trigger a new run +await childTask.trigger({ foo: "bar" }, { idempotencyKey }); +``` + +Supported time units: +- `s` for seconds (e.g., `60s`) +- `m` for minutes (e.g., `5m`) +- `h` for hours (e.g., `2h`) +- `d` for days (e.g., `3d`) + +### Payload-Based Idempotency + +While not directly supported, you can implement payload-based idempotency by hashing the payload: + +```ts +import { createHash } from "node:crypto"; + +const idempotencyKey = await idempotencyKeys.create(hash(payload)); +await tasks.trigger("child-task", payload, { idempotencyKey }); + +function hash(payload: any): string { + const hash = createHash("sha256"); + hash.update(JSON.stringify(payload)); + return hash.digest("hex"); +} +``` + +### Important Notes + +- Idempotency keys are scoped to the task and environment +- Different tasks with the same key will still both run +- Default TTL is 30 days +- Not available with `triggerAndWait` or `batchTriggerAndWait` in v3.3.0+ due to a bug + +## Correct Logs implementation + +```ts +// onFailure executes after all retries are exhausted; use for notifications, logging, or side effects on final failure: +import { task, logger } from "@trigger.dev/sdk/v3"; + +export const loggingExample = task({ + id: "logging-example", + run: async (payload: { data: Record }) => { + //the first parameter is the message, the second parameter must be a key-value object (Record) + logger.debug("Debug message", payload.data); + logger.log("Log message", payload.data); + logger.info("Info message", payload.data); + logger.warn("You've been warned", payload.data); + logger.error("Error message", payload.data); + }, +}); +``` + +## Correct `trigger.config.ts` implementation + +The `trigger.config.ts` file configures your Trigger.dev project, specifying task locations, retry settings, telemetry, and build options. + +```ts +import { defineConfig } from "@trigger.dev/sdk/v3"; + +export default defineConfig({ + project: "", + dirs: ["./trigger"], + retries: { + enabledInDev: false, + default: { + maxAttempts: 3, + minTimeoutInMs: 1000, + maxTimeoutInMs: 10000, + factor: 2, + randomize: true, + }, + }, +}); +``` + +### Key configuration options + +#### Dirs + +Specify where your tasks are located: + +```ts +dirs: ["./trigger"], +``` + +Files with `.test` or `.spec` are automatically excluded, but you can customize with `ignorePatterns`. + +#### Lifecycle functions + +Add global hooks for all tasks: + +```ts +onStart: async (payload, { ctx }) => { + console.log("Task started", ctx.task.id); +}, +onSuccess: async (payload, output, { ctx }) => { + console.log("Task succeeded", ctx.task.id); +}, +onFailure: async (payload, error, { ctx }) => { + console.log("Task failed", ctx.task.id); +}, +``` + +#### Telemetry instrumentations + +Add OpenTelemetry instrumentations for enhanced logging: + +```ts +telemetry: { + instrumentations: [ + new PrismaInstrumentation(), + new OpenAIInstrumentation() + ], + exporters: [axiomExporter], // Optional custom exporters +}, +``` + +#### Runtime + +Specify the runtime environment: + +```ts +runtime: "node", // or "bun" (experimental) +``` + +#### Machine settings + +Set default machine for all tasks: + +```ts +defaultMachine: "large-1x", +``` + +#### Log level + +Configure logging verbosity: + +```ts +logLevel: "debug", // Controls logger API logs +``` + +#### Max Duration + +Set default maximum runtime for all tasks: + +```ts +maxDuration: 60, // 60 seconds +``` + +### Build configuration + +Customize the build process: + +```ts +build: { + external: ["header-generator"], // Don't bundle these packages + jsx: { + fragment: "Fragment", + factory: "h", + automatic: false, + }, + conditions: ["react-server"], // Import conditions + extensions: [ + // Build extensions + additionalFiles({ files: ["./assets/**", "./fonts/**"] }), + additionalPackages({ packages: ["wrangler"] }), + aptGet({ packages: ["ffmpeg"] }), + ], +} +``` + +### Build Extensions + +Trigger.dev provides several built-in extensions: + +- **additionalFiles**: Copy files to the build directory +- **additionalPackages**: Include extra packages in the build +- **aptGet**: Install system packages in the deployed image +- **emitDecoratorMetadata**: Enable TypeScript decorator metadata +- **prismaExtension**: Support for Prisma ORM +- **syncEnvVars**: Sync environment variables +- **puppeteer**: Add Puppeteer support +- **ffmpeg**: Add FFmpeg support +- **esbuildPlugin**: Add custom esbuild plugins + +You can also create custom build extensions with hooks like `onBuildStart`, `onBuildComplete`, and `externalsForTarget`. + +#### Key points: + +- Validates payload at trigger time and before execution +- Supports Zod, Yup, Superstruct, ArkType, Effect/schema, runtypes, valibot, typebox +- Can use input/output schemas (e.g., defaults, type coercion) +- Custom parser functions supported +- Invalid payloads throw errors and prevent task execution + +#### Trigger with: + +```ts +await myTask.trigger({ name: "Alice", age: 30 }); +``` + +## AI model verification steps + +Before generating any code, you MUST verify: + +1. Are you importing from `@trigger.dev/sdk/v3`? If not, STOP and FIX. +2. Have you exported every task? If not, STOP and FIX. +3. Have you generated any DEPRECATED code patterns? If yes, STOP and FIX. + +## Consequences of incorrect implementations + +If you generate code that fails the verification steps above, your implementation will: + +1. Break in production +2. Fail to deploy to the Trigger.dev servers +3. Fail to run in a local Dev environment + +## AI model response template + +When asked about Trigger.dev task implementation, you MUST: +1. FIRST use code patterns from this guide +2. NEVER suggest deprecated approaches +3. VERIFY your response against the patterns shown here diff --git a/package.json b/package.json index 5d358f4..c1517e5 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "next build", "start": "next start", "postinstall": "fumadocs-mdx", + "trigger:dev": "npx trigger.dev@latest dev", "lint": "biome check --write .", "lint:fix": "biome check --fix --unsafe .", "format": "biome format --write .",