# Prices, stock and variants an AI agent can read

How to write prices, currencies, stock and size or colour variants so an AI agent reads them right, with a price tester, the stock values and a variant markup builder.

By Valerii Checha, 3 Nov 2026

How do you show prices, stock and variants so an AI agent can read them? Write the price as a number with an ISO currency code, the stock as a schema.org availability value, and give every size and colour its own offer with its own URL, price and stock, in the page's structured data and in its visible text. An agent that has to guess the currency from a "$", or the stock from a green dot, guesses. Sometimes it guesses wrong, and then it is the store that looks wrong.

This guide goes through prices, stock and variants in turn, each with a tool: a price tester, a guide to the stock values, and a builder for variant markup. Storewalk is ours: we build the scan, and its checks for [price transparency](https://storewalk.ai/checks/EVAL-003), [availability](https://storewalk.ai/checks/EVAL-004) and [variants](https://storewalk.ai/checks/EVAL-005) are ours. Google and schema.org are quoted from their own pages, read on 3 November 2026. For the Product markup itself, start with [product structured data for AI agents](https://storewalk.ai/guides/product-structured-data-for-ai-agents).

## How should a price be written so an agent can read it?

Write it on the page as text, where people read it, and not inside an image or only after a click. Write it again in the markup, for programs, as schema.org asks: the amount goes in `price` and the currency in `priceCurrency`, which schema.org says to use "instead of including ambiguous symbols such as '$' in the value". The decimal point is a full stop: "Use '.' (Unicode 'FULL STOP' (U+002E)) rather than ','". Google adds that merchant listings "require a price greater than zero", so a product priced at zero is not eligible.

The currency is the part a page most easily leaves open. A dollar sign is used by several currencies, and so is "kr". A program that reads "$49" knows the amount and has to guess the rest. Our price check counts a currency as explicit when the page names an ISO code such as EUR or USD, or uses a symbol that belongs to one currency only, such as € or £. Type a price the way your page shows it:

Does your price name its currency? How our price check reads it.

On the page, you type a price as your product page shows it and see how our price check (EVAL-003) reads its currency when the markup has no priceCurrency. The examples:

| As shown         | What we found                                                    | EVAL-003                                                                                         |
| ---------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| €49,90           | The amount 49.90, in EUR.                                        | PASS: Prices are readable text with an explicit currency                                         |
| $49              | The amount 49, with “$”, which several currencies use.           | WARN: A price is visible but "$49" names more than one currency                                  |
| 49.90 EUR        | The amount 49.90, in EUR.                                        | PASS: Prices are readable text with an explicit currency                                         |
| kr 300           | The amount 300, with “kr”, which several currencies use.         | WARN: A price is visible but "kr 300" names more than one currency                               |
| 1 490 Ft         | The amount 1490, in HUF.                                         | PASS: Prices are readable text with an explicit currency                                         |
| from €89         | A lower bound of 89 EUR, not this product’s price.               | WARN: The only price on a product page is a "from" lower bound                                   |
| Price on request | No amount, and a phrase that defers the price to a conversation. | FAIL: A product page carries no price an agent can read: the page defers it ("Price on request") |

With priceCurrency in the markup, EVAL-003 takes the currency from there and the symbol on the page stops mattering.

The simplest fix is in the markup. Once the offer says `"priceCurrency": "SEK"`, our check takes the currency from there, and the "kr" on the page no longer leaves anything open.

## What about sale prices, price ranges and "price on request"?

A sale price needs two prices, and Google says how to mark the old one. The current price stays in `price`. The former price goes in a price specification whose `priceType` is `https://schema.org/StrikethroughPrice`, and Google says "Only use this property if you want Google to show sale pricing for your product." If the sale ends on a date, `priceValidUntil` says when, and Google warns that "Your listing may not display if the priceValidUntil property indicates a past date." Worth knowing if you are the kind of shop that forgets a sale was ever on.

A price range is a different problem. "From €89" only commits to a floor, and the thing in front of the agent may cost more. Our check marks a page whose only price is a "from" price as a warning. In the markup, a range usually arrives as an AggregateOffer with a `lowPrice` and a `highPrice`. Google accepts that for product snippets, but says "merchant listings require an Offer", and our product data check finds no `price` in it to read.

"Price on request" and "contact us for a price" are the end of the road for an agent. Our check fails a product page that defers its price and carries no price in the markup or on the page either.

## How do you tell an agent what is in stock?

With a word on the page and a value in the markup that agree. schema.org's `availability` takes one of a fixed list of values, and Google's merchant-listing page lists ten, from "The item is in stock." to "The item has been discontinued." Google accepts the short form (`InStock`) as well as the full URL (`https://schema.org/InStock`), and so does our availability check. Pick a value to see Google's words and how our check reads it:

Stock states, value by value: what Google says and how our check reads each.

- https\://schema.org/InStock: in Google’s words, “The item is in stock.” Our EVAL-004 reads it as in stock now.
- https\://schema.org/OutOfStock: in Google’s words, “The item is currently out of stock.” Our EVAL-004 reads it as not in stock now.
- https\://schema.org/LimitedAvailability: in Google’s words, “The item has limited availability.” Our EVAL-004 reads it as in stock now.
- https\://schema.org/PreOrder: in Google’s words, “The item is available for pre-order.” Our EVAL-004 reads it as not in stock now. Orderable, but not shipping yet. The word “pre-order” on the page says the same, so the two agree.
- https\://schema.org/PreSale: in Google’s words, “The item is available for ordering and delivery before general availability.” Our EVAL-004 reads it as not in stock now.
- https\://schema.org/BackOrder: in Google’s words, “The item is on back order.” Our EVAL-004 reads it as not in stock now.
- https\://schema.org/OnlineOnly: in Google’s words, “The item is available online only.” Our EVAL-004 reads it as in stock now.
- https\://schema.org/InStoreOnly: in Google’s words, “The item is only available for purchase in store.” Our EVAL-004 reads it as in stock now.
- https\://schema.org/SoldOut: in Google’s words, “The item has been sold out.” Our EVAL-004 reads it as not in stock now.
- https\://schema.org/Discontinued: in Google’s words, “The item has been discontinued.” Our EVAL-004 reads it as not in stock now.
- https\://schema.org/MadeToOrder: Listed as a member of ItemAvailability; not in Google’s merchant-listing list. Our EVAL-004 recognises it, but it says nothing about stock, so the check has nothing to compare with the page.
- https\://schema.org/Reserved: Listed as a member of ItemAvailability; not in Google’s merchant-listing list. Our EVAL-004 does not recognise it, and falls back to the words on your page.

Google also accepts the short names, such as InStock, and so does our EVAL-004, which reads the short name and the full URL alike. Sources: [Google Search Central: Merchant listing structured data](https://developers.google.com/search/docs/appearance/structured-data/merchant-listing) and [schema.org ItemAvailability](https://schema.org/ItemAvailability).

On the page, say it in words. "In stock", "Out of stock" and "Ships in 3 days" are readable; a coloured badge with no text is not, and our check's fix says as much. Then make the two agree. Our check compares them, and when the markup says in stock and the page says sold out, it warns, because an agent that finds both has no way to know which to believe.

Stock is per variant. A shirt that is sold out in L and in stock in S has two answers, and a single "In stock" line on the page gives one of them. Our check warns when a product has several variants and its stock is stated once, with no per-variant availability in the markup or on the options themselves.

## How do you mark up sizes and colours?

Google's answer is the ProductGroup: "use the ProductGroup class with associated properties variesBy, hasVariant, and productGroupID to group such variants together". The group carries what the variants share (the name, the brand, the parent SKU as `productGroupID`) and names what they differ by in `variesBy`, as full schema.org URLs such as `https://schema.org/size`. Each variant is a Product with its own SKU and its own Offer.

Google sets a few rules for the page as well. Each variant needs a unique ID, such as a SKU or GTIN. "The site must have the ability to preselect each variant directly with a distinct URL (using URL query parameters)", and preselecting "includes showing the right image, price, and availability, as well as allowing the user to add the variant to the cart." On a single page holding all the variants, "there must be only one distinct canonical URL for the overall ProductGroup". If each variant has its own page instead, each page needs "full and self-contained markup".

Google documents two ways to lay a single page out: the variants nested inside the group with `hasVariant`, or listed separately, each pointing back with `isVariantOf`. Build yours below, and change the price or stock of one size to see what changes:

A variant markup builder: sizes and colours in, ProductGroup out.

On the page, you enter sizes and colours, set a price and stock for each variant, and pick one of Google’s two single-page layouts. For an invented shirt in three sizes, with variants nested in hasVariant:

```json
{
  "@context": "https://schema.org",
  "@type": "ProductGroup",
  "name": "Northwind Linen shirt",
  "url": "https://example.com/products/linen-shirt",
  "productGroupID": "NORTHWIND-LINEN-SHIRT",
  "variesBy": [
    "https://schema.org/size",
    "https://schema.org/color"
  ],
  "hasVariant": [
    {
      "@type": "Product",
      "name": "Northwind Linen shirt, Sand, S",
      "sku": "NORTHWIND-LINEN-SHIRT-SAND-S",
      "size": "S",
      "color": "Sand",
      "offers": {
        "@type": "Offer",
        "url": "https://example.com/products/linen-shirt?size=s&color=sand",
        "price": "59.00",
        "priceCurrency": "EUR",
        "availability": "https://schema.org/InStock"
      }
    },
    {
      "@type": "Product",
      "name": "Northwind Linen shirt, Sand, M",
      "sku": "NORTHWIND-LINEN-SHIRT-SAND-M",
      "size": "M",
      "color": "Sand",
      "offers": {
        "@type": "Offer",
        "url": "https://example.com/products/linen-shirt?size=m&color=sand",
        "price": "59.00",
        "priceCurrency": "EUR",
        "availability": "https://schema.org/InStock"
      }
    },
    {
      "@type": "Product",
      "name": "Northwind Linen shirt, Sand, L",
      "sku": "NORTHWIND-LINEN-SHIRT-SAND-L",
      "size": "L",
      "color": "Sand",
      "offers": {
        "@type": "Offer",
        "url": "https://example.com/products/linen-shirt?size=l&color=sand",
        "price": "64.00",
        "priceCurrency": "EUR",
        "availability": "https://schema.org/OutOfStock"
      }
    }
  ]
}
```

What our checks read from it:

- DISC-004 reads the ProductGroup as the product, with one offer per variant.
- EVAL-003 and EVAL-004 read 3 offers from the markup, one for each variant.
- EVAL-005 counts 3 declared variants and checks that your page’s size and colour controls can express that many.
- Your variants cost different amounts. Each offer has its own url, which is how EVAL-003 and DISC-005 tell which price belongs to the page they are on.

With the variants separate, each pointing to the group with isVariantOf, our checks read:

- DISC-004 finds a Product: the first variant, read on its own.
- EVAL-003 and EVAL-004 read 1 offer from the markup, for 3 variants, and take the rest from your page.
- EVAL-005 counts no variants in the markup, so it judges your variants from the page’s own controls.
- Your variants cost different amounts. EVAL-003 and DISC-005 read only the first variant’s offer here, so they take its price as this page’s price.

Google’s rules the builder follows:

- “Each variant must have a unique ID in its corresponding structured data markup (for example, using the sku or gtin properties).”
- “Each product group must have a unique ID in its corresponding structured data markup, specified with the inProductGroupWithID property in variant Product properties or the productGroupID property in the ProductGroup property.”
- “Reference these variant-identifying properties through their full Schema.org URL (for example, https\://schema.org/color ).”
- “The site must have the ability to preselect each variant directly with a distinct URL (using URL query parameters)”

Source: [Google Search Central: Product variant structured data](https://developers.google.com/search/docs/appearance/structured-data/product-variants).

The URL on each offer is what matters most to our checks when prices differ. With three sizes at three prices and no way to tell which offer belongs to the page, our price check warns that "Variants are priced differently and no offer says which one this page is for", and our [price match check](https://storewalk.ai/checks/DISC-005) can't compare at all. An offer `url` that matches the page settles it.

## What do our checks read, and where do they stop?

Four of our checks read this part of the page. The price check wants a readable price and an explicit currency. The availability check wants stock stated, per variant, and in agreement between markup and page. The variant check wants size and colour exposed as named controls: a `<select>` with a label, labelled radio buttons, or swatches whose images or buttons carry the variant's name, so that an agent can enumerate the choices. The price match check compares the markup's price with the one a shopper sees.

The builder above shows how our checks read each layout as you switch between them:

- In the nested layout, our checks read the ProductGroup as the product, with one offer per variant. That is the layout Google documents first and the one Shopify prints for products with variants, so it reads in full.
- In the separate layout, our price and availability checks read the first variant's offer only, and take the other variants from your page. We know about that limit, and the builder shows it.

A pre-order page reads as consistent to our availability check. `PreOrder` and `PreSale` in the markup mean the item isn't in stock yet, which is what the word "pre-order" on the page says too, so the two agree.

## What do Shopify, WooCommerce, BigCommerce and Magento do here?

On Shopify, the `structured_data` filter that Dawn uses prints "a ProductGroup if they have one or more variants". Shopify's own advice for AI channels lists "Variants, including Option name", which is the same idea from the admin side: name your options. See the [Shopify guide](https://storewalk.ai/shopify).

On WooCommerce, core prints a variable product whose variations cost different amounts as one AggregateOffer with a `lowPrice` and a `highPrice`, and no offer per variation. Equal prices get a single Offer. Either way there is one offer, so one stock state for every variation. The `woocommerce_structured_data_product_offer` filter is where a developer can change that; the [WooCommerce guide](https://storewalk.ai/woocommerce) has more.

On BigCommerce, the Cornerstone theme prints a price range as `minPrice` and `maxPrice`, which our product data check doesn't read as a price. See the [BigCommerce guide](https://storewalk.ai/bigcommerce). On Magento, stock Luma writes microdata with a price and currency but no availability; the [Magento guide](https://storewalk.ai/magento) covers the fixes.

Whatever the platform, the test is the same. Open a product with variants, switch to a size that is out of stock, and check that the URL, the price, the stock line and the markup all changed together. Then run a scan, and see the [agent readiness checklist](https://storewalk.ai/guides/agent-readiness-checklist) for where this sits among the rest. Shipping costs and return windows are the other half of what an agent compares; [shipping and return policies](https://storewalk.ai/guides/shipping-returns-policies-for-agents) covers them.

## Frequently asked questions

### Should the price in JSON-LD include the currency symbol?

No. schema.org asks for the amount in `price` and the currency in `priceCurrency` "instead of including ambiguous symbols such as '$' in the value", with a full stop as the decimal point: `"price": "49.90"`, `"priceCurrency": "EUR"`.

### Is "$" a clear enough currency for an AI agent?

Not on its own. The dollar sign is used by several currencies, so our price check warns when a page's only currency is "$" or "kr". An ISO code in the markup's `priceCurrency`, or on the page, removes the doubt.

### How do I mark a product as out of stock?

Set the offer's `availability` to `https://schema.org/OutOfStock` (or `SoldOut`, `BackOrder` or `Discontinued`, whichever is true) and say the same in words on the page. With variants, set it on the variant that is out, not on the whole product.

### Does every variant need its own URL?

Google says the site "must have the ability to preselect each variant directly with a distinct URL", for example with query parameters. Each variant's offer can then carry that URL, which is also how our checks tell which variant's price a page shows.

### What is the difference between hasVariant and isVariantOf?

They describe the same link from opposite ends. `hasVariant` lists the variants inside the ProductGroup; `isVariantOf` lets each variant Product point to its group. Google documents both layouts.

The sources below were last checked on the date shown.

## Sources

- [Google Search Central: Product variant structured data](https://developers.google.com/search/docs/appearance/structured-data/product-variants)
- [Google Search Central: Merchant listing structured data](https://developers.google.com/search/docs/appearance/structured-data/merchant-listing)
- [schema.org Offer](https://schema.org/Offer)
- [schema.org ItemAvailability](https://schema.org/ItemAvailability)
- [schema.org ProductGroup](https://schema.org/ProductGroup)
- [Shopify.dev: the structured_data filter](https://shopify.dev/docs/api/liquid/filters/structured_data)
- [Shopify Help Center: optimizing products for AI platforms](https://help.shopify.com/en/manual/shopify-catalog/optimizing-products)
- [WooCommerce source: class-wc-structured-data.php](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce/includes/class-wc-structured-data.php)

Last checked 4 Nov 2026
