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.
description
string
Optional. Display explanation.
semantics
Semantic[]
Optional. Advisory meaning that tooling can read and execution ignores:
signal.positive,signal.caution,signal.critical, orcustom.<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?};kindis'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, butstartInstancedoesn't refuse one. A child definition can't declarestart.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, withroleAliasesapplied. A caller it doesn't admit getsAbortDeniedError, administrators included. Omit it to let anyone who can write the instance abort it. Subworkflows stopped by their parent and instances aborted bydeleteDefinitionare not checked. RequiresexpectedMinReaderModel11.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 gatestartInstance.filter
string
Optional. GROQ visibility predicate evaluated against a candidate document during discovery. It doesn't gate
startInstance. Root document reads require asubjectfield, including implicit reads throughreferences(),sanity::versionOf(), andsanity::partOfRelease(). Use$nowfor 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.
description
string
Optional. Editor-facing explanation.
query
string
Required when
typeis'groq'. Condition over the start input context. It can't read a root document, including through implicit document functions or escaped projection scopes.$fieldsholds raw inputs, so document references are{id, type}. Use$nowordateTime($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.
description
string
Optional. Display explanation.
semantics
Semantic[]
Optional. Advisory meaning that tooling can read and execution ignores:
signal.positive,signal.caution,signal.critical, orcustom.<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 declareseditable.
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.
description
string
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 firsttruefires. Afalseresult 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.
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 (
nullor[]) and let operations fill the field.editable
true | string | string[]
Optional. Default: not editable.
trueopens 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 withinitialValue: {type: 'input'}. Requires a non-null value at start or spawn. Onsubject,doc.ref, anddoc.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, anddoc.refsonly. Omit it to accept any type, except that asubjectwithouttypesexcludessanity.*andsystem.*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
assigneeandassigneesonly. RequiresexpectedMinReaderModel8, or 9 on anassigneefield.options
{list: Array<{title: string, value: string | number}>}
Optional. Closed choice list for
string,text,number,url,date,dueDate,datetime, anddueDatetime. 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
stringandtext, values fornumber, and a narrower range inside 0 to 100 forprogress. Declare at least one bound. Other types rejectvalidation.fields
FieldShape[]
Required on
object, invalid elsewhere. Nested shapes, each acceptingtype,name,title,description,options,validation,roles,fields, andof. Nested shapes usedateanddatetime, notdueDateordueDatetime.of
FieldShape[]
Required on
array, invalid elsewhere. Member shape, with the same keys asfields.
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. Explicittypesmay include reserved types, but never engine-owned documents.doc.ref
GlobalDocumentReference | null
One resource-qualified document reference. Optional
typesrestricts accepted document types.doc.refs
GlobalDocumentReference[]
An array of resource-qualified document references. Optional
typesapplies 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 recordsfieldQueryDiscarded. 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 setscope: '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
arrayof rows shaped{label, status, assignee?, dueDate?}:labelandstatusare strings,assigneeis anassigneemember list, anddueDateis adatethat Studio deadline controls use.notes
array field
An
arrayof rows shaped{body, actor, at}(text,actor,datetime). The names match the stamps theauditoperation adds, soauditrows 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.
description
string
Optional. Display explanation.
semantics
Semantic[]
Optional. Advisory meaning that tooling can read and execution ignores:
signal.positive,signal.caution,signal.critical, orcustom.<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.
urlmust be http or https.fieldnames adoc.ref,doc.refs,subject, orrelease.reffield reachable from the activity. Render-only; never gates.filter
string
Optional. Existence condition evaluated once at stage entry. A definite
falserecords the activity asskipped, 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 withrequirements-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.
description
string
Optional. Display explanation.
semantics
ActionSemantic[]
Optional. Advisory meaning:
decision.accept,decision.decline,signal.positive,signal.caution,signal.critical, orcustom.<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
fireActionrejects it. Use'true'to fire on stage entry.filter
string
Optional. Existence condition. A non-matching action is hidden, not disabled. With
when,filterdecides whether the automation exists andwhentriggers it.roles
string[]
Optional. On a caller-fired action, desugars to a role-membership condition ANDed with
filter. On an action withwhen, it is stored as is and limits which identities may run the trigger.roleAliasesapplies either way.params
ActionParam[]
Optional. Caller-supplied values, each
{name, type, title?, description?, required?, options?, validation?}.typeisstring,number,boolean,url,dateTime,actor,doc.ref,doc.refs, orjson. A missing required param throwsActionParamsInvalidErrorand 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.seton the firing activity, appended afterops. 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.
field.updateWhere
array
Merges an object into rows selected by
where. Valid only forarrayfields; merged rows must retain the declared shape, and the merge cannot write_keyor_type.field.removeWhere
Any list field
Removes rows selected by
where. Valid for every list field, includingdoc.refs.status.set
Activity
Sets an activity to
active,done,skipped, orfailed. 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.appendthat addsactorandatstamps to anobjectvalue. Rename the stamps withstampFields: {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.
description
string
Optional. Display explanation.
Requiredmatch
{actions, types?, idRefs?, idPatterns?}
Required.
actionslists at least one ofcreate,update,delete,publish,unpublish.typesintersects the ID criteria; matching eitheridRefs(typed reads) oridPatterns(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, andguard.metadata, withidentity()and->on stored resource-local references. Only a stricttrueallows 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
$fieldsonly and no caller variables or named predicates. Each row needs a unique identity:_key, then_id, then a GDR'sid, or a scalar's string form. Missing or duplicate identities reject the spawn.Requireddefinition
{name: string, version?: number | 'latest'}
Required. Deployed child definition.
versiondefaults 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;nullomits 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 |
|---|---|---|---|
|
| always | GDR URI of the instance document. |
|
| always | Field values by name; activity fields overlay stage fields, which overlay workflow fields. A |
|
| always | Parent instance GDR URI, |
|
| always | Ancestor instance GDR URIs, root first. |
|
| always | Current stage name. |
|
| always | ISO timestamp shared by every condition in one evaluation pass. |
|
| always | Start context by entry name: values seeded at |
|
| always | Completed effect outputs, |
|
| always |
|
|
| always | Current stage activity rows, with status. |
|
| always | Every current activity is |
|
| always | Some current activity is |
|
| always | Every spawned child, as cohort rows. Settled gate: |
|
| always | Each named predicate from |
|
| caller | Acting identity, |
|
| caller | The caller matches the activity's |
|
| caller | Advisory per-permission booleans from the caller's grants; |
|
| caller | Advisory organization-level User Attributes (Enterprise), the values |
|
| caller | The fired action's arguments. Bound only while that action's effect bindings and operation |
|
| row | One |
|
| start | The engine's partition. Scope instance scans with |
|
| 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 | All always | Picks the first route whose condition is |
Activity | All always | Whether the activity exists in this stage visit. Evaluated once at entry. |
Action | All always | Fires the action automatically when |
Action | All always | Whether the automation exists. |
Action | All always, | Whether the action exists for this caller. Advisory. |
Activity | All always, | Whether the activity's caller-fired actions are enabled. Advisory. |
Field | All always, | Who may edit the field. Advisory. |
Effect | All always, | Values passed to the effect handler. |
Effect | All always, | Values passed to the effect handler. |
Operation |
| Which rows the operation changes. An unevaluable row never matches. No other variable, and no named predicate. |
Operation | Same as caller-fired, without | Which rows the operation changes. |
| Always set except named predicates; | Which children to create. |
| All always, | Each child's input values. |
| All always, | The children's |
Named predicate ( | Always set except other predicates | A reusable boolean, |
|
| Whether a start surface offers the definition for a document. Never gates |
|
| Whether a fresh standalone start may proceed. |
Guard |
| 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?
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?