Skip to main content

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 emits cart.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 no cart.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 timecheckout.completed
  • Delay1 h
  • Group bycartId
Read it as the dashboard states it back to you: if 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.
The email is not a duplicate of the push — it is the coverage. On iPhone and iPad, web push only exists for a product installed as a PWA (Add to Home Screen, iOS 16.4 or later); a regular Safari tab cannot receive one. For a mobile-heavy shop, a push-only sequence silently skips a large share of the audience. See platforms & privacy.
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.
The enricher is registered once, in your backend, with the Node SDK:
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:
Filtering on 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 to checkout.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, carrying cartId and userId
  • checkout.completed, carrying the same cartId, plus a canonical identity (id or email) 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.js served at the root of the shop’s origin
No customer data moves into Notifizz. The basket is read at send time and forgotten.

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.
A basket reminder that sends to everyone who entered it is a basket reminder that is talking to customers who already paid.

Troubleshooting

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