# Welcome email sequence: template and code

Source: https://www.courier.com/recipes/welcome-email-sequence (manifest: https://www.courier.com/recipes/welcome-email-sequence/manifest.json)

Welcome every new user, then nudge only the ones who haven't finished setup.

A welcome email sequence greets new users and helps them finish setup. It sends a welcome email at signup, a setup nudge the next day by email and Inbox to anyone who hasn't finished, and a tips email on day 3. Finishing setup skips the nudge, and you can cancel the rest anytime.

Tested 2026-10-19: passing.

The waits are Courier delay nodes. We test this exact journey with each wait shortened to seconds.

## When to send welcome emails

| Step | When | Channels | Subject |
| --- | --- | --- | --- |
| 1 | Right after signup | Email | Welcome to Acme, {{default profile.first_name "there"}} |
| 2 | 1 day after signup | Email, Inbox | Your Acme setup is almost done |
| 3 | 3 days after signup | Email | 3 things to set up in your first week |

## Copy

### Welcome (email)

Subject: "Welcome to Acme, {{default profile.first_name "there"}}", "Welcome aboard, {{profile.first_name}}", "You're in. Here's where to start"

Hi {{default profile.first_name "there"}},

Thanks for signing up. The fastest way to get going is to connect your first data source. It takes about five minutes.

Reply to this email if anything gets in your way.

Button: Get started (https://app.acme.com/setup)

### Setup nudge (email)

Subject: "Your Acme setup is almost done", "Finish setting up Acme"

Hi {{default profile.first_name "there"}},

You're one step away: connect your first data source. Pick up where you left off.

Button: Finish setup (https://app.acme.com/setup)

### Welcome (Inbox)

Subject: "Welcome to Acme"

Start here: connect your first data source.

Button: Get started (https://app.acme.com/setup)

### First-week tips (email)

Subject: "3 things to set up in your first week", "Your first week with Acme"

Hi {{default profile.first_name "there"}},

Here's what people set up first in Acme:

1. Invite your team so everyone sees the same updates.

2. Turn on Slack alerts for the events you care about.

3. Set up a weekly summary email.

Button: Open Acme (https://app.acme.com/setup)

## The journey

```json
{
  "name": "Welcome email sequence",
  "cancelation_token": "welcome-{{recipient}}",
  "nodes": [
    {
      "type": "trigger",
      "trigger_type": "api-invoke"
    },
    {
      "id": "welcome-email",
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@welcome-email"
      }
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P1D"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@setup-nudge-email"
      },
      "conditions": ["profile.setup_completed_at", "does not exist"]
    },
    {
      "type": "send",
      "channel": "inbox",
      "message": {
        "template": "@welcome-inbox"
      },
      "conditions": {
        "AND": [
          ["profile.setup_completed_at", "does not exist"],
          ["send_status.welcome-email", "was not", "OPENED"]
        ]
      }
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P2D"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@tips-email"
      }
    },
    {
      "type": "exit"
    }
  ]
}
```

## Create it in your workspace

```bash
COURIER_API_KEY=pk_... node --experimental-strip-types setup.ts
```

setup.ts

```typescript
// Creates this recipe's journey from journey.json and the templates/ folder,
// then publishes it. Run it once per workspace:
//
//   COURIER_API_KEY=pk_... node --experimental-strip-types setup.ts
//
// It prints the journey ID and the template IDs it created.
import Courier from "@trycourier/courier";
import { readFileSync, readdirSync } from "node:fs";

const client = new Courier();
const here = new URL(".", import.meta.url);
const readJson = (path: string) =>
  JSON.parse(readFileSync(new URL(path, here), "utf8"));

const spec = readJson("journey.json");

// 1. Create the journey, then the templates its send nodes use.
const journey = await client.journeys.create({
  name: spec.name,
  nodes: [spec.nodes[0], spec.nodes.at(-1)],
});

const templateIds: Record<string, string> = {};
for (const file of readdirSync(new URL("templates/", here))) {
  const key = file.replace(/\.json$/, "");
  const template = await client.journeys.templates.create(
    journey.id,
    readJson(`templates/${file}`)
  );
  await client.journeys.templates.publish(template.id, {
    templateId: journey.id,
  });
  templateIds[key] = template.id;
}

// 2. Save the nodes. In journey.json, "@<template>" stands for the ID of the
// template with that file name. Node IDs must be unique across a workspace,
// so any node ID journey.json names (used by "send_status.<id>" conditions)
// gets this journey's ID added.
const nodeIds = [
  ...JSON.stringify(spec.nodes).matchAll(/"id":"([\w-]+)"/g),
].map((m) => m[1]);
let nodes = JSON.stringify(spec.nodes).replace(/"@([\w-]+)"/g, (_, key) =>
  JSON.stringify(templateIds[key])
);
for (const id of nodeIds)
  nodes = nodes.replace(
    new RegExp(`(?<=[".])${id}(?=")`, "g"),
    `${id}-${journey.id}`
  );
await client.journeys.replace(journey.id, {
  name: spec.name,
  cancelation_token: spec.cancelation_token,
  nodes: JSON.parse(nodes),
});

// 3. Publish, so new runs use this version.
await client.journeys.publish(journey.id);
console.log(JSON.stringify({ journeyId: journey.id, templateIds }));
```

## Start it when someone signs up

```typescript
// Start the sequence when someone signs up, with the journey ID setup.ts printed.
// The templates read the first name from the user's Courier profile, so
// there's no data to pass.
import Courier from "@trycourier/courier";

const client = new Courier();

export async function startWelcomeSequence(journeyId: string, userId: string) {
  const { runId } = await client.journeys.invoke(journeyId, {
    user_id: userId,
  });
  return runId;
}
```

```python
# Start the sequence when someone signs up, with the journey ID setup.ts printed.
# The templates read the first name from the user's Courier profile, so
# there's no data to pass.
from courier import Courier

client = Courier()


def start_welcome_sequence(journey_id: str, user_id: str) -> str:
    response = client.journeys.invoke(template_id=journey_id, user_id=user_id)
    return response.run_id
```

## Stop it early

```typescript
// Record finished setup on the profile, so the day 1 nudge and Inbox copy are skipped.
// To stop the whole sequence instead, cancel the run.
import Courier from "@trycourier/courier";

const client = new Courier();

export async function markSetupComplete(userId: string) {
  await client.profiles.create(userId, {
    profile: { setup_completed_at: new Date().toISOString() },
  });
}

export async function stopWelcomeSequence(userId: string) {
  await client.journeys.cancel({ cancelation_token: `welcome-${userId}` });
}
```

```python
# Record finished setup on the profile, so the day 1 nudge and Inbox copy are skipped.
# To stop the whole sequence instead, cancel the run.
from datetime import datetime, timezone

from courier import Courier

client = Courier()


def mark_setup_complete(user_id: str) -> None:
    client.profiles.create(
        user_id,
        profile={"setup_completed_at": datetime.now(timezone.utc).isoformat()},
    )


def stop_welcome_sequence(user_id: str) -> None:
    client.journeys.cancel(cancelation_token=f"welcome-{user_id}")
```

## Templates

setup.ts reads these from a templates/ folder next to it.

templates/welcome-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Welcome (email)",
    "tags": ["recipe", "welcome-email-sequence"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "Welcome to Acme, {{default profile.first_name \"there\"}}"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "Thanks for signing up. The fastest way to get going is to connect your first data source. It takes about five minutes."
            },
            {
              "type": "action",
              "content": "Get started",
              "href": "https://app.acme.com/setup"
            },
            {
              "type": "text",
              "content": "Reply to this email if anything gets in your way."
            }
          ]
        }
      ]
    }
  }
}
```

templates/setup-nudge-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Setup nudge (email)",
    "tags": ["recipe", "welcome-email-sequence"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "Your Acme setup is almost done"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "You're one step away: connect your first data source. Pick up where you left off."
            },
            {
              "type": "action",
              "content": "Finish setup",
              "href": "https://app.acme.com/setup"
            }
          ]
        }
      ]
    }
  }
}
```

templates/welcome-inbox.json

```json
{
  "channel": "inbox",
  "notification": {
    "name": "Welcome (Inbox)",
    "tags": ["recipe", "welcome-email-sequence"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "inbox",
          "elements": [
            {
              "type": "meta",
              "title": "Welcome to Acme"
            },
            {
              "type": "text",
              "content": "Start here: connect your first data source."
            },
            {
              "type": "action",
              "content": "Get started",
              "href": "https://app.acme.com/setup"
            }
          ]
        }
      ]
    }
  }
}
```

templates/tips-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "First-week tips (email)",
    "tags": ["recipe", "welcome-email-sequence"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "3 things to set up in your first week"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "Here's what people set up first in Acme:"
            },
            {
              "type": "text",
              "content": "1. Invite your team so everyone sees the same updates."
            },
            {
              "type": "text",
              "content": "2. Turn on Slack alerts for the events you care about."
            },
            {
              "type": "text",
              "content": "3. Set up a weekly summary email."
            },
            {
              "type": "action",
              "content": "Open Acme",
              "href": "https://app.acme.com/setup"
            }
          ]
        }
      ]
    }
  }
}
```

## Hand it to your agent

```text
Add a welcome email sequence to this app, sent through Courier: a welcome email at signup, a setup nudge the next day for people who haven't finished, and tips on day 3. Follow the steps at https://www.courier.com/recipes/welcome-email-sequence.md and use the Courier skill for SDK details.

1. Create the journey and its templates in my Courier workspace by replacing Acme, the setup link, and the tips in the template files with our own, then running setup.ts from that page with COURIER_API_KEY from our environment. Save the journey ID it prints as COURIER_WELCOME_JOURNEY_ID.
2. Find where this codebase creates a new user and start the journey there. Pass only the user ID, and write first_name and email to the user's Courier profile at signup.
3. Find where a user finishes setup and set setup_completed_at on their Courier profile. Cancel the journey with the token welcome-<user_id> when an account is deleted.
4. Match this codebase's language, file layout, and error handling. Never hardcode the API key.
5. When you're done, start one run for a test user and show me the run's steps.
```

## Data fields

| Field | Type | Example | Note |
| --- | --- | --- | --- |
| profile.first_name | string | Dana | From the profile. Write it when the user signs up. The emails say "there" when it's missing |
| profile.setup_completed_at | ISO 8601 datetime | 2026-10-21T18:30:00Z | From the profile. Your app sets it when setup is done |

## FAQ

### How many emails should a welcome email sequence have?

Three in the first few days covers it: the welcome, one nudge for people who stall, and a tips email. Anything more that early tends to read as noise.

### How do I skip the nudge for people who already finished setup?

When someone finishes setup, set setup_completed_at on their Courier profile. The nudge and the Inbox copy both check that field and skip anyone who has it.

### Why does the welcome also go to the Inbox?

If someone hasn't finished setup and didn't open the welcome email by day 1, the same message shows up in your app's Inbox the next time they're in your product.

### How do I stop a welcome sequence early?

Cancel the run with the token welcome-<user_id>. That stops anything still waiting, which is useful when an account is deleted.

## Related docs

- [Build an onboarding sequence](https://www.courier.com/docs/guides/build-an-onboarding-sequence)
- [Delay node](https://www.courier.com/docs/journeys/nodes/delay)
- [Send node conditions](https://www.courier.com/docs/journeys/nodes/send)
- [Cancel a run](https://www.courier.com/docs/journeys/nodes/cancel)
- [Invoke a journey](https://www.courier.com/docs/journeys/invoke)

## Related recipes

- [Onboarding step reminders](https://www.courier.com/recipes/onboarding-incomplete-reminder-email): Point each stuck user at their next unfinished setup step, not a generic nudge.
- [Feature adoption emails](https://www.courier.com/recipes/feature-adoption-email-sequence): Nudge new users who haven't tried your key feature, and compare two versions of the first email.
- [Email verification reminders](https://www.courier.com/recipes/email-verification-reminder-email): Get new signups to confirm their email, and stop reminding them the moment they do.
