Fields
Fields carry the typed data belonging to a workflow instance.
Fields use the same basic model as fields in a Sanity schema: each declaration has a name, a type and optional presentation or validation settings. The difference is where the value lives. Workflow fields are stored on the workflow instance, and where you declare a field decides how long it lasts and where it can be read.
Document-valued fields use global document references. These references preserve the document’s project, dataset, or other Sanity resource.
Field scopes
Fields may be declared at three scopes:
- Workflow fields resolve when the instance starts and persist for its lifetime.
- Stage fields resolve on stage entry and belong to that stage visit.
- Activity fields resolve on stage entry and belong to one activity.
An operation's field reference can name its scope. If you leave the scope out, the definition compiler resolves the nearest declaration: activity, stage, then workflow. Field-read initialization has separate scope rules, described in Field-read values.
At evaluation time, $fields holds the fields in scope at the current location. If a narrower scope declares a field with the same name, that one wins.
Actions change fields through operations. The engine exposes the values to conditions as $fields. Changing a field can reveal an action or make a transition fire.
Common field types
Every field declares a type that determines the value it stores and any specialized behavior. These types recur in many workflows. For the complete list, see Field types.
- Subject:
subjectidentifies the document the workflow is about. It is workflow-scoped, there is only one per workflow, and it is usually supplied when the instance starts. Studio also uses the document types it accepts to find workflows that match a document. - Dates:
datestores YYYY-MM-DD, anddatetimestores an ISO-8601 timestamp. To mark a deadline on a workflow, stage, or activity, usedueDateordueDatetime. They store and validate exactly the same values asdateanddatetime. Each scope may declare at most one of them, and it has to be a top-level field. Nested object fields, array fields, and effect outputs usedateordatetimeinstead. AtodoListrow’sdueDateis only a field name; its kind is stilldate. - Progress:
progressstores completion from 0 through 100. Studio renders it as progress; effect handlers can report it withctx.setProgress().
For discovery and document applicability, a subject with no types list accepts ordinary content types but excludes names beginning with sanity. or system.. To use another reserved type, declare it explicitly in types. This default also applies when a new engine reads an older stored definition; it does not add a document-type check to a direct startInstance call.
Engine-owned documents cannot be subjects: listing sanity.workflow.instance, sanity.workflow.definition, or temp.system.guard in subject.types fails defineWorkflow and deploy validation. Remove those type names and use subworkflows with spawn to coordinate workflow instances.
Declare each field at the scope that owns its value:
const subject = defineField({
type: 'subject',
name: 'subject',
initialValue: {type: 'input'},
required: true,
})
const dueDate = defineField({
type: 'dueDate',
name: 'dueDate',
editable: true,
})
const dueDatetime = defineField({
type: 'dueDatetime',
name: 'dueDatetime',
editable: true,
})
const generationProgress = defineField({
type: 'progress',
name: 'generationProgress',
})dueDate and dueDatetime are model 5 field types and do not raise the reader floor. An older engine can still reject the unfamiliar field type. See Upgrade Workflows packages before deploying definitions that use them.
Assignments and role eligibility
Assignment fields store ordered lists of people and collective project roles. Both assignee and assignees use this list. An assignee field permits at most one person and any number of role members; assignees permits several people. An empty assignment is [].
At activity scope, assignee and assignees fields contribute to $assigned: listed people match; with no person listed, callers match through the listed roles. Workflow- and stage-scope fields do not affect $assigned. See Conditions.
Without roles, both assignment field types accept any valid person or collective role. Add a non-empty roles list to restrict new values. The option works at any nesting level and on effect outputs. It controls who is eligible for an assignment, and nothing else: it does not make a field editable, does not grant access to an action, and does not change $assigned.
A person is eligible when their project membership includes a listed role, directly or through a role alias. A collective role is eligible only when its value matches a listed role exactly. A replacement can retain members who no longer qualify, including in singular fields. Removing or reordering retained members is allowed; adding a member or an extra copy requires current eligibility.
const reviewer = defineField({
type: 'assignee',
name: 'reviewer',
roles: ['reviewer', 'editor'],
})New definitions containing assignee require reader model 9. Role-constrained assignees fields require model 8 unless another feature requires a higher floor. The roles constraint requires a dataset workflow resource and access to the project member directory. Follow Upgrade Workflows packages before deployment.
Add this activity to a stage that has an outgoing transition. Create the legal and compliance project roles first. See Activities and actions for placement:
import {defineActivity} from '@sanity/workflow-engine/define'
defineActivity({
name: 'approve',
fields: [
{
type: 'assignee',
name: 'seat',
roles: ['legal', 'compliance'],
initialValue: {type: 'literal', value: [{type: 'role', role: 'legal'}]},
editable: "count($fields.seat[@.type == 'user']) == 0 || $assigned",
},
],
actions: [{name: 'approve', filter: '$assigned', status: 'done'}],
})A matching role member may approve before taking this activity. To require a person to take it first, replace the action filter with:
filter: "count($fields.seat[@.type == 'user']) > 0 && $assigned",
Pass the configured engine and the instance ID returned by startInstance. The engine must use the signed-in reviewer’s client; evaluate supplies that reviewer’s account-global ID. See Engine for setup and Actors, tokens, and what’s actually enforced for identity.
import type {Engine} from '@sanity/workflow-engine'
const target = {scope: 'activity', activity: 'approve', field: 'seat'} as const
export async function takeReview(engine: Engine, instanceId: string) {
const {actor} = await engine.evaluate({instanceId})
return engine.editField({
instanceId,
target,
mode: 'append',
value: {type: 'user', id: actor.id},
})
}
export function releaseReview(engine: Engine, instanceId: string) {
return engine.editField({
instanceId,
target,
value: [{type: 'role', role: 'legal'}],
})
}Taking retains the Legal role and excludes a second person. Releasing restores this example’s Legal role assignment. To clear every member, use mode: 'unset' or set []. Older instances keep their existing storage and editing limits; see Upgrade Workflows packages.
Fields in conditions
Conditions never see the raw stored form of a field. Before evaluating a condition, the engine renders the fields in scope into the GROQ environment Workflows uses, and binds the result as $fields.
Primitive fields become primitive values. Object and array fields keep their declared shape. A loaded subject or doc.ref resolves to its document. When the document is absent from the evaluation snapshot, the rendered value contains only its _id and _type.
defineStage({
name: 'review',
fields: [
defineField({type: 'actor', name: 'reviewer'}),
defineField({type: 'boolean', name: 'approved'}),
],
activities: [
defineActivity({
name: 'approval',
actions: [
defineAction({
name: 'approve',
filter: '$actor.id == $fields.reviewer.id',
ops: [
defineOp({
type: 'field.set',
target: {field: 'approved'},
value: {type: 'literal', value: true},
}),
],
}),
],
}),
],
transitions: [
defineTransition({name: 'publish', to: 'published', when: '$fields.approved == true'}),
],
})Conditions are re-evaluated after every committed engine operation, and whenever a reactive session sees a relevant content change. So a single field update can reveal an action, satisfy a requirement, fire a triggered action, or select a transition, all in the same cascade.
Use Conditions for the full expression grammar and the other variables available beside $fields.
When referenced content is deleted
Deleting referenced content leaves the workflow instance and its audit history intact. When a required subject, doc.ref, or doc.refs target becomes unreadable, normal actions, triggered actions, and transitions cannot advance the instance. The same applies when the document exists outside the workflow's perspective.
An optional reference blocks progress only when an unmet runtime requirement or transition condition needs its content. Unrelated actions can remain available. A completed instance remains completed after its content is deleted.
Restore access or make the content available in the instance's perspective. If the field is editable, use an engine field edit to select replacement content. Aborting the instance remains available. See Handle workflows when referenced content is deleted for an explicit abort policy. Never raw-patch or delete instance documents.
Evaluations identify missing references and distinguish confirmed deletion from inaccessible content or content outside the perspective. See Evaluation insights.
Set initial values
Without initialValue, a field is only working memory. It starts empty, and actions or direct edits can fill it in.
An initial value says how a field gets its value when it enters scope. Use input for a value supplied at start, literal for a fixed value, query for a value computed from the Content Lake, or fieldRead for a value copied from an already-resolved field. See Initial-value sources for the complete forms.
Input values
Use input for a workflow-scope value supplied when an instance starts or a parent spawns it. Sanity Studio shows a start-time control for each input the document mapping has not supplied. Only workflow-scope inputs may be required. An activity input seed has no caller input, stays empty, and produces a deploy warning. Use a literal, query, field read, or working-memory field there.
defineField({
type: 'string',
name: 'priority',
initialValue: {type: 'input'},
required: true,
options: {
list: [
{title: 'Standard', value: 'standard'},
{title: 'Urgent', value: 'urgent'},
],
},
})required: true requires a non-null input at start or spawn. For subject, doc.ref, and doc.refs, selected documents must also remain readable afterward. This continuing check applies only to workflow-scope required reference fields.
Definitions with required content references need reader model 10. Complete the rollout in Upgrade Workflows packages before deploying them.
Literal values
Use literal for a fixed value authored in the workflow definition. The engine validates the value against the field type when it resolves the field.
defineField({
type: 'string',
name: 'priority',
initialValue: {type: 'literal', value: 'standard'},
options: {
list: [
{title: 'Standard', value: 'standard'},
{title: 'Urgent', value: 'urgent'},
],
},
})Query values
Use query to evaluate GROQ against the Content Lake when the field enters scope. A query can use earlier, already-resolved fields in the same scope through $fields, so declaration order matters.
fields: [
defineField({
type: 'subject',
name: 'subject',
initialValue: {type: 'input'},
required: true,
}),
defineField({
type: 'string',
name: 'intakeRequestUrl',
initialValue: {
type: 'query',
query: '*[_id == $fields.subject.id][0].intakeRequestUrl',
},
}),
]Use a query when you need referenced document content. Reference normalization can omit unrecognized values. If the normalized result fails field validation, the engine uses the field's empty value and records fieldQueryDiscarded. A failed query throws.
Field-read values
Use fieldRead to copy an earlier field in the same scope. A stage or activity seed can read workflow fields with scope: 'workflow'; an activity seed cannot read stage fields. At workflow scope, omit scope. Use path to select a nested stored value.
fields: [
defineField({
type: 'string',
name: 'requestedBy',
initialValue: {type: 'input'},
required: true,
}),
defineField({
type: 'string',
name: 'reviewOwner',
initialValue: {type: 'fieldRead', field: 'requestedBy'},
}),
]A field read copies the stored field value. It does not fetch content from a subject or doc.ref. Use a query when you need a property from the referenced document.
Operations use a related value-expression grammar. See Operations.
Choice lists
Use options.list to define the full set of allowed values for a scalar field. Each entry carries the label an interface shows and the string or number that gets stored. The list cannot be empty, values must be unique and match the field type, and every write path checks that a value is on the list: initial values, edits, operations, action parameters, and effect outputs.
defineField({
type: 'string',
name: 'priority',
options: {
list: [
{title: 'Normal', value: 'normal'},
{title: 'Urgent', value: 'urgent'},
],
},
})Validation
Use validation on supported field types to narrow the values they accept:
defineField({
type: 'number',
name: 'wordCount',
validation: {min: 300, max: 2_000},
})stringandtext:minandmaxare inclusive character counts.number:minandmaxare inclusive numeric bounds.progress: always 0–100, sominandmaxcan only narrow that range.- You can leave out either bound.
minmust not exceedmax. - Engine: Validates inputs, operations, action parameters, and effect outputs before committing. An invalid write stores nothing. If a normalized query result fails validation, the engine uses the empty value and records
fieldQueryDiscarded. - Studio: Shows an inline error on editable fields and blocks a start or action while an input is invalid.
requiredcontrols input presence and, for content references, continued readability.
Editable fields
Fields are not directly editable by default. The editable property accepts three forms: true allows any caller while the field is in scope, a single string is always a GROQ condition, and a non-empty string[] is always a list of allowed role names. A GROQ edit condition reads $actor, $assigned, $can, $attributes, and $fields.
Editing a field directly is still an engine operation. It evaluates the field’s editability, applies one transaction, records history, refreshes guards, and runs the cascade.
fields: [
// Anyone may edit while this field is in scope.
defineField({type: 'text', name: 'summary', editable: true}),
// A single string is a GROQ condition.
defineField({
type: 'text',
name: 'reviewNote',
editable: '$actor.id == $fields.reviewer.id',
}),
// A string array is role shorthand.
defineField({type: 'text', name: 'internalNote', editable: ['editor', 'admin']}),
]Reference
Field declaration
This is the object passed to defineField(...). Put field declarations in the fields array of a workflow, stage, or activity. That location decides the field’s scope.
name
string
Stable field name within its scope.
type
Field type
One stored field type or an authoring convenience.
title / description
string
Optional presentation metadata.
group
string | string[]
Optional presentation group or groups. Authoring accepts one name or a non-empty list; the stored definition normalizes membership to
string[].initialValue
InitialValue
Optional source resolved when the field enters scope.
editable
boolean | Condition | string[]
Enables direct editing, optionally gated by a condition or role shorthand.
roles
string[]
Non-empty project role names that eligible assignment values must fulfill. Valid on assignee and assignees field shapes. This facet requires model 8 or higher; singular assignee definitions require model 9.
fields
FieldShape[]
Nested shape for an
object; acceptstype,name,title,description,options,validation,fields, andof.of
FieldShape[]
Member shape for an
array; accepts the sameFieldShapekeys asfields.required
boolean
Requires a non-null value at start or spawn. Valid only on workflow-scope input fields. For subject, doc.ref, and doc.refs, selected documents must also remain readable in the workflow perspective; missing targets block actions and transitions.
types
string[]
Accepted document types for
subject,doc.ref, anddoc.refs. A subject without this list excludessanity.*andsystem.*types. Explicitly list a reserved type to accept it; engine-owned types are never valid subjects. This subject-specific default does not apply to ordinary document-reference fields.options
ChoiceOptions
Closed choice list for supported scalar types.
validation
ScalarValidation
Inclusive character-count bounds for
stringandtext, numeric bounds fornumber, or narrower 0–100 bounds forprogress.
Initial-value sources
Set one of these objects as a field declaration’s initialValue. The engine resolves it when that field enters scope. Definitions decide which values are inputs, fixed defaults, or computed; Studio mappings do not.
input
{type: ‘input’}
Takes a workflow-scope value supplied when the instance starts or a parent spawns it. Sanity Studio shows a start-time control for each unmapped workflow input. Only workflow-scope input fields may set required. Activity input seeds stay empty and produce a deploy warning.
literal
{type: ‘literal’, value}
Uses a fixed value authored in the definition.
query
{type: ‘query’, query}
Evaluates GROQ against the Content Lake when the field enters scope, with earlier fields in that scope available as $fields. Reference normalization can omit unrecognized values. A normalized result that fails validation uses the field's empty value and records fieldQueryDiscarded. A failed query throws.
fieldRead
{type: ‘fieldRead’, field, scope?, path?}
Copies an earlier stored field value in the same scope, optionally selecting path. Stage and activity seeds may set scope: 'workflow' to read workflow fields. Activity seeds cannot read stage fields. Does not load referenced content.
Authoring conveniences
Use these names as the type in defineField(...). The definition compiler expands each convenience into canonical fields.
todoList
array field
Creates todo rows shaped as {label, status, assignee?, dueDate?}. The optional assignee is a member list with at most one person and any number of roles. dueDate is an optional YYYY-MM-DD value used by Studio deadline controls.
notes
array field
Creates array rows shaped as
{body, actor, at}. The names match theauditoperation’s body and automatic actor/time stamps; append-only behavior is a consumer convention, not a separate stored field type.
Options and validation
options
{list: Array<{title: string, value: string | number}>}
Optional closed choice list for string, text, number, url, date, dueDate, datetime, and dueDatetime fields. Stored non-null values must equal one listed value.
validation
{min?: number, max?: number}
Optional inclusive character-count bounds for string and text, numeric bounds for number, or narrower 0–100 bounds for progress. Other field types do not accept validation.
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.
Visiting agent?
Workflows includes an MCP server for inspecting, operating, authoring, validating, and deploying workflows. Ask your human to set up the MCP server.