Field labels replace the previous content mapping approach with a direct, visible connection between what you write and where it goes in Studio.
When you set a content type on a Canvas document, field labels appear inline as purple headings that correspond to fields in your schema. You can see and adjust the structure of your document as you write, without switching to a separate panel.
Structure is now part of the document. Content mapping previously lived in a side panel you had to open to check the state of your document. Field labels sit inline, so structure is visible as you write.
Content-to-field assignment works differently. The background AI process that continuously mapped content is gone. Field labels can be applied in two ways: manually using the = slash command, or automatically via AI labeling that you trigger when you’re ready. After AI labeling runs, you can always review and adjust the results.
Inclusion replaces content/context toggling. Instead of marking blocks as “content” or “context,” you now toggle individual blocks between “included in Studio” and “excluded” using an eye icon in the margin. Excluded blocks stay in the document and will not be sent to Studio.
Delivering to Studio is an explicit action. Previously, connecting a Canvas document to Studio started a continuous sync. Now you first select a content type, then choose “Create new Studio document” or “Update Studio document” when you’re ready. Setting a content type alone doesn’t create anything in Studio.
Studio documents are never locked. Content mapping blocked Studio editing while a Canvas document was linked. With field labels, Studio documents can always be freely edited. When you push an update from Canvas, it flags any fields that have changed in Studio since your last update so you can review conflicts before overwriting.
Field labels can be applied to an entire document or a selected range. AI reads your content against the selected content type, sorts it according to the schema, and applies labels for you to review.
This is especially useful when bringing existing content into Canvas from tools like Google Docs or Notion and structuring it for Studio quickly.
For finer control, use the = slash command to assign a label to any block manually. You can also select text and pick “Label this content” from the context toolbar to label a specific section.
Within any field label, toggle individual blocks between included and excluded using the eye icon. Only included content flows to Studio when you create or update a document.
Excluded blocks are useful for keeping editorial notes, content briefs, reviewer comments, or alternative drafts inline without cluttering the Studio document. You can draft multiple variations within a field label and toggle which one to include before sending to Studio.
Open the content type panel and select “Create new Studio document” to send labeled content as a new draft. If your team uses Content Releases, you can create the document directly into an existing release.
Once created, the Studio document stays connected to the Canvas document. Continue refining in Canvas and push updates when ready. If fields have been edited in Studio since your last update, Canvas flags the conflicts before anything is overwritten.
The writing experience (editor, formatting, slash commands, Markdown shortcuts), AI assistance (ghostwrite, show options, rewrite), notes (context, fact, style, inspiration), and real-time collaboration all remain unchanged. Field labels re-imagine how structure is surfaced and how content moves from Canvas to Studio.
deploy_studio tool: Deploys a hosted Sanity Studio bound to an MCP-managed schema. Given an appHost, the tool provisions or redeploys a Studio at https://<appHost>.sanity.studio/ that renders the managed schema and is available in Sanity Dashboard.deploy_schema are now owned and resolved by the MCP server directly, rather than relying on a Studio codebase to publish them. get_schema, list_workspace_schemas, and the document tools read from this managed source first, falling back to Studio-deployed and legacy schemas when no managed schema exists for a workspace.Enterprise organizations managing large teams on Sanity can now use User Attributes to create parameterized roles that adapt to each user automatically. Instead of creating a separate custom role for every combination of location, department, or language, you define a small number of roles that resolve dynamically based on who the user is.
User Attributes are key-value pairs attached to a user's profile within an organization (ex: location="london", department="editorial", language="french"). Attributes come from two sources:
When both sources provide the same attribute, the Sanity value takes precedence. Removing the override reveals the SAML value again.
Attributes can be referenced in content resource filters using the user::attributes() function. Instead of creating a separate role for each branch office, define a single role with a filter like:
_type == "post" && branch == user::attributes().branch
Organizations managing dozens of locations, markets, or business units have been forced to create a separate custom role for every permutation of access. User Attributes reduces this complexity. Your roles become more powerful because they adapt to each user's context rather than requiring a static definition for every combination.
Attributes also support temporary reassignments. An administrator can override a user's location attribute in Sanity without modifying the identity provider. When the override is removed, the SAML value takes effect again on the next login.
string, integer, number, boolean, and array variants of each (except boolean).This release adds two new React hooks for working with organization metadata, expands the parameter options on the projects store, and moves the SDK to a single shared Sanity instance.
Use useOrganization to get metadata for a single organization. Pass includeMembers: true to include members, or includeFeatures: true to include features.
function OrganizationName({organizationId}: {organizationId: string}) {
const organization = useOrganization({organizationId})
return
# {organization.name}
}
const organizationWithMembers = useOrganization({organizationId, includeMembers: true})
const organizationWithFeatures = useOrganization({organizationId, includeFeatures: true})
Use useOrganizations to get metadata for every organization the current user can access.
const organizations = useOrganizations()
return (
{organizations.map((organization) => (
{organization.name}
))}
)
useOrganizations accepts the same options, plus includeImplicitMemberships.
const organizationsWithMembers = useOrganizations({includeMembers: true})
const organizationsWithFeatures = useOrganizations({includeFeatures: true})
const organizationsIncludingImplicit = useOrganizations({includeImplicitMemberships: true})
Both hooks support suspense. The release also exports new public types: Organization, OrganizationBase, OrganizationMember, and OrganizationsOptions.
useProjectsThe useProjects hook now accepts includeFeatures, onlyExplicitMembership, and organizationId as typed options. Use organizationId to scope results to a single organization, onlyExplicitMembership to exclude implicit memberships, and includeFeatures to include feature data on each project.
This release also exports new public types: Project, ProjectBase, ProjectMember, ProjectMemberRole, ProjectMetadata, and ProjectsOptions.
The SDK now uses one shared Sanity instance internally.
Blueprint stacks now support organization scope.
Some resource types, starting with Scheduled Functions, exist only at the organization level and cannot be deployed from a project-scoped stack. Organization-scoped stacks reach across multiple projects from a single blueprint, so they grow with your Sanity footprint. Project-scoped stacks remain a fine default when your work focuses on a single project, or when you don't have org-level permissions.
If you're starting fresh, use blueprints init rather than promoting. The new scope wizard asks which organization you're working in, whether to scope the new stack to that organization or to a single project, and, if you pick project scope, which project. Every choice is editable: back up to change an answer and the rest of the wizard updates to match.
Promoting a stack lifts an existing stack from a single project to its parent organization, so one stack can manage resources that span projects (including the new Scheduled Functions) without disturbing any of the project-scoped resources you already deploy.
Run blueprints promote on an existing project-scoped stack. The CLI performs a single atomic operation that switches the scope, preserves the original project ID so existing resources keep resolving, and updates your local .sanity/blueprint.config.json file.
npx sanity@latest blueprints promote
Promotion does not redeploy or rewrite anything that’s already deployed. Document functions, webhooks, CORS origins, and other project-scoped resources continue to work. The stack remembers its original project and resolves project-scoped resources against it.
After promoting, the same blueprint can declare both project-scoped and organization-scoped resources side by side. Add an explicit project attribute on a resource to target a different project from the same stack.
import {
defineBlueprint,
defineDocumentFunction,
defineScheduledFunction,
} from '@sanity/blueprints'
export default defineBlueprint({
resources: [
// Project-scoped resource, resolves against the stack’s default project
defineDocumentFunction({
name: 'on-publish',
event: {on: ['create', 'update']},
}),
// Organization-scoped resource, enabled by promotion
defineScheduledFunction({
name: 'daily-digest',
event: {expression: '0 9 * * *'},
}),
],
})
blueprints mint-deploy-token command.Promotion is one-way but risk-free. The command is also idempotent, so it’s always safe to retry. The promotion event will be listed in the logs for that stack; use the blueprints logs command to see your stack’s history.
Read Promote a stack to organization scope for a full walkthrough, or revisit the Blueprints introduction to learn how stacks fit into the bigger picture.
Scheduled Functions are now available. They run on a timer instead of in response to document events. Use them for daily digests, cache expiration, periodic content syncs, or any task that needs to happen on a fixed cadence.
Define a scheduled function with defineScheduledFunction from @sanity/blueprints. Schedules accept either a UNIX cron expression or an explicit object with minute, hour, dayOfWeek, month, and dayOfMonth fields.
import {defineBlueprint, defineScheduledFunction} from '@sanity/blueprints'
export default defineBlueprint({
resources: [
defineScheduledFunction({
name: 'expire-cache',
event: {expression: '0 0 * * *'},
}),
],
})
Inside the function, the scheduledEventHandler helper from @sanity/functions gives you typed access to the run context.
import {scheduledEventHandler} from '@sanity/functions'
export const handler = scheduledEventHandler(async ({context}) => {
const time = new Date().toLocaleTimeString()
console.log(`👋 Your Sanity Function was called at ${time}`)
})
Add a scheduled function with the CLI:
npx sanity@latest functions add --name expire-cache --type scheduled-function --language ts
Scheduled functions run independently of any single project, so they require an organization-scoped blueprint stack. If your stack is project-scoped, run npx sanity@latest blueprints promote before deploying.
Read the Create a Scheduled Function quickstart for a step-by-step walkthrough, or browse the Functions documentation.
The new ContentAgent class lets you combine your own client tools with Content Agent's server tools in a single agent loop. Server-executed tools (like Media Library access or content mutations) forward their results back to your client tools automatically, so the agent can act on both sides without you writing the orchestration. It is our implementation of the AI SDK Agent abstraction, preconfigured with safe defaults for the agent loop.
Migrate from streamText() to ContentAgent to pick this up:
Before:
streamText({ model: provider.agent(threadId) })
After:
new ContentAgent({ provider, threadId })
Act-and-answer: With this orchestration in place, the agent can call a tool and return its final answer in the same step, delivering faster results for action-oriented queries (for example, "update the issue and confirm it is done").
Content Agent can now query, organize, and update assets and collections in your Media Library. Ask the agent to find specific assets, batch-update metadata, or rearrange collections directly from the chat. Results link back to the relevant assets and collections so you can verify changes in context.
Media Library now supports folders, a nested hierarchy for organizing your assets, and shortcuts that let a single asset appear in more than one folder. Folders are powered by a new beta hierarchy primitive in Content Lake that you can also use programmatically via the API.
Create, update, and navigate folders using a familiar file-system UI. Multi-user updates are real-time: when a teammate creates, renames, or moves a folder, you see the change immediately.
The upload modal now offers a folder picker so you can choose where files land before they’re uploaded. The same option is available programmatically through a new parent query parameter on the upload endpoint.
You can also drag files from your operating system directly onto the Media Library window, the upload starts immediately and lands in the folder you’re currently viewing.
When you upload a file that already exists in the library, the modal flags the duplicate and offers a one-click action, either create a shortcut into your current folder if the asset lives elsewhere, or add the existing asset to your current folder if it isn’t filed yet.
Sometimes the same asset belongs in more than one place. Create a shortcut from the asset menu and the asset appears in the destination folder with a small badge to indicate it’s a shortcut.
You can also move an existing shortcut to a different folder from the asset sidebar. The original asset is unaffected, and only the shortcut’s home folder changes.
If you delete an asset or remove it from a folder in the app, the Media Library cleans up every shortcut to it automatically. Moving an asset between folders preserves its shortcuts.
Move and rename folders from the sidebar’s folder actions menu. Delete dialogs warn of any assets in use or when removing an asset will also clean up shortcuts in other folders.
Search has a new Location facet with folder operators, so you can scope a search to a specific folder, its descendants, or the entire library.
When you open the Media Library asset picker from a Sanity Studio, it now returns to the folder you were last in, no more re-navigating from the root every time.
Folders are built on a new hierarchy primitive in Content Lake, sanity.tree, sanity.directory, and sanity.symlink that you can drive from the standard /mutate and /query endpoints. See the folders guide for the full setup.
A new Status facet in the filter menu lets you filter assets by whether they have a draft. Combine it with the Location filter to scope a search to drafted assets within a specific folder, or use it on its own across the whole library.
This release includes various improvements and bug fixes.
For the complete changelog with all details, please visit: www.sanity.io/changelog/studio-NS4yMy4w
To upgrade to this version, run:
npm install sanity@latest
To initiate a new Sanity Studio project or learn more about upgrading, please refer to our comprehensive guide on Installing and Upgrading Sanity Studio.
| Author | Message | Commit |
|---|---|---|
| @jordanl17 | feat(core): make document action keys extensible via declaration merging (#12768) | eebdb17d97bba5a3ea793d20e7a1aa57f67f59c6 |
| @bjoerge | chore: replace pnpx with pnpm and add pkg-pr-new dependency (#12783) | 3d66c1fb7a43f35bddab609336c39804344cf4a9 |
| @bjoerge | chore(turbo): remove unused env var (#12781) | f8111fce700584f23572f5956d991c7a2e34ebda |
| @bjoerge | chore(ci): use pnpm whoami instead of npm whoami (#12780) | 2308d145b94a9484817253850ff929f65bf3d81f |
| @RitaDias | test: add tests for createCallbackResolver (#12779) | 5625f37c276b7717396cba145f5ebf4f98fd6650 |
| squiggler-app[bot] | fix(deps): update dependency @sanity/cli to ^6.5.0 (#12778) | f3d306c7bacfc0c2d95d5ff3cf8fd4efc2daf020 |
| @bjoerge | fix: restore workspace hidden property (#12775) | 8f4e6b0f4f1b761b240ec15d0314e3d54d388659 |
| @pedrobonamin | fix(core): add version into documentEvents observable (#12772) | b511ef939d0ae031505208d45f0f8f6e8ae5fb54 |
| @bjoerge | chore(ci): switch to package build for lefthook (#12767) | 838e3556fe1637c2da8a32fce6286f709fb87eae |
| @bjoerge | chore: remove pre-commit husky hook (#12766) | 752aaf67457c91d2a7c0564eb38bc528a8ddb161 |
| @bjoerge | chore: replace husky + lint staged with lefthook (#12755) | fcbdbdb66d51bc92507b1fd1131e2311a2d2a729 |
| @bjoerge | chore: upgrade pnpm to 11.0.0 (#12759) | 50a185bc76c2df32d112b9a75915f0cb2b37b2c2 |
| @bjoerge | chore(ci): drop node 20 from test matrix (#12760) | 8bc9911c100dac9cf679714075bff01d93af3be9 |
| squiggler-app[bot] | chore(deps): update dependency @tanstack/react-virtual to ^3.13.24 (#12657) | c4954c51a626b5dbd8ebd6bf0a399b3754d48e63 |
| @jordanl17 | chore: adding telemetry to track when feedback dialog is opened and closed (#12749) | 6b16652d7c67e8b10f05112f7978d93126cf7748 |
| @pedrobonamin | fix(core): reset calendar focused date when setting to current time (#12753) | ff8a7d492affa4aebd531e1ce0363940115461a9 |
| @pedrobonamin | chore(core): cleanup decision parameters schema (#12751) | aa8980b861c73a9f243e26eec71c0f0b18559d24 |
Agent Insights now includes optional telemetry to help us improve both Agent Context MCP, and also Agent Context Insights itself. You can choose to share metadata only (scores, sentiment, content gaps, token usage) or full conversation content with Sanity for model improvements. All telemetry sharing is disabled by default.
messages parameter in classifyConversation is now required.
This release fixes several issues including hidden workspace visibility, date picker calendar display, and improves CLI configuration handling.
init respects --project and --dataset flags for app templates.sanity init now defaults to TypeScript in unattended mode.Track and analyze your Agent Context conversations. A telemetry integration captures conversations as they happen, a scheduled Sanity Function runs AI classification to extract success scores, sentiment, and content gaps, and a new Studio dashboard gives you visibility into how your agent is performing.
Getting started takes a few minutes: update the package, wire up telemetry in your agent, and set up the classification function. The create-agent-with-sanity-context skill can show you – or check our docs.
This release includes behind-the-scenes stability improvements. No action is needed to update to the latest version of the MCP server.
This release improves reliability with automatic recovery from chunk-load errors, along with two fixes to login error handling in standalone apps.
The app now reloads automatically when a JavaScript chunk fails to load. This commonly happens after a new deploy invalidates cached assets, and previously left users on a broken screen until they refreshed manually. No code changes are required to opt in.
LoginError now stays synchronous in standalone apps, so error handling runs in the same tick as the failure.LoginError auto-logout flow guards against re-entry, preventing duplicate logout attempts when an error fires mid-logout.This month, the documentation brings improvements to some of our framework-specific guides, a big overhaul of the core visual editing documentation, and an assortment of platform updates to improve the user experience.
We’ve rewritten our core visual editing guides. For anyone using a supported framework like Next.js, this won’t change much. If, however, your aim is to set up visual editing on an unsupported framework, or just learn more about the underlying mechanisms, these new docs should help. There are now code-heavy guides on all major parts of the system, as well as an end-to-end example in Node.js and TypeScript.
Our existing Astro content now lives in the docs. You can find the new Astro section here, as well as a new visual editing guide.
Articles can now include tabbed content groups, letting you compare alternatives side-by-side and switch between them in place. Each tab supports rich text, code blocks, callouts, and images.
A new actions dropdown next to every article gives you one-click options to copy the article as Markdown, open it in ChatGPT or Claude for follow-up questions, and install the Sanity MCP server directly in Cursor or VS Code.
Property descriptions in API and reference tables now render through the modern Portable Text pipeline, fixing an assortment of rendering bugs in older table content.
A new Sanity Function automatically publishes generated reference documentation for many of our libraries, keeping API reference pages in sync with library releases without manual republish steps.
The Organization > Billing > Costs page in Manage has been overhauled with improved billing and usage details to help you organization admins better understand their billable usage or invoices.
It now displays:
This is available for all paid plans, including org-level enterprise customers. Enterprise customers won’t see “previous period” data until May. Displayed data does not take into account discounts, credits, or adjustments.
Webhook delivery retries have changed. Previously, deliveries that failed with a retryable error (HTTP 429 or 5xx) were retried with exponential backoff over a 30-minute window. Now, they are retried twice at 30-second intervals before being marked as failed.
This change was made to help prevent pending deliveries from accumulating behind a failing receiver.
This release includes various improvements and bug fixes.
For the complete changelog with all details, please visit: www.sanity.io/changelog/studio-NS4yMi4w
To upgrade to this version, run:
npm install sanity@latest
To initiate a new Sanity Studio project or learn more about upgrading, please refer to our comprehensive guide on Installing and Upgrading Sanity Studio.
| Author | Message | Commit |
|---|---|---|
| @EoinFalconer | feat(studio): add config option to disable ask-to-edit button (#12692) | 391d40357b514a46559927003462176463ba5665 |
| @EoinFalconer | fix(diff): deduplicate repeated inline diff segments in Portable Text (#12675) | 26c140f22db79c3e0f23f8b56627123427658e38 |
| @bjoerge | fix(ci): keep release-notes consistent for PR-less commits (#12752) | 21a31ef82447139d95a25d4597a269ca537d4247 |
| @bjoerge | fix(ci): handle commits without an associated PR (#12750) | 67682e55f6ff7c966d2c7fc8e2bdf74edb7f6448 |
| @pedrobonamin | chore(core): update invalid fields styles (#12002) | 713dd8c9cd0a69971110cccbb2b9a0bb466c4c77 |
| squiggler-app[bot] | chore(deps): update dependency @sanity/telemetry to v1 (#12664) | d4eb80ee6fe78ad70d8e022af4bc07d2f2369495 |
| squiggler-app[bot] | chore(deps): update dependency @sanity/document-internationalization to v6 (#12663) | c4b92e3bdaff83af921e53a270e35470a62996d4 |
| squiggler-app[bot] | chore(deps): update dependency @sanity/assist to v6 (#12662) | 937284298565d961949ed46bfee0df9c76764397 |
| @EoinFalconer | fix(studio): hide user menu on mobile in dashboard mode (#12684) | e51fee8a1ab7156ae8acdae168538d8d5462c441 |
| @annez | feat(telemetry): add Global Search Latency Measured event (#12709) | 317ae6b492c0261fc02cda398455e9cecdb94488 |
| squiggler-app[bot] | chore(deps): dedupe pnpm-lock.yaml (#12741) | c825f1f8ed57cb146b909a76e9d756930c94384a |
| @pedrobonamin | feat: enable vanilla-extract CSS (#12590) | c0fb87ff5ea41290df895fd45e3b0e90ce300bc1 |
| @pedrobonamin | fix(core): show json diffs for missing fields, skip _system field (#12744) | 57ebcca7be5578348a876dfa176a96b17e683f92 |
| @gu-stav | fix(feedback): associate labels with HTML form fields (#12746) | d08b8eea7ac0c2f068742bb95fa1037e23a45058 |
| @bjoerge | test(e2e): require matcher in expectError to avoid suppressing unrelated errors (#12745) | 6f1d6c2e8ffe25eaffca59cd79e1e01fc5ca45db |
| @RitaDias | fix: issue when reverting to revisions in live edits (#12729) | e0c829f35013ca75724234f01c2c5896b609b8c3 |
| @bjoerge | refactor: move store modules from _legacy directory to top-level store (#12735) | bfd3b141e648ffed03a211501c3fc9af85c6b960 |
| squiggler-app[bot] | chore(tests): generate dts tests 🤖 ✨ (#12742) | 39210533ec84595808c1c4eefbfee8255bc17e76 |
| @bjoerge | chore: switch to tsgo across the board (#12738) | 6b99ab91e042a9e2d4ecb142bd3a36e9ff644ed8 |
| @bjoerge | feat(sanity): warn on divergent auth configs for same project id (#12732) | 36b911d9f78cdcfc6358ed7768a846c802d0e96f |
| @Chrilleweb | fix(docs): code of conduct path in contributing file (#12740) | fc5f9fc2a9431fc97522783381d00b31485cb986 |
| @annez | feat(telemetry): add Document Initial Load Measured event (#12710) | 7110142050a13ede0f7beecc929a63076cdffdd5 |
| @EoinFalconer | fix(releases): add empty state for cardinality-one releases with no documents (#12687) | 379906f8c4c82de161514af20006ef2413027900 |
| @EoinFalconer | fix(form): maintain select button position with disableNew on image fields (#12683) | 91ebac8244cc0f92a36c32d3c7c046833da4d236 |
| @bjoerge | ci(workflows): drop fetch-depth: 0 from jobs that don't need history (#12736) | 0a1b5b3b17290488eb70b6b57b2470dcb6050ab5 |
| Copilot | fix(core): throw on missing projectId/dataset in getOperationStoreKey (#12609) | 583bccea9cb3c7e0d5a85feee9220e7030076705 |
| squiggler-app[bot] | chore(tests): generate dts tests 🤖 ✨ (#12734) | 7f09c2a71550ea13b35832f70939408291f50ca0 |
| @RitaDias | refactor: the menu items in viewContentReleases and ScheduledDraftsMenuItem show proper hovering (#12703) | 6ba4b9037281bbe149bf8ddec28fa4c0507baf0b |
| squiggler-app[bot] | chore(deps): update pnpm to v10.33.1 (#12660) | b45aa6e859bc0ad398adfd65ff978f83655d3f8a |
| @EoinFalconer | fix(e2e): stabilize custom release actions E2E test (#12694) | 4f5ee31672234ac765ccd1987b931a56ef9af38d |
| @RitaDias | fix: remove underline from openInNewTabIcon menu item for refs (#12724) | eb3ca24f04062ffc09415ff1dc8ce307956c6077 |
| @bjoerge | refactor: auth store (#12679) | 85df9439431563ffbd520d399139ffae4a700889 |
| @EoinFalconer | fix(e2e): bypass navbar pointer-event interception in reference autocomplete (#12717) | e354416bbeca16cf71ae866ba0ae1b5d2321abac |
| @EoinFalconer | fix(e2e): stabilize page.goto wrapper for Firefox CI load (#12712) | 164332d8ce8224ceb3c9a20284d06b6f077c0cc9 |
| @EoinFalconer | fix(ci): pass --shard through pnpm without literal -- separators (#12713) | ba7789b929c72ec73945f35b375556beddaa50c2 |
| @EoinFalconer | fix(releases): improve activity panel UX consistency (#12686) | 5b9cd5d70726fb8e557609e8a67bea082f52374e |
| @EoinFalconer | fix(form): prevent scroll jump when opening field overflow menu (#12629) | 1ce6320cbd124ca51d245e7739802f52db6db4fd |
| @EoinFalconer | fix(test): disable console intercept to prevent worker teardown races (#12716) | 032532d49baf19befc6bdaa810fd7ffcb1bc3517 |
This release brings improved workspace configuration warnings and a number of UX fixes.
Adds a new document.askToEdit.enabled config option to allow hiding the Ask to edit option which is displayed when a user lacks the required permissions to edit a document.
import {defineConfig} from 'sanity'
export default defineConfig({
// ...rest of config
document: {
askToEdit: {enabled: false},
},
})
disableNew: true on an image or file field moved the Select button from the right side to the left. The button now stays on the right consistently.