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.
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:
- No savings context: An AI agent can't calculate or surface "save 22% vs. buying separately" because it doesn't know what the items cost individually.
- No component transparency: A shopper asking "what comes in the skincare starter kit?" requires an AI to parse your product description rather than read structured component data — less reliable and less likely to generate a confident recommendation.
- No "value pack" query matching: Queries like "best starter kit", "complete set", "bundle deal for X" can't match confidently against products that don't structurally declare themselves as bundles.
Shopify bundle implementation types and AI visibility
| Bundle implementation | How AI sees it | JSON-LD type to use | Component product links | Bundle 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:
- 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.
- 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