The PDP routine module uses flexbox, border-radius, hover states, and JavaScript — none of which work in email. Gmail strips <style> tags entirely. Outlook uses Word's rendering engine. We need a different approach that achieves the same visual and strategic effect within these constraints.
Additionally, Klaviyo's show/hide logic works with profile properties only (not order history directly). So we need to sync routine state data to each customer's Klaviyo profile to drive personalisation.
Design a set of image assets (one per routine state combination) and use Klaviyo's show/hide blocks to display the correct image based on profile properties. Each image is a single PNG/JPG of the 4-step routine strip with the appropriate steps shown as owned, missing, or current.
Build the 4-step strip as a nested HTML table with inline styles. Use Klaviyo's show/hide on individual table cells to swap content (owned vs missing state). Use dynamic images for product thumbnails.
Build a lightweight image API that generates the routine strip on-the-fly per customer. Pass profile properties as URL parameters, return a rendered PNG. Embed as a dynamic image in Klaviyo.
Create a set of 600px wide PNG images — one for each customer state. Each image shows the 4-step routine strip with the appropriate visual treatment (highlighted, owned, missing).
Use a Shopify Flow or webhook to write custom properties to each customer's Klaviyo profile: their routine type (clarity/radiance), which steps they own, and their progress (1/4, 2/4, etc.).
Each email template contains multiple image blocks (one per variant), each with a show/hide condition based on the profile properties. Only one image renders per recipient.
The image links to the set PDP, concern landing page, or personalised routine page — depending on the customer's state.
These custom properties need to be synced to each customer profile (via Shopify Flow, Klaviyo webhook, or a nightly sync script):
| Property | Type | Values | Source |
|---|---|---|---|
routine_type |
Text | clarity / radiance / none | Routine Finder result or inferred from orders |
routine_step_cleanse |
Boolean | true / false | Order history — has purchased Cleanser |
routine_step_activate |
Boolean | true / false | Order history — has purchased Neutralising Gel, Brightly Serum, or Nightly Serum |
routine_step_hydrate |
Boolean | true / false | Order history — has purchased Moisturiser or Moisturiser Intense |
routine_step_protect |
Boolean | true / false | Order history — has purchased Daily Sunscreen |
routine_progress |
Number | 0 / 1 / 2 / 3 / 4 | Count of owned steps |
is_subscriber |
Boolean | true / false | Skio subscription status |
has_bought_kit |
Boolean | true / false | Order history — has purchased any trial kit |
has_completed_quiz |
Boolean | true / false | Octane AI / Routine Finder — set when quiz is submitted |
routine_type, has_completed_quiz, and the recommended products. This is the earliest signal — it arrives before any purchase. (2) Shopify order events — a Shopify Flow triggered on "Order paid" updates the step booleans and progress count via the Klaviyo API. (3) Skio subscription events — sets is_subscriber. A nightly batch sync as a safety net catches anything the real-time triggers miss (refunds, cancellations, edge cases).
We don't need an image for every possible combination of 4 boolean steps (which would be 16). Instead, we design for the realistic customer journeys. Two segments deserve special attention: quiz completers (~150-200/day who have a routine recommendation but haven't purchased) and single-product buyers (1/4 steps — the biggest routine expansion opportunity).
| Variant | Routine | State | Show/hide condition | CTA destination |
|---|---|---|---|---|
| 1. Cold (no data) | Neutral | All 4 steps shown generically, no highlighting. "Discover your routine" messaging. | not routine_type AND not has_completed_quiz |
Routine Finder quiz |
| 2. Quiz completer — Clarity | Clarity | Shows personalised Clarity routine with all 4 steps named. "Your recommended routine" header. Emphasis on the trial kit as entry point. No checkmarks — nothing purchased yet. | has_completed_quiz == 1 AND routine_type == 'clarity' AND routine_progress == 0 |
Clarity Kit PDP (or Clarity Set PDP) |
| 3. Quiz completer — Radiance | Radiance | Same as above but Radiance products | has_completed_quiz == 1 AND routine_type == 'radiance' AND routine_progress == 0 |
Radiance Kit PDP (or Radiance Set PDP) |
| 4. Kit buyer — Clarity | Clarity | "You've tried the kit" badge, arrow pointing to full-size set | has_bought_kit == 1 AND routine_type == 'clarity' AND routine_progress < 4 |
Clarity Set PDP |
| 5. Kit buyer — Radiance | Radiance | Same as above but Radiance products | has_bought_kit == 1 AND routine_type == 'radiance' AND routine_progress < 4 |
Radiance Set PDP |
| 6. Single product buyer (1/4) — Clarity | Clarity | 1 step with ✓, 3 dashed gaps. Strong "you've started — keep going" message. Progress ring shows 1/4. Highlights the NEXT most logical step to add. Price shows cost to complete. | routine_type == 'clarity' AND routine_progress == 1 AND has_bought_kit != 1 |
Clarity Set PDP |
| 7. Single product buyer (1/4) — Radiance | Radiance | Same as above but Radiance products | routine_type == 'radiance' AND routine_progress == 1 AND has_bought_kit != 1 |
Radiance Set PDP |
| 8. Partial routine (2 steps) — Clarity | Clarity | 2 ✓, 2 dashed gaps, progress ring 2/4. "Halfway there" message. | routine_type == 'clarity' AND routine_progress == 2 AND has_bought_kit != 1 |
Clarity Set PDP |
| 9. Partial routine (2 steps) — Radiance | Radiance | Same as above but Radiance products | routine_type == 'radiance' AND routine_progress == 2 AND has_bought_kit != 1 |
Radiance Set PDP |
| 10. Nearly complete (3/4) — Clarity | Clarity | 3 ✓ badges, 1 gap highlighted with "Just 1 step to go!" Strong urgency. | routine_type == 'clarity' AND routine_progress == 3 |
Missing product PDP |
| 11. Nearly complete (3/4) — Radiance | Radiance | Same as above but Radiance products | routine_type == 'radiance' AND routine_progress == 3 |
Missing product PDP |
| 12. Full routine (no sub) | Both | All 4 ✓, "Subscribe and save 20%" CTA | routine_progress == 4 AND is_subscriber != 1 |
Flawless365 page |
| 13. Active subscriber | Both | All 4 ✓, "You're on Flawless365" badge, referral CTA | is_subscriber == 1 |
Referral page or account |
That's 13 images total. The two new priority segments:
These mockups simulate how the routine strip renders as a single image block within an email. Toggle between states:
In the Klaviyo template editor, you'd add 13 image blocks stacked vertically. Each one has a show/hide condition. Only one renders per recipient. Here's the logic for each (showing Clarity examples — duplicate with 'radiance' for Radiance variants):
// Block 1: Cold (no quiz, no purchase data) Condition: not person|lookup:'routine_type' and not person|lookup:'has_completed_quiz' // Fallback — shows when we know nothing about them // Block 2: Quiz completer — Clarity (no purchase) Condition: person|lookup:'has_completed_quiz' == 1 and person|lookup:'routine_type' == 'clarity' and person|lookup:'routine_progress' == 0 // ~150-200 quiz completions/day, high intent, no conversion yet // Block 4: Kit buyer — Clarity Condition: person|lookup:'has_bought_kit' == 1 and person|lookup:'routine_type' == 'clarity' and person|lookup:'routine_progress' < 4 // Block 6: Single product buyer (1/4) — Clarity Condition: person|lookup:'routine_type' == 'clarity' and person|lookup:'routine_progress' == 1 and person|lookup:'has_bought_kit' != 1 // Key retention segment — 1 product, 3 gaps // Block 8: Partial routine (2/4) — Clarity Condition: person|lookup:'routine_type' == 'clarity' and person|lookup:'routine_progress' == 2 and person|lookup:'has_bought_kit' != 1 // Block 10: Nearly there (3/4) — Clarity Condition: person|lookup:'routine_type' == 'clarity' and person|lookup:'routine_progress' == 3 // Block 12: Complete — not subscribed Condition: person|lookup:'routine_progress' == 4 and person|lookup:'is_subscriber' != 1 // Block 13: Active subscriber (overrides all) Condition: person|lookup:'is_subscriber' == 1
routine_progress == 0 (no purchases); kit buyer includes has_bought_kit == 1 while single-product and partial checks exclude it with has_bought_kit != 1; the subscriber block uses is_subscriber == 1 which overrides all purchase states. Test with profiles at each state before going live.
The routine strip should appear in a consistent position across all emails — creating a persistent, recognisable element that reinforces the routine concept over time.
| Email type | Position | Rationale |
|---|---|---|
| Welcome flow | After main CTA, before footer | Introduces the system concept early. "Here's how the routine works." |
| Post-purchase | After order confirmation content | Shows their progress. "You're 1/4 of the way there." |
| Campaigns / promos | After main campaign content, before footer | Persistent reminder. Every campaign email subtly nudges routine completion. |
| Pre-charge (F365) | Below delivery details | Subscriber variant. "Your routine is on autopilot" + referral CTA. |
| Winback / re-engagement | Primary content position | Shows what they've built and what's missing. "Your routine misses you." |
| Browse/cart abandonment | Below abandoned product | Contextualises the abandoned product within the system. |
If you outgrow the pre-rendered image approach (e.g., you want to show the exact products a customer owns with their actual product images), here's how the dynamic image API would work:
// Klaviyo dynamic image URL https://routine-img.drsamsbunting.com/strip ?routine=clarity &steps=1,1,0,0 // owned: cleanse, activate &progress=2 &subscriber=false // In Klaviyo, the URL would be: https://routine-img.drsamsbunting.com/strip ?routine={{ person|lookup:'routine_type' }} &steps={{ person|lookup:'routine_step_cleanse'|default:'0' }},{{ person|lookup:'routine_step_activate'|default:'0' }},{{ person|lookup:'routine_step_hydrate'|default:'0' }},{{ person|lookup:'routine_step_protect'|default:'0' }} &progress={{ person|lookup:'routine_progress'|default:'0' }} &subscriber={{ person|lookup:'is_subscriber'|default:'0' }}
The API (a Vercel edge function or Cloudflare Worker) would receive these parameters and return a rendered PNG using something like Satori (from Vercel) or Puppeteer. One image block in Klaviyo, zero show/hide logic, infinite personalisation.