Optimization Guide
Shopify Sale Event & Promotional Offer Schema for AI Shopping Agents
AI shopping agents answer "best deals on ergonomic chairs right now" and "is the X brand summer sale still on?" by reading temporal Offer signals — sale price, original price, and expiry date — from structured data. Shopify's default schema outputs only the current price with no promotional context, making your sales invisible to deal-hunting AI queries.
priceValidUntil to sale Offers, use priceSpecification with a ListPrice PriceSpecification for the compare-at price, and add a SaleEvent block to sale landing pages with startDate and endDate. Never show a different sale price in JSON-LD than on the product page.
The Sale Visibility Problem
During a Shopify sale — Black Friday, summer clearance, flash deals — you may discount 20–50% of your catalog. But Shopify's default product JSON-LD doesn't know this is a sale. It outputs the current (sale) price as a static price with no compare-at, no end date, and no promotional tag. AI shopping agents have no way to distinguish your "40% off, ends Sunday" offer from your everyday regular price.
The practical cost: shoppers asking ChatGPT "where can I find the best deal on a standing desk this week?" or Perplexity "which stores have active sales on hiking boots?" cannot be matched to your sale because AI agents lack the three minimum signals — current sale price, original list price, and sale expiry — that identify an active promotion.
Promotion types and the schema signals they need
| Promotion type | Required schema signals | Optional enhancements |
|---|---|---|
| Sitewide % off sale | priceValidUntil + ListPrice in priceSpecification |
SaleEvent on homepage with event dates |
| Flash sale (hours) | priceValidUntil with datetime to the hour, availabilityEnds |
inventoryLevel if truly limited stock |
| Seasonal clearance | priceValidUntil, ListPrice, sale-specific description |
SaleEvent with "clearance" in name |
| Volume/bulk discount | eligibleQuantity QuantitativeValue (minValue), price at discount |
Multiple Offer objects per quantity tier |
| Member-only sale | eligibleCustomerType set to member/subscriber segment |
Pair with member product schema (separate guide) |
Temporal Sale Offer Schema
Add priceValidUntil and a dual-price priceSpecification to the Offer block for sale products:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Apex Pro Standing Desk",
"offers": {
"@type": "Offer",
"price": "449.00",
"priceCurrency": "USD",
"priceValidUntil": "2026-06-30T23:59:00-05:00",
"availability": "https://schema.org/InStock",
"priceSpecification": [
{
"@type": "UnitPriceSpecification",
"price": "449.00",
"priceCurrency": "USD",
"validThrough": "2026-06-30T23:59:00-05:00",
"name": "Summer sale price"
},
{
"@type": "UnitPriceSpecification",
"price": "599.00",
"priceCurrency": "USD",
"priceType": "https://schema.org/ListPrice",
"name": "Regular price"
}
]
}
}
SaleEvent block for sale landing pages
Add a SaleEvent block to your sale collection page or homepage during promotional periods:
{
"@context": "https://schema.org",
"@type": "SaleEvent",
"name": "CatalogScan Summer Sale 2026",
"description": "Up to 40% off standing desks, ergonomic chairs, and monitor arms.
Free shipping on orders over $75.",
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"url": "https://yourdomain.com/collections/summer-sale",
"location": {
"@type": "VirtualLocation",
"url": "https://yourdomain.com/collections/summer-sale"
},
"organizer": {
"@type": "Organization",
"name": "Your Brand",
"url": "https://yourdomain.com"
}
}
Shopify Liquid Implementation
To dynamically inject sale pricing signals only when a product is on sale (compare-at price is set in Shopify admin):
{% if product.compare_at_price_max > product.price_max %}
{% assign sale_end = settings.current_sale_end_date %}
"priceValidUntil": {{ sale_end | json }},
"priceSpecification": [
{
"@type": "UnitPriceSpecification",
"price": {{ product.price | money_without_currency | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"validThrough": {{ sale_end | json }}
},
{
"@type": "UnitPriceSpecification",
"price": {{ product.compare_at_price | money_without_currency | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"priceType": "https://schema.org/ListPrice"
}
]
{% endif %}
Store the sale end date in a theme setting (settings.current_sale_end_date) so it can be updated from the Shopify admin without code changes. Remove it after the sale ends to avoid stale priceValidUntil dates in structured data — expired sale signals are worse than no signals.
Common Mistakes
Showing "price": "49.99" in JSON-LD when the page shows $79.99 with a coupon required
Leaving priceValidUntil: "2026-01-15" active on June 10 — expired date signals an old sale
Only adding priceValidUntil when the sale price is the page price (automatic discount, no coupon)
Removing all sale schema properties after campaign ends — no stale dates in production
Schema Checklist
- Sale Offer has
priceValidUntilin ISO 8601 format with timezone offset priceSpecificationarray includes both sale price and ListPrice (compare-at)- ListPrice PriceSpecification has
"priceType": "https://schema.org/ListPrice" - Schema sale price matches the visible page price (not a code-gated price)
- SaleEvent block on sale landing page with
startDateandendDate - Sale schema is removed (or
priceValidUntildate has passed) after campaign ends - No contradictory signals: sale price in JSON-LD but full price on page, or vice versa
FAQ
Does Google display sale prices as rich results in search?
Yes. Google's product rich results can show the sale price alongside a strikethrough regular price in SERPs when both the sale price and ListPrice PriceSpecification are present. This increases SERP click-through rate — users can see the discount before clicking. Google Shopping (free listings) also ingests the compare-at price for badge display ("30% off" labels). This requires schema data to be consistent with what's on the product page.
How do AI agents identify an 'active sale' versus a stale compare-at price?
Via priceValidUntil. Without this property, AI agents cannot tell if the sale price is today's flash deal or a compare-at price that has been "on sale" for 18 months. Adding priceValidUntil with a real expiry date anchors the promotion in time. AI agents that rank results by promotional freshness will surface products with current, timestamped sale signals over products with generic compare-at prices and no expiry context.
Audit Your Sale Schema Coverage
CatalogScan detects products with Shopify compare-at prices that are missing priceValidUntil and ListPrice structured data. Find the gaps in your promotional schema before your next sale season.