> 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).

# Let reviewers claim an assignment

Add a role-restricted reviewer assignment to a workflow activity, let an editor claim it with editField, and limit approval to the claimer.

> [!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.



An assignment field lists who is responsible for an activity: specific people, project roles, or both. When a role is listed, anyone holding it can take the work. Claiming adds that person to the field, so the activity has one owner and nobody else in the role can act on it.

This guide adds a `reviewer` assignment to the `sign-off` activity of the quick start’s `article-review` workflow. The review starts assigned to the `editor` role. An editor claims it from a script, only the claimer can approve, and the claimer can release it back to the role.

## 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`). Run the claim script with a `SANITY_AUTH_TOKEN` that belongs to the person claiming, such as their personal token: the engine acts as the token’s identity, so the claim records whoever owns the token. [Authentication and tokens](https://www.sanity.io/docs/content-lake/http-auth) explains how to create one.
- A project member with the `editor` role. The `roles` constraint needs a dataset workflow resource, as in the quick start’s `dev` deployment, and read access to the project member directory, because eligibility checks look up each person’s project roles.

## Add an assignment to the review

Assignment fields count for `$assigned` only at activity scope, so declare `reviewer` on `sign-off`. In `workflows/article-review.ts`, replace the `sign-off` activity:

**workflows/article-review.ts**

```typescript
export const articleReview = defineWorkflow({
  // ...
  stages: [
    // drafting is unchanged
    defineStage({
      name: 'review',
      // ...
      activities: [
        defineActivity({
          name: 'sign-off',
          title: 'Review the article',
          fields: [
            defineField({
              type: 'assignee',
              name: 'reviewer',
              title: 'Reviewer',
              roles: ['editor'],
              initialValue: {type: 'literal', value: [{type: 'role', role: 'editor'}]},
              editable: "count($fields.reviewer[@.type == 'user']) == 0 || $assigned",
            }),
          ],
          actions: [
            defineAction({
              name: 'approve',
              title: 'Approve',
              filter: "count($fields.reviewer[@.type == 'user']) > 0 && $assigned",
              status: 'done',
            }),
          ],
        }),
      ],
      // ...
    }),
    // approved is unchanged
  ],
})
```

An `assignee` field stores an ordered list with at most one person and any number of roles; `assignees` allows several people. An empty assignment is `[]`. The literal initial value assigns the review to the `editor` role.

`$assigned` is true for the caller when the field lists them. When the field lists no person, callers match through the listed roles. Once a person is listed, only that person matches. Workflow- and stage-scope assignment fields do not affect `$assigned`.

`roles` controls who can be added to the field, and nothing else. It does not make the field editable, grant access to an action, or change `$assigned`. A person is eligible when their project membership includes a listed role, directly or through a role alias. A role entry is eligible only when it matches a listed role exactly. Without `roles`, any valid person or role is accepted.

The `editable` condition lets anyone edit the field while no person holds it, and only the holder once someone does. The `approve` filter requires a person in the field and requires the caller to be that person.

> [!TIP]
> Let administrators claim too
> Role matching is literal. An administrator who does not hold the `editor` role cannot claim this review. Add `roleAliases: {editor: ['administrator']}` to the definition to let the `administrator` role fulfill `editor`.

## Deploy and move an instance to review

The quick start’s deployment already acknowledges reader model 10, which covers singular `assignee` fields (model 9) and role-restricted assignments (model 8). If other runtimes share the workflow resource, see [Upgrade Workflows packages](https://www.sanity.io/docs/workflows/upgrade) first.

**npm**

```shell
npx sanity-workflows deploy
npx sanity-workflows start article-review \
  --field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}'
npx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
```

**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"}'
pnpm dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
```

**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"}'
yarn dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
```

**bun**

```shell
bunx sanity-workflows deploy
bunx sanity-workflows start article-review \
  --field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}'
bunx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
```

The instance is in `review`. Run `npx sanity-workflows fire-action INSTANCE_ID`: no action is listed under `Actions on stage 'review'`, because the filter hides `approve` until someone claims the review.

## Claim the review

Create `scripts/claim-review.ts`. It reads the caller’s identity from an evaluation, appends that person to `reviewer` with `engine.editField`, and prints the activity’s fields:

**scripts/claim-review.ts**

```typescript
import {engine} from '../../scripts/engine'

const instanceId = process.argv[2]
if (!instanceId) throw new Error('Usage: npx tsx scripts/claim-review.ts INSTANCE_ID')

const target = {scope: 'activity', activity: 'sign-off', field: 'reviewer'} as const

const {actor} = await engine.evaluate({instanceId})
const result = await engine.editField({
  instanceId,
  target,
  mode: 'append',
  value: {type: 'user', id: actor.id},
})

const review = result.instance.stages.at(-1)
const signOff = review?.activities?.find((activity) => activity.name === 'sign-off')
console.log(JSON.stringify(signOff?.fields, null, 2))
```

`mode: 'append'` adds one entry and keeps the `editor` role. `target` names the activity because the field is activity-scoped. The engine checks the `editable` condition and the `roles` list before it writes, records the edit in history, and re-evaluates the instance.

**npm**

```shell
npx tsx scripts/claim-review.ts INSTANCE_ID
```

**pnpm**

```shell
pnpm dlx tsx scripts/claim-review.ts INSTANCE_ID
```

**yarn**

```shell
yarn dlx tsx scripts/claim-review.ts INSTANCE_ID
```

**bun**

```shell
bunx tsx scripts/claim-review.ts INSTANCE_ID
```

## Verify the claim

The script prints the stored field. The editor role is still listed, and the claimer is the one person:

**CLI**

```json
[
  {
    "_key": "c3414f255ffa",
    "_type": "assignee",
    "name": "reviewer",
    "title": "Reviewer",
    "value": [
      {
        "type": "role",
        "role": "editor"
      },
      {
        "type": "user",
        "id": "USER_ID",
        "_key": "e93940d9d08d"
      }
    ],
    "roles": [
      "editor"
    ]
  }
]
```

Run `npx sanity-workflows fire-action INSTANCE_ID` with the same token. It now lists `✔ sign-off → approve  (Approve) [activity active]`. With another editor’s token the list is empty, and the evaluation explains why: `approve` carries `holderGate` with the claimer as holder.

Edits that break the rules are rejected before anything is written:

- A person without the `editor` role: `Field entry item shape invalid for "reviewer" (assignee): user "USER_ID" does not fulfill any eligible role: "editor"`.
- A second editor while the review is held: `Field "activity:sign-off.reviewer" is not editable: editor not permitted (count($fields.reviewer[@.type == 'user']) == 0 || $assigned)`.

Approve as the claimer, and the instance moves to `approved`:

**npm**

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

**pnpm**

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

**yarn**

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

**bun**

```shell
bunx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approve
```

## Release the review

To hand a held review back to the role, replace the value. Create `scripts/release-review.ts`:

**scripts/release-review.ts**

```typescript
import {engine} from '../../scripts/engine'

const instanceId = process.argv[2]
if (!instanceId) throw new Error('Usage: npx tsx scripts/release-review.ts INSTANCE_ID')

await engine.editField({
  instanceId,
  target: {scope: 'activity', activity: 'sign-off', field: 'reviewer'},
  value: [{type: 'role', role: 'editor'}],
})
console.log('Released the review to the editor role')
```

The default `set` mode replaces the whole list, restoring the `editor` role assignment. To clear every member, use `mode: 'unset'` or set `[]`; the stored value becomes `[]`. A replacement can keep members who no longer qualify for `roles`, and removing or reordering them is allowed. Adding a member, or a second copy of one, requires current eligibility.

## Let any editor approve without claiming

To let any matching role member approve without claiming first, use `filter: '$assigned'` on `approve`. To keep the action visible but disabled until someone claims, move the check into a requirement:

**workflows/article-review.ts**

```typescript
defineActivity({
  name: 'sign-off',
  // ...
  requirements: [
    {
      type: 'groq',
      name: 'claimed',
      title: 'Claim the review first',
      query: "count($fields.reviewer[@.type == 'user']) > 0",
    },
  ],
  actions: [defineAction({name: 'approve', title: 'Approve', filter: '$assigned', status: 'done'})],
})
```

With the requirement, `fire-action INSTANCE_ID` shows `✖ sign-off → approve` with `disabled: unmet requirement(s): claimed` until someone claims. Filters, requirements, and `editable` conditions are advisory; [Actors and enforcement](https://www.sanity.io/docs/workflows/actors-and-enforcement) explains what enforces a rule.

## Next steps

- Let editors claim from Sanity Studio in [Add Workflows to Studio](https://www.sanity.io/docs/workflows/studio-plugin).
- Declare and seed other fields in [Configure workflow fields](https://www.sanity.io/docs/workflows/fields).
- Look up `assignee`, `assignees`, and `roles` in [Field types](https://www.sanity.io/docs/workflows/workflow-authoring-reference) and `$assigned` in [Context-specific variables](https://www.sanity.io/docs/workflows/workflow-authoring-reference).
- Check reader-model floors for assignment features in [Upgrade Workflows packages](https://www.sanity.io/docs/workflows/upgrade).

> [!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).



