Onboarding

Team invite email with reminder: template and code

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

Passing, tested 2026-10-19 · Email, Inbox

How it works

When to send invite reminders

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.

  1. Invite sent
    Your app starts the journey
  2. Right away
    Invite email to the invitee
  3. Day 3
    Reminder email to the invitee
  4. Day 7
    Inbox note to the inviter

Stops early: Accepting the invite cancels every message still to come.

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

Messages

What each step sends

Step 1: Right after the invite is sent

Invite email to the invitee.

Email

From: Acme
Dana invited you to Growth on Acme

Hi,

Dana invited you to join Growth on Acme.

Accept invite

Other subject lines: "Join Growth on Acme"

Step 2: 3 days after the invite

Reminder email to the invitee.

Email

From: Acme
Your invite to Growth is waiting

Hi,

Dana invited you to Growth on Acme a few days ago. Join to see what the team is working on.

Accept invite

Other subject lines: "Dana is waiting for you on Acme"

Step 3: 7 days after the invite

Inbox note to the inviter.

In-app Inbox

sam@example.com hasn't joined yet

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

Resend invite

Build

Add it to your app

1. Create the journey

Run setup.ts once with your API key. It creates the templates and the journey, publishes it, and prints the journey ID you pass to step 2. Copy setup.ts, journey.json, and the templates folder from the full definition at the end of this section.

Terminal

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

2. Start it when someone sends an invite

Node.js

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

3. Stop it when the invite is accepted

Node.js

// 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}")
Full definition: journey.json, setup.ts, and the 3 templates

The invite email goes to the invitee's address right away, and a reminder follows on day 3. On day 7, the person who sent the invite gets an Inbox note.

The journey runs as the inviter, so the invitee doesn't need a Courier profile or an account yet.

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"
}
]
}

setup.ts

// 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 }));

templates/invite-email.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

{
"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

{
"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"
}
]
}
]
}
}
}

Agents

Hand it to your agent

Give this prompt to Claude Code, Cursor, or Codex with the Courier skill installed. Agents can also read the markdown version and the manifest.

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

Data fields

FieldTypeExampleNote
invite_idstringinv_7Kq2Builds the cancellation token and the accept link
invitee_emailemailsam@example.comWhere the invite and reminder go
team_namestringGrowth
profile.first_namestringDanaFrom the inviter's profile. "A teammate" is the fallback when it's missing

FAQ

Frequently asked questions

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.

Build this in your workspace

Run setup.ts against a free account and the journey is live in a minute. Prefer no code? Rebuild it in the Journeys editor.