Trial conversion sequence
A day-1 value email, a usage branch mid-trial, a bucket-timed T-3 push — gone the instant they pay.
The mid-trial branch is ctx.history.hasEvent at decision time — a count of the feature.used events your product already emits, not an exported segment.
Full write-upexport const trialOnboarding = defineJourney({
meta: {
id: "trial-onboarding",
name: "Conversion — trial onboarding",
enabled: true,
trigger: { event: Events.TRIAL_STARTED },
entryLimit: "once",
suppress: hours(12),
exitOn: [
{ event: Events.SUBSCRIPTION_CREATED },
{ event: Events.USER_DELETED },
],
},
run: async (user, ctx) => {
// Day 1 — one concrete outcome, not a feature tour.
await ctx.sleep({ duration: days(1), label: "day-1" });
if (!(await ctx.guard.isSubscribed())) return;
await sendEmail({
to: user.email,
userId: user.id,
journeyStateId: user.stateId,
template: Templates.TRIAL_FIRST_VALUE,
subject: "Get your first result today",
journeyName: user.journeyName,
});
// Mid-trial — branch on what they actually did.
await ctx.sleep({ duration: days(3), label: "mid-trial" });
if (!(await ctx.guard.isSubscribed())) return;
const usage = await ctx.history.hasEvent({
userId: user.id,
event: Events.FEATURE_USED,
within: days(4),
});
if (usage.count >= 3) {
// Engaged — sell the paid tier on what they already use.
await sendEmail({
to: user.email,
userId: user.id,
journeyStateId: user.stateId,
template: Templates.TRIAL_UPGRADE_VALUE,
subject: "You're getting value — here's what Pro adds",
journeyName: user.journeyName,
props: { usageCount: usage.count },
});
} else {
// Cold — conversion needs activation first, not a pitch.
await sendEmail({
to: user.email,
userId: user.id,
journeyStateId: user.stateId,
template: Templates.TRIAL_ACTIVATION_NUDGE,
subject: "Three days in — the fastest path to a result",
journeyName: user.journeyName,
});
}
},
});