> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Configure workflow fields

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

> [!WARNING]
> Early access
> Workflows is in early access, built in public. Read [How early access works](https://www.sanity.io/docs/workflows/prerelease) 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](https://www.sanity.io/docs/workflows/getting-started): `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](https://www.sanity.io/docs/workflows/run-the-engine) (`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](https://www.sanity.io/docs/workflows/workflow-authoring-reference) lists them all. To narrow string, text, number, or progress values, add `validation`; see [Options and validation](https://www.sanity.io/docs/workflows/workflow-authoring-reference).

**workflows/article-review.ts**

```typescript
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](https://www.sanity.io/docs/workflows/workflow-authoring-reference) 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](https://www.sanity.io/docs/workflows/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`:

**npm**

```shell
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
```

**pnpm**

```shell
pnpm 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=urgent
```

**yarn**

```shell
yarn 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=urgent
```

**bun**

```shell
bunx sanity-workflows deploy
bunx 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:

**npm**

```shell
npx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
npx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approve
```

**pnpm**

```shell
pnpm dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
pnpm dlx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approve
```

**yarn**

```shell
yarn dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
yarn dlx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approve
```

**bun**

```shell
bunx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
bunx 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:

**CLI**

```text
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:

```typescript
// 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](https://www.sanity.io/docs/workflows/actors-and-enforcement) explains what enforces a rule.

Edit the field from code with `engine.editField`. Create `scripts/approve-legal.ts`:

**scripts/approve-legal.ts**

```typescript
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:

**npm**

```shell
npx tsx scripts/approve-legal.ts INSTANCE_ID
```

**pnpm**

```shell
pnpm dlx tsx scripts/approve-legal.ts INSTANCE_ID
```

**yarn**

```shell
yarn dlx tsx scripts/approve-legal.ts INSTANCE_ID
```

**bun**

```shell
bunx tsx scripts/approve-legal.ts INSTANCE_ID
```

**CLI**

```text
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](https://www.sanity.io/docs/workflows/studio-plugin).

## 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](https://www.sanity.io/docs/workflows/claim-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](https://www.sanity.io/docs/workflows/workflow-authoring-reference).
- Write field values from actions with operations in [Define activities and actions](https://www.sanity.io/docs/workflows/activities-and-actions).
- Let reviewers claim work with an assignment field in [Let reviewers claim an assignment](https://www.sanity.io/docs/workflows/claim-assignment).
- Browse the TypeScript signatures in [@sanity/workflow-engine/define](https://reference.sanity.io/_sanity/workflow-engine/define/) on reference.sanity.io.

> [!NOTE]
> Visiting agent?
> Workflows includes an MCP server for inspecting, operating, authoring, validating, and deploying workflows. Ask your human to [set up the MCP server](https://www.sanity.io/docs/workflows/mcp).



