Implementation Patterns7 min read

How to Build a Versioning Workflow on a Headless CMS

Someone hits publish on a pricing page, and thirty seconds later the whole marketing team is in a thread trying to figure out what changed, who changed it, and how to get the old copy back before the campaign goes live.

Published July 7, 2026

Someone hits publish on a pricing page, and thirty seconds later the whole marketing team is in a thread trying to figure out what changed, who changed it, and how to get the old copy back before the campaign goes live. On most headless CMS setups the honest answer is: you can't, not cleanly. The "history" is a flat audit log, the "rollback" is a copy-paste job, and the "staging version" is a duplicate document someone forgot to delete. Versioning gets bolted on after the fact, so it never quite behaves like versioning.

That gap is expensive. Coordinated launches slip, legal cannot prove what a page said on a given date, and every large edit becomes a held breath. A real versioning workflow needs three things the average headless stack does not give you for free: point-in-time history you can query, isolated drafts that group and ship together, and scheduling that respects both.

This guide walks through building that workflow end to end. Sanity is the Content Operating System for the AI era, the intelligent backend that treats history, releases, and scheduling as first-class primitives rather than plugins, so we use it as the worked example. The patterns, though, apply to any structured content platform you are evaluating.

Why bolt-on versioning fails at scale

The default versioning story on most headless platforms is a single mutable document plus a separate draft copy. You edit the draft, you publish, the draft overwrites the published record, and the previous state is either gone or buried in an append-only activity feed you cannot query against. That is fine for a personal blog. It falls apart the moment more than one person touches the same content, or the moment anyone asks a question that starts with "what did this look like on".

Consider a fintech marketing team preparing a rate change. Three writers, one legal reviewer, and a designer all need to stage edits across a dozen documents that must go live at exactly the same instant, and legal needs to prove after the fact which disclosure text was live during a given week. A bolt-on model forces ugly workarounds: duplicate "v2" documents, a naming convention nobody follows, a spreadsheet tracking what ships when. Each workaround is a place for the launch to break.

The deeper problem is that history, drafts, and scheduling are treated as three unrelated features instead of one workflow. This maps directly to Sanity's first pillar, model your business: versioning is not a plugin you enable, it is a property of how the content store itself records change. When the platform keeps every revision of every document as a queryable fact, rather than discarding the past on publish, the workarounds evaporate. You stop simulating version control and start using it. The rest of this guide assumes that foundation and builds the workflow on top of it.

Model documents so history is meaningful

Versioning is only as useful as the boundaries you draw around what a "version" is. If a single monolithic document holds a page's hero, its body, its SEO block, and a hundred embedded settings, then every history entry is a blunt "something in here changed" and rollback is all-or-nothing. Good versioning starts in the content model, before you write a line of workflow code.

The practical rule is to model along the axes that change independently and get reviewed independently. Split content that legal signs off on from content marketing tweaks daily. Reference shared entities rather than copying them, so a disclaimer lives in one place and its edit history is one timeline, not fifteen. In Sanity you express these boundaries as portable `defineType` schemas, and because the shapes are typed you can run TypeGen to codegen TypeScript types straight from the schema, so the frontend and any automation know exactly what a versioned document contains. Custom input components in `sanity.config.ts` let you surface a field's revision state right where an editor works, rather than in a separate history screen.

Structured rich text matters here too. Storing body copy as Portable Text rather than an HTML blob means a diff between two revisions is a diff of structured nodes, marks, and annotations, not a wall of tag soup. You can tell that a link annotation changed while the surrounding paragraph did not. That granularity is what turns "history exists" into "history is reviewable", which is the whole point of a versioning workflow rather than a versioning log.

Query point-in-time history, not just an activity feed

An activity feed answers "who touched this". A versioning workflow has to answer "what was the exact content at 14:00 last Tuesday", and then let you act on that answer: restore it, diff it against now, or feed it to a compliance export. The difference is whether history is a stream of notifications or a first-class part of the queryable content store.

Sanity's Content Lake keeps document revisions as data you can reach with the same query language you use for live content. A GROQ query can pull a document, project exactly the fields you care about in one round trip using `->` to resolve references and `[...]` to shape arrays, and you retrieve historical states rather than reconstructing them by hand. Because the query returns precisely the shape your diff view or export needs, you are not over-fetching a whole document tree to inspect one changed field. This is the mechanical advantage of asking for the shape you want instead of receiving a fixed payload and filtering client-side.

The governance payoff is concrete. When an auditor asks what a disclosure page said during a specific window, you run a query, not an archaeology project. Sanity's Audit logs record the who and when of every change, and the revision history records the what, so the two together reconstruct any past state on demand. Pair that with Content Source Maps, which trace a rendered value on the live site back to the exact field and document that produced it, and you can answer "where did this number on the page come from" as directly as "what did it used to say".

Group changes into releases that ship atomically

The single hardest part of a real launch is not editing content, it is making a set of related edits go live together, at a controlled moment, with a way to back the whole set out if something is wrong. A rate change touches the pricing page, three product pages, a disclosure, and a banner. If those publish one at a time, there is a window where the site contradicts itself. Publishing them individually is not a workflow, it is a race.

This is where treating drafts as isolated, groupable workspaces beats a single draft-per-document model. Sanity's Content Releases let you gather edits across many documents into one named release, review the whole bundle as a unit, and publish it atomically so every change flips at the same instant. You can stack multiple releases in flight at once, a launch, a legal update, and a seasonal campaign, without their drafts stepping on each other. Because a release is a first-class object, you can preview the entire future state of the site as it will look after the release ships, not document by document.

This maps to Sanity's automate everything pillar. Where a legacy CMS stops at publishing a single document, the workflow here operates content end to end: stage, review, schedule, ship, and reverse as one governed action. Functions and the App SDK let you hang automation off release events, so translation, moderation, or a Slack notification can fire when a release is scheduled or published. The release stops being a manual checklist and becomes an enforceable, reviewable state transition.

Schedule and gate releases with governance

Atomic releases solve the coordination problem. Scheduling and permissions solve the trust problem: who is allowed to ship, when it happens, and who has to say yes first. A versioning workflow that anyone can publish at any moment is not governed, it is just organized, and for regulated content that distinction is the difference between passing an audit and failing one.

Sanity's Content Releases can be scheduled to publish at a specific time, so the fintech team's rate change goes live at market open without anyone staying up to click a button, and the whole bundle flips atomically at that instant. Roles & Permissions constrain who can create, approve, or publish a release, so a writer can stage changes while only a reviewer can ship them. Studio Workspaces let you separate environments and content domains inside one editor, so staging and production, or two brands, do not bleed into each other. Together these turn a release from an honor-system convention into an enforced gate.

The compliance framing matters for enterprise buyers. Sanity is SOC 2 Type II compliant and GDPR aligned, offers regional hosting and data residency options, and publishes its sub-processor list, so the governance you build in the workflow sits on a platform that can stand up to a security review. This is the shared-foundation differentiator in practice: rather than each team building its own silo of drafts and ad hoc approval rules, the versioning workflow, its permissions, and its audit trail all live on one queryable store that the whole organization reads from and writes to under the same policy.

Wire the workflow to preview and the live frontend

A versioning workflow that editors cannot see is a workflow they will not trust. The final piece is closing the loop between the staged, scheduled state and what the reader will actually experience, without abandoning the headless architecture that made the stack composable in the first place. Editors need to see the future, before it ships, in the real layout.

Sanity's Presentation Tool and Visual Editing render the live frontend inside the Studio and let an editor click an element on the page to jump straight to the field that controls it, while still editing structured content behind the scenes. Applied to versioning, this means an editor can preview a scheduled release as the site will look after it publishes, not as a raw document tree. The Live Content API streams content changes in real time, so collaborative edits and preview states update without manual refreshes, which is what makes multi-person release review actually workable rather than a series of stale screenshots.

This closes the reframe the introduction promised. Versioning stops being a defensive log you consult after something breaks and becomes a forward-looking workflow: model content so history is meaningful, query any past state on demand, bundle changes into atomic releases, gate and schedule them under real permissions, and preview the result before it goes live. Built this way on a platform that treats history and releases as primitives, the held breath before a big publish turns into a reviewed, reversible, scheduled event, which is the entire point of doing versioning properly.

Versioning and release capabilities across headless platforms

FeatureSanityContentfulStrapiStoryblok
Point-in-time historyEvery document revision is queryable data in Content Lake; retrieve any past state with a GROQ query rather than reconstruct it.Entry-level version history with restore in the web app; history is browsed in the UI rather than queried across documents.Draft and publish plus history via the Content History feature on paid tiers; depth varies by self-hosted configuration.Version history per story with restore; accessed through the editor UI rather than a general query interface.
Atomic multi-document releasesContent Releases bundle edits across many documents and publish them at one instant; multiple releases can run in flight concurrently.Releases group entries for coordinated publish; a mature feature, though release automation hooks differ from Functions.No native cross-document release bundle; coordinated publishing is typically scripted against the API or built in-house.Release feature groups changes for scheduled publish; concurrency across overlapping releases is more limited.
SchedulingContent Releases schedule to a specific time and flip the whole bundle atomically, so coordinated launches need no manual click.Scheduled publishing on individual entries and via releases; well supported across plans.Scheduling available via the publish workflow and plugins; often requires configuration on self-hosted instances.Native scheduled publishing per story and release; a core part of the editorial workflow.
Approval and permissionsRoles & Permissions gate who can create, approve, or publish a release; Studio Workspaces separate environments and brands.Roles and workflow tooling with tasks; granular approval flows can depend on higher plan tiers.Role-based access control configurable in the admin; approval workflows generally built by the team.Roles and a workflow app cover approval stages; configuration lives in the space settings.
Structured diffsPortable Text stores rich text as structured nodes, so a diff shows changed marks and annotations, not tag soup.Rich text stored as structured JSON; entry diffs are viewable in the UI though granularity varies by field type.Field-level diffs depend on field type and the editor blocks configured; rich text portability varies.Version comparison in the editor; richtext diffing surfaced through the UI rather than a portable format.
Preview of future statePresentation Tool and Visual Editing render a scheduled release as the live site will look, with click-to-field editing.Live Preview shows draft content in context; Visual Editing capabilities are delivered via a separate SDK setup.Preview requires wiring a preview route in your frontend; visual, click-to-edit preview is not built in.Visual editor with real-time preview is a core strength; release-aware future-state preview depends on setup.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.