
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
| Method | Plans | Target dataset | Includes history | Best for |
|---|---|---|---|---|
| Export and import | All | New or existing | No | Most copies, cross-project moves |
Cloud Clone (datasets copy) | Enterprise | Must be new | Yes, unless skipped | Large datasets, CI refreshes |
Export with --types, import with --missing | All | Existing | No | Seeding selected documents |
Script with @sanity/client | All | Existing | No | Selective, transformed copies |
| Dataset alias (hot swap) | Enterprise | n/a | n/a | Switching 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:
- Log in to the CLI with
npx sanity login, or have a token ready for CI. - Check your Node.js version: the current CLI needs Node.js 22.12 or later.
- Confirm the target dataset exists, or create it with
npx sanity datasets create staging. - 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.
- Tell editors if you're importing into a dataset people work in.
- Back up the target if it has content you care about. An import with
--replaceoverwrites 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:
| Flag | Behaviour | Use when |
|---|---|---|
| none | Fails on the first existing ID | Importing into an empty dataset |
--replace | Overwrites documents with the same ID | Refreshing a development dataset |
--missing | Skips documents that already exist | Adding seed content without touching existing work |
--allow-failing-assets | Skips assets that can't be fetched | Old exports with broken asset URLs |
--allow-system-documents | Imports 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:
_createdAtand_updatedAtmatch 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
--detachexists.
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 documenterror 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_updatedAtwhen their references are strengthened. Don't use it as a publish date; see common Sanity mistakes. - Relative asset paths: importing a bare
data.ndjsonwith relativefile://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:
--replaceon production silently overwrites. Use--missingor 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.


