Use case: win-back

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

You can't webhook on silence

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.)

The code

A bucket detects silence; a journey answers it

Both mirror the dormancy pair that ships in the scaffold.

src/buckets/went-dormant.ts
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.

src/journeys/winback.ts
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.

Deliverability

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.

Templates

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.

FAQ

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.

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

terminal
pnpm dlx create-hogsend@latest my-app