Most real-world AI workflows aren't a single background task — they're pipelines. You embed a document, store the vectors, notify a webhook, then send a summary email. Each step can fail independently. Trigger.dev's task API is built exactly for this: durable runs that survive restarts, automatic retries on failure, and built-in wait primitives that don't consume a worker while a run is paused.
This article covers three practical patterns: chaining tasks, implementing delays, and configuring retry logic — illustrated with an AI document-processing pipeline. Every example targets Trigger.dev v4.
Pattern 1: Chaining Tasks
Rather than building one monolithic task, break a workflow into independent tasks that trigger one another. Each task stays small, testable, and independently retryable. A parent task calls a child with childTask.trigger(payload) to hand off fire-and-forget, or triggerAndWait(payload) when it needs the child's result before continuing.
// trigger/process-document.ts import { task, logger } from "@trigger.dev/sdk"; import { embedDocument } from "./embed-document"; // Task 1: Parse and chunk the uploaded document export const processDocument = task({ id: "process-document", run: async (payload: { documentId: string; url: string }) => { const text = await fetchAndParseDocument(payload.url); const chunks = chunkText(text, { size: 512, overlap: 50 }); logger.info(`Parsed ${chunks.length} chunks`); // Trigger the next task in the pipeline (fire-and-forget) await embedDocument.trigger({ documentId: payload.documentId, chunks, }); return { chunksCount: chunks.length }; }, }); // trigger/embed-document.ts import { task } from "@trigger.dev/sdk"; import OpenAI from "openai"; import { notifyComplete } from "./notify-complete"; // Task 2: Embed and store vectors export const embedDocument = task({ id: "embed-document", run: async (payload: { documentId: string; chunks: string[] }) => { const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const response = await openai.embeddings.create({ model: "text-embedding-3-small", input: payload.chunks, }); const embeddings = response.data.map((d) => d.embedding); await supabase.from("document_chunks").insert( payload.chunks.map((chunk, i) => ({ document_id: payload.documentId, content: chunk, embedding: embeddings[i], })) ); // Chain to the notification task await notifyComplete.trigger({ documentId: payload.documentId }); }, });
Pattern 2: Built-in Delays
wait.for() pauses a run for a specified duration without holding a worker. Trigger.dev checkpoints the run's state and resumes it when the timer fires — the same durable-execution model as Temporal. You are not billed for the compute you're not using while a run waits.
// trigger/onboarding-sequence.ts import { task, wait } from "@trigger.dev/sdk"; export const onboardingSequence = task({ id: "onboarding-sequence", run: async (payload: { userId: string; email: string }) => { // Day 0: Welcome email (immediate) await sendEmail(payload.email, "welcome"); // Day 3: Tips email (wait 3 days) await wait.for({ days: 3 }); await sendEmail(payload.email, "tips"); // Day 7: Check-in email (wait 4 more days) await wait.for({ days: 4 }); await sendEmail(payload.email, "checkin"); }, });
wait.for() needs no unique key in v4. The run is automatically checkpointed at the wait point and resumed when the timer fires, and it doesn't hold a worker while suspended — so idempotency keys are no longer something you manage by hand for delays.Pattern 3: Retry Configuration
Tasks retry automatically when the run function throws. Configure the retry policy per task in its definition, or set a default for every task in trigger.config.ts. v4 uses maxAttempts (the total number of attempts, including the first).
// trigger/call-external-api.ts import { task } from "@trigger.dev/sdk"; export const callExternalApi = task({ id: "call-external-api", retry: { maxAttempts: 5, // total attempts, including the first factor: 2, // exponential backoff multiplier minTimeoutInMs: 1000, // 1s initial delay maxTimeoutInMs: 30000, // 30s max delay randomize: true, // add jitter to avoid a thundering herd }, run: async (payload: { endpoint: string }) => { const response = await fetch(payload.endpoint); // Throwing triggers a retry using the policy above if (!response.ok) throw new Error(`API error: ${response.status}`); return response.json(); }, });
Combining All Three Patterns: Document Pipeline
// trigger/document-pipeline.ts import { task, wait } from "@trigger.dev/sdk"; import { parseDocument } from "./parse-document"; import { embedChunks } from "./embed-chunks"; import { storeVectors } from "./store-vectors"; export const documentPipeline = task({ id: "document-pipeline", run: async (payload: { documentId: string; userId: string; url: string; }) => { // Step 1: Parse — retry policy lives on the parseDocument task. // triggerAndWait runs the child and waits for its result; // .unwrap() returns the output or throws on failure. const { chunks } = await parseDocument .triggerAndWait({ url: payload.url }) .unwrap(); // Step 2: Embed (embedChunks sets maxAttempts: 5 for rate limits) const { embeddings } = await embedChunks .triggerAndWait({ chunks }) .unwrap(); // Step 3: Store await storeVectors .triggerAndWait({ documentId: payload.documentId, embeddings }) .unwrap(); // Step 4: Wait 1 hour, then notify (good for async review flows) await wait.for({ hours: 1 }); // Step 5: Send completion notification await notifyUser(payload.userId, "Your document is ready for search."); }, });
Monitoring and Replaying
Every task appears in the Trigger.dev dashboard with its status, duration, input, and output. If a run fails mid-pipeline — say the embed step throws permanently after its retries — you can see exactly which task failed, fix the bug, redeploy, and replay the run from the dashboard with its original payload. Each child task triggered with triggerAndWait is its own run, so you can inspect and replay them individually too.
Replay re-runs a task with its original payload, so keep your tasks idempotent. Storing vectors twice for the same document ID should upsert or check for existence before inserting.