Quick start: run your first workflow
Define your first workflow in TypeScript, deploy it, and move a Sanity document through its stages.
Early access
Workflows is in early access, built in public. Read How early access works before you rely on it.
Sanity Workflows moves a document through named stages under rules you deploy. You author a definition that names the stages, the work inside each one, and the conditions for moving between them. Deploying the definition makes it available to run. Starting an instance runs it against one document.
This quick start deploys a three-stage article review, starts an instance against a document in your dataset, and moves that instance to its final stage. Every step runs from a terminal with the Workflows CLI. No Sanity Studio is involved.
You need:
- Node.js 20.12 or later.
- A Sanity project and a dataset you can write to.
- One published document in that dataset for the workflow to act on. Note its
_idand its_type.
Step 1: Install the packages
Two packages cover this quick start. @sanity/workflow-engine holds the definition language and the runtime. @sanity/workflow-cli deploys definitions and drives instances from a terminal, and it declares the engine as a peer dependency, so install both together. Every Workflows package one application uses has to come from the same release.
npm install @sanity/workflow-engine @sanity/workflow-cli
pnpm add @sanity/workflow-engine @sanity/workflow-cli
yarn add @sanity/workflow-engine @sanity/workflow-cli
bun add @sanity/workflow-engine @sanity/workflow-cli
The CLI reads the token from your Sanity login session. Log in once:
npx sanity@latest loginpnpm dlx sanity@latest login
yarn dlx sanity@latest login
bunx sanity@latest loginIn CI, set SANITY_AUTH_TOKEN instead of logging in. With neither, the CLI stops before it writes anything and reports No Sanity token found — run `sanity login`, or set SANITY_AUTH_TOKEN.
Step 2: Define a three-stage workflow
A definition names the stages a document passes through, the activities inside each stage, and the transitions that move an instance onward. Create workflows/article-review.ts with three stages: drafting, review, and approved.
import {
defineAction,
defineActivity,
defineField,
defineStage,
defineTransition,
defineWorkflow,
} from '@sanity/workflow-engine/define'
export const articleReview = defineWorkflow({
name: 'article-review',
title: 'Article review',
description: 'Takes one article from drafting, through an editor review, to approved.',
initialStage: 'drafting',
fields: [
defineField({
type: 'subject',
name: 'subject',
title: 'Article',
required: true,
description: 'The document this instance moves through the stages.',
initialValue: {type: 'input'},
}),
],
stages: [
defineStage({
name: 'drafting',
title: 'Drafting',
description: 'The writer is working on the article.',
activities: [
defineActivity({
name: 'write',
title: 'Write the article',
actions: [
defineAction({
name: 'submit',
title: 'Submit for review',
status: 'done',
}),
],
}),
],
transitions: [defineTransition({name: 'to-review', title: 'Send to review', to: 'review'})],
}),
defineStage({
name: 'review',
title: 'Editorial review',
description: 'An editor reads the article and approves it.',
activities: [
defineActivity({
name: 'sign-off',
title: 'Review the article',
actions: [
defineAction({
name: 'approve',
title: 'Approve',
status: 'done',
}),
],
}),
],
transitions: [
defineTransition({name: 'to-approved', title: 'Approve and finish', to: 'approved'}),
],
}),
defineStage({
name: 'approved',
title: 'Approved',
description: 'The article is approved. Nothing more to do here.',
}),
],
})The subject field is the document the instance is about. initialValue: {type: 'input'} means you supply that document when you start the instance rather than the definition fixing it, and required: true makes a start that omits it fail with a message naming the missing field.
Each non-terminal stage holds one activity with one action. Firing the action resolves its activity, because the action declares status: 'done'.
Neither transition declares a when condition, so each takes the default $allActivitiesDone: the instance leaves the stage once every activity in that stage resolves. approved declares no transitions at all, which makes it terminal.
Step 3: Configure and deploy the definition
The Workflows CLI reads a sanity.workflow.ts from the directory you run it in. That file binds your definitions to a deployment: a name, an environment tag, and the dataset the engine writes its own documents to. Create it beside your workflows/ directory.
import {defineWorkflowConfig} from '@sanity/workflow-engine/define'
import {articleReview} from './workflows/article-review'
export default defineWorkflowConfig({
deployments: [
{
name: 'dev',
tag: 'dev',
expectedMinReaderModel: 4,
workflowResource: {type: 'dataset', id: 'PROJECT_ID.DATASET_NAME'},
definitions: [articleReview],
},
],
})Replace PROJECT_ID with your Sanity project ID and DATASET_NAME with the dataset. name identifies this deployment when a config holds more than one. tag is the environment partition the engine scopes its documents to.
expectedMinReaderModel is the stored-data model version you promise every runtime reading these documents understands. 4 is the unconditional floor for engine-owned documents. A definition using a newer field kind requires a higher number, and the deploy names the number it needs rather than writing anything.
Deploy the definition:
npx sanity-workflows deploypnpm dlx sanity-workflows deploy
yarn dlx sanity-workflows deploy
bunx sanity-workflows deployThe CLI validates every definition before it writes anything, then reports what it created. npx sanity-workflows deploy --check validates without contacting the dataset, and --dry-run diffs against what is already deployed. Every command also resolves under its canonical workflows topic, so npx sanity-workflows workflows deploy does the same thing.
Step 4: Start an instance
An instance is one live run of a definition against content. Starting one pins the definition version it runs under and freezes a snapshot of it, so deploying a change later does not alter an instance already in flight. Supply the document through the subject field the definition declared as an input.
npx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"DOCUMENT_TYPE"}'pnpm dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"DOCUMENT_TYPE"}'yarn dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"DOCUMENT_TYPE"}'bunx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"DOCUMENT_TYPE"}'dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID is a global document reference: the scheme, your project ID, your dataset, and the document _id. DOCUMENT_TYPE is that document’s _type. The command prints the instance id and the stage the instance landed in:
Started dev.wf-instance.a1b2c3d4e5f6 — now at draftingKeep that id. Every command in the next step takes it. Pass the published document _id, not a draft or release version id: a versioned id is rejected before the instance is created, with Invalid GDR "…": dataset document ID "drafts.article-1" identifies a stored draft or release version.
Step 5: Move the document through the stages
Moving an instance means firing the actions that resolve its activities. The article-review definition has one action per non-terminal stage. Fire the first to resolve write in drafting:
npx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
pnpm dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
yarn dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
bunx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
Replace INSTANCE_ID with the id the start command printed. Resolving write satisfies the stage’s only activity, so the to-review transition fires inside the same call and the instance is in review before the command returns. That chain of transitions inside one call is the cascade. Fire the second action:
npx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approve
pnpm dlx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approve
yarn dlx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approve
bunx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approve
Confirm where the instance ended up:
npx sanity-workflows show INSTANCE_ID
pnpm dlx sanity-workflows show INSTANCE_ID
yarn dlx sanity-workflows show INSTANCE_ID
bunx sanity-workflows show INSTANCE_ID
show prints the state, activities, and effects of the instance. The instance is in approved and complete: approved declares no transitions, so nothing can move it further. To see what can be fired on an instance at any point, run npx sanity-workflows fire-action INSTANCE_ID with no --action.
What just moved this workflow, and what will in production
Every move in this quick start had you as its runtime. start and each fire-action started a process, ran the engine inside it, committed one change to the Content Lake, and exited. The cascade in step 5, where firing submit also moved the instance into review, ran inside that one fire-action call. The engine is a library, not a service: it acts only when your code calls it, and none of it is running now that those commands have returned.
Production needs a caller for the moves nobody is at a terminal for. A transition whose condition reads the clock does not fire when the deadline passes; something has to call tick after the clock crosses it. An effect the workflow queues stays queued until a drainer picks it up. Both are jobs for a process that runs on a schedule and on content changes, and in a Sanity project that process is usually a Sanity Function.
Next steps
- Put these workflows in front of editors in Sanity Studio
- See a complete workflow with approval data, change requests, and a review loop
- Prove a definition’s paths in memory before you deploy it
- Run the same definition in more than one environment
Visiting agent?
Workflows includes an MCP server for inspecting, operating, authoring, validating, and deploying workflows. Ask your human to set up the MCP server.

