Win-back triggered by inactivity
No app fires an “inactive” event, so a bucket watches for the absence of activity. “Was active, but nothing in 7 days” becomes an enter event the win-back journey triggers on.
Free to self-host · One scaffold command · No per-contact billing
Webhooks tell you what happened, not what stopped happening. Most tools handle 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 enter and leave events the moment someone crosses the line. Buckets are segments rather than a full CDP, which is what this needs.
A bucket detects inactivity; a journey responds
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.
The same file, executing
Dormancy detected, the check-in email, the plan check picking the offer — and the send that wins them back.
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
entryPeriod: days(60) caps re-enrollment, so a user who flaps in and out isn't enrolled in win-back more than once every 60 days.
Stop after the final nudge
Emailing people who left is one of the faster ways to lose domain reputation — and the reputation is yours, because the sends go through 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
Short answers here; the docs go deeper.
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.
Trigger win-back
on inactivity
Buckets, journeys, and suppression ship in the scaffold. Membership updates in real time — no nightly cohort export — and the journey exits the instant the user returns.
Free to self-host · One scaffold command · No per-contact billing
pnpm dlx create-hogsend@latest my-app

