Editorial Workflows

Custom Studio integrations

Build custom Editorial Workflows controls inside Sanity Studio with the Studio adapter, including a verified Document Action example.

Prerelease

Use @sanity/workflow-studio when you need Editorial Workflows controls inside a custom Sanity Studio surface. It supplies a Studio-bound engine and live React hooks; you decide how the interface looks and where it appears.

Choose the Studio integration

  • Use the Studio plugin for ready-made document form, workflow, badge, and locking interfaces.
  • Use @sanity/workflow-studio for a custom Document Action, dialog, tool, pane, input, or other component rendered inside Studio.
  • Use @sanity/workflow-sdk for an App SDK application outside Studio.

Install the adapter

Install the Studio adapter and its workflow peers in your Studio project. Keep all Editorial Workflows packages on the same version.

Create a workflow-powered Document Action

A Document Action is a React component that adds a command to Studio’s document editor. This example finds the document’s active article-review instance, opens a confirmation dialog, reads the evaluated submit verdict, and fires the workflow action only when it is allowed.

The Document Action owns the button and dialog. The Studio adapter observes workflow state and creates the reactive session. The workflow engine evaluates the action and commits the Drafting → Review transition.

Loading...
import {gdrUri, type Engine} from '@sanity/workflow-engine'
import {
  useDocumentWorkflows,
  useWorkflowEngine,
  useWorkflowSession,
} from '@sanity/workflow-studio'
import {useMemo, useState} from 'react'
import {useClient, type DocumentActionComponent} from 'sanity'

const workflowResource = {type: 'dataset', id: 'your-project.workflows'} as const

export const SubmitForReviewAction: DocumentActionComponent = (props) => {
  const [open, setOpen] = useState(false)
  const engine = useWorkflowEngine({workflowResource, tag: 'production'})
  const client = useClient({apiVersion: '2026-07-01'})
  const {projectId, dataset} = client.config()
  if (!projectId || !dataset) throw new Error('Studio client is missing its project or dataset')

  const document = useMemo(
    () => gdrUri({scheme: 'dataset', projectId, dataset, documentId: props.id}),
    [dataset, projectId, props.id],
  )
  const {instances, loading, unreadable, error} = useDocumentWorkflows({engine, document})
  const instance = instances?.find((candidate) => candidate.definition === 'article-review')
  const unavailable = error ?? (unreadable.length > 0 ? unreadable[0] : undefined)

  return {
    label: 'Submit for review',
    disabled: loading || unavailable !== undefined || instance === undefined,
    title:
      unavailable !== undefined
        ? 'Workflow state could not be read'
        : instance === undefined
          ? 'No active article review workflow'
          : undefined,
    onHandle: () => setOpen(true),
    dialog:
      open && instance
        ? {
            type: 'dialog',
            header: 'Submit for review',
            onClose: () => setOpen(false),
            content: (
              <SubmitDialog
                engine={engine}
                instanceId={instance._id}
                onClose={() => setOpen(false)}
              />
            ),
          }
        : false,
  }
}

function SubmitDialog({
  engine,
  instanceId,
  onClose,
}: {
  engine: Engine
  instanceId: string
  onClose: () => void
}) {
  const [submitting, setSubmitting] = useState(false)
  const [submitError, setSubmitError] = useState<unknown>()
  const {evaluation, ready, invalid, error, fireAction} = useWorkflowSession({engine, instanceId})
  const activity = evaluation?.currentStage.activities.find(
    (candidate) => candidate.activity.name === 'draft',
  )
  const submit = activity?.actions.find((candidate) => candidate.action.name === 'submit')

  if (invalid) return <p>This workflow instance cannot be read by this version.</p>
  if (error) return <p>Workflow state could not be loaded.</p>
  if (!ready || !evaluation) return <p>Loading workflow…</p>
  if (!activity || !submit) return <p>Submit is not available in the current stage.</p>

  const handleSubmit = async () => {
    setSubmitting(true)
    setSubmitError(undefined)
    try {
      await fireAction({activity: activity.activity.name, action: submit.action.name})
      onClose()
    } catch (caught) {
      setSubmitError(caught)
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <div>
      <p>{submit.allowed ? 'The article is ready to submit.' : 'The article cannot be submitted yet.'}</p>
      {submitError ? <p>Submission failed. Try again.</p> : null}
      <button disabled={!submit.allowed || submitting} onClick={handleSubmit} type="button">
        {submitting ? 'Submitting…' : 'Submit'}
      </button>
    </div>
  )
}

The action handles loading, unreadable workflow data, a missing instance, a disabled verdict, and a failed commit. useDocumentWorkflows can return several active instances, so select the definition your control targets instead of assuming the first result is correct.

Register the action

Add the component through Studio’s document action resolver. This example shows it only for the article schema type and preserves the existing actions.

import {defineConfig} from 'sanity'

import {SubmitForReviewAction} from './workflow-document-action'

export default defineConfig({
  projectId: 'your-project',
  dataset: 'production',
  document: {
    actions: (previous, context) =>
      context.schemaType === 'article' ? [SubmitForReviewAction, ...previous] : previous,
  },
})

Use the adapter elsewhere in Studio

The same engine and hooks work in custom Studio tools, panes, inputs, badges, and dialogs. Reach for the hook that matches the state your component needs:

  • Use useWorkflowEngine to create the Studio-bound engine. The hook keeps that engine stable while its component remains mounted.
  • Use useDocumentWorkflows to discover active workflow instances for one content document.
  • Use useWorkflowInstances to list instances for a broader custom surface.
  • Use useWorkflowSession when a component must render live evaluation state or commit actions and fields.

If several Studio surfaces share one engine configuration, expose the engine from a React context provider mounted in Studio’s layout component. The root layout wraps every tool and document surface. Keep the local hook for one isolated integration.

Understand what stays live

The adapter observes synchronized Content Lake documents through the App SDK store. A reactive session updates as the instance, referenced content, and guards change. See The reactive session for its complete state and command contract.

Uncommitted values in Studio’s form buffer are not Content Lake documents. If your custom input previews an Editorial Workflows field, pass that tentative value to previewField, commit it at a deliberate boundary with editField, and discard the preview when editing is canceled.

Action verdicts are advisory UI guidance, not a security boundary. Always render loading and error states, respect the evaluated verdict, and handle commit failures. Run effects and other background work in a server runtime rather than a Studio component.

Relevant exports

Most custom Studio surfaces use these exports:

Next steps

Visiting agent?

Was this page helpful?