Workflows

The reactive session

How a reactive session projects one workflow instance for a UI: what keeps it current, what each session state means, and when a preview becomes a commit.

Early access

A reactive session is the live, UI-facing view of one workflow instance. Use it when you are building a custom Workflows surface that must stay current as the instance, referenced content, or guards change.

An evaluation is the engine’s current read-only view of stages, activities, fields, and action verdicts. The session refreshes that evaluation as observed content changes. Writes happen only when your interface calls tick, fireAction, or editField.

What a session is bound to

A session is bound to one instance ID and one store observer. The observer is what an adapter supplies, so the session itself is store-agnostic: the same evaluation and the same commands appear whether the host is Sanity Studio, an App SDK application, or a custom adapter. Swapping the instance ID resets the projection, and the previous instance's evaluation never carries over into the next one.

Instance previews

A preview is a reduced instance row for list and dashboard surfaces: current stage, list-relevant fields, open activities, and lifecycle timestamps, without the definition snapshot or the audit history. useInstancePreviews in @sanity/workflow-react holds a live set of them. It takes an engine and a store observer, so an adapter supplies the observer: @sanity/workflow-studio wraps the hook as useWorkflowInstancePreviews, and an App SDK application builds one with makeSdkObserver from @sanity/workflow-sdk/observer. See Build a workflow interface with the App SDK for a worked example.

const {previews, loading, drained, unreadable, error} =
  useInstancePreviews({
    engine,
    observer,
    filter: {includeCompleted: true},
    pageSize: 500,
  })

previews is undefined until the first page lands, then stays newest-first. drained means every matching page has landed, so counts are totals. Before that, a count is a minimum. Render unreadable rows and error explicitly. A fetch error preserves pages that already landed.

The hook owns paging. pageSize defaults to 500, and the filter it accepts cannot set limit, before, or ids, because the hook derives them. Leave interPageDelayMs at its 0 production default; a delay only helps when watching pages arrive one at a time in a development harness. When the change feed reconnects after a failure it emits a reset, and the hook re-drains the partition so additions, edits, and deletions missed during the gap all land.

How the session stays current

The adapter observes the instance, the documents its conditions read, and its active guards. The session waits for those sources before setting ready and producing its first evaluation. Later emissions recompute that evaluation automatically.

A command still commits through the engine. After tick, fireAction, or editField settles, the session evaluates the committed result immediately instead of waiting for the observer to echo the write.

In a custom App SDK UI, the adapter observes synchronized documents from the Content Lake and feeds them into the reactive session. The editor can preview a tentative field value before the session invokes an engine commit and returns the settled evaluation.

Loading...

Session states

  • invalid means an engine-owned document cannot be read safely. A document from a newer model needs a newer engine. A document with the wrong shape needs investigation. Show the reader what to do about it, instead of a spinner.
  • error means an observer read failed. The state clears when the stream recovers.
  • evaluationError means the last evaluation attempt failed. Render it before treating a missing evaluation as loading. It clears after a successful attempt or when the instance ID changes.
  • !ready means the session is still synchronizing. A missing evaluation is a loading state only while evaluationError is undefined. Switching instance IDs clears the previous evaluation while the new instance synchronizes.
  • ready with an evaluation means the interface can render stages, activities, fields, and action verdicts.

Preview, then commit field edits

Use previewField while a person is typing. It changes the projected field value and any advisory verdicts derived from that value, but it never changes the committed stage, activity status, or history.

Every editField call is a real engine commit with history, a guard refresh, and transition evaluation, written under a revision check. A picker such as an actor, date, or boolean fires one discrete change, so committing on change is fine there. Free text such as a string or a note body has to wait for a deliberate boundary instead: blur, Enter, or Save. A commit per keystroke writes a history entry each time and races the revision check.

Call discardFieldPreview when editing ends without a commit. The next evaluation returns to the committed value.

Action verdicts

Whether an action appears, and whether it can be fired, are properties of the evaluation rather than of the authored definition. The evaluated action model is described in Activities and actions.

  • A failed filter removes the action from the interface.
  • An action with when is a cascade trigger, not a button. Describe what it will do from whenInsight. See Evaluation insights for the complete explanation contract.
  • For a visible manual action, use its verdict to decide whether the control is enabled and to explain why it is disabled.

Choose an integration

  • For Workflows in Sanity Studio, use the Studio plugin. It provides the ready-made editor and workflow interfaces.
  • Use @sanity/workflow-studio for a custom Studio integration such as a Document Action, dialog, tool, pane, or input.
  • Use @sanity/workflow-sdk in an App SDK application. It supplies the store observer the session needs. See Build a workflow interface with the App SDK for setup and resource routing.
  • Use @sanity/workflow-react directly for the hooks an adapter does not re-export, such as useInstancePreviews. Its @sanity/workflow-react/observer subpath is the adapter SPI: reach for that only when building a new store adapter. See Custom reactive adapters for the observer contract.

Reference

The hook takes engine and instanceId. Pass grantsFromPath as well when the definition's conditions use $can. It returns the current reactive state and the commands that preview or commit changes:

  • evaluation

    WorkflowEvaluation | undefined

    The latest WorkflowEvaluation that resolved successfully. It is undefined before the first success and resets when the instance ID changes. A later failed attempt leaves the last successful value in place.

  • ready

    boolean

    True after the instance, watched documents, and guards have synchronized. It is false while the session is invalid or a live read has failed. An evaluation failure does not change it.

  • invalid

    InvalidDoc | undefined

    Describes an engine-owned document that failed validation. Show remediation instead of continuing to render workflow controls.

  • error

    unknown | undefined

    The current observer failure. It clears when the live stream recovers.

  • evaluationError

    unknown | undefined

    The last evaluation failure. It clears after a successful evaluation attempt or when the instance ID changes. It does not change ready.

  • guards

    readonly MutationGuardDoc[] | undefined

    The live guard list. It is undefined before the first successful guard read or when that stream is invalid.

  • tick

    () => Promise<OperationResult>

    Re-evaluates and advances the instance through automatic actions and transitions, then commits the result.

  • fireAction

    (args: {activity: string; action: string; params?: Record<string, unknown>}) => Promise<OperationResult>

    Fires one named activity action with optional parameters and commits the result.

  • editField

    (args: {field: EditableFieldEvaluation; mode?: EditMode; value?: unknown} | {target: EditFieldTarget; mode?: EditMode; value?: unknown}) => Promise<OperationResult>

    Commits one declared editable-field operation. Call it at a deliberate editing boundary.

  • previewField

    (args: {field: EditableFieldEvaluation; mode?: EditMode; value?: unknown} | {target: EditFieldTarget; mode?: EditMode; value?: unknown}) => void

    Stages an in-memory field preview and recomputes the evaluation without writing history or persisted state.

  • discardFieldPreview

    (fieldOrTarget?: EditableFieldEvaluation | EditFieldTarget) => void

    Removes the preview for one field target, or every staged preview when no target is supplied.

Next steps

Visiting agent?

Was this page helpful?