Workflows

Custom reactive adapters

Connect Workflows to an unsupported host or data layer by implementing the store-agnostic reactive observer contract.

Early access

Use the Studio plugin inside Sanity Studio and the App SDK adapter in a custom application. Build another adapter only when a different host or data layer must drive the reactive session. See Choose an integration for how the packages layer.

Build your own adapter

@sanity/workflow-react does not care which store you use. An adapter connects its hook core to a reactive document store by implementing WorkflowObserver: four useSyncExternalStore-shaped reads, two instance-preview methods, and one write:

type ObserverState<T> =
  | {status: 'loading'}
  | {status: 'ready'; value: T}
  | {status: 'invalid'; invalid: InvalidDoc}
  | {status: 'error'; error: unknown}

interface DocStore<T> {
  subscribe: (onChange: () => void) => () => void
  getSnapshot: () => ObserverState<T>
}

interface ObservedInstances {
  instances: readonly WorkflowInstance[]
  unreadable: readonly InvalidDoc[]
}

interface WorkflowObserver {
  observeInstance: (instanceId: string) => DocStore<WorkflowInstance | null>
  observeDocs: (
    documents: SubscriptionDocument[],
    perspective: WorkflowPerspective | undefined,
  ) => DocStore<ReadonlyMap<string, SanityDocument | null>>
  observeGuards: (args: {
    instanceId: string
    resources: readonly DatasetResourceId[]
    sharedIds?: readonly string[]
  }) => DocStore<readonly MutationGuardDoc[]>
  observeInstances: (query: CompiledQuery) => DocStore<ObservedInstances>
  fetchInstancePreviews: (query: CompiledQuery) => Promise<readonly unknown[]>
  listenInstanceChanges: (args: {
    query: CompiledQuery
    onEvent: (event: InstanceChangeEvent) => void
  }) => () => void
  ensureDocumentExists: (args: EnsureDocumentExistsArgs) => Promise<void>
}

fetchInstancePreviews and listenInstanceChanges are required members. TypeScript reports the missing methods at compile time, and an incomplete JavaScript adapter fails when a preview list is used. On a preview-listener reconnect, emit a reset event after the connection is restored so consumers refetch pages that may have changed during the gap.

Every store starts in loading and resolves to ready, invalid, or error. A later successful emission clears either failure. Content documents pass through without validation. The instance and guard wrappers report a validation failure through the store's whole-store invalid state. validatedInstancesStore validates each list row instead, and returns {instances, unreadable} in a ready snapshot. Keep resolved snapshots referentially stable, including the shared NO_GUARDS identity for an empty guard list.

The builder kit lives at @sanity/workflow-react/observer. Pass the completed observer to the hooks from @sanity/workflow-react. The API reference lists the complete surface.

  • validatedInstanceStore, validatedGuardsStore, and validatedInstancesStore: validate the engine-owned streams.
  • validatedPreviewPage: parse one instance-preview page row by row.
  • combineDocStores and combineGuardStores: fan per-document and per-resource stores into one keyed source.
  • mapSnapshot: derive a memoized snapshot from a store.
  • ensureDocumentExists: the store seam's one write primitive, with the shared drafts-over-published decision matrix.
  • classifyRef, datasetResourceId, guardResourcesFor, and sameDataset: route each ref to a store that can address it.
  • useKeyed: the deterministic keyed memo the hooks and adapters share.

Next steps

Visiting agent?

Was this page helpful?