# Feature adoption email sequence: template and code

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

Nudge new users who haven't tried your key feature, and compare two versions of the first email.

A feature adoption email sequence nudges new users toward the feature that keeps people around. A week after signup it sends a tip by email and in your app's Inbox, and four days later a second tip. The first email tests two versions against each other, and using the feature stops the sequence.

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 feature adoption emails

| Step | When | Channels | Subject |
| --- | --- | --- | --- |
| 1 | 7 days after signup | Email, Inbox | See what Reports can do for you |
| 2 | 4 days later | Email | A quick way to start with Reports |

The 7-day and 4-day gaps are the journey's waits: change them in the Journeys editor.

## Copy

### Feature tip, benefit first (email)

Subject: "See what Reports can do for you", "Make the most of Acme with Reports"

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

You've been using Acme for a week, and Reports is the next thing to try. It lets you see which of your sends people actually open.

Button: Open Reports (https://app.acme.com/reports)

### Feature tip, short question (email)

Subject: "{{default profile.first_name "there"}}, have you opened Reports yet?"

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

Reports is waiting for you in Acme. Open it and see which of your sends people actually open.

Button: Open Reports (https://app.acme.com/reports)

### Feature tip (Inbox)

Subject: "Try Reports"

Open Reports and see which of your sends people actually open.

Button: Open Reports (https://app.acme.com/reports)

### Feature second tip (email)

Subject: "A quick way to start with Reports", "How to start with Reports"

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

Not sure where to begin with Reports? Open the report for your latest send and check who opened it.

Button: Open Reports (https://app.acme.com/reports)

## The journey

```json
{
  "name": "Feature adoption emails",
  "cancelation_token": "adopt-{{recipient}}",
  "nodes": [
    {
      "type": "trigger",
      "trigger_type": "api-invoke"
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P7D"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {},
      "experiment": {
        "name": "First tip email",
        "bucketingKey": "user.id",
        "variants": [
          {
            "id": "benefit",
            "name": "Benefit first",
            "weight": 1,
            "templateId": "@feature-tip-benefit-email"
          },
          {
            "id": "question",
            "name": "Short question",
            "weight": 1,
            "templateId": "@feature-tip-question-email"
          }
        ]
      }
    },
    {
      "type": "send",
      "channel": "inbox",
      "message": {
        "template": "@feature-tip-inbox"
      }
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P4D"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@feature-second-tip-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 journey waits 7 days before the first tip. 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 startFeatureAdoption(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 journey waits 7 days before the first tip. 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_feature_adoption(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 when they use the feature

```typescript
// Stop the sequence when the user first uses the feature, or when the account
// is deleted. It cancels the run, so any tip still to come is dropped.
import Courier from "@trycourier/courier";

const client = new Courier();

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

```python
# Stop the sequence when the user first uses the feature, or when the account
# is deleted. It cancels the run, so any tip still to come is dropped.
from courier import Courier

client = Courier()


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

## Templates

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

templates/feature-tip-benefit-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Feature tip, benefit first (email)",
    "tags": ["recipe", "feature-adoption-email-sequence"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "See what Reports can do for you"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "You've been using Acme for a week, and Reports is the next thing to try. It lets you see which of your sends people actually open."
            },
            {
              "type": "action",
              "content": "Open Reports",
              "href": "https://app.acme.com/reports"
            }
          ]
        }
      ]
    }
  }
}
```

templates/feature-tip-question-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Feature tip, short question (email)",
    "tags": ["recipe", "feature-adoption-email-sequence"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "{{default profile.first_name \"there\"}}, have you opened Reports yet?"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "Reports is waiting for you in Acme. Open it and see which of your sends people actually open."
            },
            {
              "type": "action",
              "content": "Open Reports",
              "href": "https://app.acme.com/reports"
            }
          ]
        }
      ]
    }
  }
}
```

templates/feature-tip-inbox.json

```json
{
  "channel": "inbox",
  "notification": {
    "name": "Feature tip (Inbox)",
    "tags": ["recipe", "feature-adoption-email-sequence"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "inbox",
          "elements": [
            {
              "type": "meta",
              "title": "Try Reports"
            },
            {
              "type": "text",
              "content": "Open Reports and see which of your sends people actually open."
            },
            {
              "type": "action",
              "content": "Open Reports",
              "href": "https://app.acme.com/reports"
            }
          ]
        }
      ]
    }
  }
}
```

templates/feature-second-tip-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Feature second tip (email)",
    "tags": ["recipe", "feature-adoption-email-sequence"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "A quick way to start with Reports"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "Not sure where to begin with Reports? Open the report for your latest send and check who opened it."
            },
            {
              "type": "action",
              "content": "Open Reports",
              "href": "https://app.acme.com/reports"
            }
          ]
        }
      ]
    }
  }
}
```

## Hand it to your agent

```text
Add a feature adoption sequence to this app, sent through Courier: a tip by email and in the Inbox 7 days after signup, with an A/B test on the email, then a second tip 4 days later, all canceled once they use our reports feature. Follow the steps at https://www.courier.com/recipes/feature-adoption-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, Reports, and the link 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_ADOPTION_JOURNEY_ID.
2. Find where this codebase creates a new user and start the journey there. Pass only the user ID, and make sure first_name is written to their Courier profile at signup.
3. Find where a user first uses the feature and cancel the journey there with the token adopt-<user_id>. Cancel it the same way 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 |

## FAQ

### How do I A/B test an email in a journey?

Put an experiment on the send node. It holds two to ten template variants with a weight each, and Courier assigns each user to one by a bucketing key. Open and click counts are tracked per variant.

### Does Courier pick the better email variant?

No. Courier splits the traffic and shows you the results for each variant. You decide when you've seen enough, then promote the variant you want. It becomes the only template on the send node.

### How do I stop the emails once someone uses the feature?

When they use it, cancel the run with the token adopt-<user_id>. Every step still to come is dropped, and nothing is stored on the profile.

### When should I send a feature adoption email?

Once they've had time to get into the product but haven't tried it. This recipe waits 7 days after signup. Change the first wait to match your own activation window.

## Related docs

- [Journey experiments](https://www.courier.com/docs/journeys/experiments)
- [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.
- [Welcome email sequence](https://www.courier.com/recipes/welcome-email-sequence): Welcome every new user, then nudge only the ones who haven't finished setup.
- [NPS survey email](https://www.courier.com/recipes/nps-survey-email): Ask engaged users for an NPS score with one click, and survey nobody more than once every 90 days.
