Hogsend is brand new.Try it
Hogsend
Use case: win-back

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

Triggering on the absence of activity

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.

The code

A bucket detects inactivity; a journey responds

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.

The run

The same file, executing

Dormancy detected, the check-in email, the plan check picking the offer — and the send that wins them back.

src/journeys/winback.ts
export const winback = defineJourney({
meta: { trigger: { event: Events.USER_DORMANCY_DETECTED } },
run: async (user, ctx) => {
await sendEmail({ template: "we-miss-you" });
await ctx.sleep({ duration: days(7) });
const paid = user.properties.plan === "paid";
await sendEmail({
template: paid
? "winback-offer"
: "whats-new",
});
},
});
The run
eventuser.dormancy_detected · doug@hogsend.comenrolled
send
We haven't seen you in a whiledeliveredopened
sleep
7 days
checkuser.planplan: "paid"
send
We'd hate to see you go — here's an optiondeliveredopenedclicked
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

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.

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

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

terminal
pnpm dlx create-hogsend@latest my-app