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

# Workflow authoring reference

Reference for Sanity Workflows authoring declarations, condition variables, decision sites, operations, guards, and subworkflow spawns.

This page lists every property the `define*` helpers in `@sanity/workflow-engine/define` accept, with the GROQ variables each condition site can read. For full TypeScript signatures, see [@sanity/workflow-engine/define on reference.sanity.io](https://reference.sanity.io/_sanity/workflow-engine/define/). For how definitions behave, see [Definitions and instances](https://www.sanity.io/docs/workflows/definitions-and-instances). For the stored instance document and evaluation output, see the [Engine API reference](https://www.sanity.io/docs/workflows/engine).

A minimal definition with one working stage, one activity, one action, and one transition:

**workflows/article-review.ts**

```typescript
import {
  defineAction,
  defineActivity,
  defineField,
  defineStage,
  defineTransition,
  defineWorkflow,
} from '@sanity/workflow-engine/define'

export const articleReview = defineWorkflow({
  name: 'article-review',
  title: 'Article review',
  initialStage: 'drafting',
  fields: [defineField({type: 'subject', name: 'subject', required: true, initialValue: {type: 'input'}})],
  stages: [
    defineStage({
      name: 'drafting',
      activities: [
        defineActivity({
          name: 'write',
          actions: [defineAction({name: 'submit', status: 'done'})],
        }),
      ],
      transitions: [defineTransition({name: 'to-approved', to: 'approved'})],
    }),
    defineStage({name: 'approved'}),
  ],
})
```

## Workflow definition

`defineWorkflow(...)` validates the definition, expands authoring shorthand, and returns the stored definition you deploy. It throws one error that lists every validation issue with its path.

### Definition properties

#### Properties

**name** (string, required)

Required. Stable identity used in deployed document IDs and subworkflow references. Lowercase letters, digits, and dashes, starting with a letter or digit.

**title** (string, required)

Required. Non-empty display name.

**description** (string)

Optional. Display explanation.

**semantics** (Semantic[])

Optional. Advisory meaning that tooling can read and execution ignores: signal.positive, signal.caution, signal.critical, or custom.<camelCase>, at most one per namespace.

**initialStage** (string, required)

Required. The stage entered when an instance starts. Must name a declared stage.

**stages** (Stage[], required)

Required. One or more stage declarations.

**fields** (FieldEntry[])

Optional. Workflow-scope fields that live for the instance lifetime.

**groups** (Group[])

Optional. Presentation groups that fields, activities, and actions may join with group. Each is {name, title?, description?, kind?}; kind is 'core' or 'details'. Advisory: the engine validates names and never acts on groups.

**predicates** (Record<string, string>)

Optional. Named conditions, each pre-evaluated and bound as the boolean $name. A predicate can read the variables marked always in the variables table, not caller variables or another predicate, and cannot reuse a built-in variable name.

**roleAliases** (Record<string, string[]>)

Optional. Roles that fulfill each authored role. Aliases widen action roles, $assigned, and assignment eligibility; they never change an actor's roles. The "*" key lists universal fulfillers.

**lifecycle** ('standalone' | 'child')

Optional. Default 'standalone'. 'child' marks a spawn-only definition. Advisory: start pickers hide child definitions, but startInstance doesn't refuse one. A child definition can't declare start.

**start** (AuthoringStartBlock)

Optional. Discovery and readiness rules for fresh standalone starts. See the start block section.

**abort** ({roles: string[]})

Optional. Limits who may call abortInstance, with roleAliases applied. A caller it doesn't admit gets AbortDeniedError, administrators included. Omit it to let anyone who can write the instance abort it. Subworkflows stopped by their parent and instances aborted by deleteDefinition are not checked. Requires expectedMinReaderModel 11.

**runtime** ({kind: 'function' | 'durableFunction' | 'selfHosted'})

Optional. Authoring-only hosting hint for the generated runtime, overriding the deployment's kind. Deploy strips it.

Reader-model requirements such as the one on `abort` are explained in [Upgrade packages](https://www.sanity.io/docs/workflows/upgrade). For `runtime`, see [Run workflows with Functions](https://www.sanity.io/docs/workflows/sanity-functions).

```typescript
defineWorkflow({
  name: 'article-review',
  title: 'Article review',
  initialStage: 'drafting',
  predicates: {legalCleared: '$fields.legalApproved == true'},
  roleAliases: {editor: ['administrator']},
  abort: {roles: ['editor']},
  // fields and stages
})
```

### Start block

The optional `start` block configures discovery and fresh standalone starts. Resuming a start and spawning a child don't rerun it. See [Start filters and requirements](https://www.sanity.io/docs/workflows/conditions) for behavior.

```typescript
start: {
  kind: 'interactive',
  filter: "status == 'draft'",
  requirements: [
    {
      type: 'singleSubject',
      name: 'one-open-review',
      title: 'Review already in progress',
    },
    {
      type: 'groq',
      name: 'article-ready',
      title: 'Article is not ready',
      query: '$fields.ready == true',
    },
  ],
}
```

#### Properties

**kind** ('interactive' | 'autonomous')

Optional. Default 'interactive'. Classifies who normally starts the workflow. It doesn't gate startInstance.

**filter** (string)

Optional. GROQ visibility predicate evaluated against a candidate document during discovery. It doesn't gate startInstance. Root document reads require a subject field, including implicit reads through references(), sanity::versionOf(), and sanity::partOfRelease(). Use $now for time; clock functions are rejected.

**requirements** (StartRequirement[])

Optional. Ordered named checks enforced before a fresh standalone start. Every one must pass.

### Start requirements

Each entry in `start.requirements` has a `type`. `groq` checks the supplied inputs. `singleSubject` permits at most one unfinished run of this definition, across deployed versions, for the same subject; it needs a workflow `subject` field whose `initialValue` is `{type: 'input'}`. See [Allow one active workflow per subject](https://www.sanity.io/docs/workflows/conditions).

#### Properties

**type** ('groq' | 'singleSubject', required)

Required. Selects the check.

**name** (string, required)

Required. Unique within the requirements array. Verdicts identify the requirement by this name.

**title** (string)

Optional. Editor-facing label.

**description** (string)

Optional. Editor-facing explanation.

**query** (string)

Required when type is 'groq'. Condition over the start input context. It can't read a root document, including through implicit document functions or escaped projection scopes. $fields holds raw inputs, so document references are {id, type}. Use $now or dateTime($now) instead of clock functions.

## Stage declaration

Pass this object to `defineStage(...)` in a workflow's `stages` array. A stage with no transitions is terminal: entering it completes the instance, and it can't declare activities.

#### Properties

**name** (string, required)

Required. Stable identity inside the definition.

**title** (string)

Optional. Display label.

**description** (string)

Optional. Display explanation.

**semantics** (Semantic[])

Optional. Advisory meaning that tooling can read and execution ignores: signal.positive, signal.caution, signal.critical, or custom.<camelCase>, at most one per namespace.

**fields** (FieldEntry[])

Optional. Fields resolved on entry and kept with this stage visit.

**activities** (Activity[])

Optional. Units of work for the stage. Not allowed on a terminal stage.

**transitions** (Transition[])

Optional. Ordered routes to other stages. Omit them to make the stage terminal.

**guards** (Guard[])

Optional. Content mutation guards registered on entry and retracted on exit.

**groups** (Group[])

Optional. Presentation groups local to the stage.

**editable** (Record<string, true | string | string[]>)

Optional. Tighten-only override keyed by in-scope field name. Each entry is ANDed with the field's own editable, so a stage can narrow editing while it is current but never open a field the baseline leaves closed. Each key must name a field of this stage that declares editable.

```typescript
defineStage({
  name: 'review',
  editable: {legalApproved: ['legal']},
  activities: [/* ... */],
  transitions: [defineTransition({name: 'to-approved', to: 'approved'})],
})
```

## Transition declaration

Pass this object to `defineTransition(...)` in a stage's `transitions` array. It declares a one-way route from that stage to another stage, without operations or effects.

#### Properties

**name** (string, required)

Required. Stable identity within the source stage.

**title** (string)

Optional. Display label.

**description** (string)

Optional. Display explanation.

**to** (string, required)

Required. Name of the destination stage.

**when** (string)

Optional. Default $allActivitiesDone; use 'true' for an unconditional route. Transitions are checked in declaration order and the first true fires. A false result moves on to the next transition; an unevaluable result, such as GROQ null, stops selection.

```typescript
defineTransition({
  name: 'to-approved',
  to: 'approved',
  when: '$allActivitiesDone && $fields.legalApproved == true',
})
```

## Field declaration

Pass this object to `defineField(...)` in the `fields` array of a workflow, stage, or activity. That location decides the field's scope: workflow fields initialize at start, and stage and activity fields initialize on each stage visit.

Workflow fields resolve when the instance starts and last for its lifetime. Stage and activity fields resolve on stage entry and belong to that stage visit or activity. Conditions see the fields in scope as `$fields`, where a narrower scope’s field shadows a wider one with the same name. An operation or field edit that omits `scope` resolves the nearest declaration: activity, then stage, then workflow. Field-read initial values follow their own scope rules; see the `fieldRead` row.

#### Properties

**name** (string, required)

Required. Field name, read as $fields.<name>. Letters, digits, and underscores, not starting with a digit.

**type** (string, required)

Required. A field type or an authoring convenience.

**title** (string)

Optional. Display label.

**description** (string)

Optional. Display explanation.

**group** (string | string[])

Optional. Group or groups to join. The stored definition normalizes membership to string[].

**initialValue** (FieldSource)

Optional. Source resolved when the field enters scope. Omit it to start empty (null or []) and let operations fill the field.

**editable** (true | string | string[])

Optional. Default: not editable. true opens direct editing to anyone in the field's scope window, a string is a condition, and a role list desugars to a role-membership condition. Advisory; a guard declares a write lock.

**required** (boolean)

Optional. Default false. Valid only on workflow-scope fields with initialValue: {type: 'input'}. Requires a non-null value at start or spawn. On subject, doc.ref, and doc.refs, selected documents must also stay readable in the workflow perspective; a missing target blocks actions and transitions.

**types** (string[])

Optional. Accepted document types, on subject, doc.ref, and doc.refs only. Omit it to accept any type, except that a subject without types excludes sanity.* and system.* types. List a reserved type to accept it. Engine-owned types are never valid subjects.

**roles** (string[])

Optional. Non-empty project roles that new assignment members must fulfill. Valid on assignee and assignees only. Requires expectedMinReaderModel 8, or 9 on an assignee field.

**options** ({list: Array<{title: string, value: string | number}>})

Optional. Closed choice list for string, text, number, url, date, dueDate, datetime, and dueDatetime. Non-empty, with unique values and non-empty titles. Stored non-null values must equal one listed value.

**validation** ({min?: number, max?: number})

Optional. Inclusive bounds: character counts for string and text, values for number, and a narrower range inside 0 to 100 for progress. Declare at least one bound. Other types reject validation.

**fields** (FieldShape[])

Required on object, invalid elsewhere. Nested shapes, each accepting type, name, title, description, options, validation, roles, fields, and of. Nested shapes use date and datetime, not dueDate or dueDatetime.

**of** (FieldShape[])

Required on array, invalid elsewhere. Member shape, with the same keys as fields.

Choice lists must be non-empty, with unique values and non-empty titles, and every value must match the field type and its validation bounds. Every write path checks the list: initial values, edits, operations, action parameters, and effect outputs. Validation bounds are inclusive, either bound may be omitted, and `min` must not exceed `max`. The engine validates inputs, operations, action parameters, and effect outputs before committing, so an invalid write stores nothing; a normalized query result that fails validation uses the field’s empty value and records `fieldQueryDiscarded`. Sanity Studio shows an inline error on editable fields and blocks a start or action while an input is invalid.

For `roles` and other reader-model requirements, see [Upgrade packages](https://www.sanity.io/docs/workflows/upgrade). For routing, eligibility, and Studio controls, see [Configure fields](https://www.sanity.io/docs/workflows/fields).

```typescript
defineField({
  type: 'string',
  name: 'priority',
  editable: ['editor'],
  initialValue: {type: 'literal', value: 'normal'},
  options: {list: [{title: 'Normal', value: 'normal'}, {title: 'Urgent', value: 'urgent'}]},
})
```

### Field types

Canonical values accepted by a field declaration’s `type`.

#### Properties

**subject** (GlobalDocumentReference | null)

The workflow's primary document. Workflow scope only; at most one. Document-aware integrations use it to discover the workflow for that document. Without types, accepts ordinary content types and excludes reserved namespaces. Explicit types may include reserved types, but never engine-owned documents.

**doc.ref** (GlobalDocumentReference | null)

One resource-qualified document reference. Optional types restricts accepted document types.

**doc.refs** (GlobalDocumentReference[])

An array of resource-qualified document references. Optional types applies to every member.

**release.ref** (ReleaseRef | null)

A reference to a Content Release, including its release name.

**string** (string | null)

A short string value.

**text** (string | null)

A multiline string value.

**number** (number | null)

A numeric value.

**progress** (number | null)

Completion from 0 through 100; fractions are allowed. Studio renders it as progress, and an effect handler can update it with ctx.setProgress(). Validation may narrow the range; choice options are not supported.

**boolean** (boolean | null)

A boolean value.

**date** (YYYY-MM-DD | null)

A date-only YYYY-MM-DD value. Studio gives any editable date field a date picker. Use this kind for ordinary dates and for nested dates, including the dueDate value inside each todoList row.

**dueDate** (YYYY-MM-DD | null)

The deadline for one workflow, stage, or activity scope. It stores and validates the same value as date, supports options, and uses the same Studio date controls. A scope may declare at most one dueDate or dueDatetime field. The field must be top-level and is optional unless required is set.

**datetime** (ISO-8601 string | null)

An ISO-8601 timestamp. Studio gives any editable datetime field a date-and-time picker. Use this kind for ordinary timestamps and nested timestamp fields.

**dueDatetime** (ISO-8601 string | null)

The deadline, including time, for one workflow, stage, or activity scope. It stores and validates the same value as datetime, supports options, and uses the same Studio date-and-time controls. A scope may declare at most one dueDate or dueDatetime field. The field must be top-level and is optional unless required is set.

**url** (string | null)

A URL string.

**actor** (Actor | null)

An actor identity and its roles, for recording who performed work.

**assignee** (Assignee[])

An ordered assignment list with at most one person and any number of role members. Empty is []. User IDs are account-global human sanityUserId values; robots cannot be assignment members.

**assignees** (Assignee[])

An ordered list of people or collective project roles. User IDs are account-global human sanityUserId values; robots cannot be assignment members.

**object** (Record<string, unknown> | null)

A compositional object. Declare its nested shape with fields.

**array** (Record<string, unknown>[])

An array of compositional objects. Declare the member shape with of.

Subject restrictions: a subject without `types` accepts ordinary content types and excludes names beginning with `sanity.` or `system.`; list a reserved type explicitly to accept it. This default also applies when a new engine reads an older stored definition, and it affects discovery only: a direct `startInstance` call does not add a document-type check for an unscoped subject. Listing an engine-owned type (`sanity.workflow.instance`, `sanity.workflow.definition`, `temp.system.guard`) in `subject.types` fails `defineWorkflow` and deploy validation; use `spawn` to coordinate workflow instances. Due dates: `dueDate` and `dueDatetime` are not valid as nested object or array fields or as effect outputs; use `date` or `datetime` there. A `todoList` row’s `dueDate` is a property name; its type is `date`. Both due-date types are model 5 and do not raise the reader floor, but an older engine can still reject the unfamiliar type.

### Initial-value sources

Set one of these objects as a field's `initialValue`. The engine resolves it once, when the field enters scope. A source doesn't make the field editable.

#### Properties

**input** ({type: 'input'})

Takes a workflow-scope value supplied at start or by a parent's spawn. Sanity Studio shows a start control for each unmapped workflow input. Stage and activity input seeds receive no value, and an activity input seed produces a deploy warning.

**literal** ({type: 'literal', value})

Uses a fixed value authored in the definition.

**query** ({type: 'query', query})

Runs GROQ against the Content Lake when the field enters scope, with earlier fields in that scope as $fields. Reference normalization can drop unrecognized values. A normalized result that fails validation stores the field's empty value and records fieldQueryDiscarded. A failed query throws.

**fieldRead** ({type: 'fieldRead', field, scope?, path?})

Copies an earlier field in the same scope, optionally selecting path. A stage or activity seed can set scope: 'workflow' to read workflow fields; an activity seed can't read stage fields. Doesn't load referenced documents.

### Authoring conveniences

Use these names as the `type` in `defineField(...)`. The definition compiler expands each convenience into canonical fields.

#### Properties

**todoList** (array field)

An array of rows shaped {label, status, assignee?, dueDate?}: label and status are strings, assignee is an assignee member list, and dueDate is a date that Studio deadline controls use.

**notes** (array field)

An array of rows shaped {body, actor, at} (text, actor, datetime). The names match the stamps the audit operation adds, so audit rows fit. Append-only is a convention, not an enforced type.

## Activities and actions

An activity is a unit of work in a stage. Actions on the activity carry every operation, effect, and spawn.

### Activity declaration

This is the object passed to `defineActivity(...)` in a stage’s `activities` array.

#### Properties

**name** (string, required)

Required. Stable activity name.

**title** (string)

Optional. Display label.

**description** (string)

Optional. Display explanation.

**semantics** (Semantic[])

Optional. Advisory meaning that tooling can read and execution ignores: signal.positive, signal.caution, signal.critical, or custom.<camelCase>, at most one per namespace.

**groups** (Group[])

Optional. Groups declared for this activity's fields and actions.

**group** (string | string[])

Optional. Groups this activity joins.

**target** ({type: 'url', url} | {type: 'field', field})

Optional. Deep link for work done outside Workflows. url must be http or https. field names a doc.ref, doc.refs, subject, or release.ref field reachable from the activity. Render-only; never gates.

**filter** (string)

Optional. Existence condition evaluated once at stage entry. A definite false records the activity as skipped, hides it, and keeps it from blocking $allActivitiesDone.

**requirements** (GroqRequirement[])

Optional. Ordered named readiness checks, each {type: 'groq', name, title?, description?, query}. An unmet one keeps the activity visible and disables its caller-fired actions with requirements-unmet.

**actions** (Action[])

Optional. Actions that resolve the work or change instance state.

**fields** (FieldEntry[])

Optional. Activity-scope fields resolved at stage entry.

```typescript
defineActivity({
  name: 'legal-check',
  filter: '$fields.needsLegal == true',
  requirements: [{type: 'groq', name: 'assigned', title: 'Only the assignee can sign off', query: '$assigned'}],
  fields: [defineField({type: 'assignees', name: 'assignees', editable: true})],
  actions: [defineAction({name: 'clear', status: 'done'})],
})
```

### Action declaration

Pass this object to `defineAction(...)` in an activity's `actions` array. Without `when`, a caller fires the action with `fireAction`, and a UI can show it as a control. With `when`, the engine fires it automatically.

#### Properties

**name** (string, required)

Required. Stable action name.

**title** (string)

Optional. Display label.

**description** (string)

Optional. Display explanation.

**semantics** (ActionSemantic[])

Optional. Advisory meaning: decision.accept, decision.decline, signal.positive, signal.caution, signal.critical, or custom.<camelCase>, at most one per namespace. Decision values are action-only. Execution ignores semantics.

**group** (string | string[])

Optional. Groups this action joins.

**when** (string)

Optional. When present, the engine fires the action as soon as the condition turns true, at most once per stage visit, and fireAction rejects it. Use 'true' to fire on stage entry.

**filter** (string)

Optional. Existence condition. A non-matching action is hidden, not disabled. With when, filter decides whether the automation exists and when triggers it.

**roles** (string[])

Optional. On a caller-fired action, desugars to a role-membership condition ANDed with filter. On an action with when, it is stored as is and limits which identities may run the trigger. roleAliases applies either way.

**params** (ActionParam[])

Optional. Caller-supplied values, each {name, type, title?, description?, required?, options?, validation?}. type is string, number, boolean, url, dateTime, actor, doc.ref, doc.refs, or json. A missing required param throws ActionParamsInvalidError and nothing commits.

**ops** (Operation[])

Optional. Instance mutations committed with the firing.

**effects** (Effect[])

Optional. External work queued by the firing.

**spawn** (Subworkflows)

Optional. Child workflows created by the firing.

**status** ('done' | 'skipped' | 'failed')

Optional. Shorthand for a final status.set on the firing activity, appended after ops. Without it, the activity stays active.

```typescript
defineAction({
  name: 'escalate',
  when: "dateTime($now) > dateTime($fields.dueDate) + 60*60*24",
  roles: ['editor'],
  ops: [defineOp({type: 'field.set', target: {field: 'escalated'}, value: {type: 'literal', value: true}})],
})
```

### Effect declaration

Pass this object to `defineEffect(...)` in an action's `effects` array. The engine queues it when the action commits, and a handler registered under its `name` runs it. The [Engine API reference](https://www.sanity.io/docs/workflows/engine) lists every property, including `bindings`, `outputs`, `retry`, and `runtime`.

```typescript
defineAction({
  name: 'notify',
  when: 'true',
  effects: [
    defineEffect({
      name: 'notify-reviewer',
      bindings: {article: '$fields.subject._id'},
      outputs: [{name: 'messageId', type: 'string'}],
    }),
  ],
})
```

## Operations and value expressions

Operations change instance state in the same commit as the action that carries them.

### Operation catalogue

Use these objects in an action’s `ops` array with `defineOp(...)`. Successful effect completions may return the eight `field.*` forms directly as runtime result data.

#### Properties

**field.set** (Any field)

Replaces the field value with a resolved value expression. Runtime-written references must target a declared resource.

**field.setIfMissing** (Nullable field)

Sets a resolved value only when the current nullable value is absent or null. List fields are always arrays and are rejected.

**field.inc** (number)

Adds a resolved finite numeric delta to the current value and rejects a non-finite result. The delta defaults to 1.

**field.dec** (number)

Subtracts a resolved finite numeric delta from the current value and rejects a non-finite result. The delta defaults to 1.

**field.unset** (Any field)

Clears the field value.

**field.append** (List field)

Appends a resolved value to a list field. The value must match the declared member shape.

**field.updateWhere** (array)

Merges an object into rows selected by where. Valid only for array fields; merged rows must retain the declared shape, and the merge cannot write _key or _type.

**field.removeWhere** (Any list field)

Removes rows selected by where. Valid for every list field, including doc.refs.

**status.set** (Activity)

Sets an activity to active, done, skipped, or failed. Available to actions, not effect completions; it defaults to the firing activity but may name a sibling activity.

**audit** (List field)

Authoring shorthand for field.append that adds actor and at stamps to an object value. Rename the stamps with stampFields: {actor?, at?}; a stamp that collides with an authored key is rejected.

Set one field from an action parameter and clear another:

```typescript
defineAction({
  name: 'change-deadline',
  params: [{name: 'deadline', type: 'dateTime', required: true}],
  ops: [
    defineOp({
      type: 'field.set',
      target: {field: 'deadline'},
      value: {type: 'param', param: 'deadline'},
    }),
    defineOp({type: 'field.unset', target: {field: 'reminderSentAt'}}),
  ],
})
```

Seed a counter only when it is missing, then increment it:

```typescript
defineAction({
  name: 'retry',
  ops: [
    defineOp({
      type: 'field.setIfMissing',
      target: {field: 'attempts'},
      value: {type: 'literal', value: 0},
    }),
    defineOp({type: 'field.inc', target: {field: 'attempts'}}),
  ],
})
```

A skipped `field.setIfMissing` writes no `opApplied` history row. Arithmetic is not idempotent on its own, so supply an idempotency key when a caller may retry.

Count before dispatch, retry only while the counter is below the limit, and route the final failure to manual recovery. The shipped boundedRetry example makes three total attempts and never queues a fourth effect.

`field.inc`, `field.dec`, and `field.setIfMissing` are model 6 operations, and they do not raise the reader floor. An older reader still accepts the instance, but does not recognize them. See [Upgrade Workflows packages](https://www.sanity.io/docs/workflows/upgrade) before deploying a definition that uses one.

Append a row that records the note, the actor, and the time. The `audit` shorthand writes the same kind of stamped row:

```typescript
defineAction({
  name: 'add-note',
  params: [{name: 'body', type: 'string', required: true}],
  ops: [
    defineOp({
      type: 'field.append',
      target: {field: 'notes'},
      value: {
        type: 'object',
        fields: {
          body: {type: 'param', param: 'body'},
          actor: {type: 'actor'},
          at: {type: 'now'},
        },
      },
    }),
  ],
})
```

Update and remove matching rows. `where` is evaluated once per row with `$row` bound to it:

```typescript
defineAction({
  name: 'complete-item',
  params: [{name: 'itemKey', type: 'string', required: true}],
  ops: [
    defineOp({
      type: 'field.updateWhere',
      target: {field: 'todoList'},
      where: '$row._key == $params.itemKey',
      value: {
        type: 'object',
        fields: {
          status: {type: 'literal', value: 'done'},
        },
      },
    }),
    defineOp({
      type: 'field.removeWhere',
      target: {field: 'watchers'},
      where: '$row.id == $actor.id',
    }),
  ],
})
```

A successful effect completion returns the `field.*` forms directly. Each target names its `workflow` or `stage` scope, and `status.set` is not allowed:

```typescript
return {
  ops: [
    {
      type: 'field.set',
      target: {scope: 'stage', field: 'generatedDocument'},
      value: {type: 'literal', value: documentRef},
    },
  ],
}
```

### Value expressions

Set one of these serializable objects as an operation’s `value`. The engine resolves it when the operation executes.

#### Properties

**literal** ({type: 'literal', value})

Uses a fixed value authored in the definition or supplied by a trusted completion result.

**fieldRead** ({type: 'fieldRead', field, scope?, path?})

Reads a field or one of its nested paths. Without scope, it searches activity, stage, then workflow fields. Doesn't fetch documents.

**param** ({type: 'param', param})

Reads a parameter supplied to the firing action.

**actor** ({type: 'actor'})

Uses the acting identity.

**now** ({type: 'now'})

Uses the shared timestamp for the engine operation.

**self** ({type: 'self'})

Uses the current instance's global document reference.

**stage** ({type: 'stage'})

Uses the current stage name.

**object** ({type: 'object', fields})

Builds an object recursively from other value expressions.

Combine value expressions into one object:

```typescript
const recordDecision = defineOp({
  type: 'field.set',
  target: {field: 'decision'},
  value: {
    type: 'object',
    fields: {
      approved: {type: 'fieldRead', field: 'approved'},
      reason: {type: 'param', param: 'reason'},
      decidedBy: {type: 'actor'},
      decidedAt: {type: 'now'},
    },
  },
})
```

## Guard declaration

Pass this object to `defineGuard(...)` in a stage's `guards` array. The guard is active for that stage visit. Guard predicates are not workflow conditions: they are delta-mode GROQ over the attempted mutation. See [Hold publishing with guards](https://www.sanity.io/docs/workflows/guards).

#### Properties

**name** (string, required)

Required. Unique across the definition. Lowercase letters, digits, and dashes, starting with a letter or digit.

**title** (string)

Optional. Display label.

**description** (string)

Optional. Display explanation.

**match** ({actions, types?, idRefs?, idPatterns?}, required)

Required. actions lists at least one of create, update, delete, publish, unpublish. types intersects the ID criteria; matching either idRefs (typed reads) or idPatterns (resource-local IDs with *) is enough. Invalid patterns, and release-version patterns that would broaden to every document, are rejected before deploy.

**predicate** (string)

Optional. Delta-mode GROQ over document.before, document.after, mutation.action, and guard.metadata, with identity() and -> on stored resource-local references. Only a strict true allows a matching mutation. If you omit `predicate`, every mutation that `match` selects is denied.

**metadata** (Record<string, GuardRead>)

Optional. Values resolved when the guard is created or refreshed and exposed as guard.metadata. A read is {type: 'self'}, {type: 'now'}, {type: 'fieldRead', field, path?} (workflow fields only), or {type: 'effectsRead', effect, path?}.

```typescript
defineGuard({
  name: 'freeze-copy',
  match: {idRefs: [{type: 'fieldRead', field: 'subject'}], actions: ['update']},
  predicate: '!delta::changedAny((title, body))',
})
```

## Subworkflow spawn declaration

Set this object as an action’s `spawn` property. When that action fires, the engine creates the declared child workflow instances in the same commit.

#### Properties

**forEach** (string, required)

Required. GROQ producing one row per child. Evaluated with workflow-scope $fields only and no caller variables or named predicates. Each row needs a unique identity: _key, then _id, then a GDR's id, or a scalar's string form. Missing or duplicate identities reject the spawn.

**definition** ({name: string, version?: number | 'latest'}, required)

Required. Deployed child definition. version defaults to the latest.

**with** (Record<string, string>)

Optional. Child input-field values, each evaluated per row with $row.

**context** (Record<string, string>)

Optional. Values evaluated once, without $row, and delivered to each child as $context. Results must be strings, numbers, booleans, or full GDRs; null omits the entry, and other objects reject the spawn.

**onExit** ('detach' | 'abort')

Optional. Default 'detach', which leaves live children running when their cohort leaves scope. 'abort' stops them recursively. Aborting the parent always stops its children.

```typescript
defineAction({
  name: 'translate',
  when: 'true',
  spawn: {
    forEach: '*[_id == $fields.subject._id][0].locales',
    definition: {name: 'translate-article'},
    with: {locale: '$row'},
  },
})
```

### Cohort row

Each element of `$subworkflows` has these properties. `stage` is `null` until the child is committed and hydrated.

```typescript
interface SubworkflowVarRow {
  _id: string
  rowKey: string
  activity: string
  action: string
  definition: string
  current: boolean
  stage: string | null
  status: 'active' | 'done' | 'aborted'
  spawnedAt: string
}
```

## Condition variables

Conditions are GROQ strings. Each site binds a fixed set of variables, and `defineWorkflow` and deploy reject a read of a variable the site never binds. The two tables come from `condition-scope.ts` in the engine source. For why sites differ, see [Caller-bound and caller-blind sites](https://www.sanity.io/docs/workflows/conditions).

### Variables

**Binding** says when a variable holds a value: **always** at every instance site, **caller** only when a caller rides the evaluation, **row** only per row, and **start** only in the start block.

| Variable | Type | Binding | Meaning |
| --- | --- | --- | --- |
| `$self` | `string` | always | GDR URI of the instance document. `*[_id == $self][0]` reads the instance in the snapshot. |
| `$fields` | `object` | always | Field values by name; activity fields overlay stage fields, which overlay workflow fields. A `subject` or `doc.ref` read returns the hydrated document when it is in the snapshot; `doc.refs` items and `release.ref` stay references (`{id, type}`). |
| `$parent` | `string \| null` | always | Parent instance GDR URI, `null` on a root instance. |
| `$ancestors` | `string[]` | always | Ancestor instance GDR URIs, root first. |
| `$stage` | `string` | always | Current stage name. |
| `$now` | `string` | always | ISO timestamp shared by every condition in one evaluation pass. |
| `$context` | `object` | always | Start context by entry name: values seeded at `startInstance` plus a parent's `spawn.context`. Never changes after start. |
| `$effects` | `object` | always | Completed effect outputs, `$effects['<effect>'].<output>`; the latest completed run wins. |
| `$effectStatus` | `object` | always | `'done'` or `'failed'` per effect, for runs queued during the current stage visit. Absent until the effect completes for this visit. |
| `$activities` | `object[]` | always | Current stage activity rows, with status. |
| `$allActivitiesDone` | `boolean` | always | Every current activity is `done` or `skipped`. The default transition condition. |
| `$anyActivityFailed` | `boolean` | always | Some current activity is `failed`. |
| `$subworkflows` | `object[]` | always | Every spawned child, as cohort rows. Settled gate: `count($subworkflows[activity == '<name>' && current && status == 'active']) == 0`. |
| `$<predicate>` | `boolean` | always | Each named predicate from `predicates`, pre-evaluated over the same scope. |
| `$actor` | `Actor` | caller | Acting identity, `{kind, id, roles}`; `undefined` without a caller. |
| `$assigned` | `boolean` | caller | The caller matches the activity's `assignee` or `assignees` field, by user ID or by role under `roleAliases`. Direct user members take precedence over role members. `false` outside an activity, without a caller, or on an empty assignment. |
| `$can` | `object` | caller | Advisory per-permission booleans from the caller's grants; `undefined` without grants. |
| `$attributes` | `object` | caller | Advisory organization-level User Attributes (Enterprise), the values `user::attributes()` reads in Content Lake. Fetched only at caller-bound sites and never stored. Reads at most 100 values. Unbound when absent, so a condition reading it fails closed; an unexpected fetch error stops the evaluation. |
| `$params` | `object` | caller | The fired action's arguments. Bound only while that action's effect bindings and operation `where` expressions evaluate. |
| `$row` | `any` | row | One `spawn.forEach` row while `spawn.with` evaluates, or the stored row under test in an operation `where`. |
| `$tag` | `string` | start | The engine's partition. Scope instance scans with `tag == $tag`. |
| `$definition` | `string` | start | Name of the definition being evaluated. |

### Condition sites

Each row lists the variables a site binds beyond the **always** set. **All always** means every variable bound **always**, including named predicates.

| Site | Binds | Decides |
| --- | --- | --- |
| Transition `when` | All always | Picks the first route whose condition is `true`, in declaration order. |
| Activity `filter` | All always | Whether the activity exists in this stage visit. Evaluated once at entry. |
| Action `when` | All always | Fires the action automatically when `true`. |
| Action `filter`, action with `when` | All always | Whether the automation exists. |
| Action `filter`, caller-fired action | All always, `$actor`, `$assigned`, `$can`, `$attributes` | Whether the action exists for this caller. Advisory. |
| Activity `requirements` | All always, `$actor`, `$assigned`, `$can`, `$attributes` | Whether the activity's caller-fired actions are enabled. Advisory. |
| Field `editable` and stage `editable` condition | All always, `$actor`, `$assigned`, `$can`, `$attributes` | Who may edit the field. Advisory. |
| Effect `bindings`, caller-fired action | All always, `$actor`, `$assigned`, `$params` | Values passed to the effect handler. |
| Effect `bindings`, action with `when` | All always, `$actor`, `$assigned` | Values passed to the effect handler. |
| Operation `where`, caller-fired action | `$row`, `$params`, `$actor`, `$self`, `$fields`, `$stage`, `$now`, `$context`, `$effects`, `$effectStatus`, `$activities`, `$allActivitiesDone`, `$anyActivityFailed` | Which rows the operation changes. An unevaluable row never matches. No other variable, and no named predicate. |
| Operation `where`, action with `when` | Same as caller-fired, without `$params` | Which rows the operation changes. |
| `spawn.forEach` | Always set except named predicates; `$fields` holds workflow fields only | Which children to create. |
| `spawn.with` | All always, `$row`, `$actor`, `$assigned` | Each child's input values. |
| `spawn.context` | All always, `$actor`, `$assigned` | The children's `$context`. |
| Named predicate (`predicates`) | Always set except other predicates | A reusable boolean, `$name`. |
| `start.filter` | `$tag`, `$definition`, `$now`, and the candidate document as root | Whether a start surface offers the definition for a document. Never gates `startInstance`. |
| `start.requirements` query | `$tag`, `$definition`, `$now`, `$fields` (raw inputs) | Whether a fresh standalone start may proceed. |
| Guard `predicate` | `document`, `mutation`, `guard` (not `$` variables) | Whether Content Lake allows the mutation. |

At `start.filter` and in a start requirement, `*` scans projected instances in the engine's partition. Each row has only `definition`, `subject`, and `completedAt`. Completed and aborted runs are included.

### Guard predicates

Guard predicates are not conditions. They read `document.before`, `document.after`, `mutation.action`, and `guard.metadata`; see the guard declaration and [Hold publishing with guards](https://www.sanity.io/docs/workflows/guards).

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



