Run the engine from your own code
Create a Workflows engine from your deployment config, then start and advance an instance from a TypeScript script.
In the quick start, the Workflows CLI started and advanced an instance for you. The CLI is a wrapper around the engine, a TypeScript library you can call from your own code. Servers, workers, and Functions use the engine the same way.
This guide creates an engine from the quick start’s dev deployment, then starts an article-review instance and moves it to review from a script. Later guides import the engine module you create here.
You need:
- The finished quick start:
article-reviewdeployed, and asanity.workflow.tsthat exports thedevdeployment. - An API token with write access to the dataset, such as an Editor token. Authentication and tokens explains how to create one.
- The
_idof the publishedarticlefrom the quick start.
Install the client and a TypeScript runner
The engine needs a Sanity client to read and write workflow documents. tsx runs the TypeScript script without a build step.
npm install @sanity/client npm install --save-dev tsx
pnpm add @sanity/client pnpm add --save-dev tsx
yarn add @sanity/client yarn add --dev tsx
bun add @sanity/client bun add --dev tsx
Create the engine
Create scripts/engine.ts. It builds the client and the engine from the dev deployment, so the script and the CLI use the same dataset and partition:
import {createClient} from '@sanity/client'
import {clientConfigFromResource, createEngine, ENGINE_API_VERSION} from '@sanity/workflow-engine'
import {dev} from '../sanity.workflow'
const token = process.env.SANITY_AUTH_TOKEN
if (!token) {
throw new Error('Set SANITY_AUTH_TOKEN to a token with write access to the dataset')
}
export const client = createClient({
...clientConfigFromResource(dev.workflowResource),
apiVersion: ENGINE_API_VERSION,
token,
useCdn: false,
})
export const engine = createEngine({
client,
workflowResource: dev.workflowResource,
partition: dev.partition,
})clientConfigFromResource turns the deployment’s workflowResource into the client’s projectId and dataset. ENGINE_API_VERSION is the API version the CLI uses. Set useCdn: false so every read returns the latest instance state.
createEngine takes the client, the resource that holds workflow documents, and the partition. The engine only sees instances in its own partition, so partition must match the deployment the instances were started under. Deployments and partitions covers running more than one.
The engine acts as the token’s identity. Every action it fires is recorded against that user or robot, and conditions such as $actor evaluate against it. Actors, tokens, and what’s actually enforced explains what that identity can and can’t do.
Start and advance an instance
Create scripts/review-article.ts. It starts an instance against the article you pass in, lists the actions the token can fire, and fires write/submit:
import {gdrRef} from '@sanity/workflow-engine'
import {dev} from '../sanity.workflow'
import {engine} from './engine'
async function main() {
const documentId = process.argv[2]
if (!documentId) {
throw new Error('Pass the article _id: npx tsx scripts/review-article.ts DOCUMENT_ID')
}
const {instance} = await engine.startInstance({
definition: 'article-review',
initialFields: [
{
type: 'subject',
name: 'subject',
value: gdrRef({res: dev.workflowResource, documentId, type: 'article'}),
},
],
})
console.log(`Started ${instance._id} at ${instance.currentStage}`)
const evaluation = await engine.evaluate({instanceId: instance._id})
for (const {activity, actions} of evaluation.currentStage.activities) {
for (const {action, allowed} of actions) {
console.log(`${activity.name}/${action.name}: ${allowed ? 'allowed' : 'not allowed'}`)
}
}
const result = await engine.fireAction({
instanceId: instance._id,
activity: 'write',
action: 'submit',
})
console.log(`Now at ${result.instance.currentStage}`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})gdrRef builds the same global document reference you typed into the quick start’s start command, from the deployment’s resource and the document _id.
evaluate reads the instance from the token’s point of view and doesn’t change it. Each action reports allowed. When an action isn’t allowed, it also reports a disabledReason.
fireAction fires write/submit, which marks the write activity done. The engine then runs the cascade: it fires every transition whose condition now holds. Here, the stage’s to-review transition fires because all of its activities are done. The CLI’s fire-action command runs the same code.
fireAction returns these values:
instance: the instance after the cascade.cascaded: the number of transitions the cascade fired.changed: whether the call changed the instance. It’sfalsewhen the call succeeds but has nothing to do, for example when the action already ran.
Run the script
Pass the token as an environment variable and the article _id as an argument. Replace YOUR_TOKEN with your API token, and DOCUMENT_ID with the article’s _id:
SANITY_AUTH_TOKEN=YOUR_TOKEN npx tsx scripts/review-article.ts DOCUMENT_ID
The script prints the new instance, the actions available in drafting, and the stage after the cascade:
Started dev.wf-instance.a1b2c3d4e5f6 at drafting
write/submit: allowed
Now at reviewCheck the result from the CLI. The CLI reads the same partition, so it shows the instance your script started. Replace INSTANCE_ID with the ID the script printed after Started:
npx sanity-workflows show INSTANCE_ID
pnpm dlx sanity-workflows show INSTANCE_ID
yarn dlx sanity-workflows show INSTANCE_ID
bunx sanity-workflows show INSTANCE_ID
The instance is in review, with write done. Fire sign-off/approve from the CLI or add it to the script to finish it.
Instance not found
If a call fails with InstanceNotFoundError and partition mismatch, the engine’s partition doesn’t match the instance. Build the engine from the same deployment that started the instance.