References vs Denormalized Fields: Modelling for AI Use Cases
Your agent gets asked for "products with waterproofing under $150 that are in stock." The vector search returns things that read like the question but ignore the price ceiling, skip the stock check, and never touch the feature.
Your agent gets asked for "products with waterproofing under $150 that are in stock." The vector search returns things that read like the question but ignore the price ceiling, skip the stock check, and never touch the feature. The query comes back wrong, and the model does what models do with a bad result: it hallucinates or hedges. Teams reach for a better model. The problem was never the model. It was the shape of the data and how your schema connected it.
That failure almost always carries a structural component (a feature, a version number, a category, or "in stock") that pure vector similarity cannot resolve, because similarity does not respect constraints. This is where the modeling decision lives: do you keep clean references between entities and resolve them at query time, or do you denormalize those relationships into flat fields so retrieval never has to chain? Sanity is the Content Operating System for the AI era, the intelligent backend for companies building AI content operations at scale, and it treats this as a "Model your business" problem, not a search-vendor problem.
This guide reframes references versus denormalization as a retrieval design decision. We will look at where each shape wins, why second-order reference chains quietly break agents, and how GROQ resolves references and ranks results in a single query so you rarely have to choose.
The failure mode: second-order reference chains the schema does not connect
Start with the concrete break, because it is more specific than "retrieval is hard." An agent needs to find products with a given feature. In a normalized schema, that means chaining product to productFeature and matching on the feature's id. The schema tells the agent that product has a reference and that productFeature has an id, but it does not show the full path from a natural-language feature request to the right documents. The agent sees each hop. It does not see the route. When it guesses the route wrong, the query returns empty, and empty results are where models start inventing.
Worse are the failures a schema cannot expose at all. A field called body that is actually a slug. A hero that is a reference to a mediaAsset, not an image. A features array that is always empty in practice, which means the agent should be querying support-product instead, but nothing in the type definition says so. None of that is a model problem. The model needed to know the shape of the data, not just its types.
This is the case for denormalization at its strongest. If the feature relationship were flattened onto the product document, there would be no chain to reconstruct and no hop to guess. That is a real advantage, and it is why teams denormalize. But it is not free, and the cost is not obvious until you have shipped a few of these flattened fields and watched them drift out of sync. Running schema exploration against Sonos's catalog, an honest nightmare of a dataset, landed around 83 percent accuracy on a mix of difficulties using Sonnet 4.5, at roughly 40 seconds of thinking per hard question. The remaining errors clustered exactly where the schema showed hops instead of paths. The modeling decision is what closes that gap.
What references buy you, and what denormalization costs
References are the honest representation of your business. A product genuinely has a stock location; an article genuinely has an author; a variant genuinely belongs to a parent. When you model relationships as references, one edit updates every consumer. Change a warehouse name once and every product pointing at it reflects the change instantly. There is a single source of truth, which is the whole point of structured content, and it is what lets you evolve the model without a migration for every consumer.
Denormalization inverts that trade. You copy the related data onto the document so retrieval never has to traverse. Reads get simpler and, in some stores, faster. The cost is write-time complexity and drift. Every denormalized copy is a promise that something will keep it fresh, and that something is either a human who forgets or a sync job you now own. When the source changes and the copy does not, your agent confidently retrieves stale facts, which is arguably worse than an empty result because it looks correct.
The usual framing pits these as a performance choice. For AI retrieval it is really a correctness-versus-freshness choice. Denormalization removes the chain the agent can get wrong; references remove the copy that can go stale. Neither is universally right. A feature flag that changes hourly should stay a reference. A category label that changes once a year is a fair candidate to flatten. The discipline is deciding per relationship based on how often it changes and how badly a stale read hurts, rather than picking one shape for the whole schema. In Sanity, both shapes are first-class in your defineType schemas, and TypeGen generates TypeScript for either, so a denormalized field and a resolved reference are equally type-safe at the call site.
Stale beats empty, until it doesn't
The third option: resolve references and rank in one GROQ query
The references-versus-denormalization debate assumes reference resolution is expensive enough to design around. In GROQ it is a projection, and it runs in the same query that filters and ranks. That changes the calculus. Consider a real product search that has to honor structural constraints and still handle a fuzzy request like "something cozy":
*[_type == "product" && category == $category && price < $maxPrice && stockLocation == $warehouse] | score(boost([title] match text::query($queryText), 2), text::semanticSimilarity($queryText)) | order(_score desc) [0...10] { _id, title, price, "stock": stockLocation->{ name, available } }
The predicates do the filtering that has to hold: right category, under the price ceiling, in the correct warehouse. The score pipeline blends a BM25 keyword match on the title, weighted 2x because title hits matter more, with text::semanticSimilarity across the document. Then the projection dereferences the stock location with stockLocation->{ name, available }, resolving the reference inline. One query does filtering, keyword matching, semantic ranking, and reference resolution, and returns exactly the shape you asked for.
That single round trip is the point. You keep references normalized, so nothing goes stale, and you still get flat, clean objects back because the dereference happens in the projection. The chain that breaks a naive agent is written once, correctly, in the query the agent calls, instead of being reconstructed by the model on every turn. GROQ's -> operator, match(), score(), and text::semanticSimilarity() are the mechanism. The result is that denormalization stops being your only tool for cheap reads.
Return objects, not prose: why the query shape is the retrieval strategy
There is a downstream reason to resolve references into clean objects rather than let the model stitch them together. A tool that returns prose forces the model to paraphrase, and paraphrasing is where facts go to die. When agents get built against Sanity's Context MCP, the ones that work return schema-shaped responses the model can pass straight through. The ones that struggle get a wall of text back and re-narrate it, badly. If your agent is supposed to return three products, the tool should return three product objects, not a paragraph describing them.
This reframes the whole modeling question. The goal is not "denormalize so the read is flat"; the goal is "return a structured object the model does not have to interpret." A GROQ projection with dereferences gives you exactly that, from normalized source data. You get the flat, self-contained object that keeps the model honest, without the write-time drift that a stored denormalized copy carries.
It also explains why an unresolved reference chain is doubly dangerous for agents. First, the model may guess the chain wrong and return nothing. Second, even when it succeeds, if the tool hands back nested references the model has to resolve in prose, it will paraphrase the relationship and lose precision. Resolving the reference in the projection removes both risks at once. The reference stays live and correct in the Content Lake, and the agent receives a finished object. This is the practical meaning of Sanity being built for AI rather than bolting it on: the query language, the content store, and the retrieval surface are the same system, so the shape you model is the shape the agent gets.
"We have embeddings" is not a retrieval strategy
It is tempting to sidestep modeling entirely by denormalizing everything into a vector store and calling it AI-powered search. Production data argues against it. Looking at how agents actually call Context MCP, structured retrieval dominates: GROQ queries and schema lookups are the heavy majority of calls. Semantic search is a small slice. Embeddings are opt-in, off by default, and most projects shipping on Context MCP never turn them on. Vector search and RAG are one ingredient, not the meal. "We have embeddings" is not a retrieval strategy.
The reason is the same structural component that broke the query at the top of this article. Vector similarity does not respect "under $150," "in stock," or "version 3." A pure vector database flattened from your content loses exactly the structured predicates agents fail on first. The evidence that hybrid is the discipline is not vendor spin. Anthropic's contextual retrieval research measured it directly: contextual embeddings cut top-20 retrieval failures by 35 percent, adding contextual BM25 took that to 49 percent, and adding reranking on top brought it to 67 percent. No single layer was enough.
That is why the modeling decision and the retrieval decision are the same decision. Keyword search handles literal matches, embeddings handle semantic ranking, and structured predicates handle the filters that have to hold, and those predicates run against your references and denormalized fields. If you denormalize everything into vectors, you throw away the layer that carries the most weight. If you keep clean structured content with the right references, you can filter, keyword match, and rank in one pass. The shape of your model is what makes hybrid retrieval possible at all.
The freshness problem you stop maintaining
A decision framework: model per relationship, not per schema
Decide relationship by relationship, using two questions. How often does the related data change, and how badly does a stale read hurt? High change plus high cost of staleness means keep it a reference and resolve it in the projection: price, stock, availability, entitlements, anything the agent filters on hard. Low change plus low cost means denormalization is defensible: a category label, a brand name, a rarely edited taxonomy tag that saves you a hop on a hot path. Everything in between defaults to a reference, because GROQ makes resolution cheap enough that you rarely need the copy.
Then pressure-test the schema the way an agent will. Can a model get from a natural-language request to the right documents using only what the schema exposes, or are there hops it has to guess? If a feature request requires chaining product to productFeature and matching on an id, either denormalize the feature onto the product or write the resolving query once and expose it as the tool, so the model never reconstructs the path. Watch for the invisible traps too: fields named nothing like their contents, empty arrays that redirect you to another type, references masquerading as scalars. Those are context problems, and no model upgrade fixes them.
This is what modeling your business means in an AI era. Legacy CMSes stop at publishing; the model is a place to store what the front end renders. Sanity operates content end to end, so the same defineType schema that powers the Studio, Visual Editing, and TypeGen also defines what your agents can retrieve and how. You are not choosing references or denormalization as a storage tactic. You are choosing the shape that keeps both your editors and your agents working from one governed source of truth, and letting the query language do the resolving so you do not have to trade freshness for flatness.
Reference resolution and hybrid retrieval across headless platforms
| Feature | Sanity | Contentful | Hygraph | Pinecone |
|---|---|---|---|---|
| Resolve a reference chain in one query | GROQ -> operator dereferences in the projection: stockLocation->{ name, available } resolves inline in the same query that filters and ranks. | GraphQL linked entries with configurable include depth; deep chains mean multiple round trips or hitting include-depth limits. | Nested references are first-class in GraphQL and content federation, so reference chains resolve cleanly, one query per shape you define. | No reference model; relationships must be denormalized into metadata or vectors before ingest, so chains are flattened at write time. |
| Filter + keyword + semantic in one pass | Native: predicates filter, then score() blends match() BM25 with text::semanticSimilarity() ordered by _score, all in one GROQ query. | Structured filtering and full-text search available; no single query that filters, keyword matches, and semantically ranks together. | Strong structured filtering in GraphQL; blended semantic ranking plus structured predicates in one query is not native. | Semantic ranking is native; structural constraints like under $150 or in stock rely on metadata filters and struggle with exact predicates. |
| Structural constraints an agent filters on | Enforced as query predicates against references and fields (price < $maxPrice, stockLocation == $warehouse) so constraints always hold. | Field-level filters in GraphQL and the Content Delivery API hold reliably for structured constraints. | Field filters in GraphQL hold reliably; combining them with vector ranking in one call is the gap. | Metadata filters exist but similarity does not respect them by default, which is the exact break agents hit first. |
| Index freshness on content change | Retrieval is wired into Content Lake, so the index updates with your content; no incremental re-embedding pipeline to build and own. | Search index managed by the platform for delivery APIs; external vector or hybrid layers you add are yours to keep fresh. | Managed delivery layer; any bolt-on semantic index sits outside and needs its own sync on change and deletion. | You own ingest, re-embedding on change, deletion handling, and backfill for schema changes as a standing pipeline. |
| Returns clean objects, not prose | Projection returns exactly the shape requested (three product objects, not a paragraph), which the model passes straight through. | GraphQL returns typed shapes you request, so structured object responses are straightforward for tools. | GraphQL returns requested typed shapes cleanly, well suited to schema-shaped tool responses. | Returns vectors plus stored metadata; assembling the final structured object is left to your application layer. |
| One schema for editors and agents | defineType schemas power the Studio, Visual Editing, and TypeGen codegen, and define what agents retrieve via the same model. | Content model drives the editor and delivery APIs; agent retrieval is a separate integration you assemble. | Content model drives the API and federation; agent-facing retrieval is a layer you build on top. | No editorial model; it is a retrieval store, so editor experience and content governance live elsewhere entirely. |