# Trial ending email sequence: template and code

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

Remind trial users before their trial ends, and stop the moment they upgrade.

A trial ending email sequence reminds users that their free trial is about to expire. It sends an email and an Inbox note 7 days before the trial ends, another a day before, with a text if the first email wasn't opened, and a final email when it ends. Upgrading stops the rest.

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 trial ending emails

| Step | When | Channels | Subject |
| --- | --- | --- | --- |
| 1 | 7 days before the trial ends, after the journey's first wait | Email, Inbox | Your trial ends on {{datetime-format trial_end_date "MMMM d" (default profile.timezone "America/New_York")}} |
| 2 | 6 days after step 1, 1 day before the trial ends | Email, Inbox. SMS too if step 1's email wasn't opened | Your trial ends tomorrow |
| 3 | 1 day after step 2, when the trial ends | Email | Your trial has ended |

The first wait assumes a 14-day trial (P7D). For another length, set it to the trial length minus 7 days, like P3D for a 10-day trial.

## Copy

### First trial reminder (email)

Subject: "Your trial ends on {{datetime-format trial_end_date "MMMM d" (default profile.timezone "America/New_York")}}", "Your {{plan_name}} trial ends on {{datetime-format trial_end_date "MMMM d" (default profile.timezone "America/New_York")}}", "Pick a plan before {{datetime-format trial_end_date "MMMM d" (default profile.timezone "America/New_York")}}"

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

Your {{plan_name}} trial ends on {{datetime-format trial_end_date "MMMM d" (default profile.timezone "America/New_York")}}. Pick a plan to keep everything running with no gap.

Not sure which plan fits? Reply to this email.

Button: Choose a plan (https://app.acme.com/billing)

### First trial reminder (Inbox)

Subject: "Your trial ends on {{datetime-format trial_end_date "MMMM d" (default profile.timezone "America/New_York")}}"

Pick a plan to keep everything running with no gap.

Button: Choose a plan (https://app.acme.com/billing)

### Trial ends tomorrow (email)

Subject: "Your trial ends tomorrow", "Last day of your {{plan_name}} trial"

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

Your {{plan_name}} trial ends tomorrow, {{datetime-format trial_end_date "MMMM d" (default profile.timezone "America/New_York")}}. Choose a plan today and nothing changes for you or your team.

Button: Choose a plan (https://app.acme.com/billing)

### Trial ends tomorrow (Inbox)

Subject: "Your trial ends tomorrow"

Choose a plan today to keep your account running.

Button: Choose a plan (https://app.acme.com/billing)

### Trial ends tomorrow (SMS)

Acme: your trial ends tomorrow. Keep your account running: https://app.acme.com/billing Reply STOP to opt out.

### Trial ended (email)

Subject: "Your trial has ended", "Your trial ended. Your work is still here"

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

Your {{plan_name}} trial ended today. Everything you set up is saved, so you can pick up where you left off when you choose a plan.

Button: Choose a plan (https://app.acme.com/billing)

## The journey

```json
{
  "name": "Trial ending reminders",
  "cancelation_token": "trial-{{recipient}}",
  "nodes": [
    {
      "type": "trigger",
      "trigger_type": "api-invoke"
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P7D"
    },
    {
      "id": "trial-7d-email",
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@trial-7d-email"
      }
    },
    {
      "type": "send",
      "channel": "inbox",
      "message": {
        "template": "@trial-7d-inbox"
      }
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P6D"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@trial-1d-email"
      }
    },
    {
      "type": "send",
      "channel": "inbox",
      "message": {
        "template": "@trial-1d-inbox"
      }
    },
    {
      "type": "send",
      "channel": "sms",
      "message": {
        "template": "@trial-1d-sms"
      },
      "conditions": {
        "AND": [
          ["send_status.trial-7d-email", "was not", "OPENED"],
          ["profile.phone_number", "exists"]
        ]
      }
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P1D"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@trial-ended-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 the trial starts

```typescript
// Start the reminders when a trial starts, with the journey ID setup.ts
// printed. The journey's first wait (P7D, for a 14-day trial) holds the first
// reminder until 7 days before the trial ends, and its later waits time the
// rest. For another trial length, change P7D to the trial length minus 7 days.
// Names come from the user's Courier profile. Runs are tagged with the user's
// ID, so stopTrialReminders takes the same ID.
import Courier from "@trycourier/courier";

const client = new Courier();

export async function startTrialReminders(
  journeyId: string,
  userId: string,
  trial: { plan: string; endsAt: Date }
) {
  const { runId } = await client.journeys.invoke(journeyId, {
    user_id: userId,
    data: {
      plan_name: trial.plan,
      trial_end_date: trial.endsAt.toISOString(),
    },
  });
  return runId;
}
```

```python
# Start the reminders when a trial starts, with the journey ID setup.ts
# printed. The journey's first wait (P7D, for a 14-day trial) holds the first
# reminder until 7 days before the trial ends, and its later waits time the
# rest. For another trial length, change P7D to the trial length minus 7 days.
# Names come from the user's Courier profile. Runs are tagged with the user's
# ID, so stop_trial_reminders takes the same ID.
from datetime import datetime

from courier import Courier

client = Courier()


def start_trial_reminders(journey_id: str, user_id: str, trial: dict) -> str:
    ends_at: datetime = trial["ends_at"]
    response = client.journeys.invoke(
        template_id=journey_id,
        user_id=user_id,
        data={
            "plan_name": trial["plan"],
            "trial_end_date": ends_at.isoformat(),
        },
    )
    return response.run_id
```

## Stop it when they upgrade

```typescript
// Stop the reminders when the account upgrades: call this from your billing
// webhook. It cancels every active run for the user, and does nothing if
// the sequence already finished.
import Courier from "@trycourier/courier";

const client = new Courier();

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

```python
# Stop the reminders when the account upgrades: call this from your billing
# webhook. It cancels every active run for the user, and does nothing if
# the sequence already finished.
from courier import Courier

client = Courier()


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

## Templates

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

templates/trial-7d-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "First trial reminder (email)",
    "tags": ["recipe", "trial-ending"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "Your trial ends on {{datetime-format trial_end_date \"MMMM d\" (default profile.timezone \"America/New_York\")}}"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "Your {{plan_name}} trial ends on {{datetime-format trial_end_date \"MMMM d\" (default profile.timezone \"America/New_York\")}}. Pick a plan to keep everything running with no gap."
            },
            {
              "type": "action",
              "content": "Choose a plan",
              "href": "https://app.acme.com/billing"
            },
            {
              "type": "text",
              "content": "Not sure which plan fits? Reply to this email."
            }
          ]
        }
      ]
    }
  }
}
```

templates/trial-7d-inbox.json

```json
{
  "channel": "inbox",
  "notification": {
    "name": "First trial reminder (Inbox)",
    "tags": ["recipe", "trial-ending"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "inbox",
          "elements": [
            {
              "type": "meta",
              "title": "Your trial ends on {{datetime-format trial_end_date \"MMMM d\" (default profile.timezone \"America/New_York\")}}"
            },
            {
              "type": "text",
              "content": "Pick a plan to keep everything running with no gap."
            },
            {
              "type": "action",
              "content": "Choose a plan",
              "href": "https://app.acme.com/billing"
            }
          ]
        }
      ]
    }
  }
}
```

templates/trial-1d-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Trial ends tomorrow (email)",
    "tags": ["recipe", "trial-ending"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "Your trial ends tomorrow"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "Your {{plan_name}} trial ends tomorrow, {{datetime-format trial_end_date \"MMMM d\" (default profile.timezone \"America/New_York\")}}. Choose a plan today and nothing changes for you or your team."
            },
            {
              "type": "action",
              "content": "Choose a plan",
              "href": "https://app.acme.com/billing"
            }
          ]
        }
      ]
    }
  }
}
```

templates/trial-1d-inbox.json

```json
{
  "channel": "inbox",
  "notification": {
    "name": "Trial ends tomorrow (Inbox)",
    "tags": ["recipe", "trial-ending"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "inbox",
          "elements": [
            {
              "type": "meta",
              "title": "Your trial ends tomorrow"
            },
            {
              "type": "text",
              "content": "Choose a plan today to keep your account running."
            },
            {
              "type": "action",
              "content": "Choose a plan",
              "href": "https://app.acme.com/billing"
            }
          ]
        }
      ]
    }
  }
}
```

templates/trial-1d-sms.json

```json
{
  "channel": "sms",
  "notification": {
    "name": "Trial ends tomorrow (SMS)",
    "tags": ["recipe", "trial-ending"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "sms",
          "elements": [
            {
              "type": "text",
              "content": "Acme: your trial ends tomorrow. Keep your account running: https://app.acme.com/billing Reply STOP to opt out."
            }
          ]
        }
      ]
    }
  }
}
```

templates/trial-ended-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Trial ended (email)",
    "tags": ["recipe", "trial-ending"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "Your trial has ended"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "Your {{plan_name}} trial ended today. Everything you set up is saved, so you can pick up where you left off when you choose a plan."
            },
            {
              "type": "action",
              "content": "Choose a plan",
              "href": "https://app.acme.com/billing"
            }
          ]
        }
      ]
    }
  }
}
```

## Hand it to your agent

```text
Add trial ending reminders to this app, sent through Courier: two reminders before a trial ends, a final note when it ends, and nothing more once the account upgrades. Follow the steps at https://www.courier.com/recipes/trial-ending-email-sequence.md and use the Courier skill for SDK details.

1. Create the journey and its six templates in my Courier workspace by running setup.ts from that page with COURIER_API_KEY from our environment. Save the journey ID it prints as COURIER_TRIAL_JOURNEY_ID. Replace the placeholder product name (Acme) and the billing link in the templates with ours, and if our trials aren't 14 days, set the journey's first wait to the trial length minus 7 days.
2. Find where this codebase starts a trial and start the journey there with the user's ID. Pass the fields in the Data fields table from our own data.
3. Find where an account upgrades (usually the billing webhook) and cancel the journey with the token trial-<user ID>. When a trial is extended, cancel it and start it again.
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. The run waits before its first reminder, so cancel the test run once you've checked it.
```

## Data fields

| Field | Type | Example | Note |
| --- | --- | --- | --- |
| profile.first_name | string | Dana | From the profile. "there" is the fallback when it's missing |
| plan_name | string | Pro |  |
| trial_end_date | string | 2026-10-29T17:00:00Z | An ISO timestamp. The templates format it as a date in the recipient's timezone |

## FAQ

### How many trial reminder emails should I send?

Two before the trial ends and one when it ends covers most trials. The journey starts when the trial starts and waits P7D before the first reminder, which suits a 14-day trial. For another length, set that first wait to the trial length minus 7 days. To change any other gap, edit the wait durations in the Journeys editor.

### How do I stop trial reminders when someone upgrades?

Each run is tagged with the user's ID, so cancel the token trial-<user_id> from your billing webhook. Courier stops every active run for that user, including a run still on its first wait, and leaves finished runs alone.

### When does a trial reminder go by SMS?

On step 2, when the first reminder email wasn't opened and the user has a phone number on their profile. Everyone else gets the email and Inbox note without the text.

### What if I extend someone's trial?

Cancel the running journey and start a new one. The waits count from the start, so for an extension of a different length, edit the first wait in a copy of the journey. The templates read the new trial_end_date you pass.

## Related docs

- [Delay node](https://www.courier.com/docs/journeys/nodes/delay)
- [Cancel a run](https://www.courier.com/docs/journeys/nodes/cancel)
- [Invoke a journey](https://www.courier.com/docs/journeys/invoke)
- [Send a dunning email from Stripe](https://www.courier.com/docs/guides/notify-from-stripe)

## Related recipes

- [Stalled trial alert](https://www.courier.com/recipes/stalled-trial-slack-alert): Hand stuck, high-value trials to a person instead of sending another email.
- [Failed payment recovery](https://www.courier.com/recipes/dunning-email-sequence): Recover failed payments with three reminders that stop the moment the card goes through.
- [Renewal reminders](https://www.courier.com/recipes/subscription-renewal-reminder-email): Remind annual customers before they renew, so the charge never comes as a surprise.
