Generated banner for Sanity Studio Upgrade
Generated logo for Sanity Studio Upgrade

Sanity Studio Upgrade

SanityOfficialPublished by Sanity

  • Skill
  • Workflow
  • Software development
  • MIT
  • No issues found

From the author

Produces a tailored Sanity Studio upgrade plan by inspecting the repository's installed versions, config, and source, then reporting only the breaking changes that actually apply. Covers Studio v3 and later; v2 projects are identified and redirected, not planned. Use this skill whenever someone wants to upgrade, migrate, or modernize a Sanity Studio across one or more major versions from v3 onward, asks what will break if they bump the `sanity` package, asks why their Studio broke after an upgrade, or asks how far behind their Studio is. Triggers on "upgrade sanity studio", "migrate our studio to v6", "bump sanity", "what breaks if we upgrade", "our studio is on an old version", "sanity upgrade plan", "is our studio out of date", "we are several majors behind". DO NOT load for upgrading non-Sanity dependencies, for Content Lake `apiVersion` questions, for content or schema migrations that change documents, or for setting up a new Sanity project.
— the description by Sanity

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The Sanity Studio upgrade planner provides a focused, repository-specific migration path for Sanity Studio projects from v3 onwards. Rather than providing a generic changelog, it inspects the current project's lockfile, configuration, and source code to identify only the breaking changes that are actually relevant. It emphasizes verification over speculation, requiring live registry lookups for versions and strictly forbidding any automated modification of the user's codebase.

What it does

  • Detects the current Sanity Studio version and identifies if the project is out of scope (v2).
  • Calculates the upgrade span from the current installed version to the latest release.
  • Identifies applicable breaking changes based on the project's specific configuration and dependencies.
  • Provides a sequence of recommended intermediate upgrade stops to isolate potential issues.
  • Assesses plugin compatibility and identifies potential dependency conflicts.
  • Generates a comprehensive report in the repository root for team review.

When to use it

  • When you need to upgrade or modernize a Sanity Studio project from v3 or later.
  • When you want to understand the specific breaking changes involved in a version bump.
  • When you need to determine how far behind your current Studio version is.
  • When you are troubleshooting a broken Studio environment following a previous upgrade attempt.

Advantages

  • Reduces noise by filtering out breaking changes that do not apply to your specific configuration.
  • Prevents common upgrade pitfalls by verifying Node.js version floors and dependency coupling.
  • Provides a safe, read-only analysis that does not modify your repository files.
  • Offers a structured, step-by-step plan that is easy to share with team members.

Usage examples

“upgrade sanity studio”
The agent inspects the local lockfile, determines the current version, and generates a SANITY-UPGRADE-PLAN.md file detailing the necessary steps to reach the latest version.
“what breaks if we upgrade our sanity studio?”
The agent analyzes the current configuration and dependencies to list only the specific breaking changes that will impact the project, excluding irrelevant ones.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/sanity-io/sanity-studio-upgrade/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip sanity-studio-upgrade.zip -d .agents/skills

Documentation by Sanity

Sanity Studio upgrade planner

Generate an upgrade plan for the Sanity Studio in the current repository.

The plan's value is subtraction. A list of every breaking change between two versions already exists; it is called the changelog, and it is not useful to someone mid-upgrade. This skill produces the much shorter list of changes that apply to this repository, plus the questions only a human can answer.

Three rules that determine whether the output is trustworthy

1. Never state a version number from memory. Look it up, and confirm it exists.

Package versions change weekly. Every version in the report must come from either the repository's lockfile (for current state) or a live registry query (for targets). If a lookup is not possible, say so in the report rather than guessing. A plan that names a stale "latest" is worse than no plan, because the reader will act on it.

There is a second, worse version of this failure: a version number that was never published at all, arrived at by assuming one package tracks another's numbering. That one fails at install. Any version you are about to describe as "lockstep" or "matches core" needs an existence check first.

The same applies to API names. An export, an import subpath, or a function signature recalled from memory is a guess, and it is a guess the reader will paste into their editor. Read it from the package's README or exports map, or mark it with an inline // VERIFY: comment at the snippet itself. See section 6 of plugins.md and section 8 of report-template.md.

2. Read and report. Never modify.

Inspect files, run read-only shell commands, query registries. Do not edit package.json, config files, or source. The reader decides what to change; a planner that edits code will be turned off. If the user explicitly asks you to perform the upgrade afterward, that is a separate task they have consented to.

Where the line actually falls, because a vague version of this rule makes an agent hesitate over commands that are fine:

  • Never: writing to any file in the repository, installing into it, or mutating a lockfile. pnpm install, npm install, npm ci and npx without --no-install are all out.

  • Fine: reading files, and network reads such as npm view, a changelog fetch, or npm pack into a temporary directory outside the repository that you then delete.

  • Recommended to the reader, not run by you: pnpm install --lockfile-only is how a dependency prediction gets settled. It belongs in the report as a command for them to run, with what to expect from it. Do not run it yourself, because it rewrites their lockfile.

The one place this skill writes anything is unpacking a tarball to inspect type declarations, and section 6 of plugins.md shows how to keep that outside the project.

3. Separate what you verified from what you inferred.

Every finding lands in one of three buckets: confirmed applicable (you found the condition in the repo), needs a human answer (you cannot determine it from code), or not applicable (omit it entirely). Never pad the report with items you could not check. An honest short report beats a comprehensive-looking one.

One consequence is easy to miss and worth stating outright: a read-only planner has no dependency resolver, so it cannot know what a tree will contain after a change. Predictions about deduplication belong in the report as expectations with the verification command attached, never as findings. See package-coupling.md.

The verification rules, in one place

These are referred to by number throughout the reference files, so they are stated once here rather than restated in each. Everything above is R1 to R4; the rest are the specific ways those go wrong.

RuleWhere it bites
R1Never state a version from memory. Take current state from the lockfile, targets from a live registry query.version-lookup.md §1
R2Read and report. Nothing in the repository changes, nothing is installed, no lockfile is mutated.Above, and plugins.md §6
R3Separate verified from inferred. Never assert what a dependency tree will contain after a change; a planner has no resolver.package-coupling.md §6, plugins.md §5
R4Confirm every recommended version was actually published before it goes in the report.version-lookup.md §3
R5Every "X requires Y" names the manifest it was read from, at the version being installed at that stop. Requirements do not carry backwards across majors.version-lookup.md §6
R6Evaluate a version range, never read it. Anything with a ||, a <, or two comparators in one clause goes through semver.satisfies first.version-lookup.md §6
R7A name in an export list is not a working export. Check its declared type and any @deprecated tag; never means removed.plugins.md §6

R6 and R7 exist because both have already produced confident, wrong, specific advice: a Node version reported as unsupported when it was fine, and a required code edit filed under "already satisfied". They are cheap to run and they fail silently when skipped.

Scope: v3 and later

This skill plans upgrades from Studio v3 onward. Studio v2 is out of scope and is a hard stop.

The reason is not that v2 is hard. It is that v2 to v3 is a rewrite of the Studio's configuration and plugin layer rather than a dependency upgrade: the config format changed, the "parts" system that v2 plugins and overrides were built on no longer exists, packages were consolidated, and much of the v2-era plugin ecosystem was never ported. A generated plan for that boundary would be a list of confident-sounding specifics that this skill cannot verify, handed to someone about to spend weeks on the work. That is the worst possible output.

If you detect v2, stop and report rather than planning. Step 1 covers detection and what to say. Do not write a plan file for a v2 project.

Procedure

Step 1: Establish ground truth, and check the version floor

Read references/detect.md and follow it. It lists the files to read, the commands to run, and the facts to record.

The single most common mistake is reading versions from package.json. Caret ranges are not installed versions. Get the resolved version from the lockfile.

The second most common mistake is reading them from the wrong lockfile. A repository can contain several installs that disagree, so decide which tree the plan is for, take every version fact from that tree, and name it in the report header.

If there is no Sanity Studio in this repository, say so and stop.

Then check the floor before doing anything else. If the resolved sanity major is below 3, or there is no sanity package and the project depends on v2-era packages such as @sanity/base or @sanity/desk-tool, or there is a sanity.json and no sanity.config.*, this is a v2 project. Stop there and tell the reader:

  1. What you detected, and the evidence for it

  2. That this skill covers v3 and later, and why v2 to v3 is a different kind of project: a rewrite of the configuration and plugin layer, not a version bump

  3. That the right starting point is the official v2 to v3 migration guide, which they should find on the Sanity docs site rather than take from you second-hand

  4. That every v2-era plugin needs its own compatibility check, since many were never ported

  5. That once they are on v3, running this skill again will plan the rest of the span, which is a much smaller job

Be useful about it. A hard stop that explains itself and points somewhere is a good answer; a refusal is not. But do not soften it into a partial plan, and do not estimate the v2 to v3 work.

If both sanity.json and sanity.config.* are present, treat the resolved sanity version as authoritative. A leftover sanity.json in an otherwise v3+ project is worth mentioning as cleanup, not a reason to stop.

Step 2: Determine the span

You need two versions: from (resolved, installed) and to (the target).

Read references/version-lookup.md for how to query current versions and changelogs authoritatively.

The target is latest. Recommend it and give the reasoning. Landing short means doing the whole job and still being behind, and more importantly, current plugin versions track current core, so an older core with current plugins often produces a worse dependency tree than the newest release does. Report the target's publish date and a one-line fallback so the reader can apply constraints you cannot see, but that is disclosure, not a hedge, and it does not change the recommendation. Deviate only for a constraint the reader has actually stated, and then give the pin a review date.

State the span explicitly in the report. A v3 project and a v5.31 project are completely different jobs and the reader needs to know which one they have.

Step 3: Load only the boundaries you cross

Read references/boundaries.md first. It is a short index: the coverage bounds, the two-edge fetch rule, and a table saying which boundary files the span needs.

Then read only those files. Boundary content is split one file per major, so a span that crosses one boundary loads one file rather than all of them. A project on 5.31 crossing into v6 reads the index, boundary.v6.md and deprecations.md, and nothing else: the v4 and v5 files would cost context and invite findings that do not apply.

Each boundary.v*.md holds the crossing into that major plus the undeclared changes inside that major's line, because a span that crosses a boundary lands somewhere in the line above it and needs both.

Then read references/package-coupling.md. It applies to every span.

Step 4: Fetch what the references do not cover

references/boundaries.md declares two bounds on its within-line coverage, and both need respecting.

Fetch the changelog for any part of the span that falls above the upper bound or below the lower bound. A project starting below the lower bound needs two fetches: from its current version up to that bound, and from the upper bound to the target. The curated middle is used as-is.

The lower bound matters as much as the upper one, and it fails more quietly. Without it you will trust the file for a range it never examined and report that nothing there applies, which reads as a verified finding and is actually an unexamined gap. Never state that a range is clean unless it sits inside the declared bounds or you fetched it yourself and got real content back.

That last clause is not padding. Below the lower bound the upstream changelog does not exist at all, so the fetch returns almost nothing, and an empty fetch is not a clean range. boundaries.md says which range this affects and what to report instead. Reporting silence as "nothing applies" there is the single easiest way for this skill to produce a confident lie.

Pay particular attention to breaking changes that shipped in minor releases without being labelled as breaking. Those are the ones that surprise people, because nobody reads minor release notes across forty releases. The references list known examples; assume more exist.

Step 5: Test applicability

For each candidate change, find the condition in the repository. If you cannot determine it from code, it goes in the human questions section, not in the findings.

Examples of the difference:

  • "Custom auth providers now replace built-ins instead of appending" → grep the config for an auth block. Present or absent. Determinable.

  • "Default search strategy changed, results will shift" → whether that matters depends on how editors search. Not determinable. Ask.

Prefer conditions you can actually evaluate. grep -r "data-slate" is a real test. "Do you have complex custom components?" is not.

Step 6: Resolve the dependency graph

Apply references/package-coupling.md. This is where most upgrades actually break, and it is the part a changelog cannot tell you, because it depends on which packages this project happens to depend on directly.

The rule that matters most: for any @sanity/* package the project depends on directly, check what version the target sanity release depends on and match that, rather than taking the package's own latest. Taking latest for a shared package like @sanity/ui puts two majors of it in one dependency tree, which produces duplicate-context errors and unstyled components that look like unrelated bugs.

Step 6b: Assess plugins

Read references/plugins.md and follow it whenever the project has any plugin, which is nearly always.

Plugins block more upgrades than the Studio itself does, so they get their own pass and their own section in the report. Three things to carry into it: verify every plugin's compatibility rather than assuming it from who publishes it; let ownership decide the path once something is incompatible, not the verdict; and verify the API shape of any snippet you write against the package, separately from verifying its version, because a compatible version can still have renamed the export you are about to recommend.

Step 7: Write the report

Follow references/report-template.md exactly. Write it to SANITY-UPGRADE-PLAN.md in the repository root unless the user names a different path.

Write a file rather than only printing to the terminal. The plan usually needs to reach people who are not at this keyboard: a lead, a reviewer, a support engineer. A file can go into a pull request or be pasted into a thread; terminal output cannot.

Judgment calls worth making explicitly

Recommend a sequence, not a single jump. Crossing several majors in one commit makes failures impossible to attribute. Suggest intermediate stops at the last release of each major, and say why: it isolates each set of changes.

But a stop is only real if it is reachable. Each one needs its plugin versions resolved, because a plugin peering only recent majors can make an intermediate stop impossible to install. If you cannot resolve a stop, label it provisional rather than presenting the sequence as validated. Fewer verified stops beat more hypothetical ones.

When you drop a stop, name what it costs. Skipping one is often right: a boundary whose only applicable change is a Node floor is not worth a commit of its own, and a stop that cannot be reached without a forced duplicate major is worse than no stop. But the resulting hop then crosses two boundaries at once, and that is the exact thing the sequence exists to prevent. A plan that shows the evidence for skipping and stays silent on the consequence reads as though the trade were free.

So say both halves: why the stop was dropped, and that a failure in the combined hop could originate at either boundary, so debugging it means bisecting rather than reading the plan. One sentence covers it. The reader can accept the trade once they can see it.

Size the work honestly. If the project has two files importing @sanity/ui and no custom auth, say the upgrade is small. Inflating scope to look thorough wastes the reader's week. Equally, if the project imports from sanity/_internal or has a large custom component surface, say the upgrade is substantial rather than producing a plan that makes hard work look easy.

Make the plan survive being skimmed. It will be. A multi-boundary plan runs to several thousand words, and the person reading it is about to spend a week on the work, not an afternoon studying the document. So the top of the plan carries the whole thing in one screen, everything below it is reference to be read as the work reaches it, and findings are grouped by what the reader has to do rather than by which release the change came from. The specific failure to design against: a change that fails silently, sitting in a uniform table between two cosmetic ones, at identical visual weight. Length is not the enemy; flat weighting is.

Write for the engineer who has to do the work, and describe the tree rather than the team. This plan is usually read by someone at the customer, often someone who did not make the decisions that produced the current state. Keep the evidence, the specificity and the severity exactly as strong as the facts support: softening a real blocker or a real cost is a disservice, and an engineer can tell. What to drop is the implied verdict. "The deploy pipeline builds from the caret ranges rather than the lockfile" and "whoever set this up got it wrong" contain the same finding, and only one of them gets acted on. Reserve "you" for what to do next. Stay inside the upgrade, too: an unused dependency is not an upgrade finding, and a plan that drifts into general code review spends the reader's attention on things that do not gate the work.

Name what you could not see. Monorepos hide things: a tsconfig.json that extends a base file outside the repository, a workspace root that hoists React, a CI config in another directory. If a fact you needed was out of reach, put it in the human questions section by name. "I could not read moduleResolution because your tsconfig extends a file outside this repo" is useful. Silently omitting it is not.

Do not promise roadmaps. If asked whether a deprecated API will return or be removed, describe its current documented state and stop. Never speculate about future releases.

Reference files

FileRead when
references/detect.mdAlways, at step 1
references/version-lookup.mdAlways, at step 2 and step 4
references/boundaries.mdAlways, at step 3. Index: coverage bounds and which boundary files to read
references/boundary.v4.mdStep 3, if the span crosses into or sits in v4
references/boundary.v5.mdStep 3, if the span crosses into or sits in v5
references/boundary.v6.mdStep 3, if the span crosses into or sits in v6
references/deprecations.mdStep 3, always. Short, and not tied to a boundary
references/package-coupling.mdAlways, at step 6
references/plugins.mdStep 6b, whenever the project has plugins
references/report-template.mdStep 7

Files

references/plugins.md

Plugins

Plugins are the most common thing that actually blocks a Studio upgrade. The Studio itself usually bumps cleanly; a plugin whose peer range does not accept the target major stops the whole job.

Contents

  1. Verify every plugin, assume nothing

  2. Enumerate what is installed

  3. Determine ownership

  4. Assess compatibility

  5. Plugin versions for intermediate stops

  6. Verify the API shape before writing a snippet

  7. Path A: plugins in the sanity-io org

  8. Path B: plugins outside it

  9. What never to recommend

  10. Drafting a useful issue

  11. Reporting


1. Verify every plugin, assume nothing

Both directions of assumption produce wrong answers in front of a customer.

Assuming a Sanity-published plugin is compatible is wrong because a plugin can lag a Studio major by days or weeks. Sanity plugins are expected to have a compatible release, and usually do, but "expected" is not "verified." Asserting it is the same freshness failure this skill exists to prevent.

Assuming a third-party plugin is stale is wrong because plenty are actively maintained and current. Telling a customer that a well-maintained plugin is abandoned is both inaccurate and unfair to whoever maintains it, and some of those maintainers are active in the Sanity community.

So keep two questions separate:

  • Is it compatible? Answered by the peer range and publish data. Same method for every plugin regardless of who owns it.

  • Who can fix it if it isn't? Answered by ownership. This determines the path, not the verdict.

2. Enumerate what is installed

Collect from every source, because the config and the manifest do not always agree:

  • The plugins array in sanity.config.*, including any workspace-specific arrays

  • Every dependency matching sanity-plugin-* or the @sanity/* scope

  • Anything imported in the config that registers Studio behavior but is named neither way

Check devDependencies too. Plugins land there by accident and still get bundled at build time, which is worth flagging on its own.

Note any package that does not resolve from the public registry. That means a private or internal registry, and nobody outside the customer's own organization can assess or fix it. Report those by name and hand them to their team rather than guessing.

3. Determine ownership

Use the repository field on the registry manifest. Do not use the package name, and do not maintain a list of plugin names.

npm view <plugin> repository --json

A repository.url containing sanity-io/ means the package lives in a Sanity-owned GitHub repository, most often the plugin monorepo at github.com/sanity-io/plugins, where the manifest also carries a directory pointing at the specific package.

Why the package name is the wrong test. sanity-plugin-media is an unscoped name, and it lives in sanity-io/plugins with Sanity.io as its author. Classifying by the @sanity/ scope would report a plugin Sanity publishes as unmaintained third-party software. The repository field gets it right, costs one field on a query you already make, and never goes stale.

What being in the monorepo does and does not mean. The monorepo's own README describes it as the home for Studio plugins maintained by Sanity staff and the community, and it holds dozens of packages across both. So presence there means releases are coordinated and someone has commit access. It is not a published support tier. There is currently no per-plugin maintenance-status metadata, so do not state or imply a support level. Describe what you can verify: where it lives, when it last published, and whether its peer range accepts the target.

4. Assess compatibility

Run the same checks for every plugin.

npm view <plugin>@latest version peerDependencies dependencies --json
npm view <plugin> time.modified --json
CheckWhat it tells you
Does every peer range resolve, not just sanity?The most commonly botched check. A plugin declaring sanity ^3.0.0 can be simultaneously unsatisfiable on @sanity/ui ^1.2.2, styled-components ^5.3.8, and react ^18.2.0. Reading only the sanity peer produces a "compatible" verdict for a plugin that will not install. Read the whole peerDependencies object and check each entry against what the tree will actually provide.
If not, does any published version work?npm view <plugin> versions --json, then inspect recent ones. Customers pin and forget; support may already exist.
Does the plugin's own @sanity/* dependency cross a major relative to what the target sanity depends on?Two majors of a shared package in one tree causes duplicate-context errors and unstyled components. A patch release of a plugin can do this, so check the specific version you plan to install, not just latest. When it happens, look for an earlier patch that still matches core, and pin to it.
Are the plugin's peerDependencies all present in the project's package.json?Missing peers often work by accident through hoisting and stop working once the tree is rebuilt.
How long since the last publish?Evidence, not a verdict. Report the date and let the reader judge.
How far is the installed version behind the latest?A large gap often means the plugin's own breaking changes are in scope too, not just the Studio's.

A plugin already violating its own peers today belongs in the report's "needs attention before you start" section (section 3 of report-template.md), not only in the upgrade blockers. If a plugin only installs because peer resolution is relaxed, that is true before anyone touches a version, and it means the plugin has to leave at the first stop rather than travelling as far as its sanity range suggests.

Read the plugin's own release notes for the versions being crossed. A plugin major has its own migration requirements, and those belong in the plan alongside the Studio's.

5. Plugin versions for intermediate stops

Section 4 answers whether a plugin has a version that works with the target. A multi-stop sequence needs that answer for every stop along the way, and this is the step most easily skipped, because a plugin's current version usually peers only recent Studio majors.

The trap is concrete. A plugin whose latest peers ^5 || ^6.0.0-0 tells you nothing about whether any published version peers ^4. If the plan recommends stopping at the last v4 release, every plugin needs a version whose range accepts v4, or that stop is not reachable at all and the sequence is wrong.

Resolve it by walking versions backwards from the newest:

npm view <plugin> versions --json
npm view <plugin>@<version> peerDependencies --json

The selection criterion has two halves, and taking only the first is the most common way a stop goes wrong. You are looking for the newest version that satisfies both:

  1. Its sanity peer range accepts that stop's major.

  2. Its own dependencies on shared packages match what that stop's core depends on. @sanity/ui and @sanity/icons are the ones that matter, because they carry theme and context.

Check the second half explicitly, per candidate version:

npm view sanity@<stop-version> dependencies --json | grep -E '@sanity/(ui|icons)'
npm view <plugin>@<candidate> dependencies peerDependencies --json | grep -E '@sanity/(ui|icons)'

Half one alone points at latest almost every time, and latest tracks current core, not the core at an intermediate stop. Following it puts two majors of a theming package in the tree at exactly the stop you were trying to isolate.

When the two halves disagree, walk back further. A plugin's shared-package dependency usually crosses a major at a patch release, so there is normally a slightly older patch on the same feature line that keeps everything you need and still matches core. Find it rather than accepting the duplicate.

Note the asymmetry that keeps this tractable: a plugin peering a range that spans several majors can stay on one version across all of those stops. Only the boundaries where a range ends need a different version. So the work is proportional to the number of range boundaries, not stops times plugins.

Two rules that make a stop real

A preparatory stop must pin. A stop described as "clean things up without changing sanity" that leaves the existing caret ranges in place is not a stop, it is a fresh resolve against whatever the registry happens to hold that day. In practice it resolves to the newest patch of every plugin, which is frequently a worse dependency tree than the carefully chosen set at the next stop. Pin a preparatory stop to the versions the current lockfile already resolves, which is what "don't change anything yet" actually means. This is doubly important when the plan elsewhere identifies unpinned ranges as the cause of deployment drift; keeping them in stop zero contradicts that finding.

Check a candidate version's peers against the rest of that stop's tree, not just against core. A plugin can satisfy the stop's sanity version and still fail against a sibling. The failure looks like unmet peer @sanity/mutator@^3.36.4: found 4.22.1, where the 4.x copy arrived through a different plugin in the same set. Compatibility is a property of the whole set at that stop, not of each plugin against core individually.

Never accept a duplicate major without first proving no version avoids it. This is the rule that catches the mistake above after the fact, and it is worth stating separately because the failure has a distinctive shape: the plan takes latest for a plugin, notices the duplicate, and then argues for it, with language like "accept the temporary duplicate, keep this stop short, and do not ship it to editors for long." That paragraph is a tell. Reasoning appears exactly where the version walk should have been, because no walk happened.

A worked example of what the walk finds. Suppose the stop is the last v5 release, whose core depends on @sanity/ui ^3.2.0, and a plugin is needed at its v5 feature line because that is where a data migration ships:

  • The plugin's latest, 5.2.4, depends on @sanity/ui ^4.0.7. Taking it means two majors of @sanity/ui at this stop.

  • Walking back: @sanity/ui 4 first appears at 5.1.26. Every version from 5.0.3 to 5.1.25 stays on @sanity/ui 3.x.

  • 5.1.2 depends on @sanity/ui ^3.2.0, which is exactly what the stop's core depends on, and it still carries the v5 migration.

So the duplicate was never necessary, and the correct pin is the older patch. Move the plugin to latest at the next stop, where core has moved to @sanity/ui 4 and the two agree again.

If a stop genuinely has no version that satisfies both halves, say so, name the versions you checked and what each depends on, and only then accept the duplicate. And do not schedule anything that depends on a working Studio UI at that stop: a data migration verified through a Studio with two theme contexts is a migration verified in the one state this whole file exists to avoid.

Three outcomes, and the plan has to say which one applies for each stop:

  • A version exists. Name it in the sequence, at that stop. Not "check at execution time."

  • No published version accepts that major. The stop is unreachable with that plugin installed. The options are to skip the stop and cross two majors at once, remove the plugin for the intermediate hop and reinstall it at the end, or drop it entirely. Recommend one and say why.

  • You could not determine it. Mark that stop provisional and state exactly what needs checking before anyone commits to the sequence.

Deferring this to "at execution time" reads as thoroughness but is the plan handing back its hardest unanswered question, dressed as a next step. A sequence presented as validated when its middle stop was never resolved is worse than one that admits the gap, because the reader will schedule work against it.

If resolving every plugin at every stop is genuinely too much work for the span, reduce the number of stops rather than leaving them unresolved. Fewer, verified stops beat more, hypothetical ones.

Never assert what the dependency tree will contain

You cannot compute a deduplicated tree from declared dependencies, so do not claim one.

This is a hard limit, not a matter of care. Reading each plugin's declared @sanity/ui version tells you nothing about what its own transitive dependencies pull, and nothing about the other shared packages (@sanity/icons, @sanity/client, @sanity/util, @sanity/types) that the same set drags along. A set assembled so that every plugin declares the same @sanity/ui major routinely resolves to two majors of it anyway, plus doubled copies of three or four other packages. Only a resolver knows.

A read-only planner has no resolver. So the honest form is an expectation plus the command that settles it:

Expected at this stop: a single @sanity/ui 3.5.x. Unverified - dependency resolution is the only thing that can confirm it. Check before committing to this stop:

pnpm install --lockfile-only && pnpm why @sanity/ui @sanity/icons @sanity/client

Write it that way at every stop, and in the final dependency section. Phrases like "a clean tree is achievable" or "expect exactly one version each" read as findings and are predictions; if one turns out wrong, every other verified claim in the document loses credibility with it.

The same limit applies to peer satisfaction across a whole set. You can confirm that each declared range could be satisfied; you cannot confirm the set resolves. Say which of the two you did.

When the reader has already run a resolve and shared the output, that is evidence and you can report it as fact, attributed to the run.

6. Verify the API shape before writing a snippet

A plugin's version being compatible says nothing about its API being what you remember. Plugin majors rename exports, move helpers behind subpaths, and change signatures, and those are exactly the details a reader copies straight out of the plan into their editor.

So: any snippet you write that names an import path, an export, a signature, or a subpath is either read from the package or marked unverified at the snippet itself. Two checks, both cheap and both read-only:

# Does the subpath exist, and what does it point at?
npm view <plugin>@<version> exports --json

# What does the plugin document as the current API?
# Fetch the README for the specific version, not the repo's main branch,
# which may be ahead of what the reader will install.
npm view <plugin>@<version> readme
A name in an export list is not a working export

The presence of a symbol is not evidence that it works, and confusing the two is worse than not checking at all, because it produces a verified-sounding "no change needed" on something that is broken.

Sanity's house deprecation pattern across packages is to keep the name and type it never, so the reader gets a targeted error instead of a bare module-resolution failure. @sanity/ui v4 does it for removed props. @sanity/icons v5 does it for root-entry icon exports from 5.1.0 on. A grep for the name in export {...} or in dist/index.d.ts finds it in every one of those cases, and it is removed in every one of those cases.

So the check is three questions, not one:

  1. Is the name exported? Necessary, not sufficient.

  2. What is its declared type? never means removed. Treat it as absent.

  3. Does it carry @deprecated? The tag text usually names the replacement, which is the fastest route to the correct snippet.

This is the one check in the skill that writes files, so unpack into a temporary directory and clean it up. Never run npm pack or tar in the repository: it drops a tarball and an extracted package/ into the project, which contradicts the read-and-report rule and leaves the reader with untracked files to explain.

d=$(mktemp -d)
npm pack --silent --pack-destination "$d" <pkg>@<version> >/dev/null && tar xzf "$d"/*.tgz -C "$d"
grep -B4 "declare const <Name>" "$d"/package/dist/index.d.ts   # type plus any @deprecated block
rm -rf "$d"

If unpacking is not possible in the environment, the registry manifest still answers most of the question on its own (npm view <pkg>@<version> exports --json), and the README covers the rest. Say which of the two you used.

The failure this prevents is specific and expensive: a symbol that resolves, types as never, is silently undefined at runtime, and gets written into the report's "already satisfied" section as needing no edit. That moves a silent failure into the one place the reader has been told not to look.

If both confirm the shape, write the snippet plainly. If either is unavailable or ambiguous, keep the snippet and mark it inline, per section 8 of report-template.md:

// VERIFY: export name and signature - check the plugin README and its
// package.json `exports` map before running this
import {someHelper} from 'some-plugin/subpath'

A parenthetical in the surrounding paragraph does not count. The snippet leaves the document; the paragraph stays behind.

The internationalization plugins specifically

sanity-plugin-internationalized-array and @sanity/document-internationalization are among the most widely installed plugins across Sanity's customer base, so a wrong snippet for either one is wrong in many places at once. Two shapes come up in almost every plan that touches them, and both need verifying rather than recalling:

  • The field-level filter helper. Plans commonly prescribe an internationalizedArrayLanguageFilter-style export as a drop-in filterField. Confirm the export name, whether it is a factory or a plain function, and what arguments it takes, from the README of the version being installed.

  • The migration subpath. Plans commonly prescribe a migration imported from a /migrations subpath. Confirm that subpath is in the exports map for that version, and that the named migration is exported from it.

Neither check needs the project installed. If you skip them, the snippet gets the inline marker.

7. Path A: plugins in the sanity-io org

If a compatible version exists, which is the expected case: recommend the specific version, note any configuration changes from the plugin's own release notes, and move on. Keep it brief; this is routine.

If no compatible version exists, treat it as unexpected and handle it differently from a third-party gap.

Do not send the customer to open a GitHub issue about Sanity's own plugin. That reads as being passed back to yourself, and it is the wrong queue.

Instead, report it as a Sanity-side gap with everything needed to escalate:

  • Plugin name and the resolved installed version

  • Its current latest version and publish date

  • Its declared peer sanity range

  • The target Studio version the range does not accept

  • What breaks without it, in one line

Then flag it explicitly for the Sanity contact on the account. In the report, put it under a heading that makes the ownership clear, for example "Blocked on a Sanity-maintained plugin", so nobody mistakes it for the customer's homework.

Give the customer honest interim options and no more: hold the upgrade until the plugin ships support, or proceed without that plugin if the functionality is genuinely replaceable. Say plainly that Sanity is being asked, rather than implying a date.

8. Path B: plugins outside the sanity-io org

Report what you found as evidence rather than as a judgment: last publish date, peer range, whether the repository is archived, and whether an issue or pull request already references the target version. Let the reader draw the conclusion; a plugin that last published two years ago speaks for itself without being called abandoned.

Then work this ladder in order. It is ordered by what Sanity can stand behind.

1. Check whether a newer version already works. Projects pin, then forget they pinned. This resolves the problem more often than expected and costs one query.

2. Look for existing work on it. An open issue or pull request referencing the target major means someone is already on it. Link it, and suggest adding their use case as a comment rather than opening a duplicate. A maintainer seeing real users blocked is more likely to ship.

3. Open a specific, useful issue. See section 10. This is a good use of the skill's time, because it holds the exact versions and errors a maintainer needs.

4. Ask whether the plugin is still needed. Underrated, and often the answer. Studio core absorbs plugin functionality over releases, so something added several majors ago may be solving a problem the Studio now solves natively. Check the current Studio feature set against what the plugin actually does before assuming it has to be replaced. Deleting a dependency is the cheapest possible fix.

5. Replace or reimplement. A maintained alternative, or the narrow piece they actually use rebuilt as a custom input. Scope this against what the project uses, not against the plugin's full feature set, which is usually much larger.

6. Defer, explicitly and temporarily. Hold the Studio at its current major until the plugin catches up, or vendor the plugin into the project. If the reader chooses this, it needs an exit condition and an owner, or it becomes permanent. Say that.

9. What never to recommend

The skill does not recommend forking a third party's plugin, patch-package, or installing with relaxed peer resolution as a solution.

The reasoning matters more than the rule. These are all decisions to take on permanent maintenance of someone else's code, or to suppress the mechanism that would otherwise warn you about incompatibility. A customer's team may legitimately choose one, but it should be their choice made with open eyes, not a recommendation carrying Sanity's name.

If the customer's team raises one, describe the tradeoff accurately and move on. Specifically on relaxed peer resolution: note that it silences the main signal that would tell them a plugin is incompatible with a future core, which matters most on exactly the upgrade they are doing.

10. Drafting a useful issue

When the ladder reaches step 3, draft it. A vague issue gets ignored; a precise one often gets a release. Include:

  • Title: plugin name, "support for Sanity Studio v<target major>"

  • The plugin version currently installed and the target Studio version

  • The plugin's declared peer sanity range, quoted

  • The exact install or runtime error, quoted

  • Whether the plugin appears to work despite the peer range, if that was tested. Maintainers often only need to widen a range.

  • What the project uses the plugin for, in one sentence. This is what tells a maintainer whether the fix is small.

  • An offer to test a prerelease

Hand the draft to the reader to post. Do not post it for them.

11. Reporting

Give plugins their own section in the report. A table for the routine cases, then a subsection per blocked plugin.

PluginInstalledLatestPeer sanityOwnerVerdict
............sanity-io / third party / privatecompatible, bump to X / blocked / needs a decision

For the owner column, state what the repository field showed rather than a support level.

For each blocked plugin, a short subsection: what it is used for, why it blocks, which path applies, and the recommended next step from that path. One paragraph each. If a first-party plugin is blocking, make sure that lands somewhere an SA will see it and forward it.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Node.js

The author says: Requires network access to the npm registry and sanity.io docs for version and changelog lookups

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Sanity
Publisher
Sanity (Official)
Source
sanity-io/agent-toolkit · skills/sanity-studio-upgrade · commit 3ed3a14
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Sanity.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download sanity-studio-upgrade.zip