# Team invite email with reminder: template and code

Source: https://www.courier.com/recipes/team-invite-reminder-email (manifest: https://www.courier.com/recipes/team-invite-reminder-email/manifest.json)

Invite a teammate, remind them if they don't join, and tell the inviter if they still haven't.

A team invite reminder makes sure invited teammates actually join. It sends the invite email when someone adds a teammate, reminds the invitee after 3 days, and leaves an Inbox note for the person who sent the invite after 7 days if it's still pending. Accepting or revoking the invite 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 invite reminders

| Step | When | Channels | Subject |
| --- | --- | --- | --- |
| 1 | Right after the invite is sent | Email | {{default profile.first_name "A teammate"}} invited you to {{team_name}} on Acme |
| 2 | 3 days after the invite | Email | Your invite to {{team_name}} is waiting |
| 3 | 7 days after the invite | Inbox |  |

## Copy

### Team invite (email)

Subject: "{{default profile.first_name "A teammate"}} invited you to {{team_name}} on Acme", "Join {{team_name}} on Acme"

Hi,

{{default profile.first_name "A teammate"}} invited you to join {{team_name}} on Acme.

Button: Accept invite (https://app.acme.com/invite/{{invite_id}})

### Invite reminder (email)

Subject: "Your invite to {{team_name}} is waiting", "{{default profile.first_name "A teammate"}} is waiting for you on Acme"

Hi,

{{default profile.first_name "A teammate"}} invited you to {{team_name}} on Acme a few days ago. Join to see what the team is working on.

Button: Accept invite (https://app.acme.com/invite/{{invite_id}})

### Invite not accepted (Inbox, to the inviter)

Subject: "{{invitee_email}} hasn't joined yet"

Your invite to {{team_name}} is still open. Resend it, or check with them that it arrived.

Button: Resend invite (https://app.acme.com/settings/team)

## The journey

```json
{
  "name": "Team invite with reminder",
  "cancelation_token": "invite-{{data.invite_id}}",
  "nodes": [
    {
      "type": "trigger",
      "trigger_type": "api-invoke"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@invite-email",
        "to": {
          "email_override": "{{data.invitee_email}}"
        }
      }
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P3D"
    },
    {
      "type": "send",
      "channel": "email",
      "message": {
        "template": "@invite-reminder-email",
        "to": {
          "email_override": "{{data.invitee_email}}"
        }
      }
    },
    {
      "type": "delay",
      "mode": "duration",
      "duration": "P4D"
    },
    {
      "type": "send",
      "channel": "inbox",
      "message": {
        "template": "@inviter-inbox"
      }
    },
    {
      "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 sends an invite

```typescript
// Start the invite when someone invites a teammate, with the journey ID setup.ts printed.
// The journey runs as the inviter, so the templates read their first name from
// their Courier profile. The emails go to the invitee's address.
import Courier from "@trycourier/courier";

const client = new Courier();

export async function startInvite(
  journeyId: string,
  inviterId: string,
  invite: { id: string; email: string; teamName: string }
) {
  const { runId } = await client.journeys.invoke(journeyId, {
    user_id: inviterId,
    data: {
      invite_id: invite.id,
      invitee_email: invite.email,
      team_name: invite.teamName,
    },
  });
  return runId;
}
```

```python
# Start the invite when someone invites a teammate, with the journey ID setup.ts printed.
# The journey runs as the inviter, so the templates read their first name from
# their Courier profile. The emails go to the invitee's address.
from courier import Courier

client = Courier()


def start_invite(journey_id: str, inviter_id: str, invite: dict) -> str:
    response = client.journeys.invoke(
        template_id=journey_id,
        user_id=inviter_id,
        data={
            "invite_id": invite["id"],
            "invitee_email": invite["email"],
            "team_name": invite["team_name"],
        },
    )
    return response.run_id
```

## Stop it when the invite is accepted

```typescript
// Stop the reminders when the invite is accepted or revoked.
import Courier from "@trycourier/courier";

const client = new Courier();

export async function stopInvite(inviteId: string) {
  await client.journeys.cancel({ cancelation_token: `invite-${inviteId}` });
}
```

```python
# Stop the reminders when the invite is accepted or revoked.
from courier import Courier

client = Courier()


def stop_invite(invite_id: str) -> None:
    client.journeys.cancel(cancelation_token=f"invite-{invite_id}")
```

## Templates

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

templates/invite-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Team invite (email)",
    "tags": ["recipe", "team-invite-reminder-email"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "{{default profile.first_name \"A teammate\"}} invited you to {{team_name}} on Acme"
            },
            {
              "type": "text",
              "content": "Hi,"
            },
            {
              "type": "text",
              "content": "{{default profile.first_name \"A teammate\"}} invited you to join {{team_name}} on Acme."
            },
            {
              "type": "action",
              "content": "Accept invite",
              "href": "https://app.acme.com/invite/{{invite_id}}"
            }
          ]
        }
      ]
    }
  }
}
```

templates/invite-reminder-email.json

```json
{
  "channel": "email",
  "notification": {
    "name": "Invite reminder (email)",
    "tags": ["recipe", "team-invite-reminder-email"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "Your invite to {{team_name}} is waiting"
            },
            {
              "type": "text",
              "content": "Hi,"
            },
            {
              "type": "text",
              "content": "{{default profile.first_name \"A teammate\"}} invited you to {{team_name}} on Acme a few days ago. Join to see what the team is working on."
            },
            {
              "type": "action",
              "content": "Accept invite",
              "href": "https://app.acme.com/invite/{{invite_id}}"
            }
          ]
        }
      ]
    }
  }
}
```

templates/inviter-inbox.json

```json
{
  "channel": "inbox",
  "notification": {
    "name": "Invite not accepted (Inbox, to the inviter)",
    "tags": ["recipe", "team-invite-reminder-email"],
    "brand": null,
    "subscription": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "inbox",
          "elements": [
            {
              "type": "meta",
              "title": "{{invitee_email}} hasn't joined yet"
            },
            {
              "type": "text",
              "content": "Your invite to {{team_name}} is still open. Resend it, or check with them that it arrived."
            },
            {
              "type": "action",
              "content": "Resend invite",
              "href": "https://app.acme.com/settings/team"
            }
          ]
        }
      ]
    }
  }
}
```

## Hand it to your agent

```text
Add team invite emails with a reminder to this app, sent through Courier: the invite right away, a reminder to the invitee after 3 days, and an in-app note to the inviter after 7 days if they still haven't joined. Follow the steps at https://www.courier.com/recipes/team-invite-reminder-email.md and use the Courier skill for SDK details.

1. Create the journey and its templates in my Courier workspace by replacing Acme and the settings 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_INVITE_JOURNEY_ID.
2. Find where this codebase creates a team invite and start the journey there, as the inviter. Pass the fields in the Data fields table from our own data, and replace the accept-link base URL in the templates with ours. Make sure the inviter's first_name is written to their Courier profile at signup.
3. Find where an invite is accepted or revoked and cancel the journey with the token invite-<invite_id>.
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 |
| --- | --- | --- | --- |
| invite_id | string | inv_7Kq2 | Builds the cancellation token and the accept link |
| invitee_email | email | sam@example.com | Where the invite and reminder go |
| team_name | string | Growth |  |
| profile.first_name | string | Dana | From the inviter's profile. "A teammate" is the fallback when it's missing |

## FAQ

### How many team invite reminders should I send?

One reminder to the invitee after a few days, then hand it back to the person who invited them. A second automated reminder rarely works better than a nudge from someone they know.

### Does the invitee need an account before they get the invite?

No. The invite and reminder go to the email address you pass in, so the invitee doesn't need a Courier profile or an account in your app yet.

### How do I stop the reminders when someone accepts an invite?

Tag each run with the invite ID, then cancel that token when the invite is accepted or revoked.

## Related docs

- [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

- [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.
- [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.
- [Batched comment notifications](https://www.courier.com/recipes/comment-notification-batching): Turn a burst of comments into one notification instead of one per comment.
