Custom reactive adapters
Connect Workflows to an unsupported host or data layer by implementing the store-agnostic reactive observer contract.
Early access
Workflows is in early access, built in public. Read How early access works before you rely on it.
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, andvalidatedInstancesStore: validate the engine-owned streams.validatedPreviewPage: parse one instance-preview page row by row.combineDocStoresandcombineGuardStores: 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, andsameDataset: route each ref to a store that can address it.useKeyed: the deterministic keyed memo the hooks and adapters share.
Next steps
- Build a workflow interface with the App SDK: build an application with the shipped adapter.
- The reactive session: understand the live projection an adapter supplies.
- API reference: find the complete observer and hook contracts.
Visiting agent?
Workflows includes an MCP server for inspecting, operating, authoring, validating, and deploying workflows. Ask your human to set up the MCP server.