Is your storefront agentic?

Model product variants with ProductGroup for AI agents

Products with sizes, colors or other options should be described as a schema.org ProductGroup with variesBy and hasVariant, where each variant is its own Product with a SKU, price, availability and URL. Without it, AI shopping agents see one product and one price and can't tell whether the size a shopper wants is in stock.

Updated · By the Medusa team

Category
Product data (25% of score)
Impact
Medium
Scanner check
Product variants are modeled in structured data

Why AI agents need variants in structured data

“Is the medium in navy in stock, and how much is it?” is a normal shopping question, and on most product pages the answer lives in a JavaScript variant picker. An agent reading the server HTML sees one price, one availability flag, and a set of swatches it can’t interpret.

Google added product variant structured data for this reason: a ProductGroup declares which properties vary (variesBy) and lists each variant (hasVariant) with its own identifier, price, availability and URL. The same data lets an AI shopping agent:

  • confirm the exact option combination the shopper asked for exists,
  • quote the right price when variants are priced differently,
  • check stock per variant instead of per product,
  • link straight to the preselected variant.

This complements variant controls, which checks that browser agents can actually select the option on the page.

What the scanner checks

On up to three sampled product pages, the scanner first decides whether the product has variants. Any of these signals counts:

  • a <select> whose name or id mentions option, size, color, variant, style or material, with more than one <option>;
  • similarly named radio inputs;
  • ARIA-stateful option buttons (aria-pressed, role="radio" with aria-checked, role="option" with aria-selected) inside the product area;
  • swatch-style elements (classes like swatch, variant-option, color-option);
  • more than one distinct Offer SKU or URL in the JSON-LD.

For each product with variants:

Finding Meaning
Modeled A ProductGroup with hasVariant or variesBy
Partly modeled Several Offers or SKUs, but no ProductGroup
Not modeled Options on the page, but the structured data describes a single product
Result Condition
Pass Every sampled product with variants is modeled.
Partial At least one product is modeled or partly modeled, but not all are modeled.
Fail No product with variants is modeled or partly modeled.
Not applicable None of the sampled products have options (excluded from the score).
Inconclusive No product page could be fetched.

How to model variants with ProductGroup

<script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "ProductGroup",
    "name": "Merino Crew Sweater",
    "productGroupID": "MCS",
    "brand": { "@type": "Brand", "name": "Your Brand" },
    "variesBy": ["https://schema.org/size", "https://schema.org/color"],
    "hasVariant": [
      {
        "@type": "Product",
        "name": "Merino Crew Sweater, Navy, M",
        "sku": "MCS-NAVY-M",
        "size": "M",
        "color": "Navy",
        "image": "https://your-store.com/images/mcs-navy.jpg",
        "offers": {
          "@type": "Offer",
          "url": "https://your-store.com/products/merino-crew?variant=4421",
          "price": "89.00",
          "priceCurrency": "USD",
          "availability": "https://schema.org/InStock"
        }
      },
      {
        "@type": "Product",
        "name": "Merino Crew Sweater, Navy, L",
        "sku": "MCS-NAVY-L",
        "size": "L",
        "color": "Navy",
        "image": "https://your-store.com/images/mcs-navy.jpg",
        "offers": {
          "@type": "Offer",
          "url": "https://your-store.com/products/merino-crew?variant=4422",
          "price": "89.00",
          "priceCurrency": "USD",
          "availability": "https://schema.org/OutOfStock"
        }
      }
    ]
  }
</script>

Steps:

  1. Output a ProductGroup with productGroupID, variesBy and hasVariant for every product that has options.
  2. Make each variant a full Product with its own sku or GTIN, its option values (size, color, material, pattern) and its own offers.
  3. Give each variant a distinct URL that preselects it (for example ?variant=<id>) and use it as the offer url.
  4. Generate it server-side from the same variant data the picker uses, so stock and prices stay in sync.

Verify with Google’s Rich Results Test: it should show one ProductGroup with one Product per variant.

How to fix it on Medusa

  • Not shipped by default. The DTC starter storefront outputs no Product JSON-LD, so there is no ProductGroup either.
  • The data is already there: Medusa products have variants with option values and per-variant calculated prices. In the product page server component, map each variant to a hasVariant entry with its sku, option values, calculated_price and stock, and set variesBy from the product’s options.
  • Pair it with the picker: the starter’s option buttons have no selected state, which the variant controls check covers. See Is Medusa agent-ready? for both fixes.

Common mistakes

  • One Product with an AggregateOffer (lowPrice/highPrice) for a sweater sold in ten sizes. Agents get a price range, not an answer.
  • Variants that share one URL, so an agent can’t link to the right one.
  • variesBy values as plain strings like "Size" instead of schema.org property URLs.
  • Listing only in-stock variants, which hides the “sold out in your size” answer agents need.

Sources

Frequently asked questions

What is a ProductGroup in schema.org?
ProductGroup is the parent of a set of product variants. It lists what the variants differ by (variesBy, for example size and color) and the variants themselves (hasVariant), each a full Product with its own SKU and offer.
Does this check apply to products without options?
No. If none of the sampled products show size, color or other options, the check is marked not applicable and doesn't affect the score.
Is a list of Offers with different SKUs enough?
It counts as partial. Agents can see that several SKUs exist but not which option values each one represents. A ProductGroup with variesBy makes that explicit.

Run your store on a platform built for agentic commerce

Medusa is open-source, headless commerce with a full Store API agents can call and storefronts you control end to end.

Deploy on Medusa Cloud

Open-source commerce with a full Store API, hosted and scaled for you. Your storefront, your data model.

Storefront skills for coding agents

Teach Claude Code, Cursor or Codex to build storefronts with the patterns these checks look for.

npx skills add medusajs/medusa-agent-skills
View the skill on GitHub