Generate Functions from your workflow definitions
Generate the Sanity Functions your workflow definitions need, connect an effect handler, and deploy them with Blueprints. Experimental.
Early access
Workflows is in early access, built in public. Read How early access works before you rely on it.
The Workflows generator reads your workflow definitions and creates the Sanity Functions they need: drains that run effects, start watchers that start autonomous workflows when documents match their start conditions, and a scheduled heartbeat for periodic reevaluation and effect recovery.
Experimental generator
Runtime generation is experimental and not ready for production use. Its commands and generated files may change. To configure the Functions yourself, follow Run Workflows with Sanity Functions.
This guide generates the Functions for the notify-reviewer effect on article-review, connects the effect handler you already wrote, deploys everything, and checks that a queued effect runs without a terminal.
You need:
- The finished quick start:
article-reviewdeployed, and asanity.workflow.tsthat exports thedevdeployment. - Handle asynchronous work with effects: the
notify-reviewereffect onarticle-review, deployed, and theeffect-handlers.tsmodule at the project root that exportseffectHandlers. - Familiarity with Sanity Functions and Blueprints, and an organization-scoped stack.
The generated Functions import each deployment from sanity.workflow.ts by its name, so the workflow deployment config must export each deployment object under that name, as the quick start does with export const dev, and include it in the default config. Deployment names must be valid JavaScript binding names: production works, prod-eu does not. A workflow with no effects, no $now conditions, no subworkflows, and no autonomous start needs no generated Functions. The effect is what gives article-review a drain and a heartbeat.
Install the generator packages
Install the Functions and Blueprints tooling and the Workflows generator in the quick start project. @sanity/workflow-engine and @sanity/workflow-cli are already installed:
npm install --save-dev --save-exact @sanity/blueprints @sanity/functions \
@sanity/workflow-blueprintpnpm add --save-dev --save-exact @sanity/blueprints @sanity/functions \
@sanity/workflow-blueprintyarn add --dev --exact @sanity/blueprints @sanity/functions \
@sanity/workflow-blueprintbun add --dev --exact @sanity/blueprints @sanity/functions \
@sanity/workflow-blueprintKeep @sanity/workflow-blueprint at the same version as @sanity/workflow-engine and @sanity/workflow-cli, and commit the updated lockfile. Each generated Function declares the versions of @sanity/functions, @sanity/workflow-blueprint, and @sanity/workflow-engine from your package.json, and the generator stops if one of them is missing.
Generate the Functions
Remove hand-written Functions first
If you deployed the Functions from Run Workflows with Sanity Functions, remove them before you generate. The generator uses the same names: it overwrites functions/wf-dev-drain-effects/index.ts and adds a second wf-dev-runtime robot token to sanity.blueprint.ts. Delete functions/wf-dev-drain-effects/, functions/wf-dev-tick-instances/, and their resources in sanity.blueprint.ts.
From the directory that contains sanity.workflow.ts, run the generator:
npx sanity-workflows blueprints generatepnpm dlx sanity-workflows blueprints generateyarn dlx sanity-workflows blueprints generatebunx sanity-workflows blueprints generateThe command first prints the runtime needs it found for each deployment, then the files it wrote:
Generated tree:
created workflows.blueprint.ts
created functions/wf-dev-drain-effects/index.ts
created functions/wf-dev-drain-effects/package.json
created functions/wf-dev-heartbeat/index.ts
created functions/wf-dev-heartbeat/package.json
created effect-handlers/all.ts
created effect-handlers/notify-reviewer.ts yours now — implement the effect
created sanity.blueprint.ts the workflow resources spread
✔ 8 file(s) written, 0 left untouchedThe command covers every deployment in the config. workflows.blueprint.ts declares a robot token, the drain, and the heartbeat, using the project and dataset from the dev deployment's workflowResource. The generator adds those resources to sanity.blueprint.ts, and creates that file if it does not exist. Generated function and robot names derive from each deployment's name, so renaming a deployment replaces its Functions. Resolve any reported naming collision or manual wiring requirement before deployment.
Every generated function builds a runtime with createRuntime from @sanity/workflow-blueprint/runtime and calls one verb on it. A drain calls runtime.drain(event), the heartbeat calls runtime.heartbeat(), and a start watcher calls runtime.start(event):
// Generated by `sanity-workflows blueprint generate` from the workflow definitions.
// Every run rewrites this file, so an edit here does not survive.
import {documentEventHandler} from '@sanity/functions'
import {createRuntime} from '@sanity/workflow-blueprint/runtime'
import {effectHandlers} from '../../effect-handlers/all'
import {dev as deployment} from '../../sanity.workflow'
export const handler = documentEventHandler(async ({context, event}) => {
const runtime = createRuntime(deployment, {context, effects: effectHandlers})
await runtime.drain(event)
})The heartbeat in functions/wf-dev-heartbeat/index.ts has the same shape. It calls runtime.heartbeat() and passes no handlers.
For a function of your own, use runtime.engine: an engine that runs the handlers passed in effects.
Connect the effect handler
effect-handlers/all.ts is the handler registry the drain imports. The generator rewrites it on every run. effect-handlers/notify-reviewer.ts is a stub that throws until you implement it. Point the stub at the handler in effect-handlers.ts, so the drain script from the effects guide and the generated drain run the same code. Replace the contents of effect-handlers/notify-reviewer.ts:
import type {EffectHandler} from '@sanity/workflow-engine'
import {effectHandlers} from '../effect-handlers'
// Runs the handler from effect-handlers.ts, so the drain script and the Function share it.
export const notifyReviewer: EffectHandler = (params, ctx) => {
const handler = effectHandlers['notify-reviewer']
if (!handler) throw new Error('effect-handlers.ts has no notify-reviewer handler')
return handler(params, ctx)
}From inside the effect-handlers/ directory, ../effect-handlers resolves to the effect-handlers.ts file at the project root. Rerun npx sanity-workflows blueprints generate: it reports the stub as kept, and --check reports that the generated tree matches the definitions.
Regeneration preserves your handler files and rewrites the generated modules. It deletes nothing. A directory under functions/ that carries the generator's header but that the current definitions no longer produce is stale: the generated Blueprint does not deploy it, blueprints generate lists it for you to remove, and blueprints generate --check fails until you do. Before deployment, test repeated delivery using ctx.effectKey, the effect's idempotency key, and confirm that your handler does not repeat external writes.
Deploy definitions and Functions
Check for drift, deploy definitions, then deploy the functions:
npx sanity-workflows blueprints generate --check
npx sanity-workflows deploy --all-partitions
npx sanity@latest blueprints deploypnpm dlx sanity-workflows blueprints generate --check
pnpm dlx sanity-workflows deploy --all-partitions
pnpm dlx sanity@latest blueprints deployyarn dlx sanity-workflows blueprints generate --check
yarn dlx sanity-workflows deploy --all-partitions
yarn dlx sanity@latest blueprints deploybunx sanity-workflows blueprints generate --check
bunx sanity-workflows deploy --all-partitions
bunx sanity@latest blueprints deployThe generated Blueprint leaves its definition resources commented out because the Blueprints service does not accept sanity.workflow resources yet. Functions resolve definitions from the Content Lake, so the definition deploy must finish first.
The generated heartbeat requires an organization-scoped stack. It requests a once-per-minute schedule. Check the Functions frequency limits before you rely on it: a schedule more frequent than your plan permits may deploy without being invoked.
Check the result
The quick start instance is already finished, so start a new one and submit it. Entering review fires the notify trigger, which queues notify-reviewer:
npx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}'
npx sanity-workflows fire-action INSTANCE_ID --activity write --action submitpnpm dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}'
pnpm dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submityarn dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}'
yarn dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submitbunx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:DATASET_NAME:DOCUMENT_ID","type":"article"}'
bunx sanity-workflows fire-action INSTANCE_ID --activity write --action submitReplace INSTANCE_ID with the ID the start command printed. The queued effect invokes the generated drain. Give it a moment, then show the instance with its history:
npx sanity-workflows show INSTANCE_ID --include historypnpm dlx sanity-workflows show INSTANCE_ID --include historyyarn dlx sanity-workflows show INSTANCE_ID --include historybunx sanity-workflows show INSTANCE_ID --include historyThe Pending effects section is gone, and the History section includes Effect settled: notify-reviewer (done). The drain Function's logs contain the handler's line, Notify the reviewer: ... is ready for review. If the effect is still listed under Pending effects, check the drain's logs. If the drain never picks it up, the heartbeat re-queues it after five minutes.
Finish the instance with npx sanity-workflows fire-action INSTANCE_ID --activity sign-off --action approve.
Check changes before deployment
When workflows.blueprint.ts exists beside the workflow config, deploy checks the generated runtime against the whole config before resolving a token or writing definitions. --check, --dry-run, --only, and deployment selectors do not bypass or narrow this check. Regenerate if generated files drift, restore missing handlers, and resolve reported wiring problems before deploying. Without a generated tree, deploy reports runtime needs and continues.
After a package upgrade or a change to runtime settings, regenerate, preserve and review your handlers, deploy definitions, then redeploy Functions. Use blueprints generate --check in CI for a standalone drift check. Editing generated modules creates drift and regeneration overwrites those edits.
Predict when generated triggers run
A start watcher observes the content documents a workflow can start on. Its trigger follows the document reads in start.filter:
What the start filter reads | When the watcher runs |
No start filter, or a filter with no document or clock dependencies beyond | Document creates only. Definitions can share one creates-only watcher. |
Known document paths | Creates and updates that change the paths the filter reads, for that definition. |
The whole document, other documents, dynamic paths, or | Creates and every update. Time passing alone does not trigger a document watcher. |
Creates-only watchers do not revisit existing documents after you deploy them. Start those workflow instances explicitly when needed. A watcher without explicit subject types excludes engine-owned documents in its trigger, and the engine applies the remaining subject type rules. Generated effect drains wake when an instance is created or its pendingEffects changes, and only when an unclaimed effect belonging to that drain is present.
A start watcher passes the whole event document to runtime.start, which reads its _rev and throws a ContractViolationError when it is missing. Give hand-built documents for sanity functions test a _rev, and keep _rev in any function projection.
Plan for heartbeat recovery
The generator emits a heartbeat when a function-hosted workflow reads $now or spawns subworkflows, or when any workflow declares a function-hosted effect. Each heartbeat ticks in-flight instances of all function-hosted workflows in the deployment. It does not tick workflows hosted as durableFunction or selfHosted.
Effect recovery is separate from workflow ticking. The heartbeat re-queues every pending entry of a function-hosted effect that no drain picked up within five minutes of being queued, on any instance in the partition, and releases expired claims on those instances. The re-queue wakes the matching drain, which runs the handler. The heartbeat itself runs no effect handler.
Each heartbeat run works on at most 30 instances for recovery and at most 30 for ticking. Later runs reach the rest, so a large backlog takes several runs.
Recovery has no guaranteed completion cadence, so do not rely on the heartbeat to bound how long a stuck effect waits. Generation also does not check that an effect's retry policy fits its Function timeout: retries run inside one drain invocation, so size timeout for the whole retry run.
Next steps
- Choose hosting and effect budgets with the runtime settings.
- Look up generation output and drift failures in the CLI reference.
- Write the drain and a scheduled tick yourself with Run Workflows with Sanity Functions.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?