Type something to search...
How to Migrate Content Between Sanity Datasets

How to Migrate Content Between Sanity Datasets

To migrate content between Sanity datasets, export the source dataset with npx sanity datasets export production production.tar.gz and import the archive into the target with npx sanity datasets import production.tar.gz --dataset staging. The archive contains every document and asset file, works on every plan and can target a dataset in the same project or another one. Add --replace to overwrite documents that already exist, or --missing to skip them. On Enterprise plans, npx sanity datasets copy production staging clones a dataset inside Sanity's infrastructure instead, including document history.

Moving content between datasets comes up all the time: refreshing a development dataset from production, seeding a new environment, testing a migration on a copy, splitting a project or moving content to a new project entirely. Each method has different rules about what gets copied, what gets overwritten and what it costs. This guide covers export and import in detail, Cloud Clone, partial copies, moving between projects, scripted copies, verifying the result and the pitfalls that catch people out.

Choosing a Method

MethodPlansTarget datasetIncludes historyBest for
Export and importAllNew or existingNoMost copies, cross-project moves
Cloud Clone (datasets copy)EnterpriseMust be newYes, unless skippedLarge datasets, CI refreshes
Export with --types, import with --missingAllExistingNoSeeding selected documents
Script with @sanity/clientAllExistingNoSelective, transformed copies
Dataset alias (hot swap)Enterprisen/an/aSwitching what apps read, without copying

If you're unsure why you need a second dataset in the first place, read Sanity datasets and environments first. Most content changes belong in production as drafts or releases.

Before You Start

A few minutes of preparation prevents most problems:

  1. Log in to the CLI with npx sanity login, or have a token ready for CI.
  2. Check your Node.js version: the current CLI needs Node.js 22.12 or later.
  3. Confirm the target dataset exists, or create it with npx sanity datasets create staging.
  4. Pause webhooks and functions that fire on the target dataset. An import is thousands of writes, and each can trigger a rebuild or an outbound request.
  5. Tell editors if you're importing into a dataset people work in.
  6. Back up the target if it has content you care about. An import with --replace overwrites without asking.

Method 1: Export and Import

Export the Source

Run this from your Studio folder, or pass --project from anywhere:

npx sanity datasets export production ./exports/production.tar.gz

The archive is a gzipped tarball with one folder inside:

production-export-2026-11-04t09-15-02-000z/
├── data.ndjson    # every document, one JSON object per line
├── assets.json    # asset document IDs and the URLs they came from
├── images/        # image binaries
└── files/         # file binaries

Useful export flags:

  • --no-drafts: export only published documents. Good for a clean staging copy.
  • --types post,author,category: export only some document types.
  • --no-assets: skip binaries and strip asset references. The result isn't a complete copy.
  • --raw: keep documents exactly as stored, without rewriting asset references.
  • --mode cursor: page through the dataset instead of streaming it in one request. Faster on large datasets, but it can miss edits made during the export.

Exports count against your API request quota, though documents are streamed to keep the cost down. Every asset document in the dataset is downloaded, referenced or not, so media-heavy datasets produce large archives.

Import Into the Target

npx sanity datasets import ./exports/production.tar.gz --dataset staging

By default the import fails if any document ID already exists in the target. Choose how to handle that:

FlagBehaviourUse when
noneFails on the first existing IDImporting into an empty dataset
--replaceOverwrites documents with the same IDRefreshing a development dataset
--missingSkips documents that already existAdding seed content without touching existing work
--allow-failing-assetsSkips assets that can't be fetchedOld exports with broken asset URLs
--allow-system-documentsImports documents whose IDs start with _.You know you need them

Two behaviours matter for refreshes. First, import never deletes: documents in the target that aren't in the archive stay put. For an exact mirror, import into a fresh, empty dataset rather than over an old one. Second, references are imported as weak references and strengthened once everything is in place, so the order of documents in the file doesn't matter.

Refresh Development From Production

Putting it together, a repeatable refresh looks like this:

# 1. Export published production content
npx sanity datasets export production ./exports/prod.tar.gz --no-drafts --overwrite

# 2. Recreate development so the copy is exact
npx sanity datasets delete development
npx sanity datasets create development --visibility public

# 3. Import
npx sanity datasets import ./exports/prod.tar.gz --dataset development

Deleting and recreating the dataset removes its history and any test content, so only do it for a disposable dataset. If you'd rather keep the dataset, use --replace and accept that documents deleted in production will linger in development.

Method 2: Cloud Clone on Enterprise

Cloud Clone copies a dataset inside Sanity's infrastructure, so nothing passes through your machine. It's part of Advanced Dataset Management on Enterprise plans.

# Copy production into a new dataset
npx sanity datasets copy production feature-checkout

# Faster: leave out document history
npx sanity datasets copy production feature-checkout --skip-history

# Leave out content releases so they don't count toward your release quota
npx sanity datasets copy production feature-checkout --skip-content-releases

# Start in the background and check back later
npx sanity datasets copy production feature-checkout --detach
npx sanity datasets copy --attach <jobId>

Things to know:

  • The target must not exist. If it does, the command fails with Target dataset "<name>" already exists. To refresh, delete the old copy first or use a new name.
  • Timestamps are kept: _createdAt and _updatedAt match the source.
  • History comes along unless you pass --skip-history, which can make large copies much faster.
  • It can take a while: large datasets with many assets can take hours, which is why --detach exists.

The same operation is available over HTTP with PUT /v2021-06-07/projects/:projectId/datasets/:dataset/copy, which returns a job ID you can poll at /jobs/:jobId. That's handy for CI pipelines that spin up a fresh dataset per pull request.

Method 3: Copying Selected Documents

Sometimes you only need a few types, such as settings and navigation documents built during development. Export just those and import without overwriting anything:

npx sanity datasets export development ./exports/seed.tar.gz --types siteSettings,navigation
npx sanity datasets import ./exports/seed.tar.gz --dataset production --missing

The catch is references. If navigation references page documents that aren't in the export, those references point at nothing in the target. Either include every referenced type, or make sure the referenced documents already exist there with the same IDs. Assets used by the exported documents are included automatically.

If you need to change documents that already exist in production, don't import over them. Write a migration instead, as described in writing content migration scripts, so editors' work isn't overwritten.

Moving Content to Another Project

Export and import work across projects. Pass --project to point each command at the right place:

npx sanity datasets export production ./exports/old.tar.gz --project oldproj1
npx sanity datasets import ./exports/old.tar.gz --project newproj2 --dataset production

Assets are uploaded to the new project, and asset references in the documents are rewritten to match. A few things don't travel with the content:

  • Schema and Studio code: copy the Studio repository and update projectId.
  • Project settings: CORS origins, webhooks, tokens and members are per project.
  • History: exports contain current documents only.
  • Legacy schedules: the old Scheduling API stored schedules outside the dataset, so they aren't exported.

Method 4: Scripted Copies With the Client

For full control, such as copying documents that match a GROQ filter or transforming them on the way, use @sanity/client with two clients. Run it with npx sanity exec or plain Node.js:

// scripts/copy-posts.ts
import { createClient } from '@sanity/client'

const base = {
  projectId: 'abc123xy',
  apiVersion: '2026-10-01',
  useCdn: false,
  token: process.env.SANITY_API_WRITE_TOKEN,
}

const source = createClient({ ...base, dataset: 'production' })
const target = createClient({ ...base, dataset: 'staging' })

const docs = await source.fetch<Array<Record<string, unknown> & { _id: string }>>(
  `*[_type in ["post", "author", "category"]]`,
  {},
  { perspective: 'published' },
)

const BATCH_SIZE = 100

for (let i = 0; i < docs.length; i += BATCH_SIZE) {
  const tx = target.transaction()
  for (const doc of docs.slice(i, i + BATCH_SIZE)) {
    const { _rev, _updatedAt, ...rest } = doc
    tx.createOrReplace(rest as { _id: string; _type: string })
  }
  await tx.commit({ visibility: 'async' })
  console.log(`Copied ${Math.min(i + BATCH_SIZE, docs.length)} of ${docs.length}`)
}

This approach has limits you need to handle yourself:

  • Assets don't come along: image references point at asset documents in the source dataset. Writing them to the target fails with a references non-existent document error unless those assets exist there. Use export and import when images are involved, or upload the assets to the target first.
  • References must resolve: copy referenced documents in the same transaction or earlier, or mark references as weak.
  • Rate limits apply: mutations are limited to 25 requests per second per IP, and the client doesn't retry rate-limited mutations. Batch documents into transactions and keep each request under the 4 MB mutation body limit.
  • Validation doesn't run: the Content Lake accepts any valid JSON, so check your transformations carefully.

The client API itself is covered in how to use Sanity's Mutation API.

Switching Datasets With Aliases

On Enterprise plans, a dataset alias points at a real dataset, and your front end can query through it by prefixing the alias name with a tilde, as in dataset: '~production'. Prepare content in a copy, then relink the alias. Leave the tilde out when you create or link it:

npx sanity datasets alias create production production-2026-11
npx sanity datasets alias link production production-2026-12

Aliases are read-only. Studios, mutations, listeners, webhooks, functions, dataset-scoped roles and the Live Content API all need the real dataset name. Treat each swap as a deployment, and reserve it for recovery or breaking migrations rather than routine content changes.

Verifying the Copy

Don't assume an import worked because it finished. Compare document counts by type in both datasets:

npx sanity documents query 'count(*[!(_id in path("drafts.**"))])' --dataset production
npx sanity documents query 'count(*[!(_id in path("drafts.**"))])' --dataset staging

For a per-type breakdown, run this query in Vision against each dataset:

{
  "posts": count(*[_type == "post"]),
  "authors": count(*[_type == "author"]),
  "images": count(*[_type == "sanity.imageAsset"])
}

Then open a few documents in the target Studio, check that images load and references resolve, and run npx sanity documents validate --dataset staging to catch documents that don't match the current schema.

Common Pitfalls

  • Webhook storms: imports trigger webhooks and functions on the target. Disable them first, especially build hooks.
  • Changed _updatedAt: documents with references get a new _updatedAt when their references are strengthened. Don't use it as a publish date; see common Sanity mistakes.
  • Relative asset paths: importing a bare data.ndjson with relative file:// asset paths fails. Import the tarball instead, or use absolute paths.
  • Stale development data: a copy made months ago tests nothing. Automate the refresh.
  • Importing over editors' work: --replace on production silently overwrites. Use --missing or a migration.
  • Exports as backups: exports are useful snapshots, but they aren't a backup strategy on their own. See Sanity backup and restore.

FAQ: Migrating Sanity Datasets

Yes. Export the dataset with npx sanity datasets export and import the archive with npx sanity datasets import. This works on every plan. The faster cloud copy command, sanity datasets copy, is part of Advanced Dataset Management on Enterprise plans.

Yes, by default. The archive includes drafts, published documents and the binary files for every image and file asset. Pass --no-drafts to leave drafts out or --no-assets to skip the binaries.

No. An import only creates or, with --replace, overwrites documents. Documents already in the target that aren't in the archive remain. For an exact copy, import into a new, empty dataset.

Not with export and import, because the archive only contains the current documents. Cloud Clone on Enterprise copies history by default unless you pass --skip-history.

Export from the old project with --project set to its ID, then import into the new one with --project and --dataset. Assets are uploaded to the new project, but schema code, CORS origins, webhooks, tokens and members need to be set up separately.

By default, an import stops if any document ID is already in the target dataset. Re-run it with --replace to overwrite those documents or --missing to skip them and import only new ones.


Conclusion

For most projects, export and import is the right way to move content between Sanity datasets. It works on every plan, carries assets with it, works across projects, and its --replace, --missing and --types flags cover full refreshes, selective seeding and everything in between. Enterprise teams can use Cloud Clone for faster copies with history, and aliases to switch datasets without copying at all.

Whichever method you use, prepare first and verify afterwards. Pause webhooks, know whether you're overwriting or skipping, keep production content flowing down rather than up, and check counts, images and references in the target before anyone relies on it.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper