Win-back emails fail because they're written for "users." Write them for the one user instead.
Every dormant-user campaign in history: "We miss you! Here's what's new." Open rate: pity clicks. The email fails because it's about your changelog - and the user left because of something in their experience.
The setup
Every dormant-user campaign in history: "We miss you! Here's what's new." Open rate: pity clicks. The email fails because it's about your changelog, and the user left because of something in their experience - they finished a project, hit a friction point, or forgot what your product was for. A win-back email that works has to reconstruct that context per user. Manually, that's impossible at scale. Generated from each user's own usage history, it's a batch job. Vevee meters my users' AI usage (@vevee/sdk), which means every dormant user has a trail: what they used, how often, and what they were doing right before they went quiet. compose() turns that trail into the email. The compose type prompt: "This user has been inactive for {daysInactive} days. From their past usage, identify what they used the product FOR, and write a short email that resumes that thread - reference their actual work, suggest the single most natural next action. Mention a new feature only if it's directly relevant to how they used the product." Data sources: user events, user usage, user attributes, popular features.
import { createClient } from "@vevee/sdk";
const vevee = createClient({ apiKey: process.env.VEVEE_SECRET_KEY! });
interface WinbackEmail {
subject: string;
body: string;
ctaText: string;
ctaHref: string;
}
for (const user of dormantUsers) { // e.g. no events in 30 days
const email = await vevee.compose<WinbackEmail>(
"winback-email",
user.id,
{ daysInactive: user.daysInactive }
);
if (email.status === "generated") {
await sendEmail(user.email, email.output);
}
// opted_out → skip or send the plain template; your call
}What goes out
The same compose type produces completely different emails depending on the trail it finds. Two real shapes - one for a user whose project ended, one for a user your pricing pushed out:
- To a user who generated 40 product photos in March and vanished - Subject: Your product-shot setup is still here. "You made 40 product images in March - then went quiet right after the spring batch. If there's a new drop coming, your presets and styles are exactly where you left them. One click to pick the thread back up."
- To a user who churned two days after hitting a quota - Subject: You hit the wall on day 12 - that wall moved. "You stopped right after maxing out the Free tier. Since then we doubled the Free limit. Your projects are intact; the ceiling that stopped you isn't where it was."
Track resurrection like a funnel
The SDK's analytics side closes the loop with reserved events. signed_in → feature_used with source: "winback_email" is your real resurrection rate - not opens, not clicks, returned to doing the thing. If you later add a discount variant, tag variant in properties and compare in the same funnel.
// CTA link lands them here:
await vevee.capture({ distinctId: userId, event: "signed_in",
properties: { source: "winback_email" } });
// did they actually do something?
await vevee.capture({ distinctId: userId, event: "feature_used",
properties: { source: "winback_email" } });Why this beats the discount
The default win-back lever is "20% off if you come back," which trains users to churn for coupons and attracts exactly the users who'll churn again. The composed email's lever is unfinished work and remembered context - "your presets are still here" - which selects for users who had a real use case. Cheaper, and the cohort it recovers is worth more. Operationally: it's a nightly cron, server-side (sk_* key), capped by the monthly compose budget in the dashboard, with per-call cost reported in usage.costMicroUsd. A thousand dormant users costs less to email than one coffee - and unlike the discount, the margin survives.
Keep reading
I rewrote my cancel flow with one LLM call. It argues better than I do.
The cancel flow is where SaaS revenue goes to die politely. "Are you sure? You'll lose access to Premium features" has never changed a single mind - but reminding a user of their own 312 generations this month is an argument.
ComposeThe trial-ending email everyone sends is the same email. Here's the one that converts
"Your trial ends in 3 days! Upgrade now to keep access." You've received a hundred of these. You've deleted a hundred of these. The email fails because it's about your product, not about the user's trial.