Building a 4-step onboarding drip that actually teaches the product
Most onboarding emails are wasted impressions. They arrive when the user is still figuring out whether the product works, and they either say too little ("welcome, here's your dashboard!") or too much ("here are all 47 features!").
EverCV's drip is 4 emails, spaced to match when each message is actually relevant.
The timing logic
STEP_1_DAY = 1 # Welcome — get them connected
STEP_2_DAY = 3 # Job search tools — they've seen the CV by now
STEP_3_DAY = 7 # Week 1 recap — do they have a CV? Are they Pro?
STEP_4_DAY = 14 # CI token + application tracker — power user features
Day 1: You're here, connect GitHub. That's it.
Day 3: If they've pushed code, they have a CV. Now they're ready to hear about tailoring and gap analysis. If they haven't pushed code yet, the second email is still relevant — it's the "here's why you want to connect" pitch.
Day 7: Check in. Do they have a CV (last_refresh set)? Include the public profile link if yes. Are they still Free? Upgrade pitch. Are they Pro? "Here's an advanced feature."
Day 14: The power-user features. CI token (one YAML step and your CV refreshes on every push) and the application tracker (track where you applied, re-tailor inline). These are features most users don't find themselves — they need to be told.
Free vs. Pro variants
Every step renders differently based on user.get("plan", "free"). Free users get an upsell block at the bottom. Pro users get a "Pro tip" block with an advanced use.
The upsell copy is different at each step too. Day 3 is product-focused ("unlock the job-matching suite"). Day 7 is ROI-focused ("$15/mo pays for one hour of not updating your resume manually"). Day 14 is feature-specific ("CI token and application tracker are Pro features").
Changing the upsell copy per step is harder to implement but converts better. A user who's been on the free plan for 14 days has already heard "here's what Pro does" — they need "here's the specific thing you can't do without it."
State tracking
The step is stored as onboarding_step on the user record in DynamoDB. It's just an int: 0 = nothing sent, 1 = step 1 sent, etc. The cron runs daily at 08:00 UTC and scans all users. next_step() computes: given the user's current step and days since sign-up, what's the next eligible step?
def next_step(user: dict) -> int | None:
step = int(user.get("onboarding_step", 0))
if step >= 4:
return None # sequence complete
days = _days_since_created(user)
thresholds = {1: 1, 2: 3, 3: 7, 4: 14}
for s in range(step + 1, 5):
if days >= thresholds[s]:
return s
return None
If a user signs up and then doesn't open a single email for two weeks, the cron will send all four emails in sequence as soon as they're past the threshold — one per day. Not ideal for the user, but fine for a launch product where the alternative is nothing.
A real production drip would add a max-per-day rate limit, a "has opened previous step" check, and probably longer gaps. For now, the logic is correct and the sequence is useful.
What's missing
- A/B testing on subject lines. The subjects are untested. "Welcome to EverCV" is fine; "one step to get your first CV" is the more useful framing, but I don't know which converts better.
- Day 30 "are you still there?" A re-engagement email for users who never connected GitHub. Not built yet.
- Unsubscribe handling. The link is in the footer but there's no handler at
/unsubscribeyet. Filed.
The drip is wired to the production cron and will run on every registered user once the Lambda is deployed. It covers the full 14-day first impression window.