Klaviyo

Klaviyo profile properties: campaign-ready reorder data

The nightly reotter_* profile properties — due products, buy-again usuals, personalized recommendations — and how to render them in any campaign or flow.

August 19, 2026

Nightly, reOtter refreshes a small set of reotter_* properties on each recently-active customer's Klaviyo profile (recent purchasers plus active subscribers). Because they live on the profile — not on an event — they work anywhere: campaigns, and flows reOtter doesn't trigger, like a subscription platform's billing reminder.

For reOtter-triggered flows, use the richer event payload instead: see Klaviyo event data. Rule of thumb: flows read the event, campaigns read the profile.

Only customers who already exist in your Klaviyo account are updated — reOtter never creates profiles.

The properties

Property Type Contents
reotter_reorder_state text due, buy_again, or none — drive show/hide conditions off this
reotter_reorder_products list due/overdue products soonest-first when due; the customer's usual repeat products when buy_again; [] when none. First entry is the best pick
reotter_recommended_products list personalized cross-sell picks from reOtter's recommendation engine; [] when none
reotter_storefront_url text the customer's personal reorder storefront — the CTA link (attributed; no UTM params)
reotter_synced_at text when reOtter last refreshed this profile

Product entries carry a lean seven-key card — every key always present: productId, variantId, productName, variantName, price, productImage, productUrl.

Where should clicks go?

Send clicks to reotter_storefront_url, not the individual productUrls. The storefront link opens the customer's personal reorder page — the same due products and recommendations they saw in the email, pre-organized and ready to add to cart — and it carries attribution, so reOtter can measure what the campaign actually drove. A productUrl is just the plain product page: useful when a design genuinely needs a direct per-product link, but there's no personalization and no attribution on the other side.

Slot counts (1–6 per section) and section toggles are configurable per store on the Profile Sync tab.

The Profile Sync tab in reOtter — master toggle with sync stats, reorder and recommended product sections with slot counts, and the property reference

Here's what a synced property looks like on a Klaviyo profile — reotter_recommended_products holding the card array:

A Klaviyo profile property showing the reotter_recommended_products JSON array of product cards

Rendering option 1 — reOtter's fields directly (recommended)

Fixed slots in the drag-and-drop editor (index .0.5):

Image src : {{ person.reotter_recommended_products.0.productImage }}
Text      : {{ person.reotter_recommended_products.0.productName }} — ${{ person.reotter_recommended_products.0.price }}
Button URL: {{ person.reotter_storefront_url }}

Or loop in a custom HTML block, with one storefront CTA under the grid:

{% for item in person.reotter_recommended_products %}
  <img src="{{ item.productImage }}" width="64">
  <p>{{ item.productName }} — ${{ item.price }}</p>
{% endfor %}
<a href="{{ person.reotter_storefront_url }}">Shop your picks</a>

This renders from data reOtter refreshed last night — zero dependency on Klaviyo's catalog state.

Rendering option 2 — native catalog lookup

Because entries carry both Shopify IDs, Klaviyo's own catalog tag also works:

{% catalog person.reotter_recommended_products.0.productId integration='shopify' %}
  <img src="{{ catalog_item.featured_image.full.src }}">
  {{ catalog_item.title }}
{% endcatalog %}
<a href="{{ person.reotter_storefront_url }}">Shop your picks</a>

Warning: a catalog lookup that can't resolve (product deleted or unpublished) makes Klaviyo skip the entire message. Guard with conditionals, or prefer option 1 for campaign sends.

Gating blocks (empty states)

  • Reorder section: show/hide on reotter_reorder_state (equals due, or does not equal none to include buy-again).
  • Recommendations: condition on reotter_recommended_products.0.productId being set.
  • Framing: due → "ready to reorder"; buy_again → "stock up on your favorites."

Drop-in blocks

Prebuilt, self-guarding universal-content blocks — irrelevant sections collapse per recipient, so a brand-new customer simply sees nothing:

  • reOtter · Smart Reorder (campaign) — due or buy-again framing, self-resolving
  • reOtter · Reorder Due Only (campaign) — renders only for due customers
  • reOtter · Recommended Products (campaign) — the personalized cross-sell grid
  • reOtter · Reorder + Recommended (campaign) — both, stacked

Klaviyo's universal content has no per-insertion options — pick the variant matching the campaign's intent, or "unlink and edit independently" to customize a copy. The (flow) blocks bind event data and belong in reOtter-triggered flows; the (campaign) blocks bind profile data and work anywhere.

The logic inside each block (paste-able)

Prefer to hand-roll the layout, or start from the same logic in your own custom HTML block? Each drop-in reduces to a few lines of Klaviyo template code — copy, then style however you like.

Smart Reorder — due or buy-again framing, renders nothing for everyone else:

{% if person.reotter_reorder_state == 'due' or person.reotter_reorder_state == 'buy_again' %}
  <h2>{% if person.reotter_reorder_state == 'due' %}Ready to reorder{% else %}Stock up on your favorites{% endif %}</h2>
  {% for item in person.reotter_reorder_products %}
    <img src="{{ item.productImage }}" alt="{{ item.productName }}" width="64">
    <p>{{ item.productName }}{% if item.variantName %} — {{ item.variantName }}{% endif %} &middot; ${{ item.price }}</p>
  {% endfor %}
  <a href="{{ person.reotter_storefront_url }}">{% if person.reotter_reorder_state == 'due' %}Reorder here{% else %}Shop your favorites{% endif %}</a>
{% endif %}

Reorder Due Only — the same section, gated to due customers only:

{% if person.reotter_reorder_state == 'due' %}
  <h2>Ready to reorder</h2>
  {% for item in person.reotter_reorder_products %}
    <img src="{{ item.productImage }}" alt="{{ item.productName }}" width="64">
    <p>{{ item.productName }}{% if item.variantName %} — {{ item.variantName }}{% endif %} &middot; ${{ item.price }}</p>
  {% endfor %}
  <a href="{{ person.reotter_storefront_url }}">Reorder here</a>
{% endif %}

Recommended Products — collapses when the customer has no recommendations:

{% if person.reotter_recommended_products.0.productId %}
  <h2>Picked for you</h2>
  {% for item in person.reotter_recommended_products %}
    <img src="{{ item.productImage }}" alt="{{ item.productName }}" width="64">
    <p>{{ item.productName }}{% if item.variantName %} — {{ item.variantName }}{% endif %} &middot; ${{ item.price }}</p>
  {% endfor %}
  <a href="{{ person.reotter_storefront_url }}">Shop recommendations</a>
{% endif %}

Reorder + Recommended is exactly the Smart Reorder section followed by the Recommended Products section — each guard fires independently, so any combination of the two renders per recipient.

Find them in Klaviyo's email editor under Add content → Universal — search "reotter":

Klaviyo's universal content picker filtered to reOtter blocks, showing the recommended-products and reorder drop-ins

Recipes

  • Billing-reminder cross-sell (your subscription platform's flow, not ours): add a reotter_recommended_products grid to the billing-reminder template — discovery content for subscribers, refreshed nightly, no reOtter event needed.
  • Campaign personalization at scale: drop Smart Reorder (campaign) into every campaign — due customers get their reorder, engaged-but-not-due customers get their usuals, everyone else sees the campaign unchanged.
  • Compliance note: the properties are pure data (no consent information) — your Klaviyo flow filters decide marketing vs transactional treatment, exactly as with any other content.

Building inside a reOtter-triggered flow? The event payload is richer: Klaviyo event data.