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.
Early access
Workflows is in early access, built in public. Read How early access works 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:
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). Run the claim script with aSANITY_AUTH_TOKENthat 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 explains how to create one. - A project member with the
editorrole. Therolesconstraint needs a dataset workflow resource, as in the quick start’sdevdeployment, 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:
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.
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 first.
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 submitpnpm 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 submityarn 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 submitbunx 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 submitThe 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:
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.
npx tsx scripts/claim-review.ts INSTANCE_IDpnpm dlx tsx scripts/claim-review.ts INSTANCE_IDyarn dlx tsx scripts/claim-review.ts INSTANCE_IDbunx tsx scripts/claim-review.ts INSTANCE_IDVerify the claim
The script prints the stored field. The editor role is still listed, and the claimer is the one person:
[
{
"_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
editorrole: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:
npx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approvepnpm dlx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approveyarn dlx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approvebunx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approveRelease the review
To hand a held review back to the role, replace the value. Create scripts/release-review.ts:
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:
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 explains what enforces a rule.
Next steps
- Let editors claim from Sanity Studio in Add Workflows to Studio.
- Declare and seed other fields in Configure workflow fields.
- Look up
assignee,assignees, androlesin Field types and$assignedin Context-specific variables. - Check reader-model floors for assignment features in Upgrade Workflows packages.
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?