Start a workflow instance

Start workflow instances from code and the CLI: build the subject reference, pre-flight start requirements, and retry a start safely.

A workflow instance is one run of a deployed definition against a piece of content. In the quick start the CLI started one for you. This guide starts article-review instances from code and from the CLI, refuses a second review of an article that already has one open, and makes a retried start safe.

At the end you have a script that checks whether a start is allowed before it writes anything, explains a refusal, and resumes a start that failed partway instead of creating a duplicate.

You need:

  • The finished quick start: article-review deployed, and a sanity.workflow.ts that exports the dev deployment.
  • scripts/engine.ts from Run the engine from your own code, which exports engine, and an API token with write access to the dataset in SANITY_AUTH_TOKEN.
  • The _id of the published article from the quick start.

Allow one open review per article

startInstance runs a definition's start requirements before it writes anything. Add a singleSubject requirement to article-review so an article can have at most one unfinished review. Edit workflows/article-review.ts:

export const articleReview = defineWorkflow({
  name: 'article-review',
  // ...
  fields: [
    defineField({
      type: 'subject',
      name: 'subject',
      title: 'Article',
      required: true,
      description: 'The document this instance moves through the stages.',
      initialValue: {type: 'input'},
    }),
  ],
  start: {
    requirements: [
      {
        type: 'singleSubject',
        name: 'one-open-review',
        title: 'Review already in progress',
      },
    ],
  },
  stages: [
    // ...
  ],
})

A singleSubject requirement refuses a start while an unfinished run of the same definition holds the same subject, across every deployed version of the definition. title is the text a refusal shows. Start filters and requirements covers GROQ requirements and the start.filter used for discovery.

Deploy the change:

npx sanity-workflows deploy

The deploy stores a new version of article-review. Instances already running stay on the version they started on, and new starts use the new version.

Write the start script

Create scripts/start-review.ts. It takes the article _id, and optionally the instance ID of an earlier attempt to retry:

import {gdrRef, instanceDocId, StartNotAllowedError} from '@sanity/workflow-engine'

import {dev} from '../sanity.workflow'
import {engine} from './engine'

async function main() {
  const [documentId, retryId] = process.argv.slice(2)
  if (!documentId) {
    throw new Error('Usage: npx tsx scripts/start-review.ts DOCUMENT_ID [INSTANCE_ID]')
  }

  const initialFields = [
    {
      type: 'subject' as const,
      name: 'subject',
      value: gdrRef({res: dev.workflowResource, documentId, type: 'article'}),
    },
  ]

  // A retry resumes its own start, so only a fresh start needs the pre-flight.
  if (!retryId) {
    const preflight = await engine.evaluateStart({definition: 'article-review', initialFields})
    if (!preflight.allowed || preflight.missingRequired.length > 0) {
      for (const requirement of preflight.requirements) {
        console.log(`${requirement.title ?? requirement.name}: ${requirement.outcome}`)
      }
      for (const field of preflight.missingRequired) console.log(`Missing input: ${field.name}`)
      for (const issue of preflight.invalidInitialFields) {
        console.log(`Invalid input: ${issue.name} (${issue.reason})`)
      }
      process.exitCode = 1
      return
    }
  }

  const instanceId = retryId ?? instanceDocId(dev.partition)
  console.log(`Starting ${instanceId}`)

  try {
    const {instance} = await engine.startInstance({
      definition: 'article-review',
      instanceId,
      initialFields,
    })
    console.log(`Started ${instance._id} at ${instance.currentStage}`)
  } catch (error) {
    if (error instanceof StartNotAllowedError) {
      const unmet = error.unmetRequirements.map((r) => r.title ?? r.name)
      console.log(`Not started: ${unmet.join(', ')}`)
      process.exitCode = 1
      return
    }
    throw error
  }
}

main().catch((error) => {
  console.error(error)
  process.exit(1)
})

Subject reference. gdrRef builds the global document reference the subject field takes, {id, type}, from the deployment's workflowResource and the document _id. Its id is the same dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID string you typed in the quick start. Pass the published _id: a drafts. or versions. ID throws VersionSpecificDatasetGdrError, because the instance's perspective, not the ID, selects draft or release content. gdrFromResource(res, documentId) returns only the id string, when you need it without a type. Global document references explains the format, and Coordinate content across projects and datasets starts on content in another dataset.

Initial fields. initialFields supplies a value for each input-sourced field the definition declares at workflow scope, matched by type and name. article-review declares one, subject. Configure fields declares more inputs.

Pre-flight. evaluateStart runs the same checks as startInstance without writing anything. Read three things from its result:

  • allowed is true only when every supplied row is valid and every start requirement is satisfied. requirements lists each requirement with its outcome: satisfied, unsatisfied, or unevaluable when an input it reads is not supplied yet.
  • missingRequired lists required inputs you have not supplied. It does not change allowed, so check it separately.
  • invalidInitialFields lists rows startInstance would reject, with a reason: undeclared, not-input, wrong-kind, duplicate, or document-not-found.

The pre-flight is advisory. Another start can land between the pre-flight and the start, so startInstance checks again and throws StartNotAllowedError with unmetRequirements when a requirement refuses it. There is no option to override a requirement.

Instance ID. The script mints an ID with instanceDocId(partition) and prints it before starting. A start is three writes: create, prime, and the first cascade. A supplied instanceId is the retry key across all three: pass the same ID again and the engine resumes that start, or does nothing if it already settled. Passing it for a different definition throws ContractViolationError. Without an ID, each call mints a new one, so a blind retry can create a second instance. The retry path skips the pre-flight, because the interrupted start already holds the subject and the requirement would refuse it.

Start an instance

Run the script with the article _id:

SANITY_AUTH_TOKEN=YOUR_TOKEN npx tsx scripts/start-review.ts DOCUMENT_ID

The script prints the instance ID, then the stage the instance settled in:

Starting dev.wf-instance.a1b2c3d4e5f6
Started dev.wf-instance.a1b2c3d4e5f6 at drafting

Verify the start rules

Run the same command again. The first review is still open, so the pre-flight refuses the start and nothing is written:

Review already in progress: unsatisfied

Now retry the first start with its instance ID, as you would after a network failure:

SANITY_AUTH_TOKEN=YOUR_TOKEN npx tsx scripts/start-review.ts DOCUMENT_ID INSTANCE_ID

Replace INSTANCE_ID with the ID the first run printed. The script prints the same instance in the same stage, and no second instance exists. Check with the CLI:

npx sanity-workflows list --document dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID

The list shows one in-flight article-review instance for the article, the one your first run started.

When a start fails partway

Start from the CLI

The CLI start command runs the same checks. Pass each input with --field, and --instance-id to retry an earlier attempt:

npx sanity-workflows start article-review \
  --field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}' \
  --instance-id INSTANCE_ID

While the article's review is open, a start without --instance-id exits with an error that names the refused requirement:

✖ Start rejected
✖ start error:
  startInstance refused definition "article-review": Review already in progress. Pre-flight the verdict with evaluateStart.

The `start` command reference lists every flag, including --version to start from an older deployed version and --json for scripts.

Next steps

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme