Skip to main content

Overview

This task sends an onboarding email sequence to a user. Each email is wrapped in retry.onThrow so that only the failed send is retried — not the entire task. Between emails, wait.for suspends the task for a configurable duration. During the wait, the task is fully paused and consumes zero resources.

Task code

trigger/email-sequence.ts
import { task, wait, retry } from "@trigger.dev/sdk";
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

export const emailSequence = task({
  id: "email-sequence",
  run: async (payload: { userId: string; email: string; name: string }) => {
    console.log(`Starting email sequence for user ${payload.userId}`);

    // Email 1 — sent immediately
    const firstEmailResult = await retry.onThrow(
      async ({ attempt }) => {
        const { data, error } = await resend.emails.send({
          from: "[email protected]",
          to: payload.email,
          subject: "Welcome to Trigger.dev",
          html: `<p>Hello ${payload.name},</p><p>Welcome to Trigger.dev</p>`,
        });

        if (error) {
          // Throwing retries only this block, not the whole task
          throw error;
        }

        return data;
      },
      { maxAttempts: 3 }
    );

    // Wait 3 days before sending the next email.
    // The task is fully suspended and uses no resources during this time.
    await wait.for({ days: 3 });

    // Email 2 — sent after 3 days
    const secondEmailResult = await retry.onThrow(
      async ({ attempt }) => {
        const { data, error } = await resend.emails.send({
          from: "[email protected]",
          to: payload.email,
          subject: "Some tips for you",
          html: `<p>Hello ${payload.name},</p><p>Here are some tips for you…</p>`,
        });

        if (error) {
          throw error;
        }

        return data;
      },
      { maxAttempts: 3 }
    );

    // Add more wait.for + retry.onThrow blocks to extend the sequence
    return { firstEmailResult, secondEmailResult };
  },
});

Key concepts

wait.for — pause without consuming resources

Calling await wait.for({ days: 3 }) suspends the task entirely. No compute is used during the wait. Once the duration elapses, the task resumes exactly where it left off.
// Wait for a specific duration
await wait.for({ days: 3 });
await wait.for({ hours: 12 });
await wait.for({ minutes: 30 });

retry.onThrow — retry a block, not the whole task

retry.onThrow wraps a code block with its own retry logic. If the block throws, only that block is retried — any work already done outside the block (earlier emails in the sequence) is not repeated.
const result = await retry.onThrow(
  async ({ attempt }) => {
    // attempt starts at 1
    const response = await someApiCall();
    if (!response.ok) throw new Error("API call failed");
    return response.data;
  },
  { maxAttempts: 3 }
);
Compare this with task-level retry (set in the retry field of task({})), which retries the entire run from the beginning. Use retry.onThrow when you want fine-grained retries within a long-running task.

Testing your task

To test this task in the Trigger.dev dashboard, use the following payload on the Test page:
{
  "userId": "123",
  "email": "[email protected]",
  "name": "Alice Testington"
}
During testing, the wait.for call will pause the task for the configured duration. If you want to skip the wait in development, you can reduce the duration (e.g. { seconds: 5 }) while testing and restore it before deploying.

OpenAI with retrying

Task-level retry configuration for OpenAI API calls

Puppeteer

Use Puppeteer to generate PDFs and scrape web pages

Build docs developers (and LLMs) love