Configure and deploy workflow definitions
Install and authenticate the workflow CLI, write the sanity.workflow.ts config that binds your definitions to a Sanity resource, and deploy them to one environment or several.
Early access
Workflows is in early access, built in public. Read How early access works before you rely on it.
Use @sanity/workflow-cli to store workflow definitions in the Content Lake. It reads sanity.workflow.ts from the current directory, validates the definitions, checks referenced project roles during deployment, and writes valid definitions to the configured workflow resource. It ships as the sanity-workflows binary. A workflow engine runs the deployed definitions.
This guide installs and authenticates the CLI, writes the config that binds your definitions to a resource, and deploys them to one environment and then to several. For what each command and flag does once you are set up, see the Workflow CLI command reference.
Before you start, you need:
- Node.js 20.12 or later.
- Matching versions of
@sanity/workflow-cli,@sanity/workflow-engine, and@sanity/workflow-blueprint. - At least one workflow definition authored with
defineWorkflow. See Definitions, instances, and stages, or work through the quick start first. - A Sanity project, and a dataset for the engine to keep its own documents in. That can be the dataset your content lives in or a separate one.
- A Sanity login session, or a token with the editor role. The next section covers both.
If your definitions reference project roles, the deployment identity must also be able to read that project's role catalog and member directory. Create the referenced roles in the project before deployment.
Install and authenticate the CLI
The CLI ships as @sanity/workflow-cli with a single binary named sanity-workflows. Run it with npx rather than installing it globally. npx uses the copy in your project when there is one, so adding it as a development dependency is how you pin the version a team and a CI job share.
# Inspect the published CLI version
npx @sanity/workflow-cli --version
# Pin matching packages for this project
npm install --save-dev --save-exact @sanity/workflow-cli \
@sanity/workflow-engine @sanity/workflow-blueprint# Inspect the published CLI version
pnpm dlx @sanity/workflow-cli --version
# Pin matching packages for this project
pnpm add --save-dev --save-exact @sanity/workflow-cli \
@sanity/workflow-engine @sanity/workflow-blueprint# Inspect the published CLI version
yarn dlx @sanity/workflow-cli --version
# Pin matching packages for this project
yarn add --dev --exact @sanity/workflow-cli \
@sanity/workflow-engine @sanity/workflow-blueprint# Inspect the published CLI version
bunx @sanity/workflow-cli --version
# Pin matching packages for this project
bun add --dev --exact @sanity/workflow-cli \
@sanity/workflow-engine @sanity/workflow-blueprintInstall the engine and Blueprint packages alongside the CLI. The CLI declares both as exact peers, so keep all three at the same version and commit the updated lockfile. Your config imports defineWorkflowConfig from the engine; the CLI uses the Blueprint package to load config and generate runtimes.
Authenticate once with the Sanity CLI. Every workflow command reuses that session.
npx sanity@latest loginpnpm dlx sanity@latest loginyarn dlx sanity@latest loginbunx sanity@latest loginIn CI there is no session to read, so set SANITY_AUTH_TOKEN instead. An explicit SANITY_AUTH_TOKEN always wins over a login session, which also makes it the way to run a one-off command as a different identity. Anything that writes needs an editor-role token. Never commit it: read it from the secret store of your CI provider and pass it through the environment.
- name: Deploy workflow definitions
run: npx @sanity/workflow-cli deploy --deployment production
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}Scope the token to every resource, not one
The token has to reach every resource the workflow references, not only the one the workflow data lives in. A definition whose subject or reference fields point into other datasets, a canvas, or a media library is read through sibling clients derived from that same token. A `sanity login` session or an organization-scoped token covers this. A token scoped to a single project deploys successfully and then fails later on a cross-resource read, which is a confusing place to discover it.
One more environment variable exists: SANITY_API_HOST overrides the API host the CLI talks to. Leave it unset unless you have been told to point at a non-production API.
Write the workflow config
Every workflow command reads a sanity.workflow.ts. It looks in the directory you run it from, then in each parent directory, and uses the first config it finds, so commands work from any subdirectory of your project. Pass --path to name a config file directly, or a directory to start that search from. The file default-exports a config built with defineWorkflowConfig, declaring one deployment per environment you ship to. TypeScript configs are transpiled on the fly, so there is no build step, and a sanity.workflow.js or sanity.workflow.mjs works the same way.
This config deploys one definition to one resource. The example acknowledges reader model 10, required by definitions with required content-reference fields. Before using that value with existing workflow data, follow Upgrade Workflows packages and verify every runtime sharing the resource can read model 10:
import type {WorkflowDeploymentInput} from '@sanity/workflow-engine'
import {defineWorkflowConfig} from '@sanity/workflow-engine/define'
import {articleReview} from './workflows/article-review'
export const production = {
name: 'production',
partition: 'prod',
expectedMinReaderModel: 10,
workflowResource: {type: 'dataset', id: '<your-project-id>.production'},
definitions: [articleReview],
} satisfies WorkflowDeploymentInput
export default defineWorkflowConfig({deployments: [production]})The named production export also supports experimental runtime generation. Generated functions import each deployment by its name. Keep the authored deployment object available under that export.
Replace <your-project-id> with your project ID. A dataset resource ID is always <projectId>.<dataset>. Anything else fails at load with invalid dataset resource id — expected "<projectId>.<dataset>".
This guide shows a production setup: a deployment named production with partition prod. The dev deployment from the quick start can stay beside it as another entry in deployments. Studio, apps, and scripts see only the partition they are configured with, so point each one at the deployment it should use.
Four keys carry the setup. name is the CLI identity of the deployment, the one --deployment matches. partition is the environment partition its stored definitions and instances are scoped to. workflowResource is the Sanity resource that holds those documents. resourceAliases binds the content a definition acts on, and you can leave it out when that content already lives in the workflowResource. The sanity.workflow.ts configuration reference lists every field, and Deployments, partitions, and resources explains the model behind them.
An invalid config fails with a clean, path-prefixed error before the command runs. Getting expectedMinReaderModel wrong is the one failure worth recognizing on sight: omit it and deploy stops with Reader-floor acknowledgement: and the model your definitions actually need. Keep it a reviewed literal, and read Upgrade Workflows packages before changing it.
Run the same workflow in more than one place
Running the same definitions in staging and production is a second entry in deployments: one definitions list, a different name, partition, and alias binding per environment. Make sure the workflowResource and partition pair differs between them, either by pointing them at different datasets or by keeping one dataset and giving them different partitions. A deployment alone on its resource can omit partition and uses default; once two deployments share a resource, each must set one.
import type {WorkflowDeploymentInput} from '@sanity/workflow-engine'
import {defineWorkflowConfig} from '@sanity/workflow-engine/define'
import {articleReview} from './workflows/article-review'
export const staging = {
name: 'staging',
partition: 'staging',
expectedMinReaderModel: 10,
workflowResource: {type: 'dataset', id: '<your-project-id>.workflows-staging'},
resourceAliases: [
{name: 'content', resource: {type: 'dataset', id: '<your-project-id>.staging'}},
{name: 'assets', resource: {type: 'media-library', id: 'YOUR_STAGING_MEDIA_LIBRARY_ID'}},
],
definitions: [articleReview],
} satisfies WorkflowDeploymentInput
export const production = {
name: 'production',
partition: 'prod',
expectedMinReaderModel: 10,
workflowResource: {type: 'dataset', id: '<your-project-id>.workflows'},
resourceAliases: [
{name: 'content', resource: {type: 'dataset', id: '<your-project-id>.production'}},
{name: 'assets', resource: {type: 'media-library', id: 'YOUR_MEDIA_LIBRARY_ID'}},
],
definitions: [articleReview],
} satisfies WorkflowDeploymentInput
export default defineWorkflowConfig({deployments: [staging, production]})This config keeps engine documents out of the content datasets. Each deployment binds content to the dataset its articles live in and assets to the Media Library that holds its images. The two environments stay independent because their workflowResource and partition pairs differ, which is explained in Deployments, partitions, and resources.
A definition refers to these resources by alias, as @content:<documentId> or @assets:<documentId>. Deploy replaces each alias with the resource the target deployment binds, so the same definition source deploys to both environments and each deployed copy holds only physical references. Bind every alias your definitions use in every deployment. --check does not resolve aliases, but --dry-run and deploy stop on a missing binding:
workflow.deployDefinitions: definition "article-review" references unbound resource alias "@assets". Bind it in the deploy target's resource map, or remove the reference.Once the config declares more than one deployment, commands stop guessing which one you mean. An interactive terminal prompts you to choose, and a non-interactive shell fails with Multiple deployments configured — pass --deployment or --partition. Name the target explicitly in CI. See Select a deployment, a partition, or an instance for what each selector means on each command.
Deploy
Deploying has three gears, and running them in order turns most deploy failures into local ones. --check validates your definitions and stops: it never contacts the dataset and never resolves a token, so it runs offline and belongs in a pre-commit hook or a pull request check. --dry-run runs every check a real deploy runs before its first write, including the reader-floor acknowledgement, the project role check, and spawn ordering, then prints a colored diff against what is already deployed. It still writes nothing, and a batch the deploy would reject fails the dry run with the same error. Not both at once: passing both fails with Pass either --check or --dry-run, not both.
If workflows.blueprint.ts exists beside your workflow config, deployment also checks generated-runtime alignment across the whole config. This happens before token resolution or writes, even with --check, --dry-run, --only, or a deployment selector. Regenerate and resolve drift or missing handlers before continuing. With no generated tree, deploy reports runtime needs and continues. See the generated runtime.
Deployment checks referenced role names against the project’s role catalog. An unknown role stops the deployment, and the error lists each definition and role with the paths that reference it. A known role with no current human member produces a warning. --dry-run runs the same check; --check does not.
# Offline: does every definition still validate?
npx @sanity/workflow-cli deploy --check --deployment production
# Online, read-only: what would change?
npx @sanity/workflow-cli deploy --dry-run --deployment production
# Write it
npx @sanity/workflow-cli deploy --deployment production# Offline: does every definition still validate?
pnpm dlx @sanity/workflow-cli deploy --check --deployment production
# Online, read-only: what would change?
pnpm dlx @sanity/workflow-cli deploy --dry-run --deployment production
# Write it
pnpm dlx @sanity/workflow-cli deploy --deployment production# Offline: does every definition still validate?
yarn dlx @sanity/workflow-cli deploy --check --deployment production
# Online, read-only: what would change?
yarn dlx @sanity/workflow-cli deploy --dry-run --deployment production
# Write it
yarn dlx @sanity/workflow-cli deploy --deployment production# Offline: does every definition still validate?
bunx @sanity/workflow-cli deploy --check --deployment production
# Online, read-only: what would change?
bunx @sanity/workflow-cli deploy --dry-run --deployment production
# Write it
bunx @sanity/workflow-cli deploy --deployment productionTo narrow a run to one definition, add --only <name>. If you change a definition, run the deploy command again. Re-running is safe: a version comes from the content of the definition, so a deploy that changes nothing writes nothing.
Before deploying definitions that require a newer reader model, complete the package rollout. An unchanged deployment does not rewrite the stored definition's model stamps. After deploying changed definition content, inspect the latest stored version:
npx @sanity/workflow-cli definitions show article-review --partition prod --jsonpnpm dlx @sanity/workflow-cli definitions show article-review --partition prod --jsonyarn dlx @sanity/workflow-cli definitions show article-review --partition prod --jsonbunx @sanity/workflow-cli definitions show article-review --partition prod --jsonReplace article-review and prod with your definition name and partition. Check version, modelVersion, and minReaderModel in the returned JSON. New writes use modelVersion: 10. A definition with required content-reference fields also needs minReaderModel: 10; a definition without them can keep a lower floor. Existing instances keep the definition version they started with.
# One environment at a time, which is what a promotion pipeline wants
npx @sanity/workflow-cli deploy --deployment staging
npx @sanity/workflow-cli deploy --deployment production
# Or every deployment in the config, in one run
npx @sanity/workflow-cli deploy --all-partitions# One environment at a time, which is what a promotion pipeline wants
pnpm dlx @sanity/workflow-cli deploy --deployment staging
pnpm dlx @sanity/workflow-cli deploy --deployment production
# Or every deployment in the config, in one run
pnpm dlx @sanity/workflow-cli deploy --all-partitions# One environment at a time, which is what a promotion pipeline wants
yarn dlx @sanity/workflow-cli deploy --deployment staging
yarn dlx @sanity/workflow-cli deploy --deployment production
# Or every deployment in the config, in one run
yarn dlx @sanity/workflow-cli deploy --all-partitions# One environment at a time, which is what a promotion pipeline wants
bunx @sanity/workflow-cli deploy --deployment staging
bunx @sanity/workflow-cli deploy --deployment production
# Or every deployment in the config, in one run
bunx @sanity/workflow-cli deploy --all-partitions--all-partitions is deliberately explicit: deploying everything is never the default. A failing deployment does not stop the others. Fix the cause and re-run; see Exit codes for how a multi-target run reports and exits.
Deploying puts the definition in the Content Lake. It does not make anything run. Nothing advances a workflow until your code calls the engine, so decide what your runtime is before you rely on deadlines or queued effects firing. See Run Workflows with Sanity Functions.
Definition sharing
Definitions are shared with Sanity by default when a deploy creates a new version. Pass --no-share-defs to opt out for one invocation. A failed share warns but does not fail the deploy.
Next steps
- Workflow CLI command reference covers every command, flag, selector, and exit code.
- Deployments, partitions, and resources explains name against partition, the storage partition that keeps environments apart, and how resource aliases resolve.
- Test a workflow before you deploy it runs a definition against an in-memory bench, with no dataset involved.
- Run Workflows with Sanity Functions is what makes deadlines fire and queued effects drain once nobody is at a terminal.
- How early access works explains the version policy and the stored-data contract behind
expectedMinReaderModel.
Visiting agent?
Workflows includes an MCP server for inspecting, operating, authoring, validating, and deploying workflows. Ask your human to set up the MCP server.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?