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. For how definitions behave, see Definitions and instances. For the stored instance document and evaluation output, see the Engine API reference.

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

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

  • Requiredname

    string

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

  • Requiredtitle

    string

    Required. Non-empty display name.

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

  • RequiredinitialStage

    string

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

  • Requiredstages

    Stage[]

    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. For runtime, see Run workflows with Functions.

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 for behavior.

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',
    },
  ],
}
  • 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.

  • Requiredtype

    'groq' | 'singleSubject'

    Required. Selects the check.

  • Requiredname

    string

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

  • title

    string

    Optional. Editor-facing label.

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

  • Requiredname

    string

    Required. Stable identity inside the definition.

  • title

    string

    Optional. Display label.

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

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.

  • Requiredname

    string

    Required. Stable identity within the source stage.

  • title

    string

    Optional. Display label.

  • Optional. Display explanation.

  • Requiredto

    string

    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.

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.

  • Requiredname

    string

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

  • Requiredtype

    string

    Required. A field type or an authoring convenience.

  • title

    string

    Optional. Display label.

  • 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. For routing, eligibility, and Studio controls, see Configure fields.

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.

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

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

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

  • Requiredname

    string

    Required. Stable activity name.

  • title

    string

    Optional. Display label.

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

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.

  • Requiredname

    string

    Required. Stable action name.

  • title

    string

    Optional. Display label.

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

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 lists every property, including bindings, outputs, retry, and runtime.

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.

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

  • 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:

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:

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

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:

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:

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.

  • 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:

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.

  • Requiredname

    string

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

  • title

    string

    Optional. Display label.

  • Optional. Display explanation.

  • Requiredmatch

    {actions, types?, idRefs?, idPatterns?}

    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?}.

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.

  • RequiredforEach

    string

    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.

  • Requireddefinition

    {name: string, version?: number | 'latest'}

    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.

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.

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.

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.

Visiting agent?

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme