CatalogScan

SEO Guide · 2026

Shopify Product Bundles and AI Shopping Agents: Structured Data Guide (2026)

Shopify has no native bundle schema — and neither does schema.org. Without custom structured data, your bundles appear to AI shopping agents as plain products with no savings context, no component breakdown, and no value comparison. Here's how to fix that.

TL;DR The Shopify Bundles app adds no bundle-specific JSON-LD. AI agents see your bundle as a generic product. The fix: use a Product schema with isRelatedTo linking each component product, an additionalProperty for bundle savings ("Save $18 vs. buying separately"), and a priceSpecification with the reference price alongside the bundle price. Availability logic must reflect that the bundle is out of stock if any single component is out of stock — handle this with Liquid conditionals checking each component variant's available property.

The bundle visibility problem: why AI agents can't understand your bundles

Shopify's product model was designed for individual SKUs. A bundle — three products sold together at a combined price — is represented in the Shopify data model as either a single product with a title like "Skincare Starter Kit (Cleanser + Toner + Moisturizer, 3-piece)" or as a product whose variants map to different bundle configurations. Neither representation tells Shopify — or AI agents — that this is a bundle.

Despite the Shopify Bundles app (launched natively in 2023), which handles inventory component management, there is still no semantic bundle metadata in Shopify's product schema, no bundle-type product flag, and no structured way to link a bundle to its components in the product data model. The Bundles app tracks inventory relationships internally but exposes nothing different to the storefront theme — your product page JSON-LD output is identical whether the product is a bundle or a standalone item.

For AI agents, this creates three distinct visibility failures:

Shopify bundle implementation types and AI visibility

Bundle implementationHow AI sees itJSON-LD type to useComponent product linksBundle savings visibility
Native Shopify Bundles app Generic product with combined title and single price Product + isRelatedTo (requires custom Liquid) None by default — must add manually None — no savings data output
Third-party bundle app (Fast Bundle, Rebundle) Generic product or widget overlay; no schema integration Product + isRelatedTo (requires custom Liquid) None — apps manage cart, not schema None — savings shown via JS widget only
Manual "bundle" product with combined title Single product; AI infers bundle from title keywords Product with isRelatedTo + additionalProperty for savings Possible — if you add isRelatedTo manually Partial — if additionalProperty savings added
Variant-based bundle (bundle configs as variants) Product with variants; AI may not understand variant = bundle size Product with AggregateOffer (price range across configs) None — variants don't map to components Partial — use variant name to signal savings
Custom line item properties (build-your-own bundle) Single product with dynamic configuration; AI sees base product only Product with additionalProperty for configuration options None — build-your-own not expressible in schema Describe in additionalProperty

AggregateOffer vs. isRelatedTo: choosing the right bundle schema approach

Two schema.org patterns are relevant for bundles, and each serves a different bundle type:

AggregateOffer is appropriate when your bundle has multiple pricing tiers — for example, a knife set sold in 3-piece, 5-piece, and 8-piece configurations at different price points. AggregateOffer expresses a price range (lowPrice / highPrice) and an offerCount. It does not communicate component structure.

isRelatedTo is the stronger choice for bundles where the value proposition is the combination of specific items. By linking the bundle product to each component product via isRelatedTo, and including each component's individual price, you give AI agents the data needed to compute bundle value and surface savings context.

Here is a complete JSON-LD example for a 3-item skincare bundle using the isRelatedTo pattern:

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Hydration Starter Kit — Cleanser, Toner & Moisturizer (3-piece)",
  "description": "Everything you need to start a hydrating skincare routine. Includes our best-selling Gentle Hydrating Cleanser (150ml), Balancing Toner (120ml), and Deep Moisture Cream (50ml). Save $18 compared to buying each item separately.",
  "sku": "SKU-HYDRATION-KIT-3P",
  "brand": {
    "@type": "Brand",
    "name": "Lumière Skincare"
  },
  "image": "https://yourdomain.com/products/images/hydration-kit.jpg",
  "offers": {
    "@type": "Offer",
    "price": "64.00",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "priceSpecification": {
      "@type": "PriceSpecification",
      "price": "64.00",
      "priceCurrency": "USD",
      "description": "Bundle price — save $18 vs. buying separately ($82.00 combined)"
    }
  },
  "additionalProperty": [
    {
      "@type": "PropertyValue",
      "name": "Bundle savings",
      "value": "Save $18 (22%) compared to buying items individually"
    },
    {
      "@type": "PropertyValue",
      "name": "Items included",
      "value": "3 full-size products: Gentle Hydrating Cleanser, Balancing Toner, Deep Moisture Cream"
    },
    {
      "@type": "PropertyValue",
      "name": "Suitable for",
      "value": "Normal, dry, and combination skin types; skincare beginners"
    }
  ],
  "isRelatedTo": [
    {
      "@type": "Product",
      "name": "Gentle Hydrating Cleanser, 150ml",
      "sku": "SKU-CLEANSER-150ML",
      "url": "https://yourdomain.com/products/gentle-hydrating-cleanser",
      "offers": {
        "@type": "Offer",
        "price": "28.00",
        "priceCurrency": "USD",
        "availability": "https://schema.org/InStock"
      }
    },
    {
      "@type": "Product",
      "name": "Balancing Toner, 120ml",
      "sku": "SKU-TONER-120ML",
      "url": "https://yourdomain.com/products/balancing-toner",
      "offers": {
        "@type": "Offer",
        "price": "26.00",
        "priceCurrency": "USD",
        "availability": "https://schema.org/InStock"
      }
    },
    {
      "@type": "Product",
      "name": "Deep Moisture Cream, 50ml",
      "sku": "SKU-MOISTURIZER-50ML",
      "url": "https://yourdomain.com/products/deep-moisture-cream",
      "offers": {
        "@type": "Offer",
        "price": "28.00",
        "priceCurrency": "USD",
        "availability": "https://schema.org/InStock"
      }
    }
  ]
}

With this schema, an AI agent has: the bundle price ($64), the combined individual price ($82 — derivable by summing the isRelatedTo component prices: $28 + $26 + $28), the savings amount and percentage (stated in additionalProperty), links to each component product's canonical page, and the use-case context (beginners, dry skin). This is enough to generate a confident "save 22% on the skincare starter kit" recommendation.

Bundle pricing: expressing savings in structured data

Schema.org has no dedicated "bundle savings" property, so you need to use a combination of approaches that AI agents can parse reliably.

Approach 1: additionalProperty for human-readable savings

The simplest and most AI-readable approach is an additionalProperty with a plain-English savings statement. AI language models parse this naturally and use it when generating recommendation copy:

{
  "@type": "PropertyValue",
  "name": "Bundle savings",
  "value": "Save $18.00 (22%) vs. buying Cleanser, Toner, and Moisturizer individually"
}

Approach 2: priceSpecification with reference price

For Google's Shopping graph, which prefers formal price structures, add a priceSpecification block alongside your bundle Offer:

"offers": {
  "@type": "Offer",
  "price": "64.00",
  "priceCurrency": "USD",
  "availability": "https://schema.org/InStock",
  "priceSpecification": [
    {
      "@type": "PriceSpecification",
      "price": "64.00",
      "priceCurrency": "USD",
      "name": "Bundle price"
    },
    {
      "@type": "PriceSpecification",
      "price": "82.00",
      "priceCurrency": "USD",
      "priceType": "https://schema.org/ListPrice",
      "name": "Individual items total"
    }
  ]
}

Google AI Mode can use the difference between the ListPrice and the actual bundle price to generate a savings callout, similar to how it handles sale/was prices on individual products.

Component product cross-links: why isRelatedTo improves AI understanding

When you include isRelatedTo entries with url properties pointing to your individual component product pages, you create a structured relationship that Google's Knowledge Graph can follow. This has two benefits:

  1. Value computation: By summing the prices in isRelatedTo component offers, AI agents can confirm (or compute) the savings — even if the additionalProperty savings value doesn't match due to price changes. The component prices serve as a verification source.
  2. Component query matching: If a shopper asks about one of the bundle components ("is the Lumière toner good?"), the Knowledge Graph connection from the toner's product page to the bundle page means the bundle may surface as a "complete set" recommendation in the response.

Using isPartOf instead of isRelatedTo on the component product pages (pointing back to the bundle) creates the reverse relationship: "this toner is part of the Hydration Starter Kit". Both directions strengthen the semantic connection.

The "save X% when bought together" pattern

The "better value" AI recommendation context — where an AI agent says "you could save 22% by buying the starter kit instead of individual items" — requires that the savings data be present in both structured data and visible page content. Google AI Mode reads both and weights pages where the savings claim appears in both locations more highly than structured-data-only claims.

In your product page template, include a visible text block that mirrors your JSON-LD savings statement:

{% comment %}Bundle savings callout — visible to users and AI crawlers{% endcomment %}
{% assign individual_total = 82 %}
{% assign bundle_price = product.price | money_without_currency | plus: 0 %}
{% assign savings = individual_total | minus: bundle_price %}
{% assign savings_pct = savings | times: 100 | divided_by: individual_total %}

<p class="bundle-savings">
  Save ${{ savings | round: 2 }} ({{ savings_pct | round: 0 }}%) compared to buying
  {{ product.metafields.bundle.component_names }} individually.
</p>

When the visible text and the JSON-LD additionalProperty savings value agree, AI agents treat the savings claim as high-confidence and are more likely to surface it in "better value" recommendation contexts.

Inventory and availability for bundles

A bundle is out of stock if any single component is out of stock. This is a logical requirement that the Shopify Bundles app enforces at the cart level, but your JSON-LD availability status needs to reflect the same logic. If your bundle JSON-LD says InStock while one component is sold out, AI agents may recommend the bundle to shoppers who will then encounter an out-of-stock state — a high-friction experience that reduces AI agent trust in your catalog data over time.

The correct Liquid pattern for bundle availability checks all component variant IDs stored in metafields:

{% comment %}
  Bundle availability check — all components must be available
  Requires metafield bundle.component_variant_ids (type: list.variant_reference)
{% endcomment %}

{% assign bundle_available = true %}
{% assign component_ids = product.metafields.bundle.component_variant_ids.value %}

{% if component_ids %}
  {% for variant_id in component_ids %}
    {% assign component_variant = all_products | map: 'variants' | flatten | where: 'id', variant_id | first %}
    {% unless component_variant.available %}
      {% assign bundle_available = false %}
    {% endunless %}
  {% endfor %}
{% endif %}

{% assign availability_url = "https://schema.org/OutOfStock" %}
{% if bundle_available and product.available %}
  {% assign availability_url = "https://schema.org/InStock" %}
{% endif %}

"offers": {
  "@type": "Offer",
  "price": "{{ product.selected_or_first_available_variant.price | money_without_currency }}",
  "priceCurrency": "{{ shop.currency }}",
  "availability": "{{ availability_url }}"
}

Note: Shopify's native Bundles app handles inventory deduction automatically when any component goes to zero stock — the bundle becomes unpurchasable. But the product.available flag in Liquid only reflects the bundle SKU's own inventory, not the component inventory state. This is why the explicit component check above is necessary for accurate JSON-LD availability output.

Practical Liquid snippet: full bundle JSON-LD template

This complete Liquid snippet generates a bundle-specific JSON-LD block using metafields to store component data. It handles availability, component cross-links, pricing, and savings output. Add it to your product.liquid template, wrapped in a conditional that checks for the "bundle" product tag or a metafield flag.

{% comment %}
  Complete bundle JSON-LD template
  Required metafields (namespace: "bundle"):
    bundle.is_bundle          (boolean)
    bundle.component_1_title  (single_line_text_field)
    bundle.component_1_url    (url)
    bundle.component_1_price  (number_decimal)
    bundle.component_1_sku    (single_line_text_field)
    bundle.component_2_title  (single_line_text_field)
    bundle.component_2_url    (url)
    bundle.component_2_price  (number_decimal)
    bundle.component_2_sku    (single_line_text_field)
    bundle.component_3_title  (single_line_text_field)
    bundle.component_3_url    (url)
    bundle.component_3_price  (number_decimal)
    bundle.component_3_sku    (single_line_text_field)
    bundle.individual_total   (number_decimal — sum of all component prices)
{% endcomment %}

{% if product.metafields.bundle.is_bundle %}
  {% assign bundle_price = product.selected_or_first_available_variant.price | money_without_currency %}
  {% assign individual_total = product.metafields.bundle.individual_total %}
  {% assign savings_amount = individual_total | minus: bundle_price %}
  {% assign savings_pct = savings_amount | times: 100 | divided_by: individual_total | round: 0 %}

<script type="application/ld+json" data-cfasync="false">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "{{ product.title | escape }}",
  "description": "{{ product.description | strip_html | truncate: 300 | escape }}",
  "sku": "{{ product.selected_or_first_available_variant.sku | escape }}",
  "brand": {
    "@type": "Brand",
    "name": "{{ product.vendor | escape }}"
  },
  "image": "{{ product.featured_image | img_url: 'master' | prepend: 'https:' }}",
  "offers": {
    "@type": "Offer",
    "price": "{{ bundle_price }}",
    "priceCurrency": "{{ shop.currency }}",
    "availability": "{% if product.available %}https://schema.org/InStock{% else %}https://schema.org/OutOfStock{% endif %}",
    "url": "{{ shop.url }}{{ product.url }}",
    "priceSpecification": [
      {
        "@type": "PriceSpecification",
        "price": "{{ bundle_price }}",
        "priceCurrency": "{{ shop.currency }}",
        "name": "Bundle price"
      },
      {
        "@type": "PriceSpecification",
        "price": "{{ individual_total }}",
        "priceCurrency": "{{ shop.currency }}",
        "priceType": "https://schema.org/ListPrice",
        "name": "Individual items total"
      }
    ]
  },
  "additionalProperty": [
    {
      "@type": "PropertyValue",
      "name": "Bundle savings",
      "value": "Save {{ savings_amount | money }} ({{ savings_pct }}%) vs. buying items separately"
    },
    {
      "@type": "PropertyValue",
      "name": "Items included",
      "value": "{{ product.metafields.bundle.component_1_title | escape }}, {{ product.metafields.bundle.component_2_title | escape }}{% if product.metafields.bundle.component_3_title != blank %}, {{ product.metafields.bundle.component_3_title | escape }}{% endif %}"
    }
  ],
  "isRelatedTo": [
    {
      "@type": "Product",
      "name": "{{ product.metafields.bundle.component_1_title | escape }}",
      "sku": "{{ product.metafields.bundle.component_1_sku | escape }}",
      "url": "{{ product.metafields.bundle.component_1_url }}",
      "offers": {
        "@type": "Offer",
        "price": "{{ product.metafields.bundle.component_1_price }}",
        "priceCurrency": "{{ shop.currency }}"
      }
    },
    {
      "@type": "Product",
      "name": "{{ product.metafields.bundle.component_2_title | escape }}",
      "sku": "{{ product.metafields.bundle.component_2_sku | escape }}",
      "url": "{{ product.metafields.bundle.component_2_url }}",
      "offers": {
        "@type": "Offer",
        "price": "{{ product.metafields.bundle.component_2_price }}",
        "priceCurrency": "{{ shop.currency }}"
      }
    }
    {% if product.metafields.bundle.component_3_title != blank %},
    {
      "@type": "Product",
      "name": "{{ product.metafields.bundle.component_3_title | escape }}",
      "sku": "{{ product.metafields.bundle.component_3_sku | escape }}",
      "url": "{{ product.metafields.bundle.component_3_url }}",
      "offers": {
        "@type": "Offer",
        "price": "{{ product.metafields.bundle.component_3_price }}",
        "priceCurrency": "{{ shop.currency }}"
      }
    }
    {% endif %}
  ]
}
</script>
{% endif %}

This template is intentionally designed to fall back gracefully: the outer {% if product.metafields.bundle.is_bundle %} check ensures that non-bundle products use your default product JSON-LD without interference. Your theme's standard product schema block handles all non-bundle products; this snippet only fires for products with the bundle metafield set to true.

Frequently asked questions

How do AI shopping agents display product bundles from Shopify?

AI shopping agents treat Shopify bundles as generic products unless structured data explicitly communicates the bundle nature. Without bundle-specific schema, an AI agent sees a product at a single price with no component breakdown or savings context. With proper bundle structured data — a Product schema using isRelatedTo to link component products, an additionalProperty for bundle savings, and a priceSpecification with the reference price — AI agents can surface the bundle in "value for money" and "complete kit" query contexts with the savings callout in the recommendation.

What is the best schema.org type for a product bundle?

There is no dedicated schema.org Bundle type. Use a standard Product schema for the bundle itself, with isRelatedTo properties linking to each component product. Each component should have its own Product schema with its individual price. This approach lets AI agents understand the component relationship, calculate individual item prices, and compare the bundle value against buying separately. Avoid using ItemList for bundles — it implies a listicle or collection, not a single purchasable product containing multiple items.

Does the Shopify Bundles app add structured data automatically?

No. The Shopify Bundles app creates bundle products and manages inventory across component SKUs, but it does not add any bundle-specific JSON-LD. The bundle product page receives Shopify's standard product JSON-LD — name, price, availability, images — with no indication that this is a bundle, no component product links, and no bundle savings markup. Merchants must add custom Liquid JSON-LD to their product template that detects bundle products and outputs the component relationships and savings data.

How can I show bundle savings in AI shopping agent results?

Use two complementary approaches. First, add an additionalProperty to your bundle JSON-LD with name "Bundle savings" and a value like "Save $18 (22%) compared to buying items separately". Second, include a priceSpecification with a ListPrice (the combined individual item total) alongside the actual bundle price — the difference is the implied saving. In your visible page content, include a human-readable savings sentence that matches the JSON-LD values. Google AI Mode reads both structured data and visible content — having both signals aligned reinforces the savings message in recommendation outputs.

Check your bundle structured data for AI agent visibility

CatalogScan identifies missing bundle schema, incorrect availability logic, and the 20+ other structured data signals that AI shopping agents use to surface — or skip — your products.

Scan your store free