Workflows

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: subject identifies 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: date stores YYYY-MM-DD, and datetime stores an ISO-8601 timestamp. To mark a deadline on a workflow, stage, or activity, use dueDate or dueDatetime. They store and validate exactly the same values as date and datetime. 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 use date or datetime instead. A todoList row’s dueDate is only a field name; its kind is still date.
  • Progress: progress stores completion from 0 through 100. Studio renders it as progress; effect handlers can report it with ctx.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.

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},
})
  • string and text: min and max are inclusive character counts.
  • number: min and max are inclusive numeric bounds.
  • progress: always 0–100, so min and max can only narrow that range.
  • You can leave out either bound. min must not exceed max.
  • 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. required controls 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.

  • 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; accepts type, name, title, description, options, validation, fields, and of.

  • of

    FieldShape[]

    Member shape for an array; accepts the same FieldShape keys as fields.

  • 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, and doc.refs. A subject without this list excludes sanity.* and system.* 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 string and text, numeric bounds for number, or narrower 0–100 bounds for progress.

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 the audit operation’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. 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.

Visiting agent?

Was this page helpful?