Configure workflow fields
Declare workflow, stage, and activity fields, configure initial values and validation, and manage role-constrained assignments.
Early access
Workflows is in early access, built in public. Read How early access works before you rely on it.
Fields hold the data a workflow instance carries: the document it is about, values supplied at start, values computed from your content, and decisions people record along the way. They use the same model as fields in a Sanity schema (a name, a type, and optional validation), but the values live on the workflow instance, not on a content document.
This guide adds five fields to the quick start’s article-review workflow. You declare them at workflow and stage scope, seed them from a start value, a query, and another field, read one in a transition condition, and make it editable so the instance can finish only after legal sign-off.
Prerequisites
- The finished quick start:
workflows/article-review.tsdeployed through thedevdeployment insanity.workflow.ts, and the_idof the published article you started it with. - The engine module from Run the engine from your own code (
scripts/engine.ts) and aSANITY_AUTH_TOKENwith write access to the dataset. The CLI has no command for editing a field, so the last step uses a short script.
Decide where each field lives
Where you declare a field decides when it gets its value and how long it lasts:
- Workflow fields, in
defineWorkflow({fields}), resolve when the instance starts and last for its lifetime. - Stage fields, in
defineStage({fields}), resolve each time the instance enters that stage and belong to that stage visit. - Activity fields, in
defineActivity({fields}), also resolve on stage entry and belong to one activity.
Conditions read the fields in scope as $fields.<name>. When a narrower scope declares a field with the same name, the narrower one wins. An operation or field edit that leaves out the scope resolves the nearest declaration: activity, then stage, then workflow.
Declare and seed the fields
Add three workflow fields and two review stage fields to workflows/article-review.ts. Every field declares a type; Field types lists them all. To narrow string, text, number, or progress values, add validation; see Options and validation.
import {
defineAction,
defineActivity,
defineField,
defineStage,
defineTransition,
defineWorkflow,
} from '@sanity/workflow-engine/define'
export const articleReview = defineWorkflow({
// ...
fields: [
defineField({
type: 'subject',
name: 'subject',
// ...
}),
defineField({
type: 'string',
name: 'priority',
title: 'Priority',
initialValue: {type: 'input'},
options: {
list: [
{title: 'Standard', value: 'standard'},
{title: 'Urgent', value: 'urgent'},
],
},
}),
defineField({
type: 'string',
name: 'articleTitle',
title: 'Article title',
initialValue: {type: 'query', query: '*[_id == $fields.subject.id][0].title'},
}),
defineField({type: 'boolean', name: 'legalApproved', title: 'Legal approved', editable: true}),
],
stages: [
// drafting is unchanged
defineStage({
name: 'review',
// ...
fields: [
defineField({type: 'dueDate', name: 'reviewDue', title: 'Review due', editable: true}),
defineField({
type: 'string',
name: 'reviewPriority',
title: 'Review priority',
initialValue: {type: 'fieldRead', field: 'priority', scope: 'workflow'},
}),
],
// ...
transitions: [
defineTransition({
name: 'to-approved',
title: 'Approve and finish',
to: 'approved',
when: '$allActivitiesDone && $fields.legalApproved == true',
}),
],
}),
// approved is unchanged
],
})Each field shows a different way to get a value. A field without initialValue starts empty and waits for an action or an edit to fill it in, as legalApproved and reviewDue do. Initial-value sources lists the complete forms.
priorityuses{type: 'input'}: you supply it when you start the instance.options.listrestricts it to the listed values, and every write path checks that list. Only workflow fields can take an input, and only workflow input fields can setrequired: true.articleTitleuses{type: 'query'}: GROQ against the Content Lake, evaluated when the field enters scope. A query can read earlier fields in the same scope, so declaration order matters. In a query,$fields.subjectis the stored reference{id, type}, so read.id. Conditions see the loaded document instead and read._id. Query initial values may use the GROQ clock functions such asnow(), and they also receive$now.reviewPriorityuses{type: 'fieldRead'}withscope: 'workflow'to copy the workflow field into the stage visit. A field read copies the stored value only. It never loads the referenced document, so use a query when you need a property of the article.reviewDueis adueDate, the deadline for the stage visit. Declare at most onedueDateordueDatetimeper scope, and only as a top-level field.
The to-approved transition now reads $fields.legalApproved. A transition without when waits for $allActivitiesDone, so the condition keeps that check and adds the field. Write GROQ conditions covers the rest of the condition language.
Deploy and start an instance
Instances keep the definition version they started with, so deploy the change and start a new instance. Pass the input field with a second --field:
npx sanity-workflows deploy
npx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}' \
--field priority=urgentpnpm dlx sanity-workflows deploy
pnpm dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}' \
--field priority=urgentyarn dlx sanity-workflows deploy
yarn dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}' \
--field priority=urgentbunx sanity-workflows deploy
bunx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}' \
--field priority=urgentA start value outside options.list is rejected before the instance is created with FieldValueShapeError. The message starts Field entry value shape invalid for "priority" (string) and lists the allowed values.
Fire the two quick start actions with the instance ID the start command printed:
npx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
npx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approvepnpm dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
pnpm dlx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approveyarn dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
yarn dlx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approvebunx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
bunx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approveVerify the seeded values
Run npx sanity-workflows show INSTANCE_ID. The instance stays in review with every activity done, because legalApproved is still empty:
article-review v2 dev.wf-instance.a1b2c3d4e5f6
Stage: review
Started: 2026-10-26 19:34:21
Completed: —
Partition: dev
⚠ STUCK — no exit transition's trigger is satisfied
Why it's stuck:
ℹ every activity is resolved, but no exit transition's `when` is true.
Likely a routing state value a trigger reads never got written.
Suggested fix:
• set-stage — Move the instance to the intended next stage to force it forward.
Stage history:
• drafting (exited 2026-10-26 19:34:21)
✔ write [done]
• review (current)
✔ sign-off [done]Add --json to print the instance document. Its fields array holds the workflow fields, with priority set to urgent, articleTitle set to the article’s title, and legalApproved set to null. The current entry in stages holds the stage fields, with reviewPriority copied as urgent.
Make a field editable
Fields are not directly editable by default. legalApproved sets editable: true, so any caller can set it while it is in scope. editable also accepts a list of project roles or a GROQ condition:
// Anyone can edit while the field is in scope.
defineField({type: 'boolean', name: 'legalApproved', title: 'Legal approved', editable: true})
// A string array lists the project roles that can edit.
defineField({type: 'boolean', name: 'legalApproved', title: 'Legal approved', editable: ['legal']})
// A string is a GROQ condition.
defineField({
type: 'boolean',
name: 'legalApproved',
title: 'Legal approved',
editable: "$fields.priority == 'urgent'",
})A role list must not be empty. An edit condition can read $actor, $assigned, $can, $attributes, and $fields. A field without editable rejects an edit with EditFieldDeniedError, for example Field "workflow:priority" is not editable: field is not declared editable. Like action filters, these checks are advisory; Actors and enforcement explains what enforces a rule.
Edit the field from code with engine.editField. Create scripts/approve-legal.ts:
import {engine} from './engine'
const instanceId = process.argv[2]
if (!instanceId) throw new Error('Usage: npx tsx scripts/approve-legal.ts INSTANCE_ID')
const result = await engine.editField({
instanceId,
target: {field: 'legalApproved'},
value: true,
})
console.log(`legalApproved set. Now at ${result.instance.currentStage}`)Run it with the instance ID. A field edit is an engine operation: the engine checks editable, commits one transaction, records history, refreshes guards, and runs the cascade. One edit can reveal an action, satisfy a requirement, fire a triggered action, or move the instance. Setting legalApproved satisfies to-approved in the same call:
npx tsx scripts/approve-legal.ts INSTANCE_IDpnpm dlx tsx scripts/approve-legal.ts INSTANCE_IDyarn dlx tsx scripts/approve-legal.ts INSTANCE_IDbunx tsx scripts/approve-legal.ts INSTANCE_IDlegalApproved set. Now at approvedRun npx sanity-workflows show INSTANCE_ID again to confirm the instance is complete in approved. Editors can also set editable fields from Sanity Studio once you add Workflows to Studio.
Assign work to people and roles
assignee and assignees fields hold people and project roles. On an activity they decide who $assigned matches, and roles restricts who can be assigned. Let reviewers claim an assignment adds one to the review stage and lets an editor take the review.
Next steps
- Look up every field property, type, initial-value source, option, and validation rule in Field declaration.
- Write field values from actions with operations in Define activities and actions.
- Let reviewers claim work with an assignment field in Let reviewers claim an assignment.
- Browse the TypeScript signatures in
@sanity/workflow-engine/defineon reference.sanity.io.
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?