Plugins API
Extend the capabilities of your studio using plugins
The plugin configuration property accepts an array of plugin definitions. The plugin configuration accepts most of the same properties as the workspace config API, the notable exceptions being dataset, projectId, auth, and theme.
Protip
While entirely optional, wrapping your plugin configuration object with the definePlugin() helper function (exported from sanity) will make most editors show helpful type information and autocomplete suggestions even if you're not using TypeScript!
The helper can also accept a function that returns a plugin configuration object. This allows you to make the plugin configurable, by passing arguments to the plugin configuration factory. For example: definePlugin((options) => ({ ... })), where options are configuration options you want to use to allow end users to modify the plugin.
import { definePlugin } from 'sanity'
export const previewUrlPlugin = definePlugin({
name: 'preview-url-plugin',
document: {
productionUrl: async (prev, { document }) => {
const slug = document.slug?.current
return slug ? `https://some-custom-url.xyz/${slug}` : prev
},
},
})Properties
Requiredname
string
Unique identifier for the plugin.
document
object | DocumentPluginOptions
Accepts custom components for document actions and badges, as well as a custom
productionUrlresolver and default configuration for new documents. Read more about the document API.form
object | SanityFormConfig
Extensions and customizations to the Studio forms. Accepts configurations for image and file asset sources as well as custom components to override the default Studio rendering. Read more about the form API.
plugins
array | PluginOptions[]
Studio plugins: takes an array of plugin declarations that can be called with or without a configuration object. Read more about plugins.
tools
array | Tool[]
Studio tools: takes an array of tool declarations that can be called with or without a configuration object. Read more about the tool API.
schema
object | SchemaPluginOptions
Schema definition: takes an array of
typesand an optional array oftemplates(initial value templates). While defining a schema is not required, there are few things inside the Studio that work without one. Read more about the schema API.studio
object | StudioComponentsPluginOptions
Accepts a
componentsobject which will let you override the default rendering of certain bits of the studio UI. Read more about studio components.onUncaughtError
function
Accepts a callback function containing an
error: Errorand anerrorInfo: ErrorInfoarguments. Commonly used by plugin developers to implement customized error handling, external logging, and telemetry.
A complete list of plugin options is available in the type reference documentation.
Handling errors
As of sanity@v6.4.0, plugin and tool authors get a new @beta hook, useStudioErrorHandler(), for delegating unrecoverable request errors to the Studio's shared error UI instead of reinventing it. The developer handles what they can locally (inline errors, toasts, fallbacks) and delegates what they can't.
It exposes two methods, both delegating to the same native Studio Error UI.
attempt(fn, options?): for retryable requests
Wraps a request so the dialog's Try again re-invokes it. Resolves with the first successful attempt; the fn receives the attempt number.
import {useEffect, useState} from 'react'
import {useStudioErrorHandler, useClient} from 'sanity'
function MemberList() {
const client = useClient({apiVersion: '2025-02-19'})
const {attempt} = useStudioErrorHandler()
const [members, setMembers] = useState<Member[]>()
useEffect(() => {
attempt(
// Each invocation issues a *fresh* request — this is what
// "Try again" re-runs.
(attemptNumber) => {
if (attemptNumber > 1) console.debug(`retry #${attemptNumber}`)
return client.fetch<Member[]>('*[_type == "member"]')
},
{retryable: true},
).then(setMembers)
}, [attempt, client])
// ...render `members`
}Gotcha
The fn must create the request when called. Retry works by invoking it again. Passing an already-started promise means "Try again" just re-awaits the same settled rejection and the dialog reappears.
// ✓ fresh request per attempt
attempt(() => client.fetch(query), {retryable: true})
// ✗ request already fired; retry does nothing useful
const promise = client.fetch(query)
attempt(() => promise, {retryable: true})attempt() also accepts an observable, but this observable must complete. Don't pass client.listen() or a long-lived Subject, as that would never resolve.
// ✓ single-shot request observable
await attempt(() => client.observable.fetch(query), {retryable: true})handle(err): for non-retryable rejections
A drop-in promise rejection handler: pass it directly to .catch(handle). Claimable errors surface the Studio error dialog and leave the promise pending; unclaimable errors are re-thrown to the next .catch(), so downstream handlers still see caller-domain errors like validation failures, permission errors, and 404s.
Because handle receives an error rather than a re-runnable request, the dialog can't offer Try again. Use attempt() when the request is safe to re-run.
client
.create(doc)
.catch(handle)
.catch((err) => {
// Caller-domain errors (validation, permissions, 404, ...)
})