Shopify Structured Data

Flash sale structured data schema
for Shopify and AI shopping agents

How to implement priceValidUntil, sale price markup, and availability signals so AI agents surface your time-limited Shopify deals — and stop showing stale sale prices after the sale ends.

TL;DR Use product.compare_at_price (not automatic discounts) for sales you want AI agents to read. Add priceValidUntil from a metafield storing the sale end date. Use availability: LimitedAvailability for low-stock urgency. Automatic discounts are checkout-only and invisible to server-rendered JSON-LD — AI agents can't read them.

Why flash sales are invisible to AI agents by default

Shopify merchants run two types of price reductions: compare-at price (strikethrough price set in the product admin) and automatic discounts (applied via Shopify Scripts, Shopify Functions, or manual discount codes). These behave very differently in structured data:

Sale methodReflected in product.price?AI-agent readable?Best for
Compare-at priceYes — product.price = sale priceYesSitewide sales, seasonal discounts, permanent markdowns
Automatic discount (Shopify Functions)No — applied at checkout onlyNoBOGO, volume tiers, member pricing
Discount codesNo — applied at cart/checkoutNoEmail campaigns, influencer codes
Shopify Scripts (Plus only)No — applied at checkoutNoComplex pricing rules

The rule is simple: if the price reduction appears in product.price via compare-at, it can go into JSON-LD. If it's applied at checkout, it cannot.

Implementing priceValidUntil in Shopify

priceValidUntil tells AI agents and search engines when the current price expires. Without it, AI agents assume the price is indefinitely valid. With a past date, they may suppress the product from sale queries.

Step 1: Create a sale end date metafield

Create a metafield definition in the Shopify admin under Settings → Custom Data → Products:

Set this value on each product when you start a sale. Leave it blank when the product is at full price.

Step 2: Output priceValidUntil conditionally in Liquid

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "{{ product.title | escape }}",
  "offers": {
    "@type": "Offer",
    "price": "{{ product.price | money_without_currency | remove: ',' }}",
    "priceCurrency": "{{ cart.currency.iso_code }}",
    "availability": "{% if product.available %}https://schema.org/InStock{% else %}https://schema.org/OutOfStock{% endif %}",
    "url": "{{ shop.url }}{{ product.url }}"
    {% if product.compare_at_price > product.price %}
    ,"priceSpecification": {
      "@type": "UnitPriceSpecification",
      "price": "{{ product.price | money_without_currency | remove: ',' }}",
      "priceCurrency": "{{ cart.currency.iso_code }}",
      "referenceQuantity": { "@type": "QuantitativeValue", "value": 1 }
    }
    {% if product.metafields.sale.end_date != blank %}
    ,"priceValidUntil": "{{ product.metafields.sale.end_date }}"
    {% endif %}
    {% endif %}
  }
}
</script>

Simpler pattern with compare-at and priceValidUntil

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "{{ product.title | escape }}",
  "offers": {
    "@type": "Offer",
    "price": "{{ product.price | money_without_currency | remove: ',' }}",
    "priceCurrency": "{{ cart.currency.iso_code }}",
    {% if product.compare_at_price > product.price %}
    "priceValidUntil": "{{ product.metafields.sale.end_date | default: '' }}",
    {% endif %}
    "availability": "{% if product.available %}https://schema.org/InStock{% else %}https://schema.org/OutOfStock{% endif %}"
  }
}

Low-stock urgency: using LimitedAvailability

Schema.org's LimitedAvailability value signals that a product has limited remaining inventory — the structured data equivalent of "Only 3 left!" When you want AI agents to surface urgency for a flash sale clearing old stock:

{% assign inventory = product.selected_or_first_available_variant.inventory_quantity %}
{
  "availability": "{% if inventory > 10 %}https://schema.org/InStock
                   {% elsif inventory > 0 %}https://schema.org/LimitedAvailability
                   {% else %}https://schema.org/OutOfStock{% endif %}"
}

Use LimitedAvailability when inventory is low (threshold is yours to define — common choices are <5 or <10 units). Do not use it for all products on sale; reserve it for genuine low-stock situations. AI agents and Google treat it as a real signal — overuse reduces trust.

AI agent behavior for flash sale queries

QueryNo sale schemaWith priceValidUntilWith LimitedAvailability
"[Brand] sale" / "[Product] discount"Current price shown, no sale signalSale flagged; price and expiry citedUrgency signal added if inventory low
"Best deals on [product category] right now"Full-price products mixed with saleSale products surface first in deal-intent queriesLow-stock products surface with urgency note
"[Brand] flash sale today"No structured signal to confirm sale is todaypriceValidUntil confirms timing to AI agent
Past sale (priceValidUntil expired)Old sale price may persist in cached resultsAI suppresses expired offer from sale queries

Stale sale price — the most damaging flash sale structured data error

If you set priceValidUntil to a past date, AI agents and Google interpret the current price as expired. This can suppress your product from all queries or, worse, confuse shoppers with outdated deal information in AI answers.

The clean solution: always set priceValidUntil from a metafield, never hard-code it. When the sale ends, clear the metafield — the conditional Liquid will omit the property entirely, which is the safest state.

Combining flash sale schema with Google Merchant Center promotions

If you use Google Merchant Center, note that Promotions in GMC and priceValidUntil in structured data are independent signals. GMC promotions control Shopping ad display. Structured data controls organic product rich results and AI agent knowledge graphs. You want both:

Both systems read sale timing independently — keeping them in sync via the same metafield end date is the cleanest approach.

Flash sale structured data checklist

CheckStatus
Sale price is set via compare-at (not automatic discount)Required for JSON-LD visibility
priceValidUntil is set from a metafield (not hard-coded)Prevents stale data after sale ends
Metafield is cleared when sale endsPrevents expired priceValidUntil errors
availability: LimitedAvailability for low-stock itemsOptional but impactful for urgency signals
sale price + compare-at price both in JSON-LD for strikethroughEnables price drop rich results
IndexNow ping on sale start and sale endEnsures crawl of updated prices within hours

Run a CatalogScan check on your product pages during a sale to verify that priceValidUntil and availability signals are present and correctly formatted in your live JSON-LD output.

Related: Out of stock structured data · Shopify B2C wholesale pricing · Structured data rich snippets guide

Frequently asked questions

Does Shopify automatically update priceValidUntil when a sale ends?

No. Shopify themes don't output priceValidUntil at all by default. If you add it from a metafield, you must clear that metafield when the sale ends — or the schema will show an expired price, which can suppress your product from sale queries.

What's the difference between automatic discounts and compare-at price for structured data?

Compare-at price is reflected in product.price and can go into JSON-LD. Automatic discounts apply only at checkout and cannot be expressed in server-rendered structured data. Only compare-at pricing is AI-agent readable.

How do AI shopping agents handle time-limited sale prices?

If priceValidUntil is in the future, agents surface the sale price. If it's missing, agents trust the current price. If it's in the past, agents may suppress the product from sale queries entirely.

Can I use schema.org to signal flash sale urgency?

Use priceValidUntil for price expiry and LimitedAvailability for low stock. There is no schema property for countdown timers — combine structured data with visible page text for maximum urgency signaling.