# Win-back email sequence: template and code

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

Bring back users who've gone quiet, and restart the clock whenever they log in.

A win-back email sequence re-engages users who stop logging in. Fourteen days after their last login it sends an email, then an Inbox note 4 days later, and a final email 3 days after that. Every login restarts the wait, so people who come back never get these messages.

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 win-back emails

| Step | When | Channels | Subject |
| --- | --- | --- | --- |
| 1 | 14 days after a user's last login | Email | It's been a while, {{default profile.first_name "there"}} |
| 2 | 4 days after the first email | Inbox |  |
| 3 | 3 days after the Inbox note | Email | Last note from us, {{default profile.first_name "there"}} |

Want a different gap? Change the P14D, P4D, and P3D waits in the journey.

## Copy

### Win-back, first email

Subject: "It's been a while, {{default profile.first_name "there"}}", "We haven't seen you in a while", "Still want to use this?"

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

It's been a couple of weeks since you last logged in. Jump back in whenever you're ready.

Button: Log back in (https://app.acme.com/login)

### Win-back, Inbox note

Subject: "Pick up where you left off"

It's been a while since your last visit.

Button: Open the app (https://app.acme.com)

### Win-back, last email

Subject: "Last note from us, {{default profile.first_name "there"}}", "Should we stop emailing you?"

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

This is the last email in this series. If you'd like to come back, your account is one click away. If something got in the way, reply and tell us.

Button: Log back in (https://app.acme.com/login)

## The journey

```json
{
  "name": "Win-back email sequence",
  "cancelation_token": "reengage-{{recipient}}",
  "nodes": [
    {
      "type": "trigger",
      "trigger_type": "api-invoke"
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P14D"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@winback-first-email"
      }
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P4D"
    },
    {
      "type": "send",
      "channel": "inbox",
      "message": {
        "template": "@winback-inbox"
      }
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P3D"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@winback-last-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 }));
```

## Restart it on every login

```typescript
// Restart the win-back sequence on every login, with the journey ID setup.ts
// printed. It cancels the user's current run and starts a new one, so the
// journey's first 14-day wait begins again. Call it at most once a day per user,
// for example on their first login of the day. Nothing is passed to the journey:
// the templates read the user's name and email from their Courier profile.
import Courier from "@trycourier/courier";

const client = new Courier();

export async function restartWinBack(journeyId: string, userId: string) {
  await client.journeys.cancel({ cancelation_token: `reengage-${userId}` });
  const { runId } = await client.journeys.invoke(journeyId, {
    user_id: userId,
  });
  return runId;
}
```

```python
# Restart the win-back sequence on every login, with the journey ID setup.ts
# printed. It cancels the user's current run and starts a new one, so the
# journey's first 14-day wait begins again. Call it at most once a day per user,
# for example on their first login of the day. Nothing is passed to the journey:
# the templates read the user's name and email from their Courier profile.
from courier import Courier

client = Courier()


def restart_win_back(journey_id: str, user_id: str) -> str:
    client.journeys.cancel(cancelation_token=f"reengage-{user_id}")
    response = client.journeys.invoke(template_id=journey_id, user_id=user_id)
    return response.run_id
```

## Templates

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

templates/winback-first-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Win-back, first email",
    "tags": ["recipe", "win-back"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "It's been a while, {{default profile.first_name \"there\"}}"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "It's been a couple of weeks since you last logged in. Jump back in whenever you're ready."
            },
            {
              "type": "action",
              "content": "Log back in",
              "href": "https://app.acme.com/login"
            }
          ]
        }
      ]
    }
  }
}
```

templates/winback-inbox.json

```json
{
  "channel": "inbox",
  "notification": {
    "name": "Win-back, Inbox note",
    "tags": ["recipe", "win-back"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "inbox",
          "elements": [
            {
              "type": "meta",
              "title": "Pick up where you left off"
            },
            {
              "type": "text",
              "content": "It's been a while since your last visit."
            },
            {
              "type": "action",
              "content": "Open the app",
              "href": "https://app.acme.com"
            }
          ]
        }
      ]
    }
  }
}
```

templates/winback-last-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Win-back, last email",
    "tags": ["recipe", "win-back"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "Last note from us, {{default profile.first_name \"there\"}}"
            },
            {
              "type": "text",
              "content": "Hi {{default profile.first_name \"there\"}},"
            },
            {
              "type": "text",
              "content": "This is the last email in this series. If you'd like to come back, your account is one click away. If something got in the way, reply and tell us."
            },
            {
              "type": "action",
              "content": "Log back in",
              "href": "https://app.acme.com/login"
            }
          ]
        }
      ]
    }
  }
}
```

## Hand it to your agent

```text
Add a win-back email sequence to this app, sent through Courier: an email 14 days after a user's last login, an Inbox note 4 days later, and a last email 3 days after that, restarted every time they log in. Follow the steps at https://www.courier.com/recipes/win-back-email-sequence.md and use the Courier skill for SDK details.

1. Create the journey and its three 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_WINBACK_JOURNEY_ID.
2. Find where a user logs in and call restartWinBack from start.ts with that journey ID and the user's ID, at most once a day per user (for example on their first login of the day). It cancels the journey with the token reengage-<user_id> and starts it again.
3. The templates read first_name and email from the user's Courier profile, so make sure those are written at signup.
4. Match this codebase's language, file layout, and error handling. Never hardcode the API key.
5. When you're done, call restartWinBack for a test user and show me the run that starts for them.
```

## Data fields

| Field | Type | Example | Note |
| --- | --- | --- | --- |
| profile.first_name | string | Dana | On the user's Courier profile. The emails say "there" when it's missing |
| profile.email | email | dana@example.com | Where the emails go |

## FAQ

### How long should I wait before sending a win-back email?

Match the gap to how often people normally use your product. This recipe uses 14 days. To change it, edit the first wait (P14D) in the Journeys editor.

### How many win-back emails should I send?

Three touches over a week is enough: an email, an Inbox note, and one last email. If they haven't come back by then, stop.

### How do I stop win-back emails when someone logs back in?

Call restartWinBack from your login code. It cancels the run with the token reengage-<user ID> and starts a new one, so the 14 days count from the latest login. Call it at most once a day per user, for example on their first login of the day. For a deleted account, cancel the same token and don't invoke again.

### Why is there no audience or daily job?

The journey's own 14-day wait does the counting, so your app only has to say that a user logged in. Nothing tracks days inactive, and nothing sends to a user who keeps logging in, because each login cancels the old run before it gets past its first wait.

## Related docs

- [Delay node](https://www.courier.com/docs/journeys/nodes/delay)
- [Send node](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

- [Cancellation follow-up](https://www.courier.com/recipes/subscription-cancellation-email): Confirm the cancellation, ask why, and make one good offer to come back.
- [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.
- [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.
