Win-back that knows when someone actually left
Inactivity isn't an event your app fires — it's the absence of one. Buckets turn “no activity for 7 days” into a trigger.
Free to self-host · One scaffold command · No per-contact billing
Webhooks tell you what happened, never what stopped happening. Most tools solve this with batch segments that update daily — PostHog cohorts recalculate in roughly 24-hour batches. A Hogsend bucket is code, evaluated in real time: membership changes fire first-class enter/leave events the moment someone crosses the line. (Buckets are segments, not a CDP — which is exactly enough here.)
A bucket detects silence; a journey answers it
Both mirror the dormancy pair that ships in the scaffold.
import { days, defineBucket } from "@hogsend/engine";
export const wentDormant = defineBucket({
meta: {
id: "went-dormant",
name: "Went dormant",
enabled: true,
timeBased: true,
criteria: (b) =>
b.all(
b.event("app.active").exists(),
b.event("app.active").within(days(7)).notExists(),
),
},
});The bucket: was active once, silent for 7 days. The cron sweep owns the time-based flip — no event signals dormancy.
import { days } from "@hogsend/core";
import { defineJourney, sendEmail } from "@hogsend/engine";
import { wentDormant } from "../buckets/went-dormant.js";
export const winback = defineJourney({
meta: {
id: "winback",
name: "Win-back",
enabled: true,
trigger: { event: wentDormant.entered }, // typed ref — typos are compile errors
entryLimit: "once_per_period",
entryPeriod: days(60),
exitOn: [
{ event: wentDormant.left }, // came back → exit immediately
{ event: "user.deleted" },
],
},
run: async (user, ctx) => {
await sendEmail({
to: user.email,
userId: user.id,
journeyStateId: user.stateId,
template: "reactivation-checkin",
subject: "We haven't seen you in a while",
journeyName: user.journeyName,
});
await ctx.sleep({ duration: days(7), label: "offer" });
await sendEmail({
to: user.email,
userId: user.id,
journeyStateId: user.stateId,
template: "conversion-winback-offer",
subject: "See what you've been missing",
journeyName: user.journeyName,
});
await ctx.sleep({ duration: days(7), label: "final" });
await sendEmail({
to: user.email,
userId: user.id,
journeyStateId: user.stateId,
template: "reactivation-final-nudge",
subject: "One last note from us",
journeyName: user.journeyName,
});
},
});The instant they come back, the bucket's left event matches exitOn and cancels the run — mid-sleep, mid-anything.
Know when to stop
Suppression is built in
Unsubscribes, bounces, and complaints hard-stop the sequence; frequency caps keep win-back from stacking on other journeys.
once_per_period
A flapping user isn't “won back” monthly — one enrollment per 60 days, enforced by the engine.
The final nudge is final
The fastest way to lose a domain reputation is emailing people who left — and it's your domain, because it's your provider.
The emails it sends ship with the scaffold
All 13 templates are React Email components in your repo. These three carry the win-back sequence.
Questions, answered
The short versions. The docs have the long ones.
Go deeper
Define a bucket whose criteria are "was active, but no activity event within 7 days." Hogsend evaluates membership in real time, and the bucket's enter event triggers the win-back journey — no nightly cohort export.
Same engine, different lifecycle stage
Turn silence
into a trigger
Buckets, journeys, and suppression ship in the scaffold — no nightly cohort export, and it stops the instant they return.
Free to self-host · One scaffold command · No per-contact billing
pnpm dlx create-hogsend@latest my-app

