> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# 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](https://www.sanity.io/docs/workflows/getting-started) 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](https://www.sanity.io/docs/workflows/getting-started): `article-review` deployed, and a `sanity.workflow.ts` that exports the `dev` deployment.
- `scripts/engine.ts` from [Run the engine from your own code](https://www.sanity.io/docs/workflows/run-the-engine), 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`:

**workflows/article-review.ts**

```typescript
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](https://www.sanity.io/docs/workflows/conditions) covers GROQ requirements and the `start.filter` used for discovery.

Deploy the change:

**npm**

```shell
npx sanity-workflows deploy
```

**pnpm**

```shell
pnpm dlx sanity-workflows deploy
```

**yarn**

```shell
yarn dlx sanity-workflows deploy
```

**bun**

```shell
bunx 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:

**scripts/start-review.ts**

```typescript
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](https://www.sanity.io/docs/workflows/global-document-references) explains the format, and [Coordinate content across projects and datasets](https://www.sanity.io/docs/workflows/cross-resource-workflows) 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](https://www.sanity.io/docs/workflows/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`:

```sh
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:

```text
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:

```text
Review already in progress: unsatisfied
```

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

```sh
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:

**npm**

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

**pnpm**

```shell
pnpm dlx sanity-workflows list --document dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID
```

**yarn**

```shell
yarn dlx sanity-workflows list --document dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID
```

**bun**

```shell
bunx 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.

> [!NOTE]
> When a start fails partway
> If a start fails after the instance document was created, `startInstance` throws `StartNotPrimedError` or `StartNotSettledError`, and the message names the instance ID. Run the script again with that ID to finish the start. To discard the unfinished start instead, abort the instance with `npx sanity-workflows abort INSTANCE_ID`.

## 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:

**npm**

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

**pnpm**

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

**yarn**

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

**bun**

```shell
bunx 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:

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

The [`start` command reference](https://www.sanity.io/docs/workflows/cli-reference) lists every flag, including `--version` to start from an older deployed version and `--json` for scripts.

## Next steps

- Finish the open instance with `npx sanity-workflows fire-action INSTANCE_ID --activity write --action submit`, then `--activity sign-off --action approve`. After that the article can be reviewed again.
- [Definitions, stages, and instances](https://www.sanity.io/docs/workflows/definitions-and-instances): what a start pins and why a running instance keeps its version.
- [Write GROQ conditions](https://www.sanity.io/docs/workflows/conditions): require inputs with GROQ start requirements.
- [Build a workflow interface with the App SDK](https://www.sanity.io/docs/workflows/app-sdk): start instances from a custom interface.
- [Run Workflows with Sanity Functions](https://www.sanity.io/docs/workflows/sanity-functions): start and advance instances without a server.
- [Engine reference](https://www.sanity.io/docs/workflows/engine): `startInstance`, `evaluateStart`, and the other engine methods.

