Sanity Blueprints
integration for Workflows. This one package owns both halves of the
sanity.workflow contract: the manifest definer and the provider that the
Blueprints API runs.
The package contains the complete resource implementation, but publishing it
does not register a new resource type. sanity.workflow becomes deployable
only after the Blueprints API imports and registers workflowProvider.
Until that server change ships, deploy definitions with
sanity workflows deploy instead of adding this resource to a live stack.
The public package is intentionally the single Workflows-owned implementation:
defineWorkflows from this package. After the
downstream Blueprints release, @sanity/blueprints becomes the discoverable
import and delegates here without duplicating Workflows validation.sanity blueprints deploy evaluates sanity.blueprint.ts and sends the
resulting sanity.workflow resource to the existing Blueprints API.workflowProvider directly from this package.
During pre-deploy, the server calls the provider's write-free init and
atomically reserves the returned external ID for the Stack. The provider's
requiresInitialization: true flag requires the server to reject an
ordinary create that has no initialized resource record.There is no separate Workflows provider package. The implementation uses only
public @sanity/client and @sanity/workflow-engine APIs, so keeping it public
does not expose Blueprints server internals. The private Blueprints provider SDK
stays in the Blueprints API repository, where TypeScript verifies that this
package's structural provider contract still matches the server contract.
defineWorkflows takes the same authoring shape as one deployments[]
entry of a sanity.workflow.ts config (WorkflowDeploymentInput — the
reader-floor acknowledgement is required). Share that authored object with
defineWorkflowConfig; do not pass a parsed config.deployments[n] (that
type leaves the floor unverified so commands that do not submit definitions
can load a missing entry):
// shared.ts
import type {WorkflowDeploymentInput} from '@sanity/workflow-engine'
export const prod = {
expectedMinReaderModel: 4,
name: 'prod',
tag: 'prod',
workflowResource: {type: 'dataset', id: 'acme.workflows'},
definitions: [articleReview],
} satisfies WorkflowDeploymentInput
// sanity.workflow.ts
import {defineWorkflowConfig} from '@sanity/workflow-engine/define'
import {prod} from './shared'
export default defineWorkflowConfig({deployments: [prod]})
// sanity.blueprint.ts
import {defineBlueprint} from '@sanity/blueprints'
import {defineWorkflows} from '@sanity/workflow-blueprint'
import {prod} from './shared'
export default defineBlueprint({
resources: [defineWorkflows(prod)],
})
The definer validates eagerly — definition structure, GROQ, and
@<handle>: alias bindings all fail at manifest evaluation, in the author's
terminal, never as a failed stack operation. The resulting resource is pure
data: the deployment rides the manifest verbatim, definitions in authored
form (alias expansion happens at provision).
Blueprints matches resources by name (default:
workflows-<deployment name>). Keep it stable after deployment: a retained
resource cannot be renamed because removing the old name is rejected.
Definitions are retain-only through blueprints: the default lifecycle is
{deletionPolicy: 'retain'}. Destroying the stack detaches the resource and
leaves every definition version in place; removing the resource from a deployed
blueprint fails instead of deleting it. Removing one definition from the
resource's deployment.definitions list stops managing that name but leaves its
deployed versions in place. Policies that promise deletion (allow, replace)
and every unsupported lifecycle key (including ownershipAction) are rejected
at manifest evaluation and again at the provider JSON boundary.
workflowProvider is the factory the Blueprints API registers. It owns the
full server-facing lifecycle adapter and delegates the engine operations to the
same exported primitives:
import {
workflowProvider,
provisionWorkflowResource,
readWorkflowResource,
rollbackWorkflowResource,
} from '@sanity/workflow-blueprint'
// Registered by the Blueprints API under workflowProvider.resourceType
// (`sanity.workflow`). Authors never call the provider directly.
void workflowProvider
// create and update are the same operation: the engine's definition deploy is
// content-addressed and create-only, so identical content is a no-op and any
// change mints the next immutable version.
const receipt = await provisionWorkflowResource({resource, client})
// Read returns the physical (workflowResource, tag) external ID and the latest
// deployed version of each definition name still managed by this manifest.
const snapshot = await readWorkflowResource({resource, client})
// When a later stack action fails, rollback removes only versions this
// successful provision created. Earlier and unchanged versions survive.
await rollbackWorkflowResource({resource, client, receipt})
The host lifecycle has two distinct phases:
workflowProvider.init. It validates the resource and
returns its deterministic (workflowResource, tag) external ID without
creating a Sanity client or reading from or writing to the Content Lake. The
Blueprints API reserves (resource type, external ID) for the Stack.workflowProvider.requiresInitialization is true. A Blueprints API host must
therefore reject ordinary create when no initialized resource record exists;
calling create directly would bypass the ownership guarantee.
client must be bound to the deployment's workflowResource with a token
that can write definition documents there. Provisioning parses and validates
the incoming resource at the boundary (parseWorkflowResource) — provider
input is JSON from the stack, never a typed value. The Blueprints retain path
must detach a destroyed stack without calling destroyWorkflowResource; that
function deliberately throws if a caller attempts physical destruction.
Rollback is compensation for a failed, uncommitted stack deployment, not a
definition-history tool. It consumes the exact deploy receipt, ignores
unchanged results, and deletes created versions in reverse dependency order
without cascading into instances. A rollback replay is deliberately
fail-closed: an absent receipt version and every engine cleanup failure are
surfaced because the engine cannot prove a prior delete completed every phase.