TL;DR
Maison Verdure sells plants and garden tools online. Two baskets in three are filled and then left. They want to come back once, kindly, and never speak to someone who has already paid — or whose basket no longer exists.The situation
The Maison Verdure backend is loud about what happens. It emitscart.updated every time someone adds a shrub, checkout.completed when the payment clears, order.shipped when the van leaves. Every one of those is an action.
Abandonment is not an action. Nobody clicks I give up. The customer closes the tab, the phone rings, the evening happens. The only trace is a basket that stopped moving.
Why the obvious approach fails
There is nocart.abandoned event, and writing one is more work than it sounds. You need a timer per basket, armed on every update, disarmed on payment, surviving a deploy and a restart, not firing twice when two workers pick the same row. That is a small scheduling system living inside a shop — code to write, test, monitor and page someone about at 3 a.m.
And it puts a marketing decision in a release. “Make it 90 minutes instead of 60” becomes a ticket.
1. Make the silence an event
Create a Reminder synthetic event from Events → New event:- If this event happens… —
cart.updated - …but none of these happen in time —
checkout.completed - Delay —
1 h - Group by —
cartId
cart.updated happens but checkout.completed does not within 1 h, fire — for each cartId.
Notifizz arms a timer when the trigger arrives, re-arms it on the next update, cancels it on the reset, and emits an event only if the hour runs out in silence. No timer table on your side, no release to change the delay.
The reminder is an extra timer laid on top, not a replacement.
cart.updated and checkout.completed keep flowing through your other campaigns exactly as before — the reminder observes them, it does not consume them. Nothing you already send changes.2. Two messages, two channels
The campaign subscribes to that reminder event and holds two messages:
Web push first, because it lands on the device even with the tab closed and costs nothing to ignore. Email a day later, because a push that arrived during a meeting is gone.
This is a
Product campaign: promotional mail, so it carries a one-click unsubscribe and honours the person’s notification preferences. A basket reminder is not a receipt — do not file it as transactional to dodge the opt-out.
3. The enricher makes the basket honest
Here is the part that decides whether this campaign is helpful or embarrassing. The email leaves 25 hours after the last basket update. In that window a plant sells out, a price changes, the customer removes half the order, or they pay from a different device. An email built from the payload captured yesterday shows a basket that no longer exists — with a total that no longer applies. Notifizz never stores that basket. The orchestrator runs at each step of the sequence, and the enricher it calls reads your database at that moment. The push shows the basket as it was an hour ago; the email shows the basket as it is this morning. Same campaign, same code, two truthful messages. The same call is the guardrail. If the basket comes back paid, empty, or gone, the orchestrator returns an empty list and nothing is sent — the run is recorded in the Outbox as No recipients and no message is created.For the developer: the enricher and the orchestrator
For the developer: the enricher and the orchestrator
The enricher is registered once, in your backend, with the Node SDK:Filtering on
cache: false matters here. The default caches an enricher’s answer for an hour, which is right for a user profile and wrong for a basket you are about to quote in an email.The orchestrator — AI-generated from the campaign description, then yours to review — reads the original payload off the reminder and asks for the live version:inStock is the small detail that keeps the message credible: you never advertise the one item the customer can no longer buy.4. Stop on the payment, not on the open
On the Recipients card, set the objective tocheckout.completed and and ends when → the objective is reached.
Someone who pays after the push never receives the email. The remaining messages of their sequence are cancelled; everybody else keeps theirs.
That gives Maison Verdure two independent guards, and they catch different things:
Belt and braces, and the braces cost one
if.
What Maison Verdure sets up
cart.updated, carryingcartIdanduserIdcheckout.completed, carrying the samecartId, plus a canonical identity (idoremail) so the objective can be credited- One enricher,
fetchCart, returning the live basket - Web push: the environment key generated in Settings → Web push, and
notifizz-sw.jsserved at the root of the shop’s origin
Rehearsing it before it is Live
Open the reminder event: the Scheduled reminders panel lists the timers armed in your non-production environments, with when each one fires — production timers are never shown here. Fire now triggers one immediately instead of waiting an hour, Cancel disarms it; both are test actions, available on non-production environments only. Whatever you fire lands in the Outbox like a normal run, so you can fill a test basket, fire the reminder, and read the message that comes out — then pay, and watch the second one not come out.Reading the results
The campaign’s Sends bar shows fewer emails than reminders fired. That is the campaign working:- People who paid between the push and the email sit in their own layer — a success, not a warning.
- Runs where the enricher found nothing left to talk about appear in the Outbox as No recipients, with the enriched payload attached so you can see what came back.
Troubleshooting
The reminder fires even though the customer paid
The reminder fires even though the customer paid
The reset never matched. Either
checkout.completed is not listed in Reset events, or it does not carry the same Grouped by value as the trigger. The timer is keyed on that value: a cart.updated grouped by cartId is only cancelled by an event carrying that exact cartId. Check the payload of a real checkout.completed in the events catalogue.The same customer gets reminded twice in one session
The same customer gets reminded twice in one session
cart.updated re-arms the timer on every change, so the hour runs from the last basket activity. That is usually what you want — someone still adding items is not abandoning anything. If you would rather count from the moment the basket was born, trigger the reminder on cart.created instead: it fires once per basket, whatever happens next.Nobody receives the web push
Nobody receives the web push
Two usual causes, both visible in Settings → Web push. The environment has no key pair generated, or
notifizz-sw.js is not served at the root of your origin — the widget verifies this from a real browser and reports it per environment. In either case the opt-in status stays unavailable and no subscription is ever created, so there is nothing to deliver to.Far fewer emails than pushes
Far fewer emails than pushes
Expected. Three populations drop out between the two messages: those who paid (removed by the objective), those whose basket came back empty or paid from the enricher (no recipient), and those who opted out of promotional mail. The Sends bar separates them — the first two are wins, and only the third is worth acting on.
The email shows an item that is out of stock
The email shows an item that is out of stock
The enricher is being served from cache. The default policy keeps an answer for an hour, keyed on the parameters you pass, so a basket fetched for the push can still be returned for the email. Declare
cache: false on the enricher that reads the basket — it is the one place where a cache hit is a bug.See also
Synthetic events
The four types Notifizz generates for you, and what each one puts in
event.properties.Objectives & conversions
What counts as done, how it is credited, and how it stops a sequence in flight.
Why enrichers
Why Notifizz reads your data at send time instead of holding a copy of it.
Migration deadline
The other side of the coin: one entry per person, five reminders, stopped on the act.