Structured Data for AI Agents: Which Schema.org Types Earn Their Keep
Most schema.org markup is invisible to machine readers. The types that are worth the effort for AI agents, how to emit them without drift, and what to build instead of more markup.
- Structured data helps machine readers most where it states facts the prose leaves implicit: dates, prices, authorship, identity, licence.
- A short, correct graph beats a long, aspirational one. Four accurate types are worth more than twenty types with placeholder values.
- Emit JSON-LD from the same source that renders the page. Hand-written markup drifts from the visible content, and contradiction is worse than absence.
- Never mark up something that is not visible on the page. It is a policy violation for search rich results and it makes the whole graph untrustworthy to any consumer.
- If the thing you are marking up is really data — a catalogue, prices, availability — publish a JSON endpoint. Structured data is a description of a page; an API is the data itself.
Structured data is the oldest machine-readable layer on the web and the one where effort is most often misspent. This page is about the subset that is worth doing for machine consumers, as part of the agent-ready website.
What structured data is actually good at
A model reading your prose can usually work out what a page is about. What it cannot reliably work out is the metadata: when this was last revised, who published it, what the price is, which currency, whether it is free, what it is part of, and what licence applies. Those are exactly the facts a JSON-LD block states unambiguously.
So the useful mental model is not "markup for rich results". It is: a machine-readable fact sheet attached to a human-readable page. Judge every type you consider adding by whether it states a fact the prose leaves implicit.
The types that earn their keep
| Type | Use for | Facts it pins down |
|---|---|---|
Article / TechArticle |
Any content page | headline, description, dateModified, language, keywords, isAccessibleForFree |
BreadcrumbList |
Pages inside a hierarchy | where this page sits; extra crawl paths into hubs |
Organization |
Site-wide identity | who publishes this, canonical URL, sameAs identities |
WebSite + SearchAction |
Homepage | the site as an entity; how to search it |
Product + Offer |
Anything with a price | price, currency, availability, SKU, seller, subscription vs one-time |
FAQPage |
Real question/answer sections | Q/A pairs as discrete, quotable units |
HowTo |
Genuinely sequential procedures | ordered steps with names and text |
Dataset |
Downloadable data | distribution URLs, formats, licence |
CollectionPage + hasPart |
Index and hub pages | the set of things this page indexes |
Two of these are underused and disproportionately valuable to machine buyers:
Product/Offeron pricing pages. If you want an agent to be able to compare your offer without parsing a table, this is the cheapest way to say it. Pair it with a JSON pricing endpoint for the machine-first path.isAccessibleForFreeonArticle. It is one boolean and it tells a crawler whether what it just fetched is the whole thing. Combined with a 402 gate, it makes your access model self-describing.
The types that usually are not worth it
- Deep
Persongraphs for every author unless authorship is genuinely part of your value. Review/AggregateRatingyou generated yourself. Fabricated or self-serving ratings are a policy problem and a trust problem.Event,Recipe,JobPostingand friends when you do not have those things. Aspirational markup is the fastest way to make your graph unreliable.speakable— narrow applicability, little published consumption.- Marking up navigation.
SiteNavigationElementdescribes your menu. Nobody needs your menu described.
Emit it from one source, or it will lie
The failure mode is always the same: markup written once by hand, content edited later, and now the graph says the price is €19 while the page says €29. A machine consumer that catches a contradiction has no way to resolve it and every reason to distrust the rest.
Derive the graph from the same data that renders the page:
// One source of truth → visible page AND graph
const jsonld = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "TechArticle",
headline: page.title, // same field the <h1> renders
description: page.description, // same field the meta tag renders
dateModified: page.updated, // same field the byline renders
inLanguage: "en",
isAccessibleForFree: !page.premium,
keywords: page.tags.join(", "),
isPartOf: { "@type": "WebSite", name: SITE.name, url: SITE.url },
publisher: { "@type": "Organization", name: SITE.name, url: SITE.url },
},
{
"@type": "BreadcrumbList",
itemListElement: trail.map((t, i) => ({
"@type": "ListItem", position: i + 1, name: t.name, item: t.url,
})),
},
],
};
Then add a CI assertion that the graph's headline equals the page's <h1> and its dateModified equals the rendered date. Two assertions catch nearly every drift bug this layer produces.
Practical rules
- JSON-LD, in
<head>. Microdata and RDFa still work but are harder to keep consistent and harder to diff in review. - One
@graphper page, not five script tags. Consumers that only read the first block will otherwise miss most of what you said. - Stable
@ids for anything referenced twice (offers, organisations), scoped to the page URL. - Absolute URLs everywhere. A relative
urlin a graph consumed out of context resolves to nothing. - Only mark up what is on the page. This is both a search-engine policy requirement and the difference between a fact sheet and a claim sheet.
- Validate on every build, not once at launch. A schema error introduced by a template change is otherwise invisible for months.
Where structured data stops and APIs start
The boundary is worth stating plainly, because a lot of effort goes into forcing data through markup that would be better published directly.
| You have | Publish as | Why |
|---|---|---|
| A page about a thing | JSON-LD on that page | It describes the page a client already fetched |
| A list of things that changes | JSON endpoint | An index in markup means fetching HTML to get data |
| Prices and tiers | Both | Markup for the crawler on the page, endpoint for the machine buyer |
| A corpus | NDJSON / one-file export | Per-page markup cannot express a dataset |
| A changelog | Atom/JSON Feed | Feeds exist for "what changed", and pollers already understand them |
The API side of this is JSON API design for agents, and the format decision — Markdown for prose, JSON for records, NDJSON for corpora — is worked through in data formats and schema.
Adjacent: provenance markup
Schema.org states facts; it does not prove them. Where authenticity matters — images, documents, anything that could be fabricated — C2PA Content Credentials cryptographically bind assertions about origin and edit history to the asset itself. That is a different guarantee from a JSON-LD author field, and the two are complementary: see C2PA content credentials.
A 45-minute pass on an existing site
- Pick your highest-value template. Check whether it emits any graph at all.
- Add or fix
Article/TechArticlewith a realdateModifiedsourced from your content, not the build clock. - Add
BreadcrumbListmatching the visible breadcrumb. - Add
OrganizationandWebSiteonce, site-wide. - On priced pages, add
Product+Offergenerated from your price data. - Delete every type that describes something you do not have.
- Add two CI assertions: graph headline matches
<h1>, graph date matches rendered date.
That sequence takes under an hour on most codebases and leaves you with a graph that is small, correct and — because it is generated — still correct next quarter.
Frequently asked questions
- Do LLMs read JSON-LD?
- When a model or its retrieval pipeline processes raw HTML, the JSON-LD block is part of what it sees, and it is the cleanest statement of facts on the page. Vendors do not publish how much weight it carries, so treat it as a cheap way to make facts unambiguous rather than as a ranking lever.
- Which types should I add first?
- The ones that describe what your page actually is: `Article`/`TechArticle` for content, `Product`/`Offer` for anything priced, `BreadcrumbList` for hierarchy, `Organization` for identity, and `FAQPage` where you genuinely have questions and answers. Then stop and check they are correct before adding more.
- Is FAQPage markup still worth adding?
- For rich results, treatment has varied by search engine and changed over time, so do not count on visual gains. As a machine-readable statement of question-answer pairs it is still useful, provided the questions and answers are visible on the page and are real questions rather than keyword shims.
- Should structured data duplicate the page content?
- It should describe it, not restate it. Titles, dates, identifiers, prices, authorship and relationships belong in the graph; the body does not. Duplicating whole articles into JSON-LD bloats every fetch for no gain.