Schema & Modelling6 min read

How to Manage Schema Evolution for AI (Adding Fields)

A content team adds one field to a schema, `summaryForAI`, ships it, and the next morning half their retrieval pipeline returns null for every document created before the migration.

Published August 29, 2026

A content team adds one field to a schema, `summaryForAI`, ships it, and the next morning half their retrieval pipeline returns null for every document created before the migration. The agent that summarizes product pages starts hallucinating because the field it was told to read is empty on 40,000 legacy entries. Nobody backfilled, nobody versioned the read path, and now a well-meaning additive change has quietly degraded every downstream AI workflow that touched it. Schema evolution sounds like a solved problem until you add fields that machines, not just humans, are supposed to consume.

Sanity is the headless content platform built as a Content Operating System, an intelligent backend where your schema is code you ship and evolve, not a rigid table you migrate with dread. That distinction matters here: adding a field for AI consumption is not just a data-model change, it is a contract change with every agent, prompt, and retrieval job reading that content. This guide treats schema evolution as governed, testable engineering. We will cover why additive changes still break things, how to model AI-facing fields so they degrade gracefully, how to backfill safely across large datasets, and how to keep the read path honest while old and new shapes coexist.

Why additive schema changes still break AI consumers

The comforting myth of schema evolution is that adding a field is safe. Removing or renaming a field breaks things; adding one is backward compatible. That holds for traditional consumers, a frontend that ignores fields it does not render, an API client that deserializes a known subset. It stops holding the moment an AI workflow is the consumer, because agents and retrieval jobs are told what to read, and they read it literally.

Consider a retrieval-augmented generation pipeline instructed to ground answers in a `summaryForAI` field. The day you add that field, it exists on the schema but is null on every document authored before the change. Your pipeline does not error. It does something worse: it silently returns empty context, and the model fills the gap with plausible invention. The failure is invisible in your CI, invisible in your CMS, and visible only as a slow rise in hallucinated answers that nobody can trace back to a Tuesday migration.

There are three distinct hazards bundled into one innocent-looking change. First, the presence gap: new field, old documents, no value. Second, the semantic gap: the field means one thing to the author who fills it and another to the prompt that reads it. Third, the consistency gap: some documents get backfilled by a batch job, others by editors, others not at all, and the AI consumer now sees three populations with different reliability.

Modeling your business well, the first pillar of a Content Operating System, means treating the AI consumer as a first-class reader of your schema. In Sanity, schemas are `defineType` and `defineField` declarations in TypeScript that you version in git alongside the code that queries them, so an added field and the GROQ query that reads it move through review together rather than drifting apart across a database console and an application repo.

Model AI-facing fields so they degrade gracefully

The fix for the presence gap starts at modeling time. An AI-facing field should never be a bare string the pipeline blindly trusts. It should carry enough structure that a consumer can tell the difference between not yet populated, deliberately empty, and machine-generated versus human-authored.

A robust pattern is to model the AI field as an object rather than a scalar: a value, a source enum (`human`, `generated`, `reviewed`), a `generatedAt` timestamp, and a `modelVersion` string. Now a retrieval job can filter on `source in ["human", "reviewed"]` and simply skip documents where the summary was never produced, instead of feeding the model an empty string it will paper over. Graceful degradation becomes a query concern, not a prayer.

In Sanity this is idiomatic. You declare the field with `defineField` as an object with typed members, and because GROQ lets you project exactly the shape you need in one round trip, the read path stays explicit: `*[_type == "product" && defined(aiSummary.value) && aiSummary.source in ["human","reviewed"]]{ _id, "summary": aiSummary.value }`. The query names its own preconditions. A document that fails them is excluded by design, not silently coerced into a null.

TypeGen closes the loop on the application side. When you regenerate types from the schema, `aiSummary` becomes a typed shape in your codebase, so the pipeline code that reads it fails to compile if you assume a string where an object now lives. The compiler catches the contract change before it reaches a model in production.

This is the difference between a schema that stops at storage and one that operates content end to end. The field does not just hold a value; it carries the provenance an AI consumer needs to decide whether to trust it, and the read path can enforce that decision in a single query.

Backfill large datasets without a maintenance window

Once the field is modeled, the presence gap on existing documents is a backfill problem, and backfill at content scale is where naive plans fall apart. You cannot lock a live editorial system for hours while a script rewrites tens of thousands of documents, and you cannot trust a single unbatched mutation run to be idempotent when it fails halfway through a network blip.

The disciplined approach is incremental and resumable. Query for the population that lacks the new field, page through it in bounded batches, generate or derive the value, and patch each document with a mutation that is safe to re-run. Crucially, you tag backfilled values with the `source: "generated"` provenance from the previous section, so a partial run leaves the dataset in a legible state: every document is either untouched, human-authored, or clearly machine-backfilled, and a resumed job can find exactly the remaining gap by querying for documents where the field is still undefined.

Sanity supports this as content automation rather than a scary external migration. Functions let you run serverless logic against Content Lake, so a backfill can be triggered, batched, and observed inside the platform, and the App SDK lets you build the same operation as an in-Studio app an operator can run and monitor. Because Content Lake is a queryable store, the backfill's own progress is a GROQ query: `count(*[_type == "product" && !defined(aiSummary)])` tells you exactly how much work remains at any moment.

The read-path guard from the previous section is what makes this safe to do live. Because your retrieval query already filters on `source` and `defined()`, a half-finished backfill never exposes a partially populated field to the AI consumer. Documents flip from excluded to included the instant they are correctly populated, and never in an in-between state. Backfill and serving coexist without a maintenance window because the query, not the clock, decides what is ready.

Keep old and new shapes coexisting behind a versioned read path

Schema evolution is rarely a single instant. For any non-trivial period, old documents and new documents coexist, and if you are evolving the meaning of an AI field rather than just adding one, you may have two shapes of the same field live at once. The consistency gap is the enemy, and the defense is a read path that is explicit about which shape it expects and versioned alongside the schema that produces it.

The anti-pattern is scattering field access across a dozen prompt templates and pipeline steps, each with its own assumptions about whether `aiSummary` is a string or an object. When you evolve the shape, you now have to find and update all of them, and the ones you miss fail silently. The pattern is to centralize the read into a single named query that encodes the contract, so there is exactly one place that knows how to normalize old and new shapes into what consumers expect.

GROQ makes this normalization tractable in the query itself. A projection can coalesce shapes, so a single query can serve a summary whether it lives in a legacy string field or a new object field: `"summary": coalesce(aiSummary.value, legacySummary)`. Consumers ask for `summary` and never learn that two generations of schema sit underneath. The read path absorbs the migration so the AI pipeline does not have to.

Because GROQ queries and schema definitions both live in your repository, the versioning is real, not conventional. A pull request that changes the field shape changes the query that reads it in the same diff, runs through the same review, and ships atomically. This is the Content Operating System providing a shared foundation rather than the silos of a database migration in one system and prompt edits in another. The contract between producer and consumer is enforced in code, in one place, under review.

Govern who can change AI-facing fields, and prove what changed

An AI-facing field is a higher-stakes surface than an ordinary editorial field, because a bad value does not just render wrong on one page, it propagates into every answer a model grounds in it. That raises a governance question most schema-evolution guides skip: who is allowed to change the shape or the values of fields that feed AI, and can you reconstruct what a model saw at the moment it generated an answer?

Governance here has two layers. The schema layer is protected by putting the change behind code review; nobody alters an AI-facing field's shape without a pull request. The data layer needs runtime controls: Roles and Permissions to scope who can edit the provenance-bearing fields, Content Releases to stage a coordinated change across many documents and publish it as one reviewable unit rather than a scatter of individual edits, and Audit logs to answer the forensic question of who changed which value when.

That audit trail is not bureaucratic overhead when AI is downstream. If a model produced a bad answer last week, you need to know what the grounding field contained last week, who set it, and whether it was human-authored or machine-generated. The provenance you modeled in section two plus the Audit logs together let you reconstruct the input, which is the only honest way to debug a hallucination that traces to your content rather than the model.

On the platform's own footing, Sanity is SOC 2 Type II compliant, supports GDPR obligations, offers regional data residency, and publishes its sub-processor list, so the governance you build on AI-facing fields sits on infrastructure that itself meets enterprise compliance expectations. Governing schema evolution is not a tax on velocity; it is what lets you evolve fast without turning every additive change into an untraceable production incident.

Evolving AI-facing fields: how platforms handle the read path, backfill, and governance

FeatureSanityContentfulStrapiHygraph
Adding an AI-facing fieldTyped defineField object (value, source, modelVersion) in a git-versioned schema, reviewed with the query that reads it in one diff.Add a field in the web content model UI; changes are made in the app rather than versioned in your repo alongside read code by default.Add a field in the content-type builder; schema lives in code, so it can be reviewed, though read queries live separately in your app.Add a field in the schema UI or via management API; field definition and consuming queries are managed in separate places.
Read path that skips unpopulated docsGROQ filters on defined(aiSummary.value) and source in [...] so half-backfilled documents are excluded by the query, not coerced to null.GraphQL/REST return the field as null; skipping unpopulated docs means client-side filtering or added query params.REST/GraphQL with filters can exclude null fields, though provenance-aware filtering depends on how you modeled the field.GraphQL filters can exclude nulls; blended provenance filtering depends on custom field modeling you add yourself.
Normalizing old and new field shapescoalesce(aiSummary.value, legacySummary) in one GROQ projection lets a single query serve both generations so consumers never see the seam.Coalescing two shapes typically happens in application code after fetch, not inside the query.Merging legacy and new shapes is handled in your application or a custom controller, not the query layer.GraphQL has no built-in field coalescing; shape normalization is done in application code.
Backfilling at scaleFunctions run batched, resumable mutations against Content Lake; count(*[!defined(aiSummary)]) reports remaining work as a live query.Backfill via the Content Management API with your own batching, rate handling, and progress tracking scripted externally.Backfill through your own scripts against the REST/GraphQL API or the database, with batching and resumability you build.Backfill via the management/mutation API with externally scripted batching and progress tracking.
Provenance on the valueModel source (human, generated, reviewed) and modelVersion as first-class typed members so consumers can decide what to trust.Provenance is possible by adding your own metadata fields; not a built-in shape for AI field trust.Provenance modeled as extra fields you define; no opinionated AI-provenance structure out of the box.Provenance modeled as additional fields you define yourself; no built-in AI-trust shape.
Type safety on the read pathTypeGen regenerates TypeScript from the schema, so pipeline code fails to compile when a field's shape changes underneath it.GraphQL codegen gives typed queries; keeping app types in sync with model edits made in the UI is a separate step.TypeScript types can be generated for content types; wiring them to your read code is a manual setup.GraphQL codegen produces typed operations; types follow the schema, syncing with app code is your responsibility.
Governing and auditing changesRoles and Permissions, Content Releases to stage a coordinated change, and Audit logs to reconstruct what a field held when a model read it.Roles, scheduled releases, and change history are available depending on plan tier; audit depth varies by subscription.RBAC and audit capabilities depend on the Enterprise edition and self-hosted configuration you run.Roles and audit features depend on plan tier; staged multi-document releases are more limited.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.