Workflows

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:

Workflow definition

defineWorkflow(...) validates the definition, expands authoring shorthand, and returns the definition you deploy. If validation fails, it throws one error that lists every issue with its path. One of the checks is that every variable a condition reads is available where the condition runs. See Condition variables.

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.

  • title

    string

    Optional. Non-empty display name. When omitted, the definition stores its name as the title, so every deployed definition has one. Omit it when it would equal the name.

  • Optional. Display explanation.

  • semantics

    Semantic[]

    Optional. Advisory meaning that tooling can read and execution ignores: signal.positive, signal.caution, signal.critical, or custom.<camelCase>, at most one per namespace.

  • RequiredinitialStage

    string

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

  • Requiredstages

    Stage[]

    Required. One or more stage declarations.

  • fields

    FieldEntry[]

    Optional. Workflow-scope fields that live for the instance lifetime.

  • groups

    Group[]

    Optional. Presentation groups that fields, activities, and actions may join with group. Each is {name, title?, description?, kind?}; kind is 'core' or 'details'. Advisory: the engine validates names and never acts on groups.

  • predicates

    Record<string, string>

    Optional. Named conditions, each pre-evaluated and bound as the boolean $name. A predicate can read the variables marked always in the variables table, not caller variables or another predicate, and cannot reuse a built-in variable name.

  • roleAliases

    Record<string, string[]>

    Optional. Roles that fulfill each authored role. Aliases widen action roles, $assigned, and assignment eligibility; they never change an actor's roles. The "*" key lists universal fulfillers.

  • lifecycle

    'standalone' | 'child'

    Optional. Default 'standalone'. 'child' marks a spawn-only definition. Advisory: start pickers hide child definitions, but startInstance doesn't refuse one. A child definition can't declare start.

  • start

    AuthoringStartBlock

    Optional. Discovery and readiness rules for fresh standalone starts. See the start block section.

  • abort

    {roles: string[]}

    Optional. Limits who may call abortInstance, with roleAliases applied. A caller it doesn't admit gets AbortDeniedError, administrators included. Omit it to let anyone who can write the instance abort it. Subworkflows stopped by their parent and instances aborted by deleteDefinition are not checked. Requires expectedMinReaderModel 11.

  • runtime

    {kind: 'function' | 'durableFunction' | 'selfHosted'}

    Optional. Authoring-only hosting hint for the generated runtime, overriding the deployment's kind. Deploy strips it.

Reader-model requirements such as the one on abort are explained in Upgrade packages. For runtime, see Runtime hosting configuration.

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 controls where a definition is offered for new runs and checks each new standalone start. It doesn’t run again when a start is resumed or when a parent spawns a child. For how it behaves, see Start filters and requirements.

start: {
  kind: 'interactive',
  filter: "status == 'draft'",
  requirements: [
    {
      type: 'singleSubject',
      name: 'one-open-review',
      title: 'Review already in progress',
    },
    {
      type: 'groq',
      name: 'article-ready',
      title: 'Article is not ready',
      query: '$fields.ready == true',
    },
  ],
}
  • kind

    'interactive' | 'autonomous'

    Optional. Default 'interactive'. Classifies who normally starts the workflow. It doesn't gate startInstance.

  • filter

    string

    Optional. GROQ visibility predicate evaluated against a candidate document during discovery. It doesn't gate startInstance. Root document reads require a subject field, including implicit reads through references(), sanity::versionOf(), and sanity::partOfRelease(). Use $now for time; clock functions are rejected.

  • requirements

    StartRequirement[]

    Optional. Ordered named checks enforced before a fresh standalone start. Every one must pass.

Start requirements

Each entry in start.requirements has a type:

  • groq runs a GROQ condition against the values supplied at start.
  • singleSubject allows at most one unfinished run of this definition per subject, across all deployed versions. It needs a workflow subject field with initialValue: {type: 'input'}. See Allow one active workflow per subject.
  • Requiredtype

    'groq' | 'singleSubject'

    Required. Selects the check.

  • Requiredname

    string

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

  • title

    string

    Optional. Editor-facing label.

  • Optional. Editor-facing explanation.

  • query

    string

    Required when type is 'groq'. Condition over the start input context. It can't read a root document, including through implicit document functions or escaped projection scopes. $fields holds raw inputs, so document references are {id, type}. Use $now or dateTime($now) instead of clock functions.

Stage declaration

Pass this object to defineStage(...) in a workflow’s stages array. A stage with no transitions is terminal. Entering a terminal stage completes the instance, so a terminal stage can’t declare activities.

  • Requiredname

    string

    Required. Stable identity inside the definition.

  • title

    string

    Optional. Display label.

  • Optional. Display explanation.

  • semantics

    Semantic[]

    Optional. Advisory meaning that tooling can read and execution ignores: signal.positive, signal.caution, signal.critical, or custom.<camelCase>, at most one per namespace.

  • fields

    FieldEntry[]

    Optional. Fields resolved on entry and kept with this stage visit.

  • activities

    Activity[]

    Optional. Units of work for the stage. Not allowed on a terminal stage.

  • transitions

    Transition[]

    Optional. Ordered routes to other stages. Omit them to make the stage terminal.

  • guards

    Guard[]

    Optional. Content mutation guards registered on entry and retracted on exit.

  • groups

    Group[]

    Optional. Presentation groups local to the stage.

  • editable

    Record<string, true | string | string[]>

    Optional. Tighten-only override keyed by in-scope field name. Each entry is ANDed with the field's own editable, so a stage can narrow editing while it is current but never open a field the baseline leaves closed. Each key must name a field of this stage that declares editable.

defineStage({
  name: 'review',
  editable: {legalApproved: ['legal']},
  activities: [/* ... */],
  transitions: [defineTransition({name: 'to-approved', to: 'approved'})],
})

Transition declaration

Pass this object to defineTransition(...) in a stage's transitions array. It declares a one-way route from that stage to another stage, without operations or effects.

  • Requiredname

    string

    Required. Stable identity within the source stage.

  • title

    string

    Optional. Display label.

  • Optional. Display explanation.

  • Requiredto

    string

    Required. Name of the destination stage.

  • when

    string

    Optional. Default $allActivitiesDone; use 'true' for an unconditional route. Transitions are checked in declaration order and the first true fires. A false result moves on to the next transition; an unevaluable result, such as GROQ null, stops selection.

defineTransition({
  name: 'to-approved',
  to: 'approved',
  when: '$allActivitiesDone && $fields.legalApproved == true',
})

Field declaration

Pass this object to defineField(...) in the fields array of a workflow, a stage, or an activity. Where you declare the field sets its scope:

  • Workflow fields get their values when the instance starts, and last for the whole instance.
  • Stage and activity fields get their values each time the instance enters the stage. They belong to that stage visit, or to that activity.

Conditions read the fields in scope as $fields. If two scopes declare a field with the same name, the narrower one wins: activity, then stage, then workflow.

Operations and field edits that leave out scope find the field the same way, starting from the nearest declaration. In an effect handler, on an instance started from a definition deployed at model 12 or later, the search starts from the activity whose action queued the effect. That applies to a bare setProgress field name and to field reads in an effect’s operations. On an instance started from an earlier definition, a bare setProgress name writes the workflow field, and those reads find the stage field, then the workflow field. Initial values that read another field follow their own rules; see the fieldRead row.

  • Requiredname

    string

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

  • Requiredtype

    string

    Required. A field type or an authoring convenience.

  • title

    string

    Optional. Display label.

  • Optional. Display explanation.

  • group

    string | string[]

    Optional. Group or groups to join. The stored definition normalizes membership to string[].

  • initialValue

    FieldSource

    Optional. Source resolved when the field enters scope. Omit it to start empty (null or []) and let operations fill the field.

  • editable

    true | string | string[]

    Optional. Default: not editable. true opens direct editing to anyone in the field's scope window, a string is a condition, and a role list desugars to a role-membership condition. Advisory; a guard declares a write lock.

  • required

    boolean

    Optional. Default false. Valid only on workflow-scope fields with initialValue: {type: 'input'}. Requires a non-null value at start or spawn. On subject, doc.ref, and doc.refs, in an instance started from a definition deployed at model 10 or later, selected documents must also stay readable in the workflow perspective, and a missing target blocks actions and transitions. An instance started from an earlier definition checks the value only at start or spawn.

  • types

    string[]

    Optional. Accepted document types, on subject, doc.ref, and doc.refs only. Omit it to accept any type, except that a subject without types excludes sanity.* and system.* types. List a reserved type to accept it. Engine-owned types are never valid subjects.

  • roles

    string[]

    Optional. Non-empty project roles that new assignment members must fulfill. Valid on assignee and assignees only. Requires expectedMinReaderModel 8, or 9 on an assignee field.

  • options

    {list: Array<{title: string, value: string | number}>}

    Optional. Closed choice list for string, text, number, url, date, dueDate, datetime, and dueDatetime. Non-empty, with unique values and non-empty titles. Stored non-null values must equal one listed value.

  • validation

    {min?: number, max?: number}

    Optional. Inclusive bounds: character counts for string and text, values for number, and a narrower range inside 0 to 100 for progress. Declare at least one bound. Other types reject validation.

  • fields

    FieldShape[]

    Required on object, invalid elsewhere. Nested shapes, each accepting type, name, title, description, options, validation, roles, fields, and of. Nested shapes use date and datetime, not dueDate or dueDatetime.

  • of

    FieldShape[]

    Required on array, invalid elsewhere. Member shape, with the same keys as fields.

A field with an options.list follows these rules:

  • The list must not be empty. Values must be unique, titles must not be empty, and every value must match the field type and its validation bounds.
  • Every write is checked against the list: initial values, edits, operations, action params, and effect outputs.

Validation bounds (min and max) are inclusive. You can leave out either one, and min can’t be greater than max.

The engine checks inputs, operations, action params, and effect outputs before it saves anything, so an invalid write stores nothing. A query-sourced value that fails validation is different: the field gets its empty value, and the engine records a fieldQueryDiscarded history entry. In Sanity Studio, an invalid input shows an inline error and blocks the start or action until it’s fixed.

For roles and other reader-model requirements, see Upgrade packages. For routing, eligibility, and Studio controls, see Configure fields.

defineField({
  type: 'string',
  name: 'priority',
  editable: ['editor'],
  initialValue: {type: 'literal', value: 'normal'},
  options: {list: [{title: 'Normal', value: 'normal'}, {title: 'Urgent', value: 'urgent'}]},
})

Field types

Canonical values accepted by a field declaration’s type.

  • subject

    GlobalDocumentReference | null

    The workflow's primary document. Workflow scope only; at most one. Document-aware integrations use it to discover the workflow for that document. Without types, accepts ordinary content types and excludes reserved namespaces. Explicit types may include reserved types, but never engine-owned documents.

  • doc.ref

    GlobalDocumentReference | null

    One resource-qualified document reference. Optional types restricts accepted document types.

  • doc.refs

    GlobalDocumentReference[]

    An array of resource-qualified document references. Optional types applies to every member.

  • release.ref

    ReleaseRef | null

    A reference to a Content Release, including its release name.

  • string

    string | null

    A short string value.

  • text

    string | null

    A multiline string value.

  • number

    number | null

    A numeric value.

  • progress

    number | null

    Completion from 0 through 100; fractions are allowed. Studio renders it as progress, and an effect handler can update it with ctx.setProgress(). Validation may narrow the range; choice options are not supported.

  • boolean

    boolean | null

    A boolean value.

  • date

    YYYY-MM-DD | null

    A date-only YYYY-MM-DD value. Studio gives any editable date field a date picker. Use this kind for ordinary dates and for nested dates, including the dueDate value inside each todoList row.

  • dueDate

    YYYY-MM-DD | null

    The deadline for one workflow, stage, or activity scope. It stores and validates the same value as date, supports options, and uses the same Studio date controls. A scope may declare at most one dueDate or dueDatetime field. The field must be top-level and is optional unless required is set.

  • datetime

    ISO-8601 string | null

    An ISO-8601 timestamp. Studio gives any editable datetime field a date-and-time picker. Use this kind for ordinary timestamps and nested timestamp fields.

  • dueDatetime

    ISO-8601 string | null

    The deadline, including time, for one workflow, stage, or activity scope. It stores and validates the same value as datetime, supports options, and uses the same Studio date-and-time controls. A scope may declare at most one dueDate or dueDatetime field. The field must be top-level and is optional unless required is set.

  • url

    string | null

    A URL string.

  • actor

    Actor | null

    An actor identity and its roles, for recording who performed work.

  • assignee

    Assignee[]

    An ordered assignment list with at most one person and any number of role members. Empty is []. User IDs are account-global human sanityUserId values; robots cannot be assignment members.

  • assignees

    Assignee[]

    An ordered list of people or collective project roles. User IDs are account-global human sanityUserId values; robots cannot be assignment members.

  • object

    Record<string, unknown> | null

    A compositional object. Declare its nested shape with fields.

  • array

    Record<string, unknown>[]

    An array of compositional objects. Declare the member shape with of.

A subject field without types accepts any ordinary content type. It excludes types whose names start with sanity. or system.. To accept one of those, list it in types.

This default applies to older stored definitions too. It only affects where a definition is offered. A direct startInstance call doesn’t check the document type of a subject that has no types.

You can’t list a document type the engine owns (sanity.workflow.instance, sanity.workflow.definition, or temp.system.guard) in subject.types. defineWorkflow and deploy both reject it. To coordinate workflow instances, use spawn.

dueDate and dueDatetime aren’t valid inside object or array fields, or as effect outputs. Use date or datetime there. In a todoList row, dueDate is a property name, and its type is date. Both due-date types belong to data model 5 and don’t raise the reader floor, but an older engine can still reject a type it doesn’t know.

Initial-value sources

Set one of these objects as a field's initialValue. The engine resolves it once, when the field enters scope. A source doesn't make the field editable.

  • input

    {type: 'input'}

    Takes a workflow-scope value supplied at start or by a parent's spawn. Sanity Studio shows a start control for each unmapped workflow input. Stage and activity input seeds receive no value, and an activity input seed produces a deploy warning.

  • literal

    {type: 'literal', value}

    Uses a fixed value authored in the definition.

  • query

    {type: 'query', query}

    Runs GROQ against the Content Lake when the field enters scope, with earlier fields in that scope as $fields. Reference normalization can drop unrecognized values. A normalized result that fails validation stores the field's empty value and records fieldQueryDiscarded. A failed query throws.

  • fieldRead

    {type: 'fieldRead', field, scope?, path?}

    Copies an earlier field in the same scope, optionally selecting path. A stage or activity seed can set scope: 'workflow' to read workflow fields; an activity seed can't read stage fields. Doesn't load referenced documents.

Authoring conveniences

Use these names as the type in defineField(...). defineWorkflow expands each one into one or more standard fields.

  • todoList

    array field

    An array of rows shaped {label, status, assignee?, dueDate?}: label and status are strings, assignee is an assignee member list, and dueDate is a date that Studio deadline controls use.

  • notes

    array field

    An array of rows shaped {body, actor, at} (text, actor, datetime). The names match the stamps the audit operation adds, so audit rows fit. Append-only is a convention, not an enforced type.

Activities and actions

An activity is a unit of work in a stage. Actions on the activity carry every operation, effect, and spawn.

Activity declaration

This is the object passed to defineActivity(...) in a stage’s activities array.

  • Requiredname

    string

    Required. Stable activity name.

  • title

    string

    Optional. Display label.

  • Optional. Display explanation.

  • semantics

    Semantic[]

    Optional. Advisory meaning that tooling can read and execution ignores: signal.positive, signal.caution, signal.critical, or custom.<camelCase>, at most one per namespace.

  • groups

    Group[]

    Optional. Groups declared for this activity's fields and actions.

  • group

    string | string[]

    Optional. Groups this activity joins.

  • target

    {type: 'url', url} | {type: 'field', field}

    Optional. Deep link for work done outside Workflows. url must be http or https. field names a doc.ref, doc.refs, subject, or release.ref field reachable from the activity. Render-only; never gates.

  • filter

    string

    Optional. Existence condition evaluated once at stage entry. A definite false records the activity as skipped, hides it, and keeps it from blocking $allActivitiesDone.

  • requirements

    GroqRequirement[]

    Optional. Ordered named readiness checks, each {type: 'groq', name, title?, description?, query}. An unmet one keeps the activity visible and disables its caller-fired actions with requirements-unmet.

  • actions

    Action[]

    Optional. Actions that resolve the work or change instance state.

  • fields

    FieldEntry[]

    Optional. Activity-scope fields resolved at stage entry.

defineActivity({
  name: 'legal-check',
  filter: '$fields.needsLegal == true',
  requirements: [{type: 'groq', name: 'assigned', title: 'Only the assignee can sign off', query: '$assigned'}],
  fields: [defineField({type: 'assignees', name: 'assignees', editable: true})],
  actions: [defineAction({name: 'clear', status: 'done'})],
})

Action declaration

Pass this object to defineAction(...) in an activity’s actions array. How the action fires depends on when:

  • Without when, a caller fires the action with fireAction, and a UI can show it as a button.
  • With when, the engine fires the action automatically when the condition is true.
  • Requiredname

    string

    Required. Stable action name.

  • title

    string

    Optional. Display label.

  • Optional. Display explanation.

  • semantics

    ActionSemantic[]

    Optional. Advisory meaning: decision.accept, decision.decline, signal.positive, signal.caution, signal.critical, or custom.<camelCase>, at most one per namespace. Decision values are action-only. Execution ignores semantics.

  • group

    string | string[]

    Optional. Groups this action joins.

  • when

    string

    Optional. When present, the engine fires the action as soon as the condition turns true, at most once per stage visit, and fireAction rejects it. Use 'true' to fire on stage entry.

  • filter

    string

    Optional. Existence condition. A non-matching action is hidden, not disabled. With when, filter decides whether the automation exists and when triggers it.

  • roles

    string[]

    Optional. On a caller-fired action, desugars to a role-membership condition ANDed with filter. On an action with when, it is stored as is and limits which identities may run the trigger. roleAliases applies either way.

  • params

    ActionParam[]

    Optional. Caller-supplied values, each {name, type, title?, description?, required?, options?, validation?}. type is string, number, boolean, url, dateTime, actor, doc.ref, doc.refs, or json. A missing required param throws ActionParamsInvalidError and nothing commits. The caller’s args are JSON values. hasOwnProperty is a reserved param name: defineWorkflow rejects an action that declares it. fireAction rejects caller args with a hasOwnProperty key that the action doesn’t declare, with ActionParamsInvalidError, and commits nothing. An instance whose definition declares a hasOwnProperty param and was deployed with Workflows 0.37 or earlier accepts that param like any declared one.

  • ops

    Operation[]

    Optional. Instance mutations committed with the firing.

  • effects

    Effect[]

    Optional. External work queued by the firing.

  • spawn

    Subworkflows

    Optional. Child workflows created by the firing.

  • status

    'done' | 'skipped' | 'failed'

    Optional. Shorthand for a final status.set on the firing activity, appended after ops. Without it, the activity stays active.

defineAction({
  name: 'escalate',
  when: "dateTime($now) > dateTime($fields.dueDate) + 60*60*24",
  roles: ['editor'],
  ops: [defineOp({type: 'field.set', target: {field: 'escalated'}, value: {type: 'literal', value: true}})],
})

Effect declaration

Pass this object to defineEffect(...) in an action's effects array. The engine queues it when the action commits, and a handler registered under its name runs it. The Engine API reference lists every property, including bindings, outputs, retry, and runtime.

defineAction({
  name: 'notify',
  when: 'true',
  effects: [
    defineEffect({
      name: 'notify-reviewer',
      bindings: {article: '$fields.subject._id'},
      outputs: [{name: 'messageId', type: 'string'}],
    }),
  ],
})

Operations and value expressions

Operations change instance state in the same commit as the action that carries them.

Operation catalogue

Use these objects with defineOp(...) in an action’s ops array. An effect handler’s successful completion can also return the eight field.* operations directly.

  • field.set

    Any field

    Replaces the field value with a resolved value expression. Runtime-written references must target a declared resource.

  • field.setIfMissing

    Nullable field

    Sets a resolved value only when the current nullable value is absent or null. List fields are always arrays and are rejected.

  • field.inc

    number

    Adds a resolved finite numeric delta to the current value and rejects a non-finite result. The delta defaults to 1.

  • field.dec

    number

    Subtracts a resolved finite numeric delta from the current value and rejects a non-finite result. The delta defaults to 1.

  • field.unset

    Any field

    Clears the field value.

  • field.append

    List field

    Appends a resolved value to a list field. The value must match the declared member shape.

  • Merges an object into rows selected by where. Valid only for array fields; merged rows must retain the declared shape, and the merge cannot write _key or _type.

  • field.removeWhere

    Any list field

    Removes rows selected by where. Valid for every list field, including doc.refs.

  • status.set

    Activity

    Sets an activity to active, done, skipped, or failed. Available to actions, not effect completions; it defaults to the firing activity but may name a sibling activity.

  • audit

    List field

    Authoring shorthand for field.append that adds actor and at stamps to an object value. Rename the stamps with stampFields: {actor?, at?}; a stamp that collides with an authored key is rejected.

Set one field from an action parameter and clear another:

defineAction({
  name: 'change-deadline',
  params: [{name: 'deadline', type: 'dateTime', required: true}],
  ops: [
    defineOp({
      type: 'field.set',
      target: {field: 'deadline'},
      value: {type: 'param', param: 'deadline'},
    }),
    defineOp({type: 'field.unset', target: {field: 'reminderSentAt'}}),
  ],
})

Seed a counter only when it is missing, then increment it:

defineAction({
  name: 'retry',
  ops: [
    defineOp({
      type: 'field.setIfMissing',
      target: {field: 'attempts'},
      value: {type: 'literal', value: 0},
    }),
    defineOp({type: 'field.inc', target: {field: 'attempts'}}),
  ],
})

A field.setIfMissing that finds a value already there writes nothing, and records no opApplied history entry. field.inc and field.dec add or subtract each time they run, so a retried call counts twice. If a caller may retry, pass an idempotency key.

To retry an effect a fixed number of times, count attempts before each dispatch. Retry only while the count is below your limit, and send the last failure to manual recovery. For example, a definition with a limit of three makes three attempts in total and never queues a fourth.

field.inc, field.dec, and field.setIfMissing belong to data model 6. They don’t raise the reader floor, so an older engine still reads the instance, but it doesn’t recognize these operations. Before you deploy a definition that uses one, see Upgrade Workflows packages.

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, stage, or activity scope, where activity is the activity whose action queued the effect, and status.set is not allowed:

return {
  ops: [
    {
      type: 'field.set',
      target: {scope: 'stage', field: 'generatedDocument'},
      value: {type: 'literal', value: documentRef},
    },
    {
      type: 'field.set',
      target: {scope: 'activity', field: 'generationStatus'},
      value: {type: 'literal', value: 'ready'},
    },
  ],
}

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. The action that carries this operation must declare a reason param:

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 while the instance is in that stage. A guard predicate isn’t a workflow condition. The Content Lake runs it in delta mode against each attempted change to a matched document. See Hold publishing with guards.

  • Requiredname

    string

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

  • title

    string

    Optional. Display label.

  • Optional. Display explanation.

  • Requiredmatch

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

    Required. actions lists at least one of create, update, delete, publish, unpublish. types intersects the ID criteria; matching either idRefs (typed reads) or idPatterns (resource-local IDs with *) is enough. Invalid patterns, and release-version patterns that would broaden to every document, are rejected before deploy.

  • predicate

    string

    Optional. Delta-mode GROQ over document.before, document.after, mutation.action, and guard.metadata, with identity() and -> on stored resource-local references. Only a strict true allows a matching mutation. If you omit `predicate`, every mutation that `match` selects is denied.

  • metadata

    Record<string, GuardRead>

    Optional. Values resolved when the guard is created or refreshed and exposed as guard.metadata. A read is {type: 'self'}, {type: 'now'}, {type: 'fieldRead', field, path?} (workflow fields only), or {type: 'effectsRead', effect, path?}.

defineGuard({
  name: 'freeze-copy',
  match: {idRefs: [{type: 'fieldRead', field: 'subject'}], actions: ['update']},
  predicate: '!delta::changedAny((title, body))',
})

Subworkflow spawn declaration

Set this object as an action’s spawn property. When that action fires, the engine creates the declared child workflow instances in the same commit.

  • RequiredforEach

    string

    Required. GROQ producing one row per child. Evaluated with workflow-scope $fields only and no caller variables or named predicates. Each row needs a unique identity: _key, then _id, then a GDR's id, or a scalar's string form. Missing or duplicate identities reject the spawn.

  • Requireddefinition

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

    Required. Deployed child definition. version defaults to the latest.

  • with

    Record<string, string>

    Optional. Child input-field values, each evaluated per row with $row.

  • context

    Record<string, string>

    Optional. Values evaluated once, without $row, and delivered to each child as $context. Results must be strings, numbers, booleans, or full GDRs; null omits the entry, and other objects reject the spawn.

  • onExit

    'detach' | 'abort'

    Optional. Default 'detach', which leaves live children running when their cohort leaves scope. 'abort' stops them recursively. Aborting the parent always stops its children.

defineAction({
  name: 'translate',
  when: 'true',
  spawn: {
    forEach: '*[_id == $fields.subject._id][0].locales',
    definition: {name: 'translate-article'},
    with: {locale: '$row'},
  },
})

Cohort row

Each element of $subworkflows has these properties. stage is null until the child instance has been created and loaded.

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 place a condition runs, called a site, provides a fixed set of variables. defineWorkflow and deploy reject a condition that reads a variable its site doesn’t provide. For why the sites differ, see Caller-bound and caller-blind sites.

defineWorkflow, validateDefinition, and deploy also reject a reference that can’t resolve where it’s read. Each error names the site. When a valid name is close, the error suggests it, for example reads $feilds, which this scope does not bind (did you mean $fields?). These references are rejected:

  • A $<name> that the site doesn’t provide and that isn’t a declared predicate. This applies at every condition site, including predicate bodies, spawn expressions, start.filter, start groq requirements, and the abort.filter of a stored definition.
  • A named predicate read in spawn.forEach, which provides no predicates. Also a predicate body that reads its own name.
  • A param the action doesn’t declare, read in a caller-fired action’s effect bindings or operation where as $params.<name>, or as a {type: 'param'} value. fireAction passes undeclared params through without checking them, so declare every param the action reads.
  • An action that declares a param named hasOwnProperty. GROQ reads every $params value through that method, so the name is reserved. fireAction also rejects a hasOwnProperty arg that the action doesn’t declare.
  • A {type: 'param'} value on an action with when. An automatic action has no caller, so it has no params.

Variables

The Binding column says when a variable has a value:

  • always: at every site that evaluates an instance.
  • caller: only when someone, such as an editor or a service, is acting. These sites are called caller-bound.
  • row: only for one row at a time, in spawn.with or an operation where.
  • start: only in the start block.

Variable

Type

Binding

Meaning

$self

string

always

GDR URI of the instance document. *[_id == $self][0] reads the instance in the snapshot.

$fields

object

always

Field values by name; activity fields overlay stage fields, which overlay workflow fields. A subject or doc.ref read returns the hydrated document when it is in the snapshot; doc.refs items and release.ref stay references ({id, type}).

$parent

string | null

always

Parent instance GDR URI, null on a root instance.

$ancestors

string[]

always

Ancestor instance GDR URIs, root first.

$stage

string

always

Current stage name.

$now

string

always

ISO timestamp shared by every condition in one evaluation pass.

$context

object

always

Start context by entry name: values seeded at startInstance plus a parent's spawn.context. Never changes after start.

$effects

object

always

Completed effect outputs, $effects['<effect>'].<output>; the latest completed run anywhere in the instance’s history wins, earlier stage visits included. Read it in effect bindings. To branch on a result, have the handler write it to a field and wait on $effectStatus.

$effectStatus

object

always

'done' or 'failed' per effect, for runs queued during the current stage visit. Absent until the effect completes for this visit. Gate transitions on it: $effectStatus['<effect>'] == 'done', or defined($effectStatus['<effect>']) for done or failed.

$activities

object[]

always

Current stage activity rows, with status.

$allActivitiesDone

boolean

always

Every current activity is done or skipped. The default transition condition.

$anyActivityFailed

boolean

always

Some current activity is failed.

$subworkflows

object[]

always

Every spawned child, as cohort rows. Settled gate: count($subworkflows[activity == '<name>' && current && status == 'active']) == 0.

$<predicate>

boolean

always

Each named predicate from predicates, pre-evaluated over the same scope.

$actor

Actor

caller

Acting identity, {kind, id, roles}; undefined without a caller.

$assigned

boolean

caller

The caller matches the activity's assignee or assignees field, by user ID or by role under roleAliases. Direct user members take precedence over role members. false outside an activity, without a caller, or on an empty assignment.

$can

object

caller

Advisory per-permission booleans from the caller's grants; undefined without grants.

$attributes

object

caller

Advisory organization-level User Attributes (Enterprise), the values user::attributes() reads in Content Lake. Fetched only at caller-bound sites and never stored. Reads at most 100 values. Unbound when absent, so a condition reading it fails closed; an unexpected fetch error stops the evaluation.

$params

object

caller

The fired action's arguments. Bound only while that action's effect bindings and operation where expressions evaluate.

$row

any

row

One spawn.forEach row while spawn.with evaluates, or the stored row under test in an operation where.

$tag

string

start

The engine's partition. In a start filter or start requirement, *[...] already scans only this partition's instances, as rows carrying definition, subject, and completedAt, so the scan needs no partition test: count(*[definition == $definition && !defined(completedAt)]) < 3. Raw GROQ against stored instance documents filters on their tag field.

$definition

string

start

Name of the definition being evaluated.

Condition sites

Each row lists the variables a site provides on top of the always set. All always means every always variable, including named predicates.

Site

Binds

Decides

Transition when

All always

Picks the first route whose condition is true, in declaration order.

Activity filter

All always

Whether the activity exists in this stage visit. Evaluated once at entry.

Action when

All always

Fires the action automatically when true.

Action filter, action with when

All always

Whether the automation exists.

Action filter, caller-fired action

All always, $actor, $assigned, $can, $attributes

Whether the action exists for this caller. Advisory.

Activity requirements

All always, $actor, $assigned, $can, $attributes

Whether the activity's caller-fired actions are enabled. Advisory.

Field editable and stage editable condition

All always, $actor, $assigned, $can, $attributes

Who may edit the field. Advisory.

Effect bindings, caller-fired action

All always, $actor, $assigned, $params

Values passed to the effect handler.

Effect bindings, action with when

All always, $actor, $assigned

Values passed to the effect handler.

Operation where, caller-fired action

$row, $params, $actor, $self, $fields, $stage, $now, $context, $effects, $effectStatus, $activities, $allActivitiesDone, $anyActivityFailed

Which rows the operation changes. An unevaluable row never matches. No other variable, and no named predicate.

Operation where, action with when

Same as caller-fired, without $params

Which rows the operation changes.

spawn.forEach

Always set except named predicates; $fields holds workflow fields only

Which children to create.

spawn.with

All always, $row, $actor, $assigned

Each child's input values.

spawn.context

All always, $actor, $assigned

The children's $context.

Named predicate (predicates)

Always set except other predicates

A reusable boolean, $name.

start.filter

$tag, $definition, $now, and the candidate document as root

Whether a start surface offers the definition for a document. Never gates startInstance.

start.requirements query

$tag, $definition, $now, $fields (raw inputs)

Whether a fresh standalone start may proceed.

Guard predicate

document, mutation, guard (not $ variables)

Whether Content Lake allows the mutation.

In start.filter and in a start requirement, * scans a reduced view of the instances in the engine’s partition. Each row has only definition, subject, and completedAt, and the scan includes completed and aborted instances.

Guard predicates

Guard predicates are not conditions. They read document.before, document.after, mutation.action, and guard.metadata; see the guard declaration and Hold publishing with guards.

Visiting agent?

Was this page helpful?