The lifecycle email layer
PostHog doesn't have yet
An event comes in — from PostHog, Stripe, or your own app. A TypeScript journey in your repo reacts: it sends the welcome, waits two days, checks what the user did, and branches. Every send, open, and click fans back out to PostHog. Email goes through your own Resend or Postmark account.
The product, running on itself
Tell it a few things about you. Drop your email at the end and a stock create-hogsend app running in production ingests the event, runs its welcome journey, and sends from hello@hogsend.com a few seconds later. A nudge follows two days on.
Same engine, same journey code you scaffold · unsubscribe is one click
Journeys are TypeScript files in your repo. Everything else follows from that.
Built by a growth engineer after 15+ years of client work — and one too many hand-rolled lifecycle stacks. The story →
PostHogisincredibleatshowingyouwhereusersdropoff.Actingonit—thewelcome,thenudge,thewin-back—hasmeantbuyingasecondplatformandsyncingyourdataintoit.Hogsendisthatlayerascode:TypeScriptjourneysinyourrepo,triggeredbytheeventsyoualreadyhave.
The emails every product should send
The flows behind every good lifecycle programme. Ten of them ship in the scaffold, ready to edit.
Pick a use case, read the journey
Each flow is one TypeScript file — trigger, durable waits, branches. Swap the provider underneath; the journey doesn't change.
import { days } from "@hogsend/core";
import { defineJourney, sendEmail } from "@hogsend/engine";
export const onboarding = defineJourney({
meta: {
id: "onboarding",
trigger: { event: "user.signed_up" },
entryLimit: "once",
exitOn: [{ event: "user.deleted" }],
},
run: async (user, ctx) => {
await sendEmail({ to: user.email, template: "activation-quickstart" });
// Park durably until THIS user creates a project — or 3 days pass.
const { timedOut } = await ctx.waitForEvent({
event: "project.created",
timeout: days(3),
});
await sendEmail({
to: user.email,
template: timedOut ? "activation-nudge" : "activation-feature-highlight",
});
},
});# provider is config, not journey code
EMAIL_PROVIDER=resend
RESEND_API_KEY=re_…Shortened from the full journey — the API is real, the provider lives in config.
Read the use caseJourneys and buckets, working together
Journeys are emails that play out over time — sleep, wait for what the user does next, and track opens and clicks as you go. Buckets are live groups of people. And every email and lifecycle event fans out to your destinations — PostHog, Segment, Slack, or any signed webhook.
Emails that play out over time
Trigger on an event, send, sleep, then branch on what happened while you waited. The control flow is plain TypeScript.
export const welcome = defineJourney({
meta: {
id: "activation-welcome",
trigger: { event: "user_signed_up" },
entryLimit: "once",
},
run: async (user, ctx) => {
await sendEmail({ to: user.email, template: "welcome" });
await ctx.sleep({ duration: days(2) });
const { found } = await ctx.history.hasEvent({
userId: user.id,
event: "feature_used",
});
if (!found) {
await sendEmail({ to: user.email, template: "nudge" });
}
},
});Agents can write all of it
The whole surface is typed code in a repo, which is the interface agents already have. An agent writes the journey file, your type-checker validates it, and hogsend journeys --json confirms it registered.
export const winback = defineJourney({ meta: { id: "winback", trigger: { event: wentDormant.entered }, exitOn: [{ event: wentDormant.left }], }, run: async (user, ctx) => { await sendEmail({ /* check-in */ }); await ctx.sleep({ duration: days(7) }); await sendEmail({ /* final nudge */ }); },});Journeys as code
Journeys are .ts files. Agents read them, write them, and open PRs against them. You review the diff like any other change.
- Add a win-back journey — when someone enters the went-dormant bucket, send a check-in, wait 7 days, then a final nudge. Stop if they come back.
- Created src/journeys/winback.ts — triggers on wentDormant.entered, exits on wentDormant.left, sends reactivation-checkin → ctx.sleep(days(7)) → reactivation-final-nudge. Ran `hogsend journeys --json` to verify — registered. Review the diff.

Studio is the read side
Every send, journey run, and contact in one dashboard — template previews with live props, resend and pause controls. Journey edits happen in your editor and ship through review.
The whole job is one loop
Activity comes in from PostHog or any webhook, the right emails go out through your provider, and what people do with them fans back out to your tools — PostHog, Segment, Slack, or anywhere. Nothing new to buy or keep in sync.
Scaffold your app
pnpm create hogsend@latest emits a thin app that pins @hogsend/engine and holds your content. Pass --domain to wire your sending domain from the start — sends redirect to your own inbox until the domain verifies.
terminal# Scaffold a thin app that pins the engine$ pnpm create hogsend@latest my-app --domain mysite.com $ cd my-app$ hogsend dev # infra, .env, migrate, API + worker → API on :3002 · Studio at /studioDefine journeys & buckets
TypeScript functions that trigger on events, send emails, wait, branch, and adapt.
journeys/welcome.tsexport const welcome = defineJourney({ meta: { id: "activation-welcome", trigger: { event: "user_signed_up" }, entryLimit: "once", }, run: async (user, ctx) => { await sendEmail({ to: user.email, template: "welcome" }); await ctx.sleep({ duration: days(2) }); const { found } = await ctx.history.hasEvent({ userId: user.id, event: "feature_used", }); if (!found) await sendEmail({ template: "nudge" }); },});Deploy & watch it run
Host with Docker or one-click Railway. Watch every send in Studio.
deploy# One-click Railway template, or your own host$ git push origin main → building hogsend-api …→ building hogsend-worker …→ migrations applied · health check /v1/health ✓ # Watch every send in Studio
What the repo gives you
Lifecycle email in a repo inherits the habits that make software dependable.
Every change has a history
Every template and journey has a git history. What the welcome email said in March is one git log away, with who changed it and why.
The same pull request as everything else
A journey ships through the same pull request as the rest of your product. Nothing goes live because someone clicked Save.
Finished tests stay on the record
Variants are code, so finished A/B tests stay in history — the losing copy, the reasoning, the result.
A dozen lines instead of a canvas
A canvas flow is forty drag-and-drops; the same logic is a dozen lines of TypeScript that fit in a diff.
The work starts at editing
The scaffold puts 10 journeys and 13 templates in your repo with one command. The work starts at editing, not building.
Costs scale with infrastructure
Self-hosted software costs the same at 50,000 contacts as at 500. Costs scale with your infrastructure, not your list.
Journeys as code
Lifecycle logic is TypeScript in your repo — reviewed, type-checked, and versioned like the rest of your product.
Your provider, your reputation
Sends go through your own Resend or Postmark account — or any provider behind the EmailProvider contract.
First-party tracking
The engine rewrites links and tracks opens, clicks, and in-email answers itself, whichever provider you plug in. The data lands in your own Postgres.
Durable execution
Journeys run as Hatchet durable tasks — a seven-day wait survives deploys, restarts, and crashes.
Durable execution, by Hatchet
Every journey runs on Hatchet, the durable execution engine underneath Hogsend. It's what lets a long ctx.sleep survive a deploy and resume two days later exactly where it left off, with retries and timeouts handled for you. Hogsend builds on Hatchet rather than rolling its own durability.
Survives deploys & restarts
A long ctx.sleep keeps running across a deploy and resumes days later, exactly where it left off.
Automatic retries & timeouts
Failed steps retry and waits expire on their own — durability you don't have to hand-roll.
Self-host it, or use Hatchet Cloud
Run Hatchet-Lite next to your app, or point at Hatchet Cloud. Same engine either way.
What it costs
There is no paid tier. You pay for hosting — the Railway template provisions Postgres, Redis, Hatchet, the API, and the worker — and for your own Resend or Postmark account. That's the entire cost structure; contact count appears in neither bill.
*List prices at the time of writing — all pricing last checked June 2026.
Hogsend is free to self-host under ELv2 — run it commercially, deploy it for clients. Contacts, events, and templates live in your own Postgres and your own repo, so leaving means taking your database with you. If you'd rather have it installed for you: one week, $2,300, deployed on your infrastructure with your first journeys live.
See for yourself
The engine on npm, the source on GitHub, a one-click deploy, and the engineer who built it — all a click away.
"Over 15+ years of client work, every engagement hit the same wall: PostHog, Resend, and a folder of webhook handlers pretending to be a lifecycle email system. I rebuilt it enough times to know exactly what it should be — so I built it once, properly, and versioned it."
Whose problem this solves
Pick your seat — one sentence on what Hogsend gives it.
Lifecycle email in the repo you already have — no second platform to buy and babysit.
Questions, answered
Still curious? The docs go deeper — including a side-by-side with PostHog Workflows.
Hogsend is source-available under the Elastic License 2.0 (ELv2), not an OSI-approved open-source license. You can read, modify, and self-host all of it for free; the only restriction is offering Hogsend itself as a managed service. All 11 packages are on npm and the full source is on GitHub.
First send in minutes
The scaffold command sets up the app, Docker, env, and ten journeys — the welcome series included. pnpm bootstrap brings the stack up. Or deploy the Railway template in a click.
pnpm dlx create-hogsend@latest my-appFree to self-host · PostHog + your provider · no per-contact pricing

