Why Hatchet?
Hatchet gives Hogsend durable execution — sleeps that survive deploys, automatic retries, and event routing — and the scaffold mints its token for you, so you never touch it directly.
You never touch Hatchet directly
Hatchet is the durable execution engine under the hood. When a journey calls await ctx.sleep({ duration: days(3) }), Hatchet is what makes that sleep survive server restarts, deploys, and crashes. When a PostHog event arrives and needs to fan out to three journeys at once, Hatchet does the routing.
The point of this page: you get all of that without learning Hatchet. @hogsend/engine wraps it completely. Your defineJourney() content becomes a Hatchet durable task; createWorker() runs the worker loop for you. And the one thing Hatchet normally asks of you — a connection token — the scaffold mints automatically for local dev.
The fastest path to a running engine
Scaffold a fresh app, then run one command. You do not bring your own Hatchet token for local development — bootstrap mints it.
pnpm dlx create-hogsend@latest my-app
cd my-app
pnpm bootstrap # Docker + .env + Hatchet token + migrate
pnpm dev # HTTP api on http://localhost:3002
pnpm worker:dev # Hatchet worker, in a second terminalpnpm bootstrap is the whole local setup, and it is idempotent — safe to re-run. It runs seven steps for you:
- Checks Docker is installed and the daemon is running.
- Prepares
.env— copies.env.exampleand writes a freshBETTER_AUTH_SECRET. - Resolves ports — if
5434/6380/7077/8888are taken it remaps to the next free port and syncs them back into.env. - Starts containers — TimescaleDB, Redis, and hatchet-lite via
docker compose up -d --wait. - Mints the Hatchet token — execs
hatchet-admin token createinside the hatchet-lite container and writesHATCHET_CLIENT_TOKENinto.env. This is the value the engine throws without; you do not create it by hand locally. - Runs migrations —
db:migrate, engine track then client track. - Mints a data-plane key — an ingest-scoped
hsk_…written toHOGSEND_API_KEY, so the data API and client SDK work the moment you boot.
When it finishes, the hatchet-lite dashboard is at http://localhost:8888 (login admin@example.com / Admin123!!). Your first journey to edit is src/journeys/welcome.ts.
Locally the Hatchet token is minted for you. The bring-your-own-token contract only applies in production — there you point HATCHET_CLIENT_TOKEN at Hatchet Cloud or a self-hosted engine and leave the rest of the env contract the same.
What Hatchet does for Hogsend
Durable sleeps
A journey that sends a welcome email, waits 2 days, checks if the user activated, then sends a follow-up — that's three days of wall-clock execution. Without durable execution you'd persist state manually, build resume logic, handle process restarts, and fight race conditions.
Hatchet makes this transparent. Your journey is a normal async function with await calls. Hatchet serializes state at each sleep boundary and resumes exactly where it left off — even if a new deploy replaced the worker in between.
// This literally pauses for 2 days and picks up here
await ctx.sleep({ duration: days(2), label: "post-welcome" });
// This code runs 2 days later, on whatever worker instance is running
const { found } = await ctx.history.hasEvent({
userId: user.id,
event: "feature_used",
});Event routing
Each journey declares a trigger event in its metadata. When an event lands at POST /v1/events, it's pushed to Hatchet, which routes it to every journey listening for that event name. If three journeys all trigger on user_signed_up, Hatchet runs all three concurrently as separate durable tasks.
Automatic retries
Email sends and other side effects run as Hatchet tasks with retries and exponential backoff. If your email provider (Resend by default) returns a 500 or times out, Hatchet retries automatically. Non-retryable errors (invalid API key, malformed email) bail out immediately.
Task visibility
The hatchet-lite dashboard (locally at localhost:8888, or Hatchet Cloud in production) shows every running task, its payload, retry history, and lets you cancel stuck runs. Useful for debugging, but not required day-to-day — Hogsend's admin API exposes the same information through its journey-state endpoints, and hogsend doctor probes overall health.
Why not just use Hatchet directly?
You could. Hatchet is a general-purpose durable execution engine. You could write journey logic as raw Hatchet tasks, build your own email integration, handle event routing, manage contact state, and wire up unsubscribe flows.
Hogsend is everything on top of Hatchet that makes it a lifecycle platform:
| Concern | Hatchet alone | Hogsend |
|---|---|---|
| Event ingestion | You build it | POST /v1/events + defineWebhookSource(); PostHog connects via the Destinations pipeline |
| Journey definition | Raw Hatchet task API | defineJourney() with metadata, guards, and a typed ctx |
| Enrollment guards | You build it | Automatic: entry limits, trigger conditions, subscription checks |
| Exit conditions | You build it | Declarative exitOn rules evaluated on every incoming event |
| Contact management | You build it | Auto-upsert from events, admin API, import/export |
| Email delivery | You build it | A swappable provider (Resend by default) plus engine-owned templates, first-party tracking, and retries |
| Bounce handling | You build it | Automatic suppression after a bounce threshold |
| Unsubscribe | You build it | Signed tokens, one-click unsubscribe, preference center |
| Outbound event stream | You build it | A 13-event catalog fanned out to PostHog/Segment/Slack/CRM via destinations |
| Observability | Hatchet dashboard | Admin API + metrics + alerting + audit logs |
@hogsend/engine is the integration work already done — and shipped as a versioned package you consume, so you get fixes and improvements via pnpm up rather than re-deriving them. You get a working lifecycle platform in minutes instead of building plumbing for weeks.
The lifecycle event stream is no longer pushed to PostHog from inside a journey — ctx.posthog.capture and ctx.identify were removed. To mirror events to PostHog, Segment, Slack, a CRM, or a warehouse, configure an outbound destination. To fire a custom event for the current user from a journey, use ctx.trigger({ event, userId, properties }), which runs the full ingest pipeline.
Why not BullMQ / Temporal / Inngest?
We evaluated other durable execution options:
- BullMQ — great for simple job queues, but no durable execution across long sleeps. You'd build state persistence and resume logic yourself. Fine for "send this email in 5 minutes," not for "wait 3 days then check if the user activated."
- Temporal — powerful but heavy. Requires its own cluster, has a steep learning curve, and is overkill for lifecycle email where the execution patterns are straightforward.
- Inngest — good developer experience, but cloud-only in practice. We wanted something self-hostable that doesn't add a SaaS dependency.
Hatchet hits the sweet spot: durable execution with event routing, self-hostable via a single Docker image (hatchet-lite), lightweight enough for a small team, and a clean Go-based engine that just works.
Hatchet-Lite vs Hatchet Cloud
Hogsend ships with hatchet-lite — a single container that bundles the engine, dashboard, and its own Postgres. This is what docker compose up runs locally and what we recommend for production on Railway.
For high-throughput workloads (hundreds of thousands of journey runs per month), you can switch to Hatchet Cloud for managed infrastructure, horizontal scaling, and SLA guarantees. The switch is a single environment-variable change — point HATCHET_CLIENT_TOKEN at the Cloud token.
For most teams getting started with lifecycle automation, hatchet-lite is more than enough.