activity-decay
What it does
Section titled “What it does”Timer decay: “active in the last 30 days” as a per-user journey instead of
a nightly sweep. The trigger sets the flag and the execution waits; every
later event restarts the execution, which resets the 30-day silence clock
with a fresh, empty journal; 30 days of silence unsets the flag (removed,
not set to false, so exists predicates stop matching) and the execution
completes. The next event starts a new one.
"*" is activity as it really arrives: whatever names your apps send,
page_view and purchase and login alike. It never matches an event Cowliss
wrote itself (those are all under system.), which is what stops the
trait write below from flipping a segment whose entry event restarts this
journey, forever.
Example: add it with cow add example activity-decay; rename the trait to
your domain’s vocabulary, or narrow the pattern (checkout.*, or a list)
if only some events should count as activity.
| Trigger | { event: "*" } |
| Tags | demo, retention |
| Consent purpose | emailMarketing |
The pattern
Section titled “The pattern”Timer loop as computed state. “Active in the last 30 days” expressed as a per-user durable timer instead of a nightly sweep: the trigger event sets the flag, every further activity event restarts the execution (which resets the silence clock with an empty journal), and 30 days of silence removes the flag (unset, not false, since exists predicates stop matching only when the key is gone). The execution then completes; the next activity starts a fresh one.
Reach for it when
Section titled “Reach for it when”Any rolling-window state a segment should see: active_30d, at_risk, slipping_away. Pair it with a segment on the trait and a second journey triggered by that segment’s entry: the composition pattern.
Make it yours
Section titled “Make it yours”Rename the trait and the activity event. The window ("30d") is plain configuration. api.restart() is what keeps a journey that loops for months bounded: it ends the execution and starts a fresh one under the same workflow id, so the journal never grows past one window.
The journey
Section titled “The journey”import { defineJourney } from "@cowliss/cli/journeys";
/** * Timer decay: "active in the last 30 days" as a per-user journey instead of * a nightly sweep. The trigger sets the flag and the execution waits; every * later event restarts the execution, which resets the 30-day silence clock * with a fresh, empty journal; 30 days of silence unsets the flag (removed, * not set to false, so `exists` predicates stop matching) and the execution * completes. The next event starts a new one. * * `"*"` is activity as it really arrives: whatever names your apps send, * page_view and purchase and login alike. It never matches an event Cowliss * wrote itself (those are all under `system.`), which is what stops the * trait write below from flipping a segment whose entry event restarts this * journey, forever. * * Example: add it with `cow add example activity-decay`; rename the trait to * your domain's vocabulary, or narrow the pattern (`checkout.*`, or a list) * if only some events should count as activity. */export default defineJourney({ trigger: { event: "*" }, purpose: "emailMarketing", tags: ["demo", "retention"], run: async (_event, api) => { await api.traits.set("active_30d", true); const active = await api.waitForEvent("*", { timeout: "30d" }); if (active) { // This is the loop, and it is bounded: restart ends this execution and // starts a fresh one under the same workflow id with an empty journal, // so a user who stays active for years never grows one. await api.restart({ event: active }); } await api.traits.unset("active_30d"); },});Its scenario
Section titled “Its scenario”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.
{ "user": { "id": "usr_1", "traits": {} }, "events": [{ "at": "10d", "event": "page_view" }], "expect": [ { "activity": "setTrait", "input": { "key": "active_30d", "value": true } }, { "activity": "restart" } ]}Try it
Section titled “Try it”# copy it into your projectcow add example activity-decay
# run its scenario on a virtual clockcow test activity-decay --scenario scenarios/activity-decay.json