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-reviewdeployed, and asanity.workflow.tsthat exports thedevdeployment. scripts/engine.tsfrom Run the engine from your own code, which exportsengine, and an API token with write access to the dataset inSANITY_AUTH_TOKEN.- The
_idof 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 new start while an unfinished instance of the same definition already has the same subject. It checks instances of every deployed version of the definition. title is the text shown when the requirement refuses a start. For GROQ requirements, and for the start.filter that decides where a workflow is offered, see Start filters and requirements.
Deploy the change:
npx sanity-workflows deploypnpm dlx sanity-workflows deploy
yarn dlx sanity-workflows deploy
bunx sanity-workflows deployThe 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. The subject field takes a global document reference (GDR): an object with an id and a type. gdrRef builds it from the deployment’s workflowResource and the document _id. The 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. The instance’s perspective decides whether it reads draft or release content, not the ID. If you need only the id string, without a type, use gdrFromResource(res, documentId).
To learn more about the format, see Global document references. To act on content in another dataset, see Coordinate content across projects and datasets.
Initial fields. initialFields gives a value to each field the definition declares as an input (initialValue: {type: 'input'}) at workflow scope. Each row matches a field by type and name. article-review declares one input, subject. To declare more inputs, see Configure workflow fields.
Pre-flight. evaluateStart runs the same checks as startInstance without writing anything. Read three things from its result:
allowed:trueonly when every supplied row is valid and every start requirement is satisfied.requirementslists each requirement with itsoutcome. The outcome issatisfied,unsatisfied, orunevaluable.unevaluablemeans the requirement reads an input you haven’t supplied yet.missingRequired: the required inputs you haven’t supplied. It doesn’t affectallowed, so check it separately.invalidInitialFields: the rowsstartInstancewould reject. Each row has areason, such asundeclared,not-input,wrong-kind,duplicate, ordocument-not-found.
The pre-flight is advisory. Another start can happen between the pre-flight and your start. So startInstance checks the requirements again. If a requirement refuses the start, it throws StartNotAllowedError, and unmetRequirements lists the requirements that failed. You can’t override a requirement.
evaluateStart finds existing instances with a GROQ query, so it can miss a commit made in the last second or so. For details, see commit visibility.
Instance ID. The script creates an instance ID with instanceDocId(partition) and prints it before it starts the instance. If the start fails, you can retry it with that ID:
- If the start was interrupted, the engine finishes it.
- If the start already finished, the call does nothing.
- If the ID belongs to an instance of a different definition, the call throws
ContractViolationError.
Without an ID, each call creates a new one, so a blind retry can create a second instance. The retry path skips the pre-flight. The interrupted start already holds the subject, so the singleSubject requirement would refuse it.
Start an instance
Run the script with the article _id. Replace YOUR_TOKEN with an API token that has write access to the dataset, and DOCUMENT_ID 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 draftingVerify 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: unsatisfiedNow 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
pnpm dlx sanity-workflows list --document dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID
yarn dlx sanity-workflows list --document dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID
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.
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:
npx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}' \
--instance-id INSTANCE_IDpnpm dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}' \
--instance-id INSTANCE_IDyarn dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}' \
--instance-id INSTANCE_IDbunx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}' \
--instance-id INSTANCE_IDWhile 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
- 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: what a start pins and why a running instance keeps its version.
- Write GROQ conditions: require inputs with GROQ start requirements.
- Build a workflow interface with the App SDK: start instances from a custom interface.
- Generate Functions from your workflow definitions: start autonomous workflows and advance instances without a server.
- Engine reference:
startInstance,evaluateStart, and the other engine methods.