← Back to Blog
TechnicalSEOSEO StrategyContent Strategy

Schema for AI Agents: What Actually Gets Parsed

I ship nine schema types on this site. Six are documented rich results in Google's gallery, five earn nothing here, and the published tests say AI assistants read JSON-LD as plain text rather than parsing it. Here is the actual markup and what each type earns.

SPSantosh Paudel· September 6, 2026· 15 min read
Table of contents

Short answer: as of September 2026, the structured data on your site earns two different things, and only one of them is a visual reward. Google still renders rich results from a documented list — Article, Breadcrumb, Product, Local Business, Review Snippet and about twenty others in its search gallery. AI assistants are a different story: the published tests say ChatGPT, Claude and Perplexity do not parse JSON-LD as structured data during retrieval. They tokenise the script block as text, if they read it at all. Schema still matters for AI. What it does is help Google's index resolve who you are, which is a different job from feeding a parser inside an assistant.

I run nine schema types on santoshpaudel.me. Six of the nine are documented features in that gallery. One of the nine had a measurable problem behind it. Five earn nothing on this site today. Here is the sorting.

What this site actually emits

Everything below lives in components/portfolio/JsonLd.tsx in the repo that serves this page: OrganizationSchema, PersonSchema, BlogPostingSchema, BreadcrumbSchema, FaqSchema, ServiceSchema, LocalBusinessSchema, AggregateRatingSchema, WebAppSchema.

TypeGoogle rich result todayPrerequisite you probably missVerdict here
BlogPostingArticle — headline, date, image in some surfacesimage must actually resolve; 164 of my 453 built pages shipped no og:imageKeep
BreadcrumbListBreadcrumb trail replaces the raw URL in the snippetPath must match a real crawlable hierarchyKeep — cheapest win on the list
OrganizationDocumented in the gallery, no snippet of its ownA stable @id reused across every pageKeep — this is the entity layer
PersonNot a gallery feature; ProfilePage is the one that isA stable @id reused across every pageKeep — this is the entity layer
ServiceNone. Not in the galleryKeep for entity clarity, expect zero SERP change
FAQPageRetired 7 May 2026Dead weight; harmless to leave
WebApplicationSoftware App rich resultoffers.price and aggregateRating or reviewNot eligible as I ship it
Review on my own PersonReview snippet — star ratingsReviews must be about someone elseIneligible by policy
ProfessionalService (my LocalBusinessSchema)Local business panelA real presence in the place you mark upShips on 40 cities I have no presence in

Five of those nine rows return nothing today: Service, FAQPage, WebApplication, Review and ProfessionalService. That is roughly the cargo cult rate I would expect on any site that added schema by copying a plugin's defaults. The one gallery feature I could plausibly earn and do not ship is ProfilePage, which wants an author page with a single clear owner. That is the gap I would fill next.

The one schema fix with a measurable problem behind it

In my Search Console data for the 85 days to 3 September 2026, the query santosh paudel — my own name — ranked at average position 8.2 and produced zero clicks. Position eight for your own name is a bad sign on its own. Underneath it, the site was emitting a different anonymous Person node on every page. Blog posts declared an author. Service pages declared a provider. The testimonials page declared a reviewee. None of them shared an identifier, so nothing told Google these were the same human.

Before: N nodes, one person

{ "@context": "https://schema.org", "@type": "Person", "name": "Santosh Paudel" }

That block shipped on every built page with slightly different sibling properties. Google had 389 of those pages indexed, so what it had on file was 389 candidate entities that happened to share a name.

After: one @id, referenced everywhere

export const PERSON_ID = `${SITE_URL}/#person`
export const ORG_ID = `${SITE_URL}/#organization`

The canonical node is declared once, from the root layout:

{
  "@context": "https://schema.org",
  "@type": "Person",
  "@id": "https://www.santoshpaudel.me/#person",
  "name": "Santosh Paudel",
  "alternateName": ["Santosh Poudel", "Santosh Paudel Nepal"],
  "url": "https://www.santoshpaudel.me",
  "image": "https://www.santoshpaudel.me/assets/og-santosh.jpg",
  "sameAs": [
    "https://www.linkedin.com/in/santoshpaudel7211",
    "https://x.com/santoshpaudel",
    "https://github.com/Hideaway420",
    "https://www.youtube.com/@growwithsantosh",
    "https://www.instagram.com/growwithsantosh.ai"
  ],
  "jobTitle": "AI Marketing Strategist",
  "worksFor": { "@id": "https://www.santoshpaudel.me/#organization" },
  "address": {
    "@type": "PostalAddress",
    "addressLocality": "Kathmandu",
    "addressCountry": "Nepal"
  },
  "knowsAbout": [
    "AI Marketing",
    "Personal Branding",
    "Content Strategy",
    "Marketing Automation"
  ]
}

That is the node as emitted, with two contact properties and the description string clipped for length. Everything shown is verbatim, hostname included. The www is load-bearing: SITE_URL is https://www.santoshpaudel.me, both @id values interpolate from it, and markup that says www on half its pages hands the index two hostnames to reconcile on top of two people. The sameAs array carries even more weight. Those five URLs are how an index ties this node to profiles it already holds records for, so one typo there costs you the entire point of the exercise.

Every other schema on the site now carries that @id wherever it mentions me. Those nodes still repeat a name and a url alongside it, which is what BlogPostingSchema and ServiceSchema really emit:

{
  "@type": "BlogPosting",
  "headline": "Schema for AI Agents: What Actually Gets Parsed",
  "author": {
    "@id": "https://www.santoshpaudel.me/#person",
    "@type": "Person",
    "name": "Santosh Paudel",
    "url": "https://www.santoshpaudel.me"
  },
  "publisher": {
    "@id": "https://www.santoshpaudel.me/#organization",
    "@type": "Organization",
    "name": "Santosh Paudel"
  }
}

The duplicated properties are harmless. Once the @id is present, a consumer merges this node into the canonical one instead of minting a new entity, and the repeated name simply agrees with what it already has. What broke the site was the version carrying no @id at all.

The alternateName line is doing real work: my surname transliterates from Nepali two ways, and Nepal is the only country where this site converts — 8 clicks from 74 impressions at position 9.2, a 10.8% CTR, against 970 US impressions at position 38.8 with zero clicks.

Evidence caveat, because this is a tier-C topic and I would rather say it than imply otherwise: I have not seen a position change I can attribute to the @id fix. Entity consolidation is a slow signal, and I shipped it in the same week as a 61% content cut (267 published posts down to 103) and a canonical hostname fix that had been suppressing indexing for three months. Anyone claiming to have isolated the schema variable out of that is guessing.

What AI agents actually do with your JSON-LD

This is where the popular advice and the published tests disagree.

Google's own AI features documentation is blunt: "You don't need to create new machine readable files, AI text files, or markup to appear in these features. There's also no special schema.org structured data that you need to add."

Two tests worth reading

SearchVIU ran eight scenarios across five systems in October 2025 — ChatGPT, Claude, Gemini, Perplexity and Google AI Mode — checking whether each could report a product price. Their write-up reports that on direct fetch Gemini found 4 of 8, ChatGPT 3 of 8, and Claude 0 of 8. In the scenario where the price existed only in JSON-LD, none of the five extracted it. The rest of those numbers deserve a moment too: Claude's verbatim answer was that it could not find any prices displayed on the page, on pages where the price sat in plain visible HTML. So the study refutes "AI reads your schema" and it does not vindicate "AI reliably reads your HTML" either. Extraction was patchy across the board, and worst where the fact was hidden in markup.

Patrick Ryall's review of AI crawler behaviour describes Mark Williams-Cook's February 2026 "Duck Test": a fabricated address placed only inside deliberately invalid JSON-LD. ChatGPT and Perplexity both returned the address. Invalid JSON-LD cannot be parsed as JSON-LD, so they were reading the script block as text.

What that changes

Little about whether you ship schema. A lot about what you expect from it.

  • Don't hide facts in markup. If a number matters, put it in visible prose or a table. The same discipline that makes a post extractable for a human skimmer makes it extractable for a model.
  • Server-render it. GPTBot, ClaudeBot and PerplexityBot don't execute JavaScript. That is a measured result: Vercel's analysis with MERJ, published December 2024 across 569 million GPTBot requests and 370 million ClaudeBot requests on its network, found that none of the major AI crawlers render JavaScript. Schema injected client-side reaches nobody. In Next.js that means the <script type="application/ld+json"> belongs in a server component, which is what the file above is.
  • Machine-readable surfaces are a separate job. If you want an explicit agent-facing layer, that is what an llms.txt file does — and even that is still a bet with no measured return.

If you are auditing what your existing pages emit before deciding what to keep, that inventory is the first thing I run in a content audit: schema, canonicals and orphan pages all fall out of the same crawl.

Five types I ship that earn nothing

Service

Service is not a feature in the search gallery, so there is no snippet to win and there never was one. I keep it because it names what each service page covers and hangs that off the canonical Person @id, which buys entity clarity and zero pixels. If you shipped it expecting stars or sitelinks, stop.

ProfessionalService, on cities I do not operate in

LocalBusinessSchema emits @type: "ProfessionalService", an Organization subtype rather than the literal LocalBusiness most tutorials paste. It renders on the 40 city pages generated from lib/locals.ts: London, New York, Sydney and the rest. None of those cities is in Nepal, and Nepal is the only country where this site converts. So my local markup describes markets I have no presence in, while the market that produces the clicks gets none. Local business panels also lean on a verified Business Profile in practice, not on JSON-LD alone. This one is on the list to either ground in a real service area or delete outright.

FAQPage

Google's documentation changelog records that FAQ rich results stopped appearing in Google Search on 7 May 2026, and that the feature documentation was removed afterwards. The markup is still valid schema.org and causes no harm. It wins no pixels.

That followed an announcement on 12 June 2025, reported by Search Engine Land, that Google would drop seven more types: Book Actions, Course Info, Claim Review, Estimated Salary, Learning Video, Special Announcement and Vehicle Listing. Google said the removal would roll out over the coming weeks and months and gave no end date, so 12 June is the date it was announced, not the date any of them stopped rendering. The stated reason was that the types were not commonly used and no longer added enough value for users.

I keep my FAQ blocks anyway. The reason is the format: a question-shaped H3 with a direct answer under it is what an AI answer lifts from, and that works whether or not a <script> tag wraps it.

Reviews about myself

My component is called AggregateRatingSchema. It emits an array of Review nodes hung off my own Person @id, with no aggregateRating property anywhere in it. Google's review snippet guidance is explicit: if the entity being reviewed controls the reviews about itself, pages using LocalBusiness or any other Organization type are ineligible for the star feature. Person is not on the supported list for review snippets in the first place. Two independent reasons this shows nothing.

WebApplication with no rating

I ship five client-side calculators at /tools, each with WebApplication markup and offers.price: "0". Google's software app documentation confirms WebApplication is a supported type and lists the required properties: name, offers.price, and either aggregateRating or review. I have two of the three. No rating, no rich result — and since I would be rating my own tools, see the previous section.

A validator you can actually run

Rather than pasting URLs into the Rich Results Test one at a time, this reads a page and counts the failure modes that actually bit me. Node 18+, no dependencies. It self-checks against a fixture, so node check-jsonld.mjs proves the logic before you point it at anything.

// check-jsonld.mjs
import assert from 'node:assert/strict'

const SCRIPT_RE =
  /<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi

export function extractJsonLd(html) {
  const out = []
  for (const m of html.matchAll(SCRIPT_RE)) {
    try { out.push(JSON.parse(m[1].trim())) }
    catch (e) { out.push({ __parseError: e.message }) }
  }
  return out
}

function flatten(blocks) {
  const all = []
  const push = (n) => {
    if (Array.isArray(n)) return n.forEach(push)
    if (!n || typeof n !== 'object') return
    all.push(n)
    if (n['@graph']) push(n['@graph'])
  }
  blocks.forEach(push)
  return all
}

const typeOf = (n) => [].concat(n['@type'] ?? []).join(',')

export function audit(html) {
  const blocks = extractJsonLd(html)
  const all = flatten(blocks)
  const persons = all.filter((n) => typeOf(n).includes('Person'))
  const apps = all.filter((n) => /Application/.test(typeOf(n)))
  return {
    blocks: blocks.length,
    broken: blocks.filter((b) => b.__parseError).length,
    personNodes: persons.length,
    anonymousPersons: persons.filter((n) => !n['@id']).length,
    distinctPersonIds: new Set(persons.map((n) => n['@id']).filter(Boolean)).size,
    apps: apps.length,
    appsMissingRating: apps.filter((n) => !n.aggregateRating && !n.review).length,
  }
}

const fixture = `
<script type="application/ld+json">{"@type":"Person","name":"Santosh Paudel"}</script>
<script type="application/ld+json">{"@type":"Person","@id":"https://www.santoshpaudel.me/#person","name":"Santosh Paudel"}</script>
<script type="application/ld+json">{"@type":"WebApplication","name":"ROI Calculator","offers":{"price":"0"}}</script>
<script type="application/ld+json">{ not json }</script>`

const r = audit(fixture)
assert.equal(r.blocks, 4)
assert.equal(r.broken, 1)
assert.equal(r.anonymousPersons, 1)   // the bug Google saw on 389 indexed pages
assert.equal(r.distinctPersonIds, 1)
assert.equal(r.appsMissingRating, 1)  // no rating -> no Software App rich result
console.log('self-check passed:', r)

const url = process.argv[2]
if (url) console.log(url, audit(await (await fetch(url)).text()))

Two signals to watch. anonymousPersons > 0 on a page that also carries a canonical @id is the entity-splitting bug. broken > 0 means a template interpolated an unescaped quote into your JSON — silent, invisible in the browser, and it kills the entire block.

What I would ship on a new site, in order

  1. Organization and Person with stable @id values, declared once and referenced everywhere. Do this before anything else; it is the only piece other schemas depend on.
  2. BreadcrumbList, generated from the route. Free, and it changes the visible snippet.
  3. Article or BlogPosting with a real image. Skip it if you cannot guarantee the image exists — a broken image property is worse than no property.
  4. LocalBusiness only if you have a genuine service area, and only for the places you actually serve. I got this backwards: my markup covers 40 cities I have no presence in and skips Kathmandu, where the clicks come from.
  5. Stop. Service, FAQPage, WebApplication, self-reviews and city-page ProfessionalService cost about twenty minutes each and, on current evidence, return nothing.

Generating all of this from the route tree rather than hand-writing it per page is the same problem as generating your sitemap and robots.txt — solve it once in the layout. And if the question underneath all this is really "how do I get mentioned in AI answers," the lever is being the specific, citable source on a question worth answering.

FAQ

Does schema markup help you rank in ChatGPT or Perplexity?

No published evidence says it does directly. Google's AI features documentation states that no special structured data is needed for AI Overviews or AI Mode, and the SearchVIU tests found that a fact placed only in JSON-LD was extracted by none of the five systems tested. Schema helps indirectly, by helping search indexes resolve who you are — and those indexes are what several AI systems retrieve from.

Is FAQ schema still worth adding in 2026?

Not for rich results. Google stopped showing them on 7 May 2026. The FAQ content is still worth writing, because question-and-answer formatting is what AI answers extract. Write the H3 and the answer; the <script> tag is optional.

What is the @id property in JSON-LD for?

It gives a node a stable global identifier so separate blocks across separate pages refer to the same entity instead of minting a new one each time. Without it, a Person block on 400 pages is 400 unrelated candidates. With it, they are one.

Do AI crawlers execute JavaScript?

GPTBot, ClaudeBot and PerplexityBot do not. Vercel's December 2024 analysis with MERJ, covering hundreds of millions of crawler requests across its network, found that none of the major AI crawlers render JavaScript, so anything injected client-side is invisible to them. Server-render your JSON-LD. In Next.js that means emitting it from a server component, not from a useEffect.

How do I check my structured data is valid?

Google's Rich Results Test for eligibility on a single URL, and the Schema Markup Validator for syntax. For a whole site, script it — the auditor above runs over any URL in a few lines and catches the two failures manual spot-checks miss: duplicate anonymous entities, and JSON that silently failed to parse.


Wondering which of your schema types are actually earning anything? Most sites I look at ship four or five that Google retired years ago and miss the one entity node everything else should point at. Get a content audit or get in touch.

Free resource

Get the AI-SEO Content Checklist

A practical checklist for getting your own content cited by Google AI Overviews, ChatGPT, and Perplexity — not just ranked.

No spam. Unsubscribe anytime.

Browse all free guides →

Run this on your own numbers

Content ROI & Payback Calculator — free, no signup, runs in your browser.

Open the calculator →

Want to implement this with guidance?

Santosh helps founders turn insights like this into real systems.

SEO Content Strategy

External Resources

Further Reading & Tools

Related Posts

01
14 min
SEO StrategyContent Strategy
TodaySEO Strategy

Perplexity freshness and the publishing cadence maths

What is actually published about freshness in AI citations, where the two biggest datasets disagree, and the arithmetic for when refreshing an old page beats writing a new one.

Read article
02
13 min
SEOContent Strategy
TodayAnalytics & Data Marketing

Zero-Click Search ROI: My Content Payback Math

My site earned 22 clicks from 4,553 impressions in 85 days, a 0.48% CTR. I put that number through a content payback model. At my real impression yield the programme would need a 42.5% CTR to break even.

Read article
03
14 min
SEOSEO Strategy
TodayAnalytics & Data Marketing

Which Keywords Have You Already Lost to AI Overviews?

Impressions holding while CTR collapses is the AI Overview footprint. Here is the BigQuery query, a standard-library Python triage script, and what happened when I ran the same test against my own 4,553 impressions and 22 clicks.

Read article