Build Production Background Jobs with Trigger.dev v3 and Next.js

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

Modern web applications need to handle work that should not block a user request — sending emails, processing images, syncing data with third-party APIs, generating reports, or running AI pipelines. Without a background job system, you are stuck with fragile workarounds: setTimeout hacks, edge function timeouts, or spinning up a separate worker server.

Trigger.dev v3 solves this by giving you a TypeScript-native background job framework that runs your tasks on managed infrastructure with built-in retries, logging, and observability. You write tasks as plain TypeScript functions, trigger them from your Next.js API routes or Server Actions, and Trigger.dev handles the rest — execution, retries, scheduling, and monitoring.

In this tutorial, you will build a user onboarding automation system — when a new user signs up, a background workflow sends a welcome email, generates a personalized avatar, syncs the user to a CRM, and schedules a follow-up email for 3 days later. Along the way, you will master Trigger.dev v3 tasks, error handling, multi-step workflows, scheduled jobs, and production deployment.

Prerequisites

Before starting, make sure you have:

  • Node.js 20+ installed
  • A free Trigger.dev account — sign up at the Trigger.dev website
  • Basic knowledge of React and TypeScript
  • Familiarity with Next.js App Router (API routes, Server Actions)
  • A code editor (VS Code recommended)

What You Will Build

A user onboarding automation system with:

  • Welcome email task — sends a formatted email when a user signs up
  • Avatar generation task — creates a personalized avatar using an external API
  • CRM sync task — pushes new user data to an external CRM
  • Multi-step onboarding workflow — orchestrates all tasks in sequence with error handling
  • Scheduled follow-up — sends a follow-up email 3 days after signup
  • Cron job — daily digest of new signups for the admin team
  • Full observability — logs, runs, and errors visible in the Trigger.dev dashboard

Step 1: Create the Next.js Project

Scaffold a new Next.js 15 project:

npx create-next-app@latest trigger-onboarding --typescript --tailwind --eslint --app --src-dir --use-npm
cd trigger-onboarding

Install the Trigger.dev SDK and CLI:

npm install @trigger.dev/sdk
npm install -D trigger.dev

The @trigger.dev/sdk package provides the runtime API for defining and triggering tasks. The trigger.dev CLI handles local development and deployment.

Step 2: Initialize Trigger.dev

Run the init command to set up Trigger.dev in your project:

npx trigger.dev@latest init

This command does several things:

  1. Creates a trigger.config.ts file at the project root
  2. Creates a src/trigger/ directory for your task files
  3. Adds your project reference and API key to .env.local
  4. Updates package.json with dev and deploy scripts

Your trigger.config.ts should look like this:

import { defineConfig } from "@trigger.dev/sdk/v3";
 
export default defineConfig({
  project: "proj_your_project_id",
  runtime: "node",
  logLevel: "log",
  retries: {
    enabledInDev: true,
    default: {
      maxAttempts: 3,
      minTimeoutInMs: 1000,
      maxTimeoutInMs: 10000,
      factor: 2,
    },
  },
  dirs: ["src/trigger"],
});

The retries configuration is important — it sets a default retry policy for all tasks. When a task fails, Trigger.dev will retry it up to 3 times with exponential backoff (1s, 2s, 4s delays).

Step 3: Create Your First Task — Welcome Email

Create a file at src/trigger/welcome-email.ts:

import { task, logger } from "@trigger.dev/sdk/v3";
 
export interface WelcomeEmailPayload {
  userId: string;
  email: string;
  name: string;
}
 
export const sendWelcomeEmail = task({
  id: "send-welcome-email",
  retry: {
    maxAttempts: 5,
  },
  run: async (payload: WelcomeEmailPayload) => {
    logger.info("Sending welcome email", {
      userId: payload.userId,
      email: payload.email,
    });
 
    // In production, replace with your email provider (Resend, SendGrid, etc.)
    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        from: "welcome@yourapp.com",
        to: payload.email,
        subject: `Welcome to Our App, ${payload.name}!`,
        html: `
          <h1>Welcome, ${payload.name}!</h1>
          <p>We are excited to have you on board.</p>
          <p>Here are some things you can do to get started:</p>
          <ul>
            <li>Complete your profile</li>
            <li>Explore the dashboard</li>
            <li>Connect your first integration</li>
          </ul>
        `,
      }),
    });
 
    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Failed to send email: ${error}`);
    }
 
    const result = await response.json();
 
    logger.info("Welcome email sent successfully", {
      emailId: result.id,
    });
 
    return { emailId: result.id, sentAt: new Date().toISOString() };
  },
});

Key things to notice:

  • task() defines a background task with a unique id
  • logger provides structured logging visible in the Trigger.dev dashboard
  • retry overrides the default retry policy — emails are critical, so we retry 5 times
  • Throwing an error triggers a retry automatically
  • The return value is stored and visible in the dashboard for debugging

Step 4: Create the Avatar Generation Task

Create src/trigger/generate-avatar.ts:

import { task, logger } from "@trigger.dev/sdk/v3";
 
export interface AvatarPayload {
  userId: string;
  name: string;
}
 
export const generateAvatar = task({
  id: "generate-avatar",
  retry: {
    maxAttempts: 3,
  },
  run: async (payload: AvatarPayload) => {
    logger.info("Generating avatar", { userId: payload.userId });
 
    // Use DiceBear API for avatar generation
    const initials = payload.name
      .split(" ")
      .map((n) => n[0])
      .join("")
      .toUpperCase();
 
    const seed = encodeURIComponent(payload.name);
    const avatarUrl = `https://api.dicebear.com/8.x/initials/svg?seed=${seed}&chars=${initials.length}`;
 
    // Download the SVG and convert to a stored URL
    const response = await fetch(avatarUrl);
    if (!response.ok) {
      throw new Error(`Avatar generation failed: ${response.statusText}`);
    }
 
    const svgContent = await response.text();
 
    // In production, upload to your storage (S3, Cloudflare R2, etc.)
    // For this tutorial, we simulate storage
    const storedUrl = `/avatars/${payload.userId}.svg`;
 
    logger.info("Avatar generated", {
      userId: payload.userId,
      url: storedUrl,
    });
 
    return { avatarUrl: storedUrl, svgSize: svgContent.length };
  },
});

Step 5: Create the CRM Sync Task

Create src/trigger/sync-crm.ts:

import { task, logger } from "@trigger.dev/sdk/v3";
 
export interface CrmSyncPayload {
  userId: string;
  email: string;
  name: string;
  signupDate: string;
}
 
export const syncToCrm = task({
  id: "sync-to-crm",
  retry: {
    maxAttempts: 4,
    minTimeoutInMs: 2000,
    maxTimeoutInMs: 30000,
    factor: 3,
  },
  run: async (payload: CrmSyncPayload) => {
    logger.info("Syncing user to CRM", {
      userId: payload.userId,
      email: payload.email,
    });
 
    // Simulate CRM API call (replace with HubSpot, Salesforce, etc.)
    const crmResponse = await fetch(
      `${process.env.CRM_API_URL}/contacts`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.CRM_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          email: payload.email,
          name: payload.name,
          properties: {
            signup_date: payload.signupDate,
            source: "web_app",
            lifecycle_stage: "subscriber",
          },
        }),
      }
    );
 
    if (!crmResponse.ok) {
      const error = await crmResponse.text();
      logger.error("CRM sync failed", { error });
      throw new Error(`CRM sync failed: ${error}`);
    }
 
    const contact = await crmResponse.json();
 
    logger.info("CRM sync completed", {
      crmContactId: contact.id,
    });
 
    return { crmContactId: contact.id };
  },
});

Notice the retry configuration here — CRM APIs can be flaky, so we use a more aggressive backoff (factor: 3) with a higher maximum timeout of 30 seconds.

Step 6: Build the Multi-Step Onboarding Workflow

Now for the powerful part — orchestrating all tasks into a single workflow. Create src/trigger/onboarding-workflow.ts:

import { task, logger, wait } from "@trigger.dev/sdk/v3";
import { sendWelcomeEmail } from "./welcome-email";
import { generateAvatar } from "./generate-avatar";
import { syncToCrm } from "./sync-crm";
 
export interface OnboardingPayload {
  userId: string;
  email: string;
  name: string;
}
 
export const onboardingWorkflow = task({
  id: "onboarding-workflow",
  retry: {
    maxAttempts: 1, // The workflow itself should not retry — individual tasks handle retries
  },
  run: async (payload: OnboardingPayload) => {
    logger.info("Starting onboarding workflow", {
      userId: payload.userId,
    });
 
    // Step 1: Send welcome email and generate avatar in parallel
    const [emailResult, avatarResult] = await Promise.all([
      sendWelcomeEmail.triggerAndWait({
        userId: payload.userId,
        email: payload.email,
        name: payload.name,
      }),
      generateAvatar.triggerAndWait({
        userId: payload.userId,
        name: payload.name,
      }),
    ]);
 
    logger.info("Email and avatar completed", {
      emailOk: emailResult.ok,
      avatarOk: avatarResult.ok,
    });
 
    // Step 2: Sync to CRM (depends on previous steps completing)
    const crmResult = await syncToCrm.triggerAndWait({
      userId: payload.userId,
      email: payload.email,
      name: payload.name,
      signupDate: new Date().toISOString(),
    });
 
    logger.info("CRM sync completed", { crmOk: crmResult.ok });
 
    // Step 3: Schedule a follow-up email for 3 days later
    const followUpHandle = await sendFollowUpEmail.trigger(
      {
        userId: payload.userId,
        email: payload.email,
        name: payload.name,
      },
      {
        delay: "3d", // Trigger.dev supports human-readable delays
      }
    );
 
    logger.info("Follow-up email scheduled", {
      followUpId: followUpHandle.id,
    });
 
    return {
      emailResult: emailResult.ok ? emailResult.output : null,
      avatarResult: avatarResult.ok ? avatarResult.output : null,
      crmResult: crmResult.ok ? crmResult.output : null,
      followUpScheduled: followUpHandle.id,
    };
  },
});
 
// Follow-up email task
const sendFollowUpEmail = task({
  id: "send-follow-up-email",
  retry: {
    maxAttempts: 5,
  },
  run: async (payload: OnboardingPayload) => {
    logger.info("Sending follow-up email", {
      userId: payload.userId,
    });
 
    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        from: "hello@yourapp.com",
        to: payload.email,
        subject: `How's it going, ${payload.name}?`,
        html: `
          <h1>Hey ${payload.name}!</h1>
          <p>You signed up 3 days ago — how is everything going?</p>
          <p>Need help getting started? Reply to this email and we will help!</p>
        `,
      }),
    });
 
    if (!response.ok) {
      throw new Error(`Follow-up email failed: ${await response.text()}`);
    }
 
    return { sent: true };
  },
});

This workflow demonstrates several powerful Trigger.dev features:

  • triggerAndWait() — triggers a child task and waits for it to complete, returning the result
  • Promise.all() — runs independent tasks in parallel for better performance
  • trigger() with delay — schedules a task to run in the future (3 days later)
  • Workflow orchestration — one parent task coordinates multiple child tasks

Step 7: Add a Scheduled Cron Job

Create src/trigger/daily-digest.ts for a daily admin digest:

import { schedules, logger } from "@trigger.dev/sdk/v3";
 
export const dailySignupDigest = schedules.task({
  id: "daily-signup-digest",
  cron: "0 9 * * *", // Every day at 9:00 AM UTC
  run: async () => {
    logger.info("Running daily signup digest");
 
    // Fetch yesterday's signups from your database
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);
 
    // Replace with your actual database query
    const newUsers = await fetchNewUsers(yesterday);
 
    if (newUsers.length === 0) {
      logger.info("No new signups yesterday");
      return { sent: false, reason: "no_new_signups" };
    }
 
    // Send digest email to admin
    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        from: "reports@yourapp.com",
        to: "admin@yourapp.com",
        subject: `Daily Signup Digest — ${newUsers.length} new users`,
        html: `
          <h1>Daily Signup Report</h1>
          <p>${newUsers.length} new users signed up yesterday:</p>
          <table>
            <tr><th>Name</th><th>Email</th><th>Time</th></tr>
            ${newUsers
              .map(
                (u) =>
                  `<tr><td>${u.name}</td><td>${u.email}</td><td>${u.createdAt}</td></tr>`
              )
              .join("")}
          </table>
        `,
      }),
    });
 
    if (!response.ok) {
      throw new Error(`Digest email failed: ${await response.text()}`);
    }
 
    logger.info("Digest sent", { userCount: newUsers.length });
    return { sent: true, userCount: newUsers.length };
  },
});
 
// Mock function — replace with your database query
async function fetchNewUsers(since: Date) {
  // Example: Prisma, Drizzle, or raw SQL
  // return await db.user.findMany({ where: { createdAt: { gte: since } } });
  return [
    {
      name: "Alice",
      email: "alice@example.com",
      createdAt: since.toISOString(),
    },
  ];
}

The schedules.task() function creates a cron-triggered task. The cron expression 0 9 * * * means "every day at 9:00 AM UTC". You can use any standard cron expression.

Step 8: Trigger Tasks from Next.js

Now wire the workflow into your Next.js application. Create an API route at src/app/api/signup/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { tasks } from "@trigger.dev/sdk/v3";
import type { onboardingWorkflow } from "@/trigger/onboarding-workflow";
 
export async function POST(request: NextRequest) {
  const body = await request.json();
  const { email, name, userId } = body;
 
  if (!email || !name || !userId) {
    return NextResponse.json(
      { error: "Missing required fields" },
      { status: 400 }
    );
  }
 
  // Trigger the onboarding workflow — returns immediately
  const handle = await tasks.trigger<typeof onboardingWorkflow>(
    "onboarding-workflow",
    {
      userId,
      email,
      name,
    }
  );
 
  return NextResponse.json({
    message: "Signup successful! Onboarding started.",
    runId: handle.id,
  });
}

Key insight: tasks.trigger() returns immediately — it does not wait for the background job to complete. The user gets an instant response while the onboarding workflow runs asynchronously. The handle.id can be used to check the status later.

You can also trigger tasks from Server Actions. Create src/app/actions.ts:

"use server";
 
import { tasks } from "@trigger.dev/sdk/v3";
import type { onboardingWorkflow } from "@/trigger/onboarding-workflow";
 
export async function signupAction(formData: FormData) {
  const email = formData.get("email") as string;
  const name = formData.get("name") as string;
  const userId = crypto.randomUUID();
 
  const handle = await tasks.trigger<typeof onboardingWorkflow>(
    "onboarding-workflow",
    { userId, email, name }
  );
 
  return { success: true, runId: handle.id };
}

Step 9: Build a Simple Signup Form

Create src/app/page.tsx:

"use client";
 
import { useState } from "react";
import { signupAction } from "./actions";
 
export default function SignupPage() {
  const [status, setStatus] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
 
  async function handleSubmit(formData: FormData) {
    setLoading(true);
    try {
      const result = await signupAction(formData);
      setStatus(
        `Signed up successfully! Onboarding run: ${result.runId}`
      );
    } catch (error) {
      setStatus("Something went wrong. Please try again.");
    } finally {
      setLoading(false);
    }
  }
 
  return (
    <main className="flex min-h-screen items-center justify-center bg-gray-50">
      <div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
        <h1 className="mb-6 text-2xl font-bold text-gray-900">
          Sign Up
        </h1>
        <form action={handleSubmit} className="space-y-4">
          <div>
            <label
              htmlFor="name"
              className="block text-sm font-medium text-gray-700"
            >
              Name
            </label>
            <input
              type="text"
              id="name"
              name="name"
              required
              className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
            />
          </div>
          <div>
            <label
              htmlFor="email"
              className="block text-sm font-medium text-gray-700"
            >
              Email
            </label>
            <input
              type="email"
              id="email"
              name="email"
              required
              className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
            />
          </div>
          <button
            type="submit"
            disabled={loading}
            className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
          >
            {loading ? "Signing up..." : "Sign Up"}
          </button>
        </form>
        {status && (
          <p className="mt-4 rounded bg-green-50 p-3 text-sm text-green-700">
            {status}
          </p>
        )}
      </div>
    </main>
  );
}

Step 10: Run the Development Server

Start both the Next.js dev server and the Trigger.dev dev worker:

# Terminal 1: Next.js
npm run dev
 
# Terminal 2: Trigger.dev worker
npx trigger.dev@latest dev

The Trigger.dev dev command connects to the Trigger.dev cloud and runs your tasks locally. Open the Trigger.dev dashboard to see real-time logs, task runs, and errors.

Now test the flow:

  1. Open your app at http://localhost:3000
  2. Fill in the signup form and submit
  3. Watch the Trigger.dev dashboard — you will see the onboarding workflow start, the welcome email and avatar tasks run in parallel, then the CRM sync, and finally the follow-up email scheduled for 3 days later

Step 11: Error Handling and Retry Strategies

Trigger.dev provides granular control over error handling. Let us enhance the welcome email task with custom error handling:

import { task, logger, retry } from "@trigger.dev/sdk/v3";
 
export const sendWelcomeEmailV2 = task({
  id: "send-welcome-email-v2",
  retry: {
    maxAttempts: 5,
    minTimeoutInMs: 1000,
    maxTimeoutInMs: 60000,
    factor: 2,
  },
  onFailure: async (payload, error, params) => {
    // Called after ALL retries are exhausted
    logger.error("Welcome email permanently failed", {
      userId: payload.userId,
      error: error.message,
      attempts: params.attemptCount,
    });
 
    // Send to a dead-letter queue, alert on Slack, etc.
    await fetch(process.env.SLACK_WEBHOOK_URL!, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        text: `Failed to send welcome email to ${payload.email} after ${params.attemptCount} attempts: ${error.message}`,
      }),
    });
  },
  run: async (payload: WelcomeEmailPayload) => {
    // Task implementation...
  },
});

The onFailure callback runs only after all retries are exhausted — it is your last chance to handle the failure (alert on Slack, write to a dead-letter queue, etc.).

You can also use retry.onThrow for fine-grained retry control within a task:

run: async (payload) => {
  // Only retry this specific operation, not the entire task
  const result = await retry.onThrow(
    async () => {
      const res = await fetch("https://flaky-api.com/data");
      if (!res.ok) throw new Error("API call failed");
      return res.json();
    },
    { maxAttempts: 3, randomize: true }
  );
 
  // Continue with the result...
  logger.info("Got data", { result });
};

Step 12: Add Task Metadata and Tags

Tags and metadata make tasks searchable and filterable in the dashboard:

import { task, logger, metadata, tags } from "@trigger.dev/sdk/v3";
 
export const processOrder = task({
  id: "process-order",
  run: async (payload: { orderId: string; userId: string; amount: number }) => {
    // Add tags for filtering in the dashboard
    tags.add("order", payload.orderId);
    tags.add("user", payload.userId);
 
    // Add metadata that appears in the run details
    metadata.set("orderAmount", payload.amount);
    metadata.set("currency", "USD");
 
    // Update progress
    metadata.set("status", "processing");
 
    // ... process the order ...
 
    metadata.set("status", "completed");
 
    return { processed: true };
  },
});

Step 13: Batch Operations with batchTrigger

When you need to process multiple items, use batch triggering:

import { tasks } from "@trigger.dev/sdk/v3";
import type { sendWelcomeEmail } from "@/trigger/welcome-email";
 
// Send emails to multiple users at once
export async function sendBulkEmails(
  users: Array<{ userId: string; email: string; name: string }>
) {
  const handle = await tasks.batchTrigger<typeof sendWelcomeEmail>(
    "send-welcome-email",
    users.map((user) => ({
      payload: user,
    }))
  );
 
  return {
    batchId: handle.batchId,
    runCount: handle.runs.length,
  };
}

Trigger.dev processes batch items with controlled concurrency, preventing you from overwhelming downstream services.

Step 14: Environment Variables and Configuration

Add your environment variables to .env.local:

# Trigger.dev
TRIGGER_SECRET_KEY=tr_dev_your_secret_key
 
# Email (Resend)
RESEND_API_KEY=re_your_api_key
 
# CRM
CRM_API_URL=https://api.your-crm.com
CRM_API_KEY=your_crm_key
 
# Notifications
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/your/webhook

For production, add these same variables in the Trigger.dev dashboard under your project settings. Environment variables configured there are injected into your tasks at runtime on the Trigger.dev cloud.

Step 15: Deploy to Production

Deploy your tasks to the Trigger.dev cloud:

npx trigger.dev@latest deploy

This command:

  1. Bundles your task code
  2. Uploads it to Trigger.dev cloud
  3. Makes your tasks available for production triggering

Your Next.js app triggers tasks via the SDK, and Trigger.dev cloud executes them on managed infrastructure. No servers to provision, no workers to manage.

For CI/CD integration, add the deploy command to your pipeline:

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
 
jobs:
  deploy-trigger:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx trigger.dev@latest deploy
        env:
          TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
 
  deploy-nextjs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build
      # Deploy to Vercel, Railway, etc.

Step 16: Monitor and Debug

The Trigger.dev dashboard gives you full observability:

  • Runs list — see all task executions with status, duration, and timestamps
  • Run details — view logs, payload, output, and retry history for each run
  • Workflow trace — visualize multi-step workflows with parent-child task relationships
  • Errors — filter and search failed runs with full stack traces
  • Schedules — manage and monitor cron jobs

Use structured logging liberally — every logger.info(), logger.warn(), and logger.error() call appears in the run details with its metadata.

Testing Your Implementation

To verify everything works:

  1. Start the dev server and Trigger.dev worker in two terminals
  2. Submit the signup form — you should see an instant response
  3. Check the Trigger.dev dashboard — the onboarding workflow should show:
    • Welcome email and avatar generation running in parallel
    • CRM sync running after both complete
    • Follow-up email scheduled for 3 days later
  4. Test error handling — temporarily break an API key and verify retries work
  5. Check the cron job — it should appear in the Schedules tab, set to run daily at 9 AM UTC

Troubleshooting

Tasks not appearing in the dashboard: Make sure npx trigger.dev@latest dev is running. Check that your trigger.config.ts has the correct dirs pointing to your task files.

Tasks failing with "Module not found": Trigger.dev bundles tasks separately from Next.js. Ensure all imports in task files are self-contained and do not import from Next.js-specific modules (like next/server).

Retry not working in development: Set enabledInDev: true in your trigger.config.ts retries configuration.

Environment variables not available in tasks: In development, tasks read from .env.local. In production, configure environment variables in the Trigger.dev dashboard.

Project Structure

Here is the final project structure:

trigger-onboarding/
  src/
    app/
      api/
        signup/
          route.ts          # API route to trigger onboarding
      actions.ts            # Server Action for form submission
      page.tsx              # Signup form UI
    trigger/
      welcome-email.ts      # Welcome email task
      generate-avatar.ts    # Avatar generation task
      sync-crm.ts           # CRM sync task
      onboarding-workflow.ts # Multi-step workflow orchestrator
      daily-digest.ts       # Scheduled cron job
  trigger.config.ts         # Trigger.dev configuration
  .env.local                # Environment variables

Next Steps

Now that you have a working background job system, consider:

  • Adding more workflows — order processing, report generation, data pipelines
  • Implementing idempotency — use unique keys to prevent duplicate task execution
  • Setting up alerting — configure Slack or email notifications for failed tasks
  • Using Trigger.dev with a database — combine with Prisma or Drizzle for data persistence
  • Exploring fan-out patterns — use batchTriggerAndWait for processing large datasets
  • Self-hosting — Trigger.dev v3 can be self-hosted if you need to run on your own infrastructure

Conclusion

Trigger.dev v3 brings background job processing to the TypeScript ecosystem with a developer experience that feels native to Next.js. Instead of managing Redis queues, worker processes, and retry logic yourself, you write plain TypeScript functions and let Trigger.dev handle the infrastructure.

In this tutorial, you built a complete user onboarding automation system with parallel task execution, multi-step workflows, scheduled cron jobs, error handling with retries, and production deployment. The same patterns apply to any background processing need — from sending emails to processing payments to running AI pipelines.

The key takeaways are:

  1. Tasks are just TypeScript functions — no special syntax or DSL to learn
  2. triggerAndWait enables workflow orchestration — compose complex workflows from simple tasks
  3. Retries and error handling are built in — configure once, handle failures gracefully
  4. Scheduled tasks replace cron servers — no infrastructure to manage for recurring jobs
  5. The dashboard provides full observability — debug production issues with logs, traces, and metadata

Want to read more tutorials? Check out our latest tutorial on Mastering Twilio SMS: A Beginner’s Guide to Node.js Messaging for Business.

Discuss Your Project with Us

We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.

Let's find the best solutions for your needs.

Related Articles