# Send email from Trigger.dev

Send transactional email from [Trigger.dev](https://trigger.dev) tasks with the Groundmail Node SDK: welcome emails after sign-up, receipts after a payment job, or a drip sequence that waits days between emails. Trigger.dev retries failed tasks, so each send below carries an idempotency key and a retry never emails the same person twice.

---

## Prerequisites

- A Groundmail account with a [verified sending domain](https://docs.groundmail.dev/domains). A subdomain such as `mail.yourdomain.com` works well.
- An API key (`gm_…`) from **API keys** in the dashboard. A [scoped key](https://docs.groundmail.dev/api-keys) with only `emails:send` is enough.
- A Trigger.dev project using `@trigger.dev/sdk` (v4).

---

## 1. Add the API key

**Local dev:** `npx trigger.dev dev` reads `.env`, `.env.development` and `.env.local`, so add it there:

```bash
GROUNDMAIL_API_KEY=gm_your_key_here
```

**Deployed tasks:** in the Trigger.dev dashboard, open **Environment Variables**, click **Add new** and set `GROUNDMAIL_API_KEY` for each environment (staging, prod). Deploys don't read `.env.production`, so set it in the dashboard.

Install the SDK:

```bash
npm install groundmail
```

---

## 2. A task that sends an email

```ts
// trigger/welcome-email.ts
import { task } from '@trigger.dev/sdk'
import { Groundmail } from 'groundmail'

const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)

export const welcomeEmail = task({
  id: 'welcome-email',
  retry: { maxAttempts: 5 },
  run: async (payload: { email: string; name: string }, { ctx }) => {
    const { id } = await groundmail.emails.send({
      from: 'App <no-reply@mail.yourdomain.com>',
      to: payload.email,
      subject: `Welcome, ${payload.name}`,
      html: `<p>Hi ${payload.name}, thanks for signing up.</p>`,
      // Same run, same key: a retry returns the first send instead of sending again.
      idempotencyKey: `welcome-${ctx.run.id}`,
    })
    return { emailId: id }
  },
})
```

Trigger it from your backend. Import the task as a type only, so the task code stays out of your app bundle:

```ts
import { tasks } from '@trigger.dev/sdk'
import type { welcomeEmail } from '~/trigger/welcome-email'

await tasks.trigger<typeof welcomeEmail>('welcome-email', {
  email: user.email,
  name: user.name,
})
```

`ctx.run.id` stays the same across retries of one run, which is what makes it a safe idempotency key. Groundmail keeps keys for 24 hours and replays the original response (`msg_…` id and status) for a repeat.

---

## 3. A drip sequence: wait, then send

`wait.for` pauses the run. On Trigger.dev Cloud, waits longer than 5 seconds don't count toward compute, so waiting days costs nothing.

```ts
// trigger/onboarding.ts
import { task, wait } from '@trigger.dev/sdk'
import { Groundmail } from 'groundmail'
import { hasCreatedProject } from '~/lib/users' // your own lookup

const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)
const from = 'App <no-reply@mail.yourdomain.com>'

export const onboarding = task({
  id: 'onboarding-drip',
  run: async (payload: { userId: string; email: string }, { ctx }) => {
    await groundmail.emails.send({
      from,
      to: payload.email,
      subject: 'Welcome aboard',
      html: '<p>Here is how to get started.</p>',
      idempotencyKey: `onboarding-1-${ctx.run.id}`,
    })

    await wait.for({ days: 3 })

    // Decide at send time: skip the nudge if they already did the thing.
    if (await hasCreatedProject(payload.userId)) return { skipped: true }

    await groundmail.emails.send({
      from,
      to: payload.email,
      subject: 'Need a hand with your first project?',
      html: '<p>Reply to this email and we will help.</p>',
      idempotencyKey: `onboarding-2-${ctx.run.id}`,
    })
  },
})
```

`wait.for` takes `seconds`, `minutes`, `hours`, `days`, `weeks`, `months` or `years`. Use `wait.until({ date })` for a fixed time.

To delay the whole run instead, pass `delay` when you trigger it: `tasks.trigger<typeof welcomeEmail>('welcome-email', payload, { delay: '1h' })`.

---

## Trigger.dev delay or Groundmail `scheduled_at`?

Both work. Pick by where the decision lives:

| You want to | Use |
|-------------|-----|
| Send one known email at a set time | `scheduledAt` on the send (`'2026-11-01T09:00:00Z'` or a `Date`). Groundmail stores it, and you can reschedule or cancel it with `groundmail.emails.update` / `groundmail.emails.cancel`. No task needs to wait. |
| Check something before sending (did they upgrade, did they verify) | `wait.for` in a task, then send. |

Scheduled emails can't carry attachments. See [Schedule, reschedule, and cancel](https://docs.groundmail.dev/emails#schedule-reschedule-and-cancel).

---

## Don't retry errors that won't fix themselves

The SDK throws `GroundmailError` with the HTTP `status` and a stable `code`. A `5xx` or `429` is worth retrying; a `4xx` like an unverified From domain will fail every time. Stop those early with `AbortTaskRunError`:

```ts
import { AbortTaskRunError } from '@trigger.dev/sdk'
import { GroundmailError } from 'groundmail'

try {
  await groundmail.emails.send({ /* … */ })
} catch (err) {
  if (err instanceof GroundmailError && err.status >= 400 && err.status < 500 && err.status !== 429) {
    throw new AbortTaskRunError(`${err.code ?? err.status}: ${err.message}`)
  }
  throw err // let Trigger.dev retry
}
```

---

## Troubleshooting

| Problem | Fix |
|---------|-----|
| `GROUNDMAIL_API_KEY` is undefined in a deployed run | Set it under **Environment Variables** for that environment, then redeploy. `.env` files only apply to `trigger.dev dev`. |
| `401 Unauthorized` | The key was revoked or belongs to another team. Create a new one under **API keys**. |
| `domain is not verified` | Send from an address on a domain you verified under **Domains**. |
| A retry sent a second email | Pass an `idempotencyKey` built from `ctx.run.id`, not a random value or a timestamp. |
| `429` from Groundmail | Your account hit a sending limit. Let the task retry with backoff, and see [Sending limits](https://docs.groundmail.dev/sending-limits). |

Related: [Node.js SDK](https://docs.groundmail.dev/frameworks/node) · [Send email API](https://docs.groundmail.dev/emails) · [Inngest](https://docs.groundmail.dev/integrations/inngest) · [Upstash QStash](https://docs.groundmail.dev/integrations/qstash)
