Use case: trial conversion

Trial emails driven by usage, not days remaining

“Your trial ends in 3 days” converts nobody who hasn't found value. Branch on what they did; stop the second they pay.

Free to self-host · One scaffold command · No per-contact billing

Countdown spam is a state machine with one state

Every trial email tool can count down. The conversion question is behavioral: did they hit the milestone that predicts paying? If yes, ask early. If no, sell the milestone — not the deadline. That requires your email tool to see product usage, and to stop instantly when Stripe says they paid.

The journey

Branch on usage; exit on payment

It mirrors the trial-upgrade journey that ships in the scaffold.

src/journeys/trial-conversion.ts
import { days } from "@hogsend/core";
import { defineJourney, sendEmail } from "@hogsend/engine";

export const trialConversion = defineJourney({
  meta: {
    id: "trial-conversion",
    name: "Trial conversion",
    enabled: true,
    trigger: { event: "trial.started" },
    entryLimit: "once",
    exitOn: [
      // The built-in Stripe preset feeds this in — the journey is
      // cancelled the moment they pay, even mid-wait.
      { event: "subscription.created" },
      { event: "user.deleted" },
    ],
  },

  run: async (user, ctx) => {
    await ctx.sleep({ duration: days(3), label: "usage-check" });

    const { found: hitMilestone } = await ctx.history.hasEvent({
      userId: user.id,
      event: "usage.milestone_reached",
    });

    if (hitMilestone) {
      // The upgrade ask, at the moment of value — not the deadline.
      await sendEmail({
        to: user.email,
        userId: user.id,
        journeyStateId: user.stateId,
        template: "conversion-usage-milestone",
        subject: "You're on a roll — here's what the paid plan unlocks",
        journeyName: user.journeyName,
      });
    }

    await ctx.sleep({ duration: days(7), label: "trial-ending" });

    await sendEmail({
      to: user.email,
      userId: user.id,
      journeyStateId: user.stateId,
      template: "conversion-trial-expiring",
      subject: "Your trial ends in 3 days — don't lose your progress",
      journeyName: user.journeyName,
    });
  },
});

exitOn: subscription.created cancels the journey mid-wait — nobody gets a “trial ending!” email after their card was charged.

For “3 days before expiry at their 9am”, swap the fixed sleep for ctx.sleepUntil(...) with ctx.when — the timezone-aware fluent scheduler that respects your configured send window. See the journeys guide.

Measurement

Measure what matters

Branch on behavior, not opens

Clicks and conversion events are reliable; opens are directional — Apple Mail Privacy Protection inflates them. The milestone event is the signal worth branching on.

Conversions, via PostHog

Conversion events fan out to PostHog — and on to Meta or Google via PostHog's Destinations pipeline. See conversion tracking.

Chain journeys with ctx.trigger

ctx.trigger chains converts straight into onboarding and non-converts into win-back.

Templates

The emails it sends ship with the scaffold

All 13 templates are React Email components in your repo. These three carry the conversion sequence.

FAQ

Questions, answered

The short versions. The docs have the long ones.

Go deeper

Declare exitOn: [{ event: "subscription.created" }] in the journey meta. The built-in Stripe webhook preset feeds the event in, and Hogsend cancels the journey immediately — even if it's mid-wait.

Trial emails that stop
when Stripe says stop

The scaffold ships 10 journeys and 13 templates to start from — wire the Stripe preset and the upgrade sequence is one reviewable file.

Free to self-host · One scaffold command · No per-contact billing

terminal
pnpm dlx create-hogsend@latest my-app