<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shaun Reynolds</title>
    <description>The latest articles on DEV Community by Shaun Reynolds (@emailmarketingtools).</description>
    <link>https://dev.to/emailmarketingtools</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4017757%2F874cba9c-732b-4234-a132-fec8e1587368.png</url>
      <title>DEV Community: Shaun Reynolds</title>
      <link>https://dev.to/emailmarketingtools</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emailmarketingtools"/>
    <language>en</language>
    <item>
      <title>How we built a live has_active_wishlist flag for Shopify (and why it was harder than it looks)</title>
      <dc:creator>Shaun Reynolds</dc:creator>
      <pubDate>Mon, 06 Jul 2026 12:58:46 +0000</pubDate>
      <link>https://dev.to/emailmarketingtools/how-we-built-a-live-hasactivewishlist-flag-for-shopify-and-why-it-was-harder-than-it-looks-5h70</link>
      <guid>https://dev.to/emailmarketingtools/how-we-built-a-live-hasactivewishlist-flag-for-shopify-and-why-it-was-harder-than-it-looks-5h70</guid>
      <description>&lt;p&gt;Most Shopify wishlist apps have the same quiet flaw: the wishlist data lives in the app, and the merchant's email platform never sees it. The store &lt;em&gt;knows&lt;/em&gt; who wants what — but can't email on it.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://wishlist-ninja.com" rel="noopener noreferrer"&gt;WishList Ninja&lt;/a&gt; to fix exactly that. Here's how the interesting bits work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: profile properties, not just events
&lt;/h2&gt;

&lt;p&gt;Everyone fires events ("Added to Wishlist"). Events are fine for triggering flows, but useless for &lt;em&gt;segmentation&lt;/em&gt; — you can't build a clean segment of "people with items saved right now" from a two-month-old event.&lt;/p&gt;

&lt;p&gt;So on every save/remove we write &lt;strong&gt;profile properties&lt;/strong&gt; to Klaviyo, Mailchimp or Omnisend in real time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;has_active_wishlist&lt;/code&gt; — true &lt;strong&gt;only while the wishlist has items in it&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wishlist_count&lt;/code&gt; — items currently saved&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wishlist_value&lt;/code&gt; — what they're worth&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;top_saved_item&lt;/code&gt; — the product they want most&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That first flag sounds trivial. It isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a boolean was the hard part&lt;br&gt;
**&lt;br&gt;
&lt;code&gt;has_active_wishlist&lt;/code&gt; has to flip **false&lt;/strong&gt; again — otherwise it's just "has ever used the wishlist," which is worthless. So every path that empties a wishlist has to re-evaluate it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shopper removes the last item ✅&lt;/li&gt;
&lt;li&gt;shopper buys the last item (orders webhook marks items purchased) ✅&lt;/li&gt;
&lt;li&gt;guest list merges into an account ✅&lt;/li&gt;
&lt;li&gt;and the subtle one: saved &lt;em&gt;collections&lt;/em&gt; don't count. A shopper following a collection but holding zero items is not "active intent." Items only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule, five code paths. The rule lives in exactly one function so the paths can't drift apart.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;7 flow triggers from 2 webhooks&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
The email moments merchants actually want are mostly &lt;em&gt;store-side&lt;/em&gt; changes, not shopper actions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Added to Wishlist · Price Drop · On Sale · Low Stock · Back in Stock · Collection Saved · New in Saved Collection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Five of those seven come from diffing &lt;code&gt;products/update&lt;/code&gt; webhooks against what shoppers have saved: price went down → find everyone holding that product → fire &lt;code&gt;Price Drop&lt;/code&gt; into their ESP. No polling, no cron scanning the catalog.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Multi-ESP fan-out without domino failures&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Supporting three ESPs means three APIs that can each be down, rate-limited, or misconfigured. Early version: Klaviyo throwing meant Mailchimp and Omnisend never got the event. Bad.&lt;/p&gt;

&lt;p&gt;Now each delivery is isolated — a failure is caught and logged, and the job only retries if &lt;strong&gt;nothing&lt;/strong&gt; was delivered. One dead API key can't block the other platforms.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;The storefront budget: ~12KB&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
The whole storefront (hearts on product cards, slide-out drawer, wishlist page, share links, save-for-later on the cart) is vanilla JS, minified to under ~12KB. No framework, no fonts, no CSS libraries. Core Web Vitals are a ranking signal; an app that slows the store is stealing from the merchant to fund itself.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Stack&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Laravel + MySQL + Redis on Cloudways, Shopify theme app extension for the storefront, Mailjet for transactional email — and a large amount of Claude Code for the build itself.&lt;/p&gt;

&lt;p&gt;Launching on the Shopify App Store now. If you run (or build for) a Shopify store, I'd love your take: &lt;a href="https://wishlist-ninja.com" rel="noopener noreferrer"&gt;wishlist-ninja.com&lt;/a&gt; 🥷&lt;/p&gt;

&lt;p&gt;Happy to go deeper on any of this in the comments — especially the webhook diffing.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>marketing</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
