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
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 whole sequence is one file
It mirrors the welcome journey that ships in the scaffold — trigger, durable wait, branch, exit.
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.
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.
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.
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.
Same engine, different lifecycle stage
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
pnpm dlx create-hogsend@latest my-app


