Skip to content

winback

Winback: purchase_completed starts (or restarts) an execution and marks the profile as having bought recently; every further purchase inside 30 days resets the silence clock; after 30 days of silence the flag is unset and one winback email goes out. The next purchase starts a fresh execution.

Example: add it with cow add example winback; rename the purchase event, the flag trait, and the email to your domain’s vocabulary.

Trigger { event: "purchase_completed" }
Tags demo, retention
Consent purpose emailMarketing

Silence timer with a send at the end of it. Every purchase inside the window resets the clock, so the email only fires for genuinely quiet customers, at exactly the moment they lapsed. The loop is plain while: control flow in a journey is just TypeScript.

Winback and churn-prevention flows over any recurring signal: purchases, logins, sessions, uploads.

Rename the purchase event, the flag trait, and the email. The 30-day window is configuration. Chain it: the winback email’s goal is a purchase_completed that starts the next execution and re-marks the customer. For a window measured in months, swap the loop for api.restart() as activity-decay does.

import { defineJourney } from "@cowliss/cli/journeys";
/**
* Winback: `purchase_completed` starts (or restarts) an execution and marks
* the profile as having bought recently; every further purchase inside 30
* days resets the silence clock; after 30 days of silence the flag is unset
* and one winback email goes out. The next purchase starts a fresh execution.
*
* Example: add it with `cow add example winback`; rename the purchase event,
* the flag trait, and the email to your domain's vocabulary.
*/
export default defineJourney({
trigger: { event: "purchase_completed" },
purpose: "emailMarketing",
tags: ["demo", "retention"],
run: async (_event, api) => {
await api.traits.set("purchased_recently", true);
while (await api.waitForEvent("purchase_completed", { timeout: "30d" })) {
// Another purchase inside 30 days: loop to reset the silence timer.
}
await api.traits.unset("purchased_recently");
const me = await api.profile.get();
await api.email.send({
template: "winback",
props: {
firstName:
typeof me.traits.firstName === "string"
? me.traits.firstName
: undefined,
},
});
},
});

The example ships this scenario, copied in beside the journey. cow test replays it on a virtual clock: the trigger fires at the start, events arrive at their offsets, and expect is the ordered list of capability calls the run must make, each input matched as a subset.

winback.json
{
"user": {
"id": "usr_1",
"traits": { "email": "[email protected]", "firstName": "Ada" }
},
"events": [
{ "at": "10d", "event": "purchase_completed" },
{ "at": "25d", "event": "purchase_completed" }
],
"expect": [
{
"activity": "setTrait",
"input": { "key": "purchased_recently", "value": true }
},
{ "activity": "unsetTrait", "input": { "key": "purchased_recently" } },
{ "activity": "sendEmail", "input": { "template": "winback" } }
]
}
Terminal window
# copy it into your project
cow add example winback
# run its scenario on a virtual clock
cow test winback --scenario scenarios/winback.json