Conditions
Write GROQ conditions over the engine’s bounded instance snapshot: what the snapshot holds, which sites bind the caller, named predicates, and the start filter and requirements.
Conditions are GROQ expressions that answer a yes-or-no question about workflow state. Where a condition sits — its site — decides what data it can read and what its answer controls.
const articleReview = defineWorkflow({
name: 'article-review',
title: 'Article review',
initialStage: 'review',
fields: [
defineField({type: 'subject', name: 'subject', initialValue: {type: 'input'}, required: true}),
defineField({type: 'boolean', name: 'approved'}),
],
stages: [
defineStage({
name: 'review',
transitions: [
defineTransition({name: 'publish', to: 'published', when: '$fields.approved == true'}),
],
}),
defineStage({name: 'published'}),
],
})What conditions read
The engine evaluates each condition as GROQ against a context it builds for that one decision. A condition is not a free-ranging Content Lake query. The condition variable reference lists the GROQ variables available at each condition site.
For a running instance, the engine builds a limited in-memory snapshot. It holds:
- The current instance, its ancestor instances, and live spawned child instances.
- Accessible content documents declared by
subject,doc.ref, anddoc.refsfields at workflow or current-stage scope. - The release document declared by a
release.reffield.
Documents that are missing or unreadable are not there at all, and the engine does not follow references beyond the first hop. Single reference fields resolve to full documents in $fields, while doc.refs stays an array of reference identities. See Fields and Global document references for those contracts.
Use $now for the engine’s evaluation time. It is an ISO-8601 string; use dateTime($now) when an expression needs a datetime value. Definition validation rejects now() and dateTime::now() in conditions, predicates, spawn expressions, start.filter, and GROQ start requirements. Replace them with $now and dateTime($now), respectively. This lets the runtime identify expressions that need clock-driven reevaluation.
Query-based field initial values are the exception: they run when a field enters scope, may use the GROQ clock functions, and also receive $now. See Query values.
Caller-bound and caller-blind sites
A condition site either has a caller acting at it or it does not, and that decides whether the condition can read one. Three sites are caller-bound: the filter on an action a caller fires, an activity’s requirements, and an editable predicate. Those three bind $actor, $assigned, $can, and $attributes, so a rule such as "only the assigned reviewer may approve" belongs at one of them.
Routing is caller-blind on purpose. A transition’s when, an activity’s filter, and a cascade-fired action’s when and filter are re-evaluated by whatever legitimate call comes along next, so they have to give the same answer no matter which token drives them. Reading a caller at one of those sites would be asking about a caller who is not there, so defineWorkflow rejects the definition, and deploy runs the same check again, rather than letting the condition quietly never match. Gate on instance state instead, such as a field an action wrote, or say who may run the action with roles.
$params carries the arguments of the action being fired. It is bound only while that action’s effect bindings and its operations’ where expressions are being evaluated. It is not part of the caller-bound projection, and a cascade-fired action never binds it at all, because no caller supplied the arguments.
Every one of these checks is advisory: it tells a surface which controls to disable and why. See Actors, tokens, and what’s actually enforced for what an actor is and for which restriction stops a client that never calls the engine.
Reuse named predicates
When several decisions use the same instance rule, declare it once in predicates and reference it as $name. A named predicate can read instance variables, but not caller-bound variables or another named predicate. attributes is reserved for the synthetic $attributes bag, so rename a predicate with that name before you upgrade.
defineWorkflow({
// ...
predicates: {
readyToPublish: '$fields.approved == true && $allActivitiesDone',
},
stages: [
defineStage({
name: 'review',
transitions: [
defineTransition({
name: 'publish',
to: 'published',
when: '$readyToPublish',
}),
],
}),
],
})Start filters and requirements
Starting has two decisions. start.filter controls discovery. start.requirements is an ordered list of checks that run before a fresh standalone start. See the start block reference for the complete nested interfaces.
A subject field identifies the document the workflow is about. See Field types for its declaration contract.
defineWorkflow({
name: 'article-review',
title: 'Article review',
initialStage: 'review',
fields: [
defineField({
type: 'subject',
name: 'subject',
types: ['article'],
initialValue: {type: 'input'},
required: true,
}),
defineField({
type: 'boolean',
name: 'approved',
initialValue: {type: 'input'},
}),
],
start: {
filter: "status == 'draft'",
requirements: [
{
type: 'singleSubject',
name: 'one-open-review',
title: 'Review already in progress',
},
{
type: 'groq',
name: 'approved',
title: 'Approval required',
query: '$fields.approved == true',
},
],
},
stages: [defineStage({name: 'review'})],
})The subject field makes this workflow available for articles, and the filter narrows discovery to drafts. A fresh startInstance call succeeds only when no unfinished run of this definition holds the same subject and the supplied approved input is true. Start requirements are not evaluated when you resume an unfinished start, or when a parent spawns a child.
The root document is the candidate supplied to start.filter. Reads through references(), sanity::versionOf(), and sanity::partOfRelease() count as root reads when evaluated in that scope; ^ also counts when it reaches that root from a nested scope. A start.filter that reads the root requires a subject field. GROQ start requirements cannot read a root document; use their supplied variables instead. Their $fields values are raw inputs, so a document reference has {id, type}, not a hydrated document.
Allow one active workflow per subject
Workflows identifies each subject with a global document reference, which combines the document ID with its dataset, project, or other Sanity resource. Because the identity is complete, unrelated documents that happen to share an ID never block each other.
Use a singleSubject requirement when this definition may have only one unfinished run for the same subject. The rule applies across deployed versions of that definition. A different definition does not block it.
start: {
requirements: [
{
type: 'singleSubject',
name: 'one-open-review',
description: 'Finish the existing review before starting another.',
},
],
}The workflow must declare one first-class subject field with initialValue: {type: 'input'}. Completing the existing run permits another start.
Query the raw snapshot
The * operator searches this snapshot. It does not open a Content Lake query. Use the condition variables whenever they give you the state you need, and reach for the raw instance only for state they do not expose, such as stage-visit history:
defineTransition({
name: 'finish-after-rework',
to: 'done',
// $stage == "review" is true on every visit.
// Read the raw stage entries to detect a return visit.
when:
'count(*[_id == $self][0].stages[name == "review"]) > 1',
})The engine provides $stage, the current stage name. It does not provide a $stages history variable. The current-stage check is true on both the first and later review visits, while the $self query counts the stored stage entries, so it can tell a return visit apart.
Building a custom interface or diagnostic? See Evaluation insights to explain the outcome of these conditions without changing how you compose them.
Reference
Not every condition site receives the same variables. This reference lists the context available at each site.
Instance variables
These variables describe the running instance and are available at instance condition sites.
$fields
Rendered field values
Activity fields overlay stage fields, which overlay workflow fields. References hydrate only when their documents are in the evaluation snapshot.
$stage
Current stage name
Identifies the instance’s current stage.
$activities
Current activity rows
Includes each activity’s current status.
$allActivitiesDone
boolean
True when every current activity is done or skipped.
$anyActivityFailed
boolean
True when any current activity has failed.
$effects
Completed effect outputs
Contains declared outputs from completed effects.
$effectStatus
Effect statuses
Reports done or failed for effects queued during the current stage visit.
$subworkflows
Child workflow registry
Faceted by activity, definition, and status.
$self
Current instance GDR URI
Identifies the workflow instance document.
$parent
Parent instance
Identifies the parent instance, or null for a root instance.
$ancestors
Ancestor instances
Lists ancestor instances in root-first order.
$now
ISO timestamp
The clock value for the current evaluation.
$context
Start context
The context supplied when the instance started.
Context-specific variables
These variables appear only at the sites that supply their context.
$actor
Actor identity
Identifies the caller at caller-bound sites.
$assigned
boolean
True when the caller matches the activity’s singular or plural assignment fields. Direct user members take precedence over all role members. With roles alone, runtime matching applies role aliases. An empty assignment is false. See Fields for routing and eligibility.
$can
Permission values
Contains advisory permission values resolved for the caller.
$attributes
Record<string, unknown>
Organization-level User Attributes at caller-bound sites. Unavailable values stay unbound.
$params
Action parameters
Available to expressions carried by a caller-fired action.
$row
Current row
Available to row-oriented expressions such as operation where and spawn with.
$attributes uses the workflow spelling for values that Content Lake role filters read through user::attributes(). The engine fetches it only for caller-bound soft gates, never stores it, and treats it as advisory. When it is expected to be absent, it stays unbound and any condition that depends on it fails closed. When the fetch fails unexpectedly, that evaluation stops. Today the fetch reads one page of up to 100 active values.
Instance decision sites
These conditions make decisions about a running instance.
Transition when
Instance variables
Selects the first eligible outgoing transition in declaration order.
Activity filter
Instance variables
Determines whether the activity belongs to the current stage visit.
Action when
Instance variables
Triggers the action automatically when the condition is true.
Action filter, requirements, and editable predicates
Instance variables plus caller context
Controls caller-facing availability. These checks are advisory, not a security boundary.
Action expression sites
These expressions shape work after an action fires.
Effect input bindings
Action context
Resolve the input passed to an effect. Caller-fired actions may bind $params.
Operation where
Action context plus $row
Tests each stored array row before applying the operation.
Spawn forEach
Workflow field context
Selects the rows used to create child instances.
Spawn context
Action context
Constructs the start context for each child instance.
Spawn with
Action context plus $row
Constructs child inputs from the current row. Spawn expressions do not bind $can or $params.
Start decision sites
Start visibility and start permission use different inputs.
start.filter
Candidate document context
Controls discovery. It can read the candidate document, $tag, $definition, and $now. It cannot read $fields and does not gate startInstance.
start.requirements[]
Start input context
Ordered creation gates. GROQ requirements can read $tag, $definition, $now, and raw input $fields; singleSubject enforces definition-scoped subject exclusivity.
Guard predicates
Guard predicates use Content Lake delta GROQ with their own variables and enforcement model.
Visiting agent?
Workflows includes an MCP server for inspecting, operating, authoring, validating, and deploying workflows. Ask your human to set up the MCP server.