Studio

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

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

  • Requirednamestring

    Unique identifier for the plugin

  • documentobject | DocumentPluginOptions

    Accepts custom components for document actions and badges, as well as a custom productionUrl resolver and default configuration for new documents. Read more about the document API.

  • formobject | SanityFormConfig

    Extensions / 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.

  • pluginsarray | PluginOptions[]

    Studio plugins - takes an array of plugin declarations that can be called with or without a configuration object. Read more about plugins.

  • toolsarray | 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.

  • schemaobject | SchemaPluginOptions

    Schema definition - takes an array of types and an optional array of templates (initial value templates). While defining a schema is not required, there are few things inside the studio that works without one. Read more about the schema API.

  • studioobject | StudioComponentPluginOptions

    Accepts a components object which will let you override the default rendering of certain bits of the studio UI. Read more about studio components.

  • titlestring

    Human-readable name for the plugin

  • onUncaughtErrorfunction

    Accepts a callback function containing an error: Error and and errorInfo: ErrorInfo arguments. Commonly used by plugin developers to implement customized error handling, external logging, and telemetry.

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`
}

⚠️ 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})

Was this page helpful?