Most SaaS apps send a welcome email and then go quiet until the trial expires. That gap is where you lose users. They signed up with intent, hit a wall during setup, and never came back. No nudge, no guide, no reason to return.
This post walks through a five-email onboarding sequence that covers the full trial lifecycle: from signup to conversion. Each email has a specific job, a specific send time, and production-ready React Email code you can drop into a Next.js app with Resend. Personalized onboarding sequences like this consistently drive 27% higher activation rates compared to generic blasts, and well-timed sequences regularly hit 40%+ open rates.
Here is the full sequence at a glance:
| Timing | Goal | Key Metric | |
|---|---|---|---|
| Welcome | Immediate | Complete account setup | Setup completion rate |
| Email verification | Immediate | Verify address, set expectations | Verification rate |
| Getting started guide | Day 1 (2 hrs after signup) | Drive first key action | Feature activation rate |
| Trial midpoint check-in | Day 7 | Re-engage, surface unused features | DAU / retention |
| Trial ending nudge | Day 12 of 14 | Convert to paid | Trial-to-paid conversion |
The 5-email onboarding sequence for a 14-day SaaS trial
The five emails, step by step
Each email below includes the reasoning behind its timing, a recommended subject line, and the full React Email template. All templates use inline styles for maximum email client compatibility and are typed with TypeScript interfaces for the props you will pass from your sending logic.
Welcome email (immediate, on signup)
The welcome email has one job: get the user back into your app to complete setup. Not three jobs. Not a product tour crammed into an email. One clear CTA that takes them to the next step in their onboarding flow.
Timing: Send within seconds of signup. Every minute of delay reduces open rates. The user is actively engaged right now.
Subject line: Welcome to [Product] - let's get you set up
import {
Html, Head, Preview, Body, Container,
Heading, Text, Button, Hr, Link, Img,
} from "@react-email/components";
interface WelcomeEmailProps {
name: string;
setupUrl: string;
trialDaysLeft: number;
}
export default function WelcomeEmail({
name = "Alex",
setupUrl = "https://app.yourproduct.com/setup",
trialDaysLeft = 14,
}: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>
Welcome to YourProduct - let's get you set up
</Preview>
<Body style={main}>
<Container style={container}>
<Img
src="https://yourproduct.com/logo.png"
width={120}
height={36}
alt="YourProduct"
/>
<Heading style={heading}>Welcome, {name}</Heading>
<Text style={paragraph}>
You've got {trialDaysLeft} days to explore everything
YourProduct can do. Most teams see value within the
first 10 minutes of setup.
</Text>
<Text style={paragraph}>
The fastest way to get started: connect your first
data source. It takes about 2 minutes.
</Text>
<Button href={setupUrl} style={button}>
Complete your setup
</Button>
<Text style={muted}>
If you have questions, reply to this email. A real
person will get back to you.
</Text>
<Hr style={divider} />
<Text style={footer}>
YourProduct, Inc. · 123 Main St, San Francisco, CA
</Text>
</Container>
</Body>
</Html>
);
}
const main = {
backgroundColor: "#f6f9fc",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
backgroundColor: "#ffffff",
margin: "40px auto",
padding: "40px",
maxWidth: "520px",
borderRadius: "8px",
};
const heading = {
fontSize: "22px",
fontWeight: "600" as const,
color: "#1a1a1a",
margin: "24px 0 16px",
};
const paragraph = {
fontSize: "15px",
lineHeight: "24px",
color: "#4a4a4a",
margin: "0 0 16px",
};
const button = {
backgroundColor: "#2563eb",
borderRadius: "6px",
color: "#ffffff",
fontSize: "15px",
fontWeight: "600" as const,
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "12px 24px",
margin: "24px 0",
};
const muted = {
fontSize: "13px",
color: "#999",
margin: "0 0 16px",
};
const divider = { borderColor: "#e6e6e6", margin: "24px 0" };
const footer = { fontSize: "12px", color: "#999", margin: "0" };Email verification (immediate)
This one is functional, not marketing. The user needs to verify their address before they can use your product fully. Keep it short, keep the CTA obvious, and set expectations about what happens after verification.
Timing: Immediate, sent alongside or just after the welcome email. Some teams combine these into one email - that works too, but separating them keeps each message focused.
Subject line: Verify your email address
import {
Html, Head, Preview, Body, Container,
Heading, Text, Button, Hr, Img,
} from "@react-email/components";
interface EmailVerificationProps {
name: string;
verificationUrl: string;
expiresInHours: number;
}
export default function EmailVerification({
name = "Alex",
verificationUrl = "https://app.yourproduct.com/verify?token=abc123",
expiresInHours = 24,
}: EmailVerificationProps) {
return (
<Html>
<Head />
<Preview>Verify your email to activate your account</Preview>
<Body style={main}>
<Container style={container}>
<Img
src="https://yourproduct.com/logo.png"
width={120}
height={36}
alt="YourProduct"
/>
<Heading style={heading}>Verify your email</Heading>
<Text style={paragraph}>Hi {name},</Text>
<Text style={paragraph}>
Click the button below to verify your email address
and activate your account. This link expires in{" "}
{expiresInHours} hours.
</Text>
<Button href={verificationUrl} style={button}>
Verify email address
</Button>
<Text style={muted}>
If you didn't create an account with YourProduct,
you can safely ignore this email.
</Text>
<Hr style={divider} />
<Text style={footer}>
YourProduct, Inc. · 123 Main St, San Francisco, CA
</Text>
</Container>
</Body>
</Html>
);
}
const main = {
backgroundColor: "#f6f9fc",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
backgroundColor: "#ffffff",
margin: "40px auto",
padding: "40px",
maxWidth: "520px",
borderRadius: "8px",
};
const heading = {
fontSize: "22px",
fontWeight: "600" as const,
color: "#1a1a1a",
margin: "24px 0 16px",
};
const paragraph = {
fontSize: "15px",
lineHeight: "24px",
color: "#4a4a4a",
margin: "0 0 16px",
};
const button = {
backgroundColor: "#2563eb",
borderRadius: "6px",
color: "#ffffff",
fontSize: "15px",
fontWeight: "600" as const,
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "12px 24px",
margin: "24px 0",
};
const muted = {
fontSize: "13px",
color: "#999",
margin: "0 0 16px",
};
const divider = { borderColor: "#e6e6e6", margin: "24px 0" };
const footer = { fontSize: "12px", color: "#999", margin: "0" };Getting started guide (Day 1, 2 hours after signup)
Two hours in, the user has either started exploring or abandoned your app entirely. This email gives them a concrete path forward: three specific actions that lead to their first "aha" moment. Not ten features. Not a link to your docs. Three things.
Timing: 2 hours after signup. Early enough that they remember signing up, late enough that it does not pile onto the welcome and verification emails.
Subject line: 3 things to do in your first hour with [Product]
import {
Html, Head, Preview, Body, Container, Section,
Heading, Text, Button, Hr, Img,
} from "@react-email/components";
interface GettingStartedProps {
name: string;
dashboardUrl: string;
steps: {
title: string;
description: string;
timeEstimate: string;
}[];
}
export default function GettingStarted({
name = "Alex",
dashboardUrl = "https://app.yourproduct.com",
steps = [
{
title: "Connect your first data source",
description:
"Link your database or API to start pulling in live data.",
timeEstimate: "2 min",
},
{
title: "Create your first dashboard",
description:
"Pick a template or start from scratch. Either way, you'll have a working view in minutes.",
timeEstimate: "5 min",
},
{
title: "Invite a teammate",
description:
"Collaboration is where the real value kicks in. Share your workspace with one person.",
timeEstimate: "1 min",
},
],
}: GettingStartedProps) {
return (
<Html>
<Head />
<Preview>
3 things to do in your first hour with YourProduct
</Preview>
<Body style={main}>
<Container style={container}>
<Img
src="https://yourproduct.com/logo.png"
width={120}
height={36}
alt="YourProduct"
/>
<Heading style={heading}>
Get the most out of your trial
</Heading>
<Text style={paragraph}>Hi {name},</Text>
<Text style={paragraph}>
Teams that complete these three steps in their first
day are 3x more likely to stick around. Each one
takes a few minutes.
</Text>
{steps.map((step, i) => (
<Section key={i} style={stepSection}>
<Text style={stepNumber}>
{i + 1}. {step.title}
</Text>
<Text style={stepDescription}>
{step.description}
</Text>
<Text style={stepTime}>
~ {step.timeEstimate}
</Text>
</Section>
))}
<Button href={dashboardUrl} style={button}>
Open your dashboard
</Button>
<Text style={muted}>
Stuck on any of these? Reply to this email and
we'll walk you through it.
</Text>
<Hr style={divider} />
<Text style={footer}>
YourProduct, Inc. · 123 Main St, San Francisco, CA
</Text>
</Container>
</Body>
</Html>
);
}
const main = {
backgroundColor: "#f6f9fc",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
backgroundColor: "#ffffff",
margin: "40px auto",
padding: "40px",
maxWidth: "520px",
borderRadius: "8px",
};
const heading = {
fontSize: "22px",
fontWeight: "600" as const,
color: "#1a1a1a",
margin: "24px 0 16px",
};
const paragraph = {
fontSize: "15px",
lineHeight: "24px",
color: "#4a4a4a",
margin: "0 0 16px",
};
const stepSection = {
margin: "16px 0",
padding: "16px",
backgroundColor: "#f9fafb",
borderRadius: "6px",
};
const stepNumber = {
fontSize: "15px",
fontWeight: "600" as const,
color: "#1a1a1a",
margin: "0 0 4px",
};
const stepDescription = {
fontSize: "14px",
lineHeight: "22px",
color: "#4a4a4a",
margin: "0 0 4px",
};
const stepTime = {
fontSize: "12px",
color: "#999",
margin: "0",
};
const button = {
backgroundColor: "#2563eb",
borderRadius: "6px",
color: "#ffffff",
fontSize: "15px",
fontWeight: "600" as const,
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "12px 24px",
margin: "24px 0",
};
const muted = {
fontSize: "13px",
color: "#999",
margin: "0 0 16px",
};
const divider = { borderColor: "#e6e6e6", margin: "24px 0" };
const footer = { fontSize: "12px", color: "#999", margin: "0" };Trial midpoint check-in (Day 7)
By day 7, your user has either built a habit or forgotten about you. This email serves both audiences. For active users, it recaps their progress and highlights features they have not tried yet. For inactive users, it is a gentle re-engagement nudge with a direct offer of help.
Timing: Day 7 of a 14-day trial. This is the psychological midpoint where urgency starts to feel real without being aggressive.
Subject line: Your first week with [Product] - here's what's next
import {
Html, Head, Preview, Body, Container, Section,
Heading, Text, Button, Hr, Img,
} from "@react-email/components";
interface TrialMidpointProps {
name: string;
daysLeft: number;
actionsCompleted: number;
totalActions: number;
unusedFeatures: string[];
dashboardUrl: string;
calendarUrl: string;
}
export default function TrialMidpoint({
name = "Alex",
daysLeft = 7,
actionsCompleted = 2,
totalActions = 5,
unusedFeatures = ["Automated reports", "Slack integration"],
dashboardUrl = "https://app.yourproduct.com",
calendarUrl = "https://cal.com/yourproduct/onboarding",
}: TrialMidpointProps) {
return (
<Html>
<Head />
<Preview>
Your first week with YourProduct - here's what's next
</Preview>
<Body style={main}>
<Container style={container}>
<Img
src="https://yourproduct.com/logo.png"
width={120}
height={36}
alt="YourProduct"
/>
<Heading style={heading}>
Week one: {actionsCompleted}/{totalActions} setup
steps done
</Heading>
<Text style={paragraph}>Hi {name},</Text>
<Text style={paragraph}>
You've been using YourProduct for a week now, and
you've completed {actionsCompleted} of{" "}
{totalActions} setup steps. Nice progress.
</Text>
<Text style={paragraph}>
You have {daysLeft} days left in your trial. Here
are a couple of features you haven't tried yet that
most teams find valuable:
</Text>
<Section style={featureSection}>
{unusedFeatures.map((feature, i) => (
<Text key={i} style={featureItem}>
→ {feature}
</Text>
))}
</Section>
<Button href={dashboardUrl} style={button}>
Continue setup
</Button>
<Text style={paragraph}>
Want a quick walkthrough? Book a 15-minute call with
our team:
</Text>
<Button href={calendarUrl} style={secondaryButton}>
Book a walkthrough
</Button>
<Hr style={divider} />
<Text style={footer}>
YourProduct, Inc. · 123 Main St, San Francisco, CA
</Text>
</Container>
</Body>
</Html>
);
}
const main = {
backgroundColor: "#f6f9fc",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
backgroundColor: "#ffffff",
margin: "40px auto",
padding: "40px",
maxWidth: "520px",
borderRadius: "8px",
};
const heading = {
fontSize: "22px",
fontWeight: "600" as const,
color: "#1a1a1a",
margin: "24px 0 16px",
};
const paragraph = {
fontSize: "15px",
lineHeight: "24px",
color: "#4a4a4a",
margin: "0 0 16px",
};
const featureSection = {
margin: "16px 0",
padding: "16px",
backgroundColor: "#f9fafb",
borderRadius: "6px",
};
const featureItem = {
fontSize: "14px",
color: "#1a1a1a",
margin: "0 0 8px",
fontWeight: "500" as const,
};
const button = {
backgroundColor: "#2563eb",
borderRadius: "6px",
color: "#ffffff",
fontSize: "15px",
fontWeight: "600" as const,
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "12px 24px",
margin: "24px 0",
};
const secondaryButton = {
backgroundColor: "#ffffff",
borderRadius: "6px",
border: "1px solid #d1d5db",
color: "#1a1a1a",
fontSize: "14px",
fontWeight: "500" as const,
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "10px 20px",
margin: "8px 0 24px",
};
const divider = { borderColor: "#e6e6e6", margin: "24px 0" };
const footer = { fontSize: "12px", color: "#999", margin: "0" };The key detail here is personalization. The actionsCompleted and unusedFeaturesprops are driven by your app's usage data. A midpoint email that says "you've connected 2 data sources but haven't tried automated reports" converts far better than a generic "check out these features" blast.
Trial ending nudge (Day 12 of 14-day trial)
This is the conversion email. Two days before the trial expires, urgency is real but not panic-inducing. The email needs three things: a clear deadline, social proof that the product is worth paying for, and a one-click path to upgrade.
Timing: Day 12. Not day 14 (too late - they have already mentally moved on) and not day 10 (too early - no urgency). Two days gives them time to get budget approval or discuss with their team.
Subject line: Your trial ends in 2 days
import {
Html, Head, Preview, Body, Container, Section,
Heading, Text, Button, Hr, Img,
} from "@react-email/components";
interface TrialEndingProps {
name: string;
daysLeft: number;
planName: string;
price: string;
billingPeriod: string;
upgradeUrl: string;
usageHighlight: string;
}
export default function TrialEnding({
name = "Alex",
daysLeft = 2,
planName = "Pro",
price = "$29/mo",
billingPeriod = "monthly",
upgradeUrl = "https://app.yourproduct.com/billing/upgrade",
usageHighlight = "You've created 12 dashboards and invited 3 teammates",
}: TrialEndingProps) {
return (
<Html>
<Head />
<Preview>
Your trial ends in {daysLeft} days
</Preview>
<Body style={main}>
<Container style={container}>
<Img
src="https://yourproduct.com/logo.png"
width={120}
height={36}
alt="YourProduct"
/>
<Heading style={heading}>
Your trial ends in {daysLeft} days
</Heading>
<Text style={paragraph}>Hi {name},</Text>
<Text style={paragraph}>
{usageHighlight}. To keep access to your work and
your team's data, upgrade to the {planName} plan
before your trial expires.
</Text>
<Section style={pricingBox}>
<Text style={pricingPlan}>{planName} Plan</Text>
<Text style={pricingAmount}>{price}</Text>
<Text style={pricingDetail}>
Billed {billingPeriod}. Cancel anytime.
</Text>
</Section>
<Button href={upgradeUrl} style={button}>
Upgrade to {planName}
</Button>
<Section style={proofSection}>
<Text style={proofText}>
"We switched from spreadsheets to YourProduct and
cut our reporting time by 60%. It paid for itself
in the first week."
</Text>
<Text style={proofAuthor}>
- Jamie L., Head of Operations at Acme Co
</Text>
</Section>
<Text style={muted}>
Not ready to upgrade? Your data will be saved for
30 days after your trial ends. You can pick up
where you left off anytime.
</Text>
<Hr style={divider} />
<Text style={footer}>
YourProduct, Inc. · 123 Main St, San Francisco, CA
</Text>
</Container>
</Body>
</Html>
);
}
const main = {
backgroundColor: "#f6f9fc",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
backgroundColor: "#ffffff",
margin: "40px auto",
padding: "40px",
maxWidth: "520px",
borderRadius: "8px",
};
const heading = {
fontSize: "22px",
fontWeight: "600" as const,
color: "#1a1a1a",
margin: "24px 0 16px",
};
const paragraph = {
fontSize: "15px",
lineHeight: "24px",
color: "#4a4a4a",
margin: "0 0 16px",
};
const pricingBox = {
margin: "24px 0",
padding: "20px",
backgroundColor: "#f9fafb",
borderRadius: "8px",
border: "1px solid #e5e7eb",
textAlign: "center" as const,
};
const pricingPlan = {
fontSize: "14px",
fontWeight: "600" as const,
color: "#1a1a1a",
margin: "0 0 4px",
};
const pricingAmount = {
fontSize: "28px",
fontWeight: "700" as const,
color: "#1a1a1a",
margin: "0 0 4px",
};
const pricingDetail = {
fontSize: "13px",
color: "#999",
margin: "0",
};
const button = {
backgroundColor: "#2563eb",
borderRadius: "6px",
color: "#ffffff",
fontSize: "15px",
fontWeight: "600" as const,
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "12px 24px",
margin: "0 0 24px",
};
const proofSection = {
margin: "0 0 24px",
padding: "16px",
borderLeft: "3px solid #e5e7eb",
};
const proofText = {
fontSize: "14px",
lineHeight: "22px",
color: "#4a4a4a",
fontStyle: "italic" as const,
margin: "0 0 8px",
};
const proofAuthor = {
fontSize: "13px",
color: "#999",
margin: "0",
};
const muted = {
fontSize: "13px",
color: "#999",
margin: "0 0 16px",
};
const divider = { borderColor: "#e6e6e6", margin: "24px 0" };
const footer = { fontSize: "12px", color: "#999", margin: "0" };Two details worth calling out. First, the usageHighlightprop reflects the user's actual activity. "You've created 12 dashboards" is harder to ignore than "you've been using our product." Second, the "data saved for 30 days" line reduces anxiety. Nobody wants to feel trapped into an impulse purchase.
Wiring it up with Resend
Each template above is a React component. To actually send them, you need a sending layer that triggers the right email at the right time. Here is the dispatch function using Resend:
import { Resend } from "resend";
import WelcomeEmail from "@/emails/welcome";
import EmailVerification from "@/emails/email-verification";
import GettingStarted from "@/emails/getting-started";
import TrialMidpoint from "@/emails/trial-midpoint";
import TrialEnding from "@/emails/trial-ending";
const resend = new Resend(process.env.RESEND_API_KEY);
interface UserContext {
email: string;
name: string;
signupDate: Date;
setupUrl: string;
verificationUrl: string;
dashboardUrl: string;
upgradeUrl: string;
}
export async function sendWelcomeSequence(user: UserContext) {
// Email 1: Welcome (immediate)
await resend.emails.send({
from: "onboarding@yourproduct.com",
to: user.email,
subject: `Welcome to YourProduct - let's get you set up`,
react: WelcomeEmail({
name: user.name,
setupUrl: user.setupUrl,
trialDaysLeft: 14,
}),
});
// Email 2: Verification (immediate)
await resend.emails.send({
from: "noreply@yourproduct.com",
to: user.email,
subject: "Verify your email address",
react: EmailVerification({
name: user.name,
verificationUrl: user.verificationUrl,
expiresInHours: 24,
}),
});
}
// Schedule remaining emails with your job queue
// (e.g., Inngest, BullMQ, or a cron-based system)
export async function sendGettingStarted(user: UserContext) {
await resend.emails.send({
from: "onboarding@yourproduct.com",
to: user.email,
subject:
"3 things to do in your first hour with YourProduct",
react: GettingStarted({
name: user.name,
dashboardUrl: user.dashboardUrl,
}),
});
}
export async function sendTrialMidpoint(
user: UserContext,
progress: {
actionsCompleted: number;
totalActions: number;
unusedFeatures: string[];
}
) {
await resend.emails.send({
from: "onboarding@yourproduct.com",
to: user.email,
subject:
"Your first week with YourProduct - here's what's next",
react: TrialMidpoint({
name: user.name,
daysLeft: 7,
actionsCompleted: progress.actionsCompleted,
totalActions: progress.totalActions,
unusedFeatures: progress.unusedFeatures,
dashboardUrl: user.dashboardUrl,
calendarUrl: "https://cal.com/yourproduct/onboarding",
}),
});
}
export async function sendTrialEnding(
user: UserContext,
usage: { highlight: string }
) {
await resend.emails.send({
from: "onboarding@yourproduct.com",
to: user.email,
subject: "Your trial ends in 2 days",
react: TrialEnding({
name: user.name,
daysLeft: 2,
planName: "Pro",
price: "$29/mo",
billingPeriod: "monthly",
upgradeUrl: user.upgradeUrl,
usageHighlight: usage.highlight,
}),
});
}The timing strategy
The specific send times matter more than most teams realize. Here is the reasoning behind each one:
Timing breakdown
Immediate (emails 1-2): Welcome and verification emails have the highest open rates of any transactional email because the user is actively engaged. Delay these by even 10 minutes and open rates drop measurably.
2 hours (email 3): The getting started guide arrives after the user has had time to explore on their own but before they lose momentum. This gap prevents inbox fatigue from three emails landing at once.
Day 7 (email 4):The midpoint check-in catches users at the natural "should I keep using this?" decision point. By day 7, they have enough experience to evaluate the product but enough trial time left to act on your suggestions.
Day 12 (email 5): Two days before expiration creates urgency without desperation. It gives the user time to loop in a manager for budget approval or discuss with their team. Day 14 is too late - they have already mentally closed the door.
Frequency guidelines
Five emails over 14 days is roughly one email every three days. That is a comfortable cadence for most users. Going above this threshold - say, daily emails - will increase unsubscribe rates and spam complaints without meaningfully improving conversions.
- Maximum frequency: No more than one email per day during onboarding. The day-one double (welcome + verification) is the only exception, and those serve different purposes.
- Suppress if active:If the user is actively using your product the hour before a scheduled email, consider delaying it. An email saying "come back" while they are actively using the app feels broken.
- Respect unsubscribes: Every email in the sequence should include an unsubscribe link. If a user opts out after email 2, they should not receive emails 3-5. This is not just good practice - it is required by CAN-SPAM and GDPR.
Making it personal at scale
The templates above use dynamic props like actionsCompleted, unusedFeatures, and usageHighlight. These are not optional nice-to-haves. They are the difference between a 15% click rate and a 30% click rate.
The data you need already exists in your app:
- Setup progress: Query your onboarding checklist state. How many steps has the user completed? Which ones are missing?
- Feature adoption: Track which core features the user has interacted with. The midpoint email highlights features they have not touched yet.
- Usage summary:For the trial-ending email, pull a concrete metric: "You've created 12 dashboards" or "Your team has logged 47 hours." Specific numbers make the value tangible.
The implementation is straightforward. Before each scheduled email, query your database for the user's current state and pass it as props to the React Email component. No external personalization engine required.
Measuring what works
Each email in the sequence should be measured against a different metric. Open rates alone do not tell you if your sequence is working.
| Primary Metric | Target | How to Measure | |
|---|---|---|---|
| Welcome | Setup completion rate | 60%+ | Track users who complete setup within 24 hrs of open |
| Verification | Verification rate | 85%+ | Verified emails / total signups |
| Getting started | Feature activation | 40%+ | Users who complete 1+ suggested action within 48 hrs |
| Midpoint check-in | Re-engagement | 25%+ | Inactive users who return to the app within 72 hrs |
| Trial ending | Conversion rate | 8-12% | Users who upgrade within 48 hrs of receiving email |
Target metrics for each email in the onboarding sequence
If your welcome email has a great open rate but low setup completion, the problem is not the email - it is what happens when they click through. If your trial-ending email has low open rates, test your subject line. Measure the right thing for each email.
Common mistakes to avoid
Cramming multiple CTAs into one email
Every email should have one primary action. The welcome email drives to setup. The trial-ending email drives to upgrade. When you add three buttons - "Complete setup," "Read our docs," "Join our community" - click rates drop because users freeze. Pick the one action that matters most for that stage of the journey.
Sending the same sequence to everyone
A user who has already connected their data source and invited their team should not receive a "getting started" email telling them to do those things. At minimum, skip emails when the user has already completed the action. Better yet, tailor the content based on what they have and have not done.
Using a no-reply from address
Onboarding emails should come from a real, monitored address like onboarding@yourproduct.com. Users reply to these emails with questions, bug reports, and feature requests. A noreply@ address for onboarding tells users you do not care about their experience. The one exception is the verification email, where noreply@ is acceptable since it is purely transactional.
A five-email onboarding sequence is not about sending more email. It is about sending the right email at the right moment in the user's journey. Each message has one job, one CTA, and arrives when the user is most likely to act on it. Build the sequence once with React Email, wire it up with Resend and a job queue, and let usage data drive the personalization. The templates above give you a working foundation - adapt the copy, timing, and CTAs to your product's specific activation milestones.