@sanity/migrate
Programmatic toolkit for running dry-run and live content migrations against Sanity.io datasets.
Repository Health
Technical Analysis
@sanity/migrate is the library that powers content migrations for Sanity.io projects. It gives you the building blocks to define a migration, stream every document in a dataset (from the live API or a dataset export archive), transform it with typed mutation helpers, and apply the resulting changes as batched, concurrency-limited transactions.
The package is consumed purely as an API — there are no CLI commands in this repository. The user-facing sanity migrations create|list|run commands in the Sanity CLI call into this library’s public surface (defineMigration, run, dryRun, and the mutation builders) to do the actual work. Everything is built around Node/web streams and async iterators so a migration can walk millions of documents without holding the whole dataset in memory.
Because migrations can be run against a live production dataset, the library treats mutation outcomes carefully: transactions are assigned client-side IDs up front, non-4xx failures are distinguished from outright API rejections, and an UnknownTransactionOutcomeError is thrown with a direct link into the dataset’s transaction log whenever a request’s outcome couldn’t be confirmed, rather than silently assuming success or failure.
What You Get
defineMigration()for declaring a migration with a title, optional document-type/GROQ filter targeting, and either per-node transform functions or a raw async-iterable mutation generator- A dry-run engine (
dryRun) that streams a dataset export through disk-buffered temp files and reports the mutations a migration would produce without writing anything - A live runner (
run) that batches mutations up to the API’s body-size limit, applies them with bounded concurrency, and tracks per-transaction progress via anonProgresscallback - Typed mutation and patch builders (
create,createOrReplace,createIfNotExists,patch,del,set,unset,insert,inc, and more) for constructing document changes without hand-writing raw Sanity mutation payloads - Multiple document sources — a live
fromExportEndpointstreamed from the Sanity Export HTTP API,fromExportArchivefor reading a local dataset export.tar.gz, andfromDocumentsfor in-memory arrays — behind one common streaming interface - A restricted context client (
createContextClient) exposed to migration scripts, plus a filtered-documents accessor so a migration can look up related documents by ID while it runs
Common Use Cases
- Renaming, restructuring, or backfilling a field across every document of a given type in a Sanity dataset
- Dry-running a large schema change against a downloaded dataset export before ever touching the live project
- Writing a one-off data-cleanup script for a content team migrating off a legacy schema
- Building custom tooling (beyond the Sanity CLI) that needs to enumerate and mutate a dataset’s documents at scale with controlled concurrency and safe failure handling
Under The Hood
Architecture
The package is organized as a small stack the CONTRIBUTING guide documents explicitly: a public API surface under src/_exports/index.ts (defineMigration, run, dryRun, the mutation builders), a runner layer (src/runner/) that implements dry-run and live execution, mutation batching, and progress reporting, and a sources/utilities layer (src/sources/, src/fetch-utils/, src/it-utils/, src/fs-webstream/, src/tar-webstream/) that supplies the document streams the runner consumes. dryRun and run share the same collectMigrationMutations core, so a migration produces identical mutations whether it’s being previewed or actually applied — the only difference is whether the runner writes them. Large exports are buffered through a temp file (bufferThroughFile) behind an AbortController so the buffering stops as soon as mutation collection finishes, keeping memory bounded regardless of dataset size.
Tech Stack
TypeScript compiled to ESM via SWC with a separate pkg-utils pass for type declarations, targeting Node 22+. It depends on @sanity/client, @sanity/mutate, @sanity/types, and @sanity/util for talking to the Sanity API and sharing document/mutation types, groq-js for evaluating GROQ filters against streamed documents, p-map for bounded-concurrency mutation dispatch, and fast-fifo/debug for internal queuing and diagnostic logging. Linting runs through Sanity’s shared ESLint config, formatting through oxfmt, and knip checks for unused dependencies — all wired into CI alongside build, typecheck, and test on both Ubuntu and Windows.
Code Quality
Fifteen Vitest test files cover the mutation builders, tar/fs stream utilities, async-iterator helpers, and the runner’s transaction-batching and normalization logic. The CONTRIBUTING guide codifies explicit conventions — ES Module imports with .js extensions, no any (use unknown and narrow), and errors that are thrown with a cause chain rather than swallowed, which the codebase follows consistently (e.g. UnknownTransactionOutcomeError wraps the original fetch failure and lists exactly which transaction IDs are in an unconfirmed state). CI enforces build, formatting, lint, tsc --noEmit, knip, and the test suite on every PR across two operating systems.
API Design
defineMigration() is a pure type-inference identity function — it exists only to give editors autocompletion and catch shape errors, with zero runtime behavior, which keeps the authoring experience lightweight. run and dryRun expose near-identical signatures so switching between preview and live execution is a one-line change. Rather than hide the hard problem of at-most-once mutation delivery, the library surfaces it directly: UnknownTransactionOutcomeError names the specific unconfirmed transaction IDs and links straight to the dataset’s transaction-log query for each one, plus a warning that non-idempotent operations may double-apply on retry — an honest, actionable failure mode instead of a silent one.