Skip to main content
Intelliger
Ecommerce Product Discovery

An Agent-Readable Product Data Schema with Valid and Invalid Examples

A practical agent-readable product data schema for products, variants, offers, evidence and compatibility, with validation rules and failing fixtures.

Catalog engineer matching a physical running shoe to structured product data on a laptop
Intelliger
Reviewed 11 August 2026 · 13 minute read

An agent-readable product schema must separate the product concept, purchasable variant, merchant offer and volatile availability. It needs typed attributes, stable identifiers, explicit units, provenance and compatibility evidence. A single block of marketing copy with a price is not enough. The schema below is a practical internal model, not a new public standard, and includes fixtures that a validator should reject.

This article is for commerce data engineers building retrieval or shopping-agent pipelines. The outcome is a versioned schema boundary you can map from feeds, APIs and structured data without letting the model guess which SKU can actually be purchased.

Use it as a spoke beside the ecommerce product discovery guide and the large-catalog retrieval architecture.

Define product, variant and offer separately

A product describes a commercial concept, such as a trail shoe model. A variant describes a purchasable configuration, such as size 43 in blue. An offer describes one merchant's terms for that variant in a market. Inventory and delivery observations describe changing state for that offer.

Product
  -> Variant
      -> Offer
          -> Inventory observation
          -> Delivery promise

This distinction is compatible with established commerce data. Schema.org separates Product and Offer, with properties for SKU, GTIN, price, currency and availability. Schema.org Product and Schema.org Offer are useful interchange formats, but an agent runtime usually needs stricter field requirements and provenance than the vocabulary alone enforces.

A typed internal schema

The following TypeScript is an illustrative internal contract:

type DecimalString = `${number}`;
type IsoDateTime = string;
type EvidenceRef = {
  source: 'merchant_api' | 'manufacturer' | 'certifier';
  uri: string;
  digest: `sha256:${string}`;
  observedAt: IsoDateTime;
};

type Product = {
  schemaVersion: '2026-08-11';
  productId: `product:${string}`;
  title: string;
  brand: string;
  description: string;
  taxonomy: string[];
  identifiers: { gtin?: string; mpn?: string };
  attributes: Record<string, AttributeValue>;
  evidence: EvidenceRef[];
};

type AttributeValue =
  | { type: 'text'; value: string }
  | { type: 'boolean'; value: boolean }
  | { type: 'number'; value: DecimalString; unit: string }
  | { type: 'enum'; value: string; vocabulary: string };

type Variant = {
  variantId: `variant:${string}`;
  productId: Product['productId'];
  sku: string;
  optionValues: Record<string, string>;
  attributes: Record<string, AttributeValue>;
  compatibility: CompatibilityClaim[];
};

type CompatibilityClaim = {
  targetId: string;
  relation: 'compatible_with' | 'replaces' | 'requires';
  status: 'verified' | 'merchant_asserted' | 'unknown';
  evidence?: EvidenceRef;
};

type Offer = {
  offerId: `offer:${string}`;
  merchantId: `merchant:${string}`;
  variantId: Variant['variantId'];
  market: string;
  currency: string;
  listPrice: DecimalString;
  validFrom: IsoDateTime;
  validUntil?: IsoDateTime;
  purchaseUrl: string;
};

Use decimal strings or integer minor units for money. Binary floating point can introduce equality and rounding errors. Units should come from a controlled vocabulary, not free text such as about 2kg.

A valid machine-readable example

{
  "schemaVersion": "2026-08-11",
  "product": {
    "productId": "product:acme:trail-shoe-8",
    "title": "Trail Shoe 8",
    "brand": "Acme",
    "description": "Water-resistant trail shoe with a replaceable insole.",
    "taxonomy": ["footwear", "trail_running"],
    "identifiers": { "mpn": "TS8" },
    "attributes": {
      "weight": { "type": "number", "value": "0.31", "unit": "kg" },
      "water_resistant": { "type": "boolean", "value": true }
    },
    "evidence": [
      {
        "source": "manufacturer",
        "uri": "https://manufacturer.example/products/ts8/spec",
        "digest": "sha256:4af2c8...",
        "observedAt": "2026-08-11T08:00:00Z"
      }
    ]
  },
  "variant": {
    "variantId": "variant:acme:trail-shoe-8:blue:43",
    "productId": "product:acme:trail-shoe-8",
    "sku": "TS8-BLU-43",
    "optionValues": { "color": "blue", "size_eu": "43" },
    "attributes": {},
    "compatibility": []
  },
  "offer": {
    "offerId": "offer:merchant-7:TS8-BLU-43:DE",
    "merchantId": "merchant:merchant-7",
    "variantId": "variant:acme:trail-shoe-8:blue:43",
    "market": "DE",
    "currency": "EUR",
    "listPrice": "129.90",
    "validFrom": "2026-08-11T00:00:00Z",
    "validUntil": "2026-08-18T00:00:00Z",
    "purchaseUrl": "https://merchant.example/de/products/ts8?variant=blue-43"
  }
}

The example is agent-readable because identities join cleanly, values are typed, price has currency and validity, and factual claims have provenance. It does not claim the item is currently in stock. That requires a live observation.

Invalid example: product and offer are collapsed

{
  "id": "trail-shoe",
  "name": "Best Trail Shoe",
  "details": "Blue or red, sizes 39 to 46, usually in stock",
  "price": 129.9,
  "compatible": true
}

A validator should reject this record because:

  • id is not namespaced or stable enough to join across systems.
  • variant choices are embedded in prose.
  • price has no currency, market, merchant or validity.
  • binary floating point is used for money.
  • "usually in stock" is neither a current fact nor a timestamped observation.
  • compatibility has no target, relationship or evidence.
  • "Best" is promotional language, not a product attribute.

Invalid example: identifiers contradict each other

{
  "productId": "product:acme:trail-shoe-8",
  "variant": {
    "variantId": "variant:acme:trail-shoe-8:red:42",
    "productId": "product:acme:trail-shoe-9",
    "sku": "TS8-BLU-43",
    "optionValues": { "color": "red", "size_eu": "42" }
  },
  "offer": {
    "offerId": "offer:merchant-7:TS8-BLU-43:DE",
    "variantId": "variant:acme:trail-shoe-8:blue:43",
    "currency": "EUR",
    "listPrice": "129.90"
  }
}

Every field is syntactically plausible, which makes this fixture more useful. The variant points at product 9, its SKU and options disagree, and the offer references another variant. Schema validation alone may miss the referential errors. Add graph-level invariants.

Validation rules that matter to agents

Implement at least four validation layers.

Shape validation

Reject missing required fields, additional unknown fields, malformed dates, invalid URLs and unsupported schema versions. JSON Schema is suitable here.

Referential validation

Verify that every variant points to an existing product and every offer points to an existing variant. Enforce uniqueness of merchantId + market + offerId and stable reuse rules for SKUs.

Semantic validation

Confirm that unit and attribute types match the category vocabulary. Shoe size is not a free-form length. Voltage should use an agreed unit. GTINs, when present, need valid lengths and check digits. Google Merchant Center similarly requires stable item IDs and maps variant groups, GTIN, price, currency and availability to defined fields. Google's supported product structured-data attributes provides a practical interoperability reference.

Freshness validation

Check validUntil and observedAt. Do not silently turn an expired offer into an active one. Mark the record unknown and fetch authoritative state.

function validateBundle(bundle: Bundle, now: Date): Issue[] {
  const issues: Issue[] = [];
  if (bundle.variant.productId !== bundle.product.productId)
    issues.push({ code: 'VARIANT_PRODUCT_MISMATCH' });
  if (bundle.offer.variantId !== bundle.variant.variantId)
    issues.push({ code: 'OFFER_VARIANT_MISMATCH' });
  if (
    bundle.offer.validUntil &&
    Date.parse(bundle.offer.validUntil) <= now.getTime()
  )
    issues.push({ code: 'OFFER_EXPIRED' });
  if (!isDecimal(bundle.offer.listPrice))
    issues.push({ code: 'PRICE_FORMAT_INVALID' });
  return issues;
}

Evolve the schema without changing old facts

Schema evolution becomes dangerous when a new mapper changes the meaning of records already in the index. Treat the normalized bundle as an immutable event. Store its schema version, mapper version and source digest together. A correction produces a new bundle revision; it does not rewrite the evidence behind the old one.

type CatalogRevision = {
  bundleId: `bundle:${string}`;
  revision: number;
  schemaVersion: '2026-08-11';
  mapperVersion: `mapper:${string}`;
  sourceDigest: `sha256:${string}`;
  supersedes?: `${CatalogRevision['bundleId']}@${number}`;
  indexedAt: IsoDateTime;
};

Suppose a merchant sends weight: "310" without a unit. Mapper 4 assumes grams, while mapper 5 rejects the field. Reprocessing the same source under mapper 5 must not silently turn the indexed value into an absent attribute. Emit a new revision, retain the earlier decision, and make retrieval select the latest accepted revision explicitly.

Add a migration test for every schema or mapper change:

  1. Run the old fixture corpus through both mapper versions.
  2. Diff the normalized output field by field.
  3. Classify each difference as intended, prohibited or review-required.
  4. Re-run ranking and compatibility tests using both revisions.
  5. Require an operator decision for changed identifiers, money, units or compatibility.

This catches a failure that JSON Schema cannot: a record can remain valid while its commercial meaning changes. Keep readers backward-compatible for at least the migration window. Reject a future schema version you do not understand instead of guessing how to down-convert it.

Keep discovery facts apart from live state

Index titles, descriptions, stable attributes, evidence digests and compatibility relationships. Retrieve price, inventory, promotions and delivery estimates from authoritative merchant systems close to the transaction.

Google's product-data guidance requires price and availability to match the landing page and checkout, and recommends frequent updates when those values change. Google Merchant product-data specification is designed for listings rather than autonomous execution, but its mismatch rules show why stale commerce state cannot be treated as harmless metadata.

For the runtime pattern, read live price, inventory and delivery for shopping agents. For discoverability problems, see why stores become invisible to shopping agents.

Reproducible fixture suite

Create a directory of exact inputs and expected reason codes:

fixtures/product-schema/
  valid-basic.json
  invalid-missing-currency.json
  invalid-product-reference.json
  invalid-expired-offer.json
  invalid-unknown-unit.json
  invalid-compatibility-without-target.json
  invalid-price-number.json
  invalid-duplicate-offer-id.json

Run each through shape and graph validation. Require deterministic, sorted codes:

{
  "fixture": "invalid-product-reference.json",
  "valid": false,
  "codes": ["OFFER_VARIANT_MISMATCH", "VARIANT_PRODUCT_MISMATCH"]
}

Then add mutation tests. Change one identifier, currency, unit, timestamp or evidence digest in a valid fixture. A useful suite proves the validator rejects the mutation for the expected reason, not merely that some error occurred.

Security and abuse cases

Treat every merchant-controlled string as untrusted data. Product descriptions can contain instructions aimed at the model. Never place catalog text in a system-message role or permit it to alter tool policy.

Other failure cases include duplicate SKUs across merchants, a GTIN reused for a refurbished item, an offer that changes price without a new observation time, incompatible unit conversions, hidden adult or restricted-product classifications, and evidence URIs whose content changes while the URL remains stable. Store digests and apply category-specific policy before retrieval results reach the model.

Current Intelliger boundary

The canonical Commerce Graph, merchant connector framework, product-schema mapping and Agent Search Console are target-state components in Intelliger's August 2026 agentic-commerce blueprint. They are not deployed product claims.

OATI's current developer preview covers trust objects, canonical signing, deterministic Commerce constraints, receipts, middleware and conformance vectors. Its Commerce profile and sandbox demonstrate signed price and budget controls for a paid API transaction, not a production retail catalog ingestion service.

Explore the intended commerce direction in Intelliger's agentic commerce architecture and the current open trust layer in the OATI documentation.

Implementation checklist

  • Give products, variants, offers and merchants separate stable IDs.
  • Type attributes and normalize units with controlled vocabularies.
  • Represent money as decimal strings or minor-unit integers.
  • Bind price to currency, market, merchant and validity.
  • Record provenance and digests for consequential claims.
  • Model compatibility as a relationship with a target and status.
  • Enforce referential integrity beyond JSON Schema.
  • Reject expired offers instead of silently extending them.
  • Keep volatile state out of the descriptive search index.
  • Treat catalog content as untrusted input to the model.
  • Publish valid, invalid and mutation fixtures with reason codes.

Review note: a commerce data-model specialist should verify category vocabularies, identifier reuse rules, regulated attributes and market-specific price requirements before publication or production use.

Download or copy the fixture structure above, then use the AI search for ecommerce implementation guide to place schema validation before catalog indexing.