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 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 deploypnpm dlx sanity-workflows deployyarn dlx sanity-workflows deploybunx 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. 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:
allowedistrueonly when every supplied row is valid and every start requirement is satisfied.requirementslists each requirement with itsoutcome:satisfied,unsatisfied, orunevaluablewhen an input it reads is not supplied yet.missingRequiredlists required inputs you have not supplied. It does not changeallowed, so check it separately.invalidInitialFieldslists rowsstartInstancewould reject, with areason:undeclared,not-input,wrong-kind,duplicate, ordocument-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_IDThe 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_IDReplace 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_IDpnpm dlx sanity-workflows list --document dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_IDyarn dlx sanity-workflows list --document dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_IDbunx sanity-workflows list --document dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_IDThe 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.
- Run Workflows with Sanity Functions: start and advance instances without a server.
- Engine reference:
startInstance,evaluateStart, and the other engine methods.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?