In-app onboarding only works when the user is in the app. For users who don't complete onboarding in their first session (30-50% of new signups), email sequences are the primary channel for bringing them back. The key is deep links: every email CTA should open the app directly to the relevant screen, not to a generic web page or app store listing.
For personalization strategies, see Personalized Onboarding Flows with Deep Link Data. For reducing drop-off, see Reducing Onboarding Drop-Off: 10 Proven Strategies. For a broader look at onboarding fundamentals, see Onboarding Best Practices for Mobile Apps in 2026.
Email Sequence Structure
The Standard Sequence
A typical onboarding email sequence sends 5-7 emails over the first 14 days:
| Day | Purpose | Deep Link Target | |
|---|---|---|---|
| 0 | Welcome | Confirm signup, set expectations | /home |
| 1 | Getting started | Guide to first action | /onboarding/first-action |
| 3 | Feature highlight | Show key feature they haven't used | /feature/{feature_name} |
| 5 | Social proof | Show what others are doing | /explore or /trending |
| 7 | Progress check | Remind them of incomplete setup | /onboarding/resume |
| 10 | Advanced feature | Introduce a power feature | /feature/{advanced_feature} |
| 14 | Feedback request | Ask how it's going | /feedback |
Conditional Logic
Not every user should receive every email. Skip emails based on user behavior:
async function shouldSendEmail(userId, emailId) {
const user = await getUser(userId);
switch (emailId) {
case 'getting_started':
// Skip if user already performed the first action
return user.hasCompletedFirstAction === false;
case 'feature_highlight':
// Skip if user already used the featured capability
return user.hasUsedFeature('sharing') === false;
case 'progress_check':
// Skip if onboarding is already complete
return user.onboardingCompleted === false;
case 'feedback_request':
// Only send if user has been active at least once in the last 7 days
return user.lastActiveAt > daysAgo(7);
default:
return true;
}
}
Deep Links in Emails
Why Deep Links Matter in Onboarding Emails
Without deep links:
- User taps "Get Started" in email
- Opens a web page
- Sees "Download our app" or "Open in App"
- Taps that link
- App opens to the home screen
- User has to navigate to the right feature
With deep links:
- User taps "Get Started" in email
- App opens directly to the relevant screen
Every extra step loses 20-30% of users. Deep links cut 4 steps down to 1. For a technical guide on using Universal Links within email, see Universal Links in Email: Complete Guide.
Generating Email Deep Links
async function generateEmailDeepLink(userId, emailId, destination) {
const link = await Tolinku.createLink({
path: destination,
params: {
utm_source: 'email',
utm_medium: 'onboarding_sequence',
utm_campaign: emailId,
user_id: userId,
},
fallback: {
web: `https://yourapp.com${destination}`, // Fallback for users without the app
},
});
return link.url;
}
Handling Deep Links from Email
When the user taps a deep link from an email:
function handleEmailDeepLink(url, params) {
// Track email engagement
analytics.track('email_deep_link_opened', {
campaign: params.utm_campaign,
destination: new URL(url).pathname,
source: 'email',
});
// Check authentication
if (user.isAuthenticated === false) {
pendingDeepLink.save(url);
navigation.navigate('Login');
return;
}
// Navigate to the destination
handleRoute(url);
}
Fallback for Users Without the App
Some users might have uninstalled the app. The deep link should fall back to a web experience:
// In your email template
const deepLink = await generateEmailDeepLink(userId, 'getting_started', '/onboarding/first-action');
// The Tolinku link will:
// 1. Open the app if installed (directly to /onboarding/first-action)
// 2. Redirect to the web fallback if not installed
// 3. Optionally redirect to the app store with deferred deep link
Email Content Patterns
Email 1: Welcome (Day 0)
Subject: Welcome to [App]! Here's what to do first.
Hi {firstName},
Welcome to [App]. You're all set.
Here's the one thing to do right now:
[Create your first {item}] ← deep link to /create
It takes about 30 seconds, and you'll see why 50,000 people use [App] every day.
{CTA Button: "Create Your First {Item}" → deep link}
Email 2: Getting Started (Day 1)
Only sent if the user hasn't performed their first action:
Subject: Quick tip to get the most out of [App]
Hi {firstName},
Most new users start by {first action description}.
Here's a 30-second walkthrough:
1. Open [App]
2. Tap "Create"
3. Choose a template or start from scratch
{CTA Button: "Open [App]" → deep link to /create}
Need help? Reply to this email and we'll point you in the right direction.
Email 3: Feature Highlight (Day 3)
Highlight a feature the user hasn't discovered:
Subject: Did you know [App] can {feature benefit}?
Hi {firstName},
One feature our users love: {feature name}.
{1-2 sentences explaining what it does and why it's useful.}
{Screenshot or GIF showing the feature}
{CTA Button: "Try {Feature Name}" → deep link to /feature/sharing}
Email 5: Progress Check (Day 7)
For users who haven't completed onboarding:
Subject: You're almost there
Hi {firstName},
You've completed {completedSteps} of {totalSteps} setup steps.
Here's what's left:
☑ Created account
☑ {completed step}
☐ {next incomplete step}
☐ {remaining step}
{CTA Button: "Finish Setup" → deep link to /onboarding/resume}
This takes about {estimatedTime} minutes.
Personalization by Acquisition Source
Referral Users
function getReferralEmailSequence(referralData) {
return [
{
day: 0,
subject: `Welcome! ${referralData.referrerName} sent you a gift`,
template: 'referral_welcome',
deepLink: '/onboarding?ref=true',
data: {
referrerName: referralData.referrerName,
reward: referralData.rewardAmount,
},
},
{
day: 1,
subject: `Claim your ${referralData.rewardAmount} reward`,
template: 'referral_activation',
deepLink: '/rewards',
condition: (user) => user.hasCompletedQualifyingAction === false,
},
];
}
Campaign Users
function getCampaignEmailSequence(campaignData) {
return [
{
day: 0,
subject: `Welcome! Your ${campaignData.promoCode} code is active`,
template: 'campaign_welcome',
deepLink: `/shop?promo=${campaignData.promoCode}`,
data: {
promoCode: campaignData.promoCode,
discount: campaignData.discountAmount,
},
},
];
}
Timing Optimization
Send Time Optimization
Don't send all emails at the same time of day. Test different send times:
function getOptimalSendTime(user) {
// If we know when the user is most active
if (user.peakActivityHour) {
return user.peakActivityHour;
}
// Default send times by email type
const defaults = {
welcome: 'immediate', // Send immediately after signup
getting_started: '10:00', // Morning, when people start tasks
feature_highlight: '14:00', // Afternoon, when engagement peaks
progress_check: '09:00', // Morning, fresh start feeling
feedback_request: '11:00', // Mid-morning, not too early
};
return defaults;
}
Time Zone Awareness
Send emails at the user's local time, not UTC:
function scheduleEmail(userId, emailConfig) {
const user = getUser(userId);
const timezone = user.timezone || detectTimezoneFromIP(user.signupIP);
const sendAt = convertToTimezone(emailConfig.sendTime, timezone);
emailQueue.add({
userId,
templateId: emailConfig.template,
sendAt,
deepLink: emailConfig.deepLink,
data: emailConfig.data,
});
}
Measuring Email Sequence Performance
Key Metrics
| Metric | Formula | Benchmark |
|---|---|---|
| Open rate | Opens / Delivered | 40-60% (onboarding emails are high-priority) |
| Click rate | Clicks / Delivered | 10-20% |
| Deep link open rate | App opens from email / Clicks | 60-80% |
| Activation after email | Users who activate within 24h of email open | 5-15% |
| Unsubscribe rate | Unsubscribes / Delivered | < 0.5% |
Attribution
Track which email drove the activation:
analytics.track('activation_event', {
action: 'first_project_created',
attributedEmail: lastEmailOpened.campaign, // 'getting_started'
timeFromEmail: Date.now() - lastEmailOpened.timestamp,
deepLinkUsed: true,
});
Sequence-Level Analysis
async function analyzeEmailSequence(sequenceId) {
const emails = getSequenceEmails(sequenceId);
for (const email of emails) {
const sent = await countSent(email.id);
const opened = await countOpened(email.id);
const clicked = await countClicked(email.id);
const activated = await countActivatedAfter(email.id, { within: '24h' });
console.log(email.id, {
sent,
openRate: (opened / sent * 100).toFixed(1) + '%',
clickRate: (clicked / sent * 100).toFixed(1) + '%',
activationRate: (activated / sent * 100).toFixed(1) + '%',
});
}
}
Common Mistakes
1. Too Many Emails
Sending 10 emails in 14 days overwhelms users and increases unsubscribes. Stick to 5-7 emails, and skip emails when the user has already taken the target action. For users who stop engaging entirely, consider transitioning them to a re-engagement campaign.
2. Generic CTAs
"Open App" is a weak CTA. Be specific: "Create Your First Project," "See What's Trending," "Finish Setup." The CTA should tell the user exactly what they'll do when they tap.
3. No Deep Links
Sending users to a web page that says "Download our app" is a broken experience. Every email CTA should deep link directly into the app.
4. Ignoring Mobile Rendering
Most onboarding emails are opened on mobile. Test your emails on iOS Mail, Gmail app, and Outlook mobile. Keep subject lines under 40 characters and CTAs large enough to tap easily.
For deep linking features, see Tolinku deep linking. For onboarding use cases, see the onboarding documentation.
Get deep linking tips in your inbox
One email per week. No spam.