Workflows

Reusable UI components

Add assignment, date, member, and workflow-diagram controls to a custom Workflows interface.

Workflows provides reusable React components in two packages. @sanity/workflow-components contains assignment and date controls, timestamps, and status indicators. @sanity/workflow-diagram renders workflow stages and transitions. Both work in App SDK applications, Studio integrations, and other React hosts.

These packages provide presentation, not an application framework. Your host supplies project members and workflow data, chooses which evaluated fields to render, and connects edits and actions through the reactive session.

Install the packages

Install the Workflows packages and the React, styled-components, and Sanity UI versions used by these examples:

Keep all Workflows packages on one release. React, React DOM, and styled-components must each resolve to one shared copy in your application.

If your interface uses App SDK hooks, complete the App SDK installation before connecting these controls to live data.

Workflows components and diagrams use Sanity UI 4. Wrap your interface in its theme provider:

import {studioTheme, ThemeProvider} from '@sanity/ui'

<ThemeProvider theme={studioTheme}>
  <WorkflowInterface />
</ThemeProvider>

Components

Choose a component by the interaction your interface needs. Each table lists the props a host application sets for that component family.

Select people for personal assignments or select project roles to offer work to a group. If any person is assigned, role selections no longer put that task in other people’s lists. Store a role’s machine name, not its display title. Role aliases let people with another role qualify for an assignment without adding the task to their role group’s list.

Assignment badges

Use badges to display stored user and role assignees.

import {AssigneeBadges} from '@sanity/workflow-components'

<AssigneeBadges assignees={assignees} vocabulary={{members, roles}} />

Assignment stacks

Use AssigneeStack to display an assignment in a dense row. People appear as round avatars, roles as square avatars, and optional glyphs follow both. At most three slots are visible, including an overflow count. Your host supplies accessible labels and full names in surrounding or hover content.

import {AssigneeStack} from '@sanity/workflow-components'

<AssigneeStack members={assignedMembers} roles={assignedRoles} />
  • members

    readonly {id: string; displayName: string; imageUrl?: string; loginProvider?: string}[]

    Resolved members to show as round avatars.

  • roles

    readonly MemberRole[]

    Project roles shown as square avatars. The title supplies initials and the machine name supplies color. Your host supplies full-name tooltips and accessible labels.

  • glyphs

    readonly {icon: ReactNode; key: string}[]

    Optional trailing glyphs. They render after members and roles and remain visible when avatars overflow.

Member and role avatars

Use MemberAvatar for a project member and RoleAvatar for a project role. Missing member photos fall back to initials. Role avatars are square and use the role title when available. Your host supplies accessible labels and full-name tooltips for individual avatars and avatar groups.

import {MemberAvatar, MemberAvatarGroup} from '@sanity/workflow-components'

<MemberAvatar imageUrl={member.imageUrl} name={member.displayName} />
<MemberAvatarGroup members={members} />
  • Display name used to derive initials. Supply an accessible label in the surrounding UI.

  • MemberAvatar.imageUrl

    string | undefined

    Optional profile image URL.

  • MemberAvatar.loginProvider

    string | undefined

    Optional sign-in provider badge. Google, GitHub, and saml-prefixed providers have marks; unrecognized providers render no badge.

  • MemberAvatar.size

    AvatarSize | AvatarSize[] | undefined

    Optional Sanity UI avatar size. Defaults to 0.

  • RoleAvatar.role

    MemberRole

    The project role to render. A direct host must wrap the avatar with its own tooltip and accessible name.

  • RoleAvatar.size

    AvatarSize | AvatarSize[] | undefined

    Optional Sanity UI avatar size.

  • MemberAvatarGroup.members

    readonly MemberAvatarData[]

    Member records with id, displayName, and optional imageUrl. MemberAvatarData is exported for consumers that supply these records.

  • MemberAvatarGroup.maxLength

    number | undefined

    Maximum stack length before overflow is collapsed. Defaults to 3.

Member picker

Use MemberPicker for choosing one project member.

import {MemberPicker} from '@sanity/workflow-components'

<MemberPicker
  {...members}
  onSelect={setMember}
  selectedIds={selectedIds}
/>

Assignee picker

Use AssigneePicker for assignments that accept people and roles. Pass maxUsers={1} for a singular assignee field: selecting another person replaces the current person and keeps the selected roles. eligibleRoles limits new choices to people who qualify for those roles. Role rows show a count of eligible people.

This example starts with work offered to the editor role and allows one person to be selected. Replace editor with a role in your project. Supply directory from useProjectMembers(projectId) and, if present, roleAliases from the workflow definition returned by the engine. This example keeps selection in local React state; the field-editing examples on this page show how to commit it.

  • AssigneePicker.members

    readonly ProjectMember[]

    Resolved project members available to assign.

  • AssigneePicker.roles

    readonly MemberRole[]

    Project role catalog. An empty array falls back to roles held by loaded members.

  • Shows the loading state.

  • AssigneePicker.error

    string | undefined

    Shows the member-loading failure.

  • AssigneePicker.value

    readonly Assignee[]

    Current user and role assignees.

  • AssigneePicker.onChange

    (value: readonly Assignee[]) => void

    Receives the complete next assignee value.

  • AssigneePicker.eligibleRoles

    readonly string[] | undefined

    Required project role names for new assignment choices. Existing selections remain visible and removable. Omit to offer all available people and roles.

  • AssigneePicker.roleAliases

    RoleAliases | undefined

    The definition’s normalized role aliases. They widen which people satisfy eligibleRoles and contribute to role-member counts; they do not change literal inbox ownership.

  • AssigneePicker.maxUsers

    number | undefined

    Set to 1 to replace the selected person while preserving selected roles. Omit to allow multiple people. Other numeric values do not limit selection.

Pickers place the current user first, then sort other people by display name. Search matches names, email addresses, role names, and role titles. Hovering a person shows their email, identity-provider badge, current-user status, and project roles. Use eligibleRoles and roleAliases to constrain new assignment choices. Continue to respect the evaluated field and action verdicts when committing changes.

A non-empty role catalog controls the offered roles, including roles with no holders. If the catalog is empty or unavailable, the picker uses member-held roles. A role already stored in the value remains visible and removable.

Date controls

Use DatePicker for selection only. ClearableDatePicker adds an explicit clear action.

import {ClearableDatePicker, DatePicker} from '@sanity/workflow-components'

<DatePicker onSelect={setDate} value={date} />

<ClearableDatePicker
  clearLabel="Clear due date"
  onClear={() => setDueDate(undefined)}
  onPick={setDueDate}
  value={dueDate}
/>

Progress

Use ProgressBar to display a progress field. It does not edit the field.

import {ProgressBar} from '@sanity/workflow-components'

<ProgressBar label="Content import" value={62.5} />

Timestamps and workflow status

Use these components to show when work started and whether it has finished:

  • AgoStamp displays a compact age and an absolute local timestamp on hover. Pass an ISO timestamp and update the caller-owned now value; the component starts no timer. Invalid timestamps display a dash, and future timestamps display “just now.”
  • ActivityStatusIcon displays active, done, failed, or skipped activity status. Unknown values use a muted empty icon. Include a text label when the icon is the only status indicator.
  • StartIncompleteBadge marks a workflow whose start has not finished.

Pass an activity’s status, the workflow’s start timestamp, and its incomplete-start state from your host. This example renders them under the Sanity UI theme provider configured for the interface:

To reuse whole tables inside Studio, see Embed tables in a custom Studio tool.

Supply project members

Member-aware components consume ProjectMembersState, which contains members, the project role catalog, and loading state. App SDK applications can load it with useProjectMembers(projectId). See Edit project-member assignments.

A resolved ProjectMember.id is the account-global sanityUserId stored in user assignments. If the profile cannot supply that ID, the row retains its project membership ID. The engine rejects new assignments that still use an unresolved project ID.

  • assignmentMatch(members, identity)(members, identity): AssignmentMatch: 'user' | 'role' | undefined

    Reports whether the supplied identity holds the assignment directly or through a literal project role. Any direct user member takes precedence over every role route. Role aliases do not affect this match.

    Parameters

    • members

      readonly Assignee[]

      The complete user-and-role assignment list.

    • identity

      AssignmentIdentity

      An object with the account-global userId and literal project roles.

  • roleMemberCount(args)(args.members, args.requiredRoles, args.roleAliases): number

    Counts distinct people who can fulfill any supplied role. It includes literal and alias-based eligibility and deduplicates by account-global user ID.

    Parameters

    • args.members

      readonly ProjectMember[]

      The loaded project member directory.

    • args.requiredRoles

      readonly string[]

      The project role names whose eligible people should be counted.

    • args.roleAliases

      RoleAliases | undefined

      The workflow definition’s normalized role aliases. Pass undefined when no aliases apply.

  • memberCountLabel(count): string

    Formats a count as 0 members, 1 member, or 2 members.

    Parameters

    • count

      number

      The number of distinct people.

Role titles are presentation data. An assignee value continues to store the machine role name, so changing a project role's title does not change persisted workflow data.

Pass the resolved state to any member-aware component. The component renders loading and error states from the same object.

Connect editable fields

Editable controls render values from session.evaluation.editableFields. Commit changes through session.editField so the engine preserves workflow history and consistency. The App SDK guide shows the complete pattern.

The recipes connect these presentation components to evaluated fields and session commits. The wrappers in these recipes belong to the application, not the component package.

export function WorkflowEditableFields({
  evaluation,
  members,
  session,
}: {
  evaluation: WorkflowEvaluation
  members: ProjectMembersState
  session: Pick<WorkflowSession, 'editField'>
}) {
  return evaluation.editableFields.map((field) => {
    const key = `${field.scope}:${field.activity ?? ''}:${field.name}`
    if (isAssignmentField(field)) {
      return <AssignmentPicker field={field} key={key} members={members} session={session} />
    }
    if (isDateField(field)) {
      return <DateField field={field} key={key} session={session} />
    }
    return null
  })
}

The complete recipe includes the value-shape guards used by isAssignmentField and isDateField.

Render an enabled editor only when the evaluated field is editable. Surface rejected commits instead of assuming an optimistic edit succeeded.

Render the workflow diagram

Pass the session’s WorkflowEvaluation to WorkflowDiagram. The pinned definition describes the graph, currentStage marks the active node, history draws the visited path, and evaluation supplies live explanations.

Loading...
import {Card, Stack, Text} from '@sanity/ui'
import {WorkflowDiagram} from '@sanity/workflow-diagram'
import type {WorkflowEvaluation} from '@sanity/workflow-engine'

export function WorkflowDiagramRecipe({evaluation}: {evaluation: WorkflowEvaluation}) {
  return (
    <Card border padding={4} radius={3}>
      <Stack gap={4}>
        <Stack gap={2}>
          <Text size={2} weight="semibold">
            Workflow diagram
          </Text>
          <Text muted size={1}>
            Current stage and visited path
          </Text>
        </Stack>
        <WorkflowDiagram
          currentStage={evaluation.instance.currentStage}
          definition={evaluation.definition}
          evaluation={evaluation}
          explain
          history={evaluation.instance.history}
          key={evaluation.instance._id}
        />
      </Stack>
    </Card>
  )
}
  • definition

    WorkflowDefinition

    Complete workflow definition to render.

  • currentStage

    string | undefined

    Stage to highlight as current.

  • history

    readonly HistoryEntry[] | undefined

    Tones visited stages and traversed transitions.

  • gatedTransitions

    readonly string[] | undefined

    Transition names that should show a locked marker.

  • local

    LocalView | undefined

    Limits the graph to context around the current stage.

  • static

    boolean | undefined

    Disables pan, zoom, and zoom controls.

  • devMode

    boolean | undefined

    Shows machine names and raw conditions.

  • explain

    boolean | undefined

    Shows human-readable gate explanations.

  • evaluation

    WorkflowEvaluation | undefined

    Adds live verdicts when explain is enabled.

  • height

    number | string | undefined

    Canvas height, defaulting to 360. Without fill, a numeric height is a pixel ceiling and smaller graphs shrink the canvas. CSS strings and heights with fill enabled are used as supplied.

  • fill

    boolean

    Fill the parent’s width and use height as supplied. The initial fit centers the graph without enlarging it beyond 1:1. Defaults to false.

The diagram uses color, line style, and opacity to distinguish run state. The current stage carries the accent color. Completed stages carry checkmarks. Traversed transitions are solid, while transitions not taken are dashed. Unvisited stages that can no longer be reached from the current stage fade with their transitions.

Hovering a stage restores its immediate neighborhood and animates dashed outgoing transitions toward their targets. Hovering a transition animates that transition. The animation is disabled when the user prefers reduced motion.

Diagram theming

WS_CARD_TOKENS maps diagram colors to the surrounding Sanity UI card. Use --ws-path to customize the traversed path.

If a wrapper using WS_CARD_TOKENS needs explicit typography, set --ws-font and --ws-font-weight on it or derive them from useTheme_v2().font.text.

Key the diagram by instance ID to reset the viewport when opening a different workflow. Ordinary updates preserve the reader’s pan and zoom.

Enable explain to show gate explanations. Pass the live evaluation to add current verdicts.

Next steps

Visiting agent?

Was this page helpful?