Use case: onboarding

Onboarding that waits for behavior, not the calendar

Day-2, day-4, day-7 drips email everyone the same. A journey watches what each user actually does — in TypeScript you can read.

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

The drip problem

A timed sequence sends “how's it going?” to someone who's been in the product all morning, and “just checking in!” to someone who churned at signup. Both are the same bug: the sequence can't see product events — and your product events are in PostHog. The fix isn't better copy. It's branching, and branching is code.

The journey

The whole sequence is one file

It mirrors the welcome journey that ships in the scaffold — trigger, durable wait, branch, exit.

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

export const onboarding = defineJourney({
  meta: {
    id: "onboarding",
    name: "Onboarding",
    enabled: true,
    trigger: { event: "user.signed_up" },
    entryLimit: "once",
    exitOn: [{ event: "user.deleted" }],
  },

  run: async (user, ctx) => {
    await sendEmail({
      to: user.email,
      userId: user.id,
      journeyStateId: user.stateId,
      template: "activation-quickstart",
      subject: "Welcome — here's the shortest path to a first win",
      journeyName: user.journeyName,
    });

    // Park the journey — durably — until THIS user creates a project,
    // or 3 days pass. Survives deploys, restarts, and crashes.
    const { timedOut } = await ctx.waitForEvent({
      event: "project.created",
      timeout: days(3),
    });

    await sendEmail({
      to: user.email,
      userId: user.id,
      journeyStateId: user.stateId,
      template: timedOut ? "activation-nudge" : "activation-feature-highlight",
      subject: timedOut
        ? "Stuck? Here's the 2-minute version"
        : "Nice — here's what to try next",
      journeyName: user.journeyName,
    });

    await ctx.sleep({ duration: days(4), label: "pre-community" });

    // Re-check after the long wait — unsubscribes don't exit a journey.
    if (await ctx.guard.isSubscribed()) {
      await sendEmail({
        to: user.email,
        userId: user.id,
        journeyStateId: user.stateId,
        template: "activation-community",
        subject: "Join the community",
        journeyName: user.journeyName,
      });
    }
  },
});

Trigger, durable wait, branch — one file, one reviewable diff.

In production

Why it holds up

ctx.waitForEvent

Parks the journey durably until the user acts — or doesn't. The branch is an if statement.

exitOn

Removes anyone who deletes their account mid-sequence, even mid-wait.

Entry limits

entryLimit: "once" means re-signups don't get re-welcomed.

ctx.when

Times any send to 9am in their timezone, inside your send window — timezone auto-resolved from PostHog person properties, then the contact, then your default.

Templates

The emails it sends ship with the scaffold

All 13 templates are React Email components in your repo. These four cover activation — edit them like any other component.

FAQ

Questions, answered

The short versions. The docs have the long ones.

Go deeper

Point a PostHog webhook destination at your Hogsend ingest endpoint; the user.signed_up event enrolls users in the journey automatically. Signup events can also come from Clerk or Supabase via built-in signed presets, or from your own backend via the Data API.

Onboarding your team
can code-review

The scaffold ships 10 journeys and 13 templates to start from — including a welcome sequence shaped like this one.

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

terminal
pnpm dlx create-hogsend@latest my-app