Workflows

Configure workflow fields

Declare workflow, stage, and activity fields, configure initial values and validation, and manage role-constrained assignments.

Early access

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.ts deployed through the dev deployment in sanity.workflow.ts, and the _id of the published article you started it with.
  • The engine module from Run the engine from your own code (scripts/engine.ts) and a SANITY_AUTH_TOKEN with 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.

  • priority uses {type: 'input'}: you supply it when you start the instance. options.list restricts 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 set required: true.
  • articleTitle uses {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.subject is 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 as now(), and they also receive $now.
  • reviewPriority uses {type: 'fieldRead'} with scope: '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.
  • reviewDue is a dueDate, the deadline for the stage visit. Declare at most one dueDate or dueDatetime per 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=urgent

A 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 approve

Verify 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_ID
legalApproved set. Now at approved

Run 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

Visiting agent?

Was this page helpful?

Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change site theme