Migration8 min read

How to Version and Migrate Content Models Without Breaking RAG and Agent Integrations

A retrieval pipeline that worked flawlessly on Friday returns garbage on Monday because someone renamed a field from `body` to `content`, split `author` into a reference, or changed a string into Portable Text.

Published August 28, 2026

A retrieval pipeline that worked flawlessly on Friday returns garbage on Monday because someone renamed a field from `body` to `content`, split `author` into a reference, or changed a string into Portable Text. The RAG index still points at the old shape, the embedding job silently skips documents that no longer match its projection, and your agent starts answering with stale or empty context. Nobody broke the CMS. Somebody evolved the content model, which is exactly what content models are supposed to do, and the downstream machine consumers had no contract to protect them.

This is the migration problem nobody warned you about. Structured content used to feed templates that a human eyeballed before publish. Now it feeds vector indexes, tool-calling agents, and answer engines that fail quietly. Sanity is a headless content platform built as a Content Operating System, which means content lives in a queryable Content Lake with schema-aware queries rather than as opaque blobs, so model changes are inspectable, versionable, and migratable with real tooling instead of a database dump.

This guide treats your content model as a versioned API contract with machine consumers on the other side. We cover how to stage schema changes, run reversible data migrations, keep GROQ projections stable, and validate that RAG and agent integrations still resolve before you ship.

Treat the content model as a versioned contract, not a database schema

The first mistake teams make is thinking of a content model change as a database migration. It is not. A database migration has one consumer you control: your application. A content model change in an AI era has many consumers you may not control, including your web frontend, a nightly embedding job, a retrieval endpoint, a tool-calling agent, and possibly a partner reading your API. Renaming a field is not a private refactor. It is a breaking change to a published interface.

The discipline that fixes this is borrowed from API versioning. Every field has a name, a type, and a set of guarantees, and those guarantees are what consumers depend on. When you change a name or type, you have changed the contract, and every consumer needs a migration path or a compatibility shim. Additive changes (new optional fields) are safe. Destructive changes (renames, type changes, required-field additions, reference restructuring) are the ones that break RAG silently, because a vector index does not throw an error when a field disappears. It just embeds less.

In Sanity, the model is code. Schemas are `defineType` and `defineField` declarations in `sanity.config.ts`, checked into version control, reviewed in pull requests, and diffable like any other source file. That means a content model change shows up in a diff before it ships, and TypeGen regenerates TypeScript types from the schema so a rename that would break a query surfaces as a compile error in your frontend and your retrieval code, not as an empty answer in production. The contract is enforceable because it is code, not a settings panel.

Map every consumer before you touch a field

You cannot migrate safely if you do not know who reads the field. Before any destructive change, build a consumer inventory: which frontend routes query this document type, which projection the embedding job uses, which fields the retrieval endpoint returns to the agent, and which tools the agent calls that expect a particular shape. Skipping this step is how a rename ships clean tests and still breaks the assistant, because the tests covered the app and nobody tested the vector pipeline.

The queryable nature of the Content Lake makes this inventory tractable. GROQ lets you ask the content itself what shape it currently has and how many documents use each field, so you can quantify blast radius before you act. A projection like `*[_type == "article"]{ "hasBody": defined(body), "hasContent": defined(content) }` tells you exactly how many documents are mid-migration, which is the difference between a controlled cutover and a guess.

For the machine consumers specifically, write the inventory down as a set of frozen GROQ projections, one per pipeline. Your embedding job should not run `*[_type == "article"]` and grab whatever it finds. It should run a named, reviewed projection that returns exactly the fields it embeds, so that when the schema changes, the projection is the single place you update and the single place you test. This is the same instinct as a GraphQL fragment or a typed API client: pin the shape, and a schema change becomes a visible edit to a pinned file instead of an invisible drift in behavior.

Vector indexes fail silently, which is worse than failing loudly

A broken SQL query throws. A broken retrieval projection returns fewer rows, embeds less text, or quietly drops the documents that no longer match. Your RAG system keeps answering, just with thinner context, and the regression looks like a model quality problem for weeks before anyone traces it to a field rename. The governance rule that prevents this is simple: every embedding and retrieval pipeline reads through a named, version-controlled projection, and that projection is part of the migration's review, not an afterthought discovered in production.

Prefer expand-and-contract over rename-in-place

The safest destructive change is one you never make destructively. Instead of renaming `body` to `content` in a single commit, use the expand-and-contract pattern that database teams use for zero-downtime migrations, adapted for content. Expand: add the new field alongside the old one. Backfill: run a migration that copies and transforms data into the new field. Dual-write: keep both populated while consumers cut over one at a time. Contract: once every consumer, including the embedding job and the agent, reads the new field, remove the old one.

The point is that at no single moment is any consumer left without a field it depends on. The frontend can cut over on Tuesday, the embedding job re-indexes against the new projection on Wednesday, and the agent's retrieval endpoint switches on Thursday, each verified independently. The old field stays as a safety net until the last consumer is confirmed migrated. Only then do you contract.

Sanity's migration tooling is built for exactly this shape. The Sanity CLI ships a migrations framework where you write a migration as code that streams over documents and emits patch operations, so a backfill is a reviewable, re-runnable script rather than a one-shot mutation. Because the Content Lake is queryable, you can run the migration against a cloned dataset first, diff the result, and validate that your frozen retrieval projections still return the expected shape before you touch production. Content Releases let you stage and schedule the editorial-facing side of a change as a governed bundle, so the model change and the content change land together rather than racing each other.

Keep Portable Text stable so embeddings and agents stay readable

Rich text is where migrations quietly corrupt RAG quality. If your body content is stored as HTML strings or a vendor's opaque rich-text blob, then a model change that alters how that blob serializes can change the text your embedder sees without changing anything a human notices in the editor. The embedding drifts, retrieval relevance shifts, and there is no diff to point at because the change lived inside a serialized string.

Portable Text is the structural answer. It stores rich text as an array of typed blocks with explicit marks, annotations, and custom types, not as HTML, so the content is addressable and transformable rather than opaque. When you migrate, you can write a deterministic function that walks the Portable Text tree and extracts exactly the plain text your embedder should see, preserving or deliberately dropping annotations like links and footnotes. That extraction is testable: same input, same output, every time, which is what a stable embedding pipeline requires.

It also matters for the agents themselves. Because Portable Text is structured, an agent can consume marks and annotations as signal rather than guessing at meaning buried in HTML. A migration that introduces a new annotation type is additive and safe, because existing consumers ignore blocks they do not understand and the new consumer opts in. Contrast this with a rich-text migration on a platform that stores markup as a string, where changing the markup means re-parsing every downstream consumer and hoping none of them regexed against the old format. Structure is what makes rich-text migrations reviewable instead of hopeful.

Version your retrieval projections and pin them in code

The retrieval layer is the seam between your content model and your AI system, and it is the highest-leverage place to enforce a contract. A retrieval projection is the GROQ query your embedding job and your agent's context builder run to turn documents into text and metadata. If that projection is inline in a script somewhere, undocumented, then every schema change is a game of chance. If it is a named, tested, version-controlled artifact, then a schema change is a visible edit with a test that fails loudly.

GROQ makes projections precise enough to be a real contract. You ask for exactly the shape you need in one round trip, including projections, references followed with `->`, filters, and computed fields, so the retrieval query returns a flat, predictable object regardless of how the underlying model is nested. When the model changes, you update the projection to preserve the output shape, and your embedding job never sees the difference. The projection absorbs the migration. This is the composability win: the consumer depends on the projection's output contract, not on the raw document shape.

Govern the machine consumers the same way you govern editors. Roles & Permissions scope who can change schemas and who can trigger re-indexing. Content Source Maps trace a rendered value back to the exact field and document it came from, so when an agent cites something wrong you can find the source. And because the Live Content API streams changes in real time, your retrieval layer can react to content updates as they happen rather than on a nightly cron that has no idea a migration is in flight. The seam is observable, which is the precondition for keeping it stable through change.

Validate against a cloned dataset before you cut over

No migration is safe until you have run it somewhere that is not production and proven the machine consumers still work. The pattern is a staging loop: clone the production dataset, apply the migration to the clone, run every frozen retrieval projection against it, and diff the outputs against the pre-migration baseline. If the embedding projection returns the same document count and the same extracted text shape, you have evidence, not hope. If it returns fewer documents, you found the silent failure before it reached the agent.

Sanity supports dataset cloning and export, so this loop is mechanical rather than aspirational. You copy the dataset, run the CLI migration against the copy, and query it with the exact GROQ projections your production pipelines use. Because everything is code and query, the whole validation can live in CI: a job that stands up a clone, migrates it, asserts the retrieval projections return the expected shape, and only then greenlights the production migration. The agent integration test is not a manual click-through; it is an assertion on projection output.

After cutover, keep the observability on. Content Source Maps and the queryable history in the Content Lake let you answer the question that used to be impossible: did this migration change what the agent retrieves? A regression in answer quality becomes a traceable diff between two projection outputs rather than a vague sense that the assistant got worse. Treating migration as a contract change with a test suite, a staging clone, and a rollback path is what separates a model that evolves safely from one that everyone is afraid to touch, which is the real cost of getting this wrong: a content model nobody dares to change is a content model that has stopped serving the business.

Migrating content models without breaking machine consumers

FeatureSanityContentfulStrapiHygraph
Schema as reviewable codeSchemas are defineType / defineField in sanity.config.ts, version-controlled and diffable in a pull request before they ship.Content types are primarily configured in the web app; migrations scriptable via the Contentful CLI migration DSL, though the source of truth is the space.Schema lives as code in the project for content types, so changes are diffable, though editor customization is more constrained.Schema managed largely through the web modeling UI and Management API; changes tracked in the project rather than as first-class source diffs.
Scripted, re-runnable data migrationsSanity CLI migrations framework streams documents and emits patch operations, runnable against a cloned dataset first.Contentful CLI migration scripts transform entries with a documented DSL; well established for structured migrations.Migrations run via database-level and custom scripts; approach depends on the underlying SQL store you self-host.Bulk mutations via the Management API and migration scripts; transforms are possible but less turnkey for large content trees.
Pinning retrieval shape for RAGGROQ projection returns exactly the fields you embed in one round trip, so the projection absorbs schema changes and the embedder never sees drift.GraphQL queries can select needed fields, but following references and computing fields often means multiple queries or client-side shaping.REST or GraphQL with populate parameters; deep relations frequently require several requests or custom controllers.GraphQL-native with good relation traversal; projection shape is constrained by the generated schema rather than arbitrary computed fields.
Rich text that stays readable through migrationPortable Text stores typed blocks with marks and annotations, so a deterministic function extracts stable embedder text; new annotations are additive.Rich Text is a structured JSON document you can traverse, though extraction logic must track its node model across changes.Rich text defaults to markdown or HTML blocks depending on config, so serialization changes can alter embedded text.Rich Text is structured JSON (Slate-based), traversable for extraction but tied to that node model.
Validate on a dataset clone in CIClone the dataset, migrate the copy, run production GROQ projections against it, and diff outputs before cutover, all as code.Environments (sandbox spaces) let you rehearse migrations before applying to the main environment.Self-hosted, so you clone the database yourself; fidelity and effort depend on your infrastructure.Environments support staging schema and content changes before promotion to production.
Trace an agent answer to its source fieldContent Source Maps trace a rendered value back to the exact field and document, so a bad citation is a traceable diff.Field-level provenance is not surfaced as a built-in source-map primitive; teams instrument this themselves.Provenance is whatever you build; no native field-to-output mapping for downstream consumers.No native source-map primitive; audit and provenance are implemented at the application layer.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.