Deployments, partitions, and resources
How a deployment binds definitions to a Sanity resource: name versus partition, the storage partition that keeps environments apart, and the aliases that point a definition at real content.
Early access
Workflows is in early access, built in public. Read How early access works before you rely on it.
A deployment is one entry in the deployments array of a sanity.workflow.ts config. It binds a set of workflow definitions to the Sanity resource that stores them, gives their instances an environment partition to live in, and declares the engine data model every runtime reading that resource can interpret. Every CLI command reaches a definition through a deployment.
This page explains the model that config expresses. To install the CLI and write your first config, see Configure and deploy workflow definitions. Every key, type, default, and validation error is in the sanity.workflow.ts configuration reference.
Deployment identities: name and partition
A deployment has two identifiers and they answer different questions. name is its unique CLI identity, so it always resolves to exactly one deployment. Use --deployment <name> when a command must target one. partition labels the stored definition and instance partition, and may group deployments across different workflow resources.
Because one partition can cover several deployments and a name never can, the two selectors do not behave the same way on every command. The Workflow CLI command reference has the full matrix, including which commands treat a partition as a group and which require it to resolve to exactly one deployment.
Storage partitions: workflowResource and partition
One pair must stay unique: no two deployments may share both a workflowResource and a partition. That pair is the storage partition, so two deployments sharing it would write into the same place and fight over definition versions. The config rejects the collision at load and names both entries.
Renamed in 0.37
Before 0.37 this was called tag. Rename each tag key to partition and keep the same value; deleting the key instead moves the deployment to the default partition. See the Workflows release notes.
Environments are kept apart by that pair, not by anything inside the definition. The same definition deployed under two partitions produces two independent sets of instances that never see each other. To run one definition in both staging and production, either point the two deployments at different datasets or keep one dataset and give them different partitions.
partition is optional for a deployment that is alone on its workflowResource, and it then resolves to default. Several deployments on one resource must each set a distinct partition. When you add a second deployment to a resource, make the first one explicit as partition: 'default' so its address does not change. Separate resources may each use default. The rule checks the configuration only; it does not discover deployments in the Content Lake or in other configs.
Engine documents store the partition in a field named tag, so raw GROQ against stored definitions and instances filters on tag, as in tag == $tag.
Engine document storage
The documents a workflow owns, its deployed definitions and the instances it spawns, are written to the workflowResource: one Sanity resource. The CLI derives the project and dataset of its client from it, so every deployment must declare it. A dataset, with id <projectId>.<dataset>, is the common case. Canvas, media-library, and dashboard resources are accepted too. Point it at a dedicated dataset when you would rather keep engine documents out of the dataset your editors work in.
Resource aliases
Content a workflow reads and writes is addressed through resourceAliases. A definition points at content by alias name rather than a hardcoded dataset, as in {id: '@content:article-123', type: 'article'}, and resourceAliases binds each alias to a real resource. Using aliases is what lets the same definition ship to production or staging by binding differently.
You only need an alias for content in a different resource from the workflowResource. A definition can reference a document in the same resource by bare id, because bare ids root at the workflowResource. So the smallest single-dataset setup is a workflowResource and definitions, with no resourceAliases at all.
At deploy, each @<handle>: reference expands to the bound resource, and nothing alias-shaped survives into the deployed definition. A deployment that references an alias it does not bind fails rather than deploying against the wrong resource.
Two alias patterns look similar and do different jobs. Two aliases in one deployment mean one workflow touching more than one resource at once, such as a cross-brand publish reading two datasets together; see One workflow across resources. Coordinate content across projects and datasets binds one such alias. The same alias name bound differently across deployments means one workflow shipped to several equivalent environments; see Run the same workflow in more than one place.
Read routing and the write surface
An engine holds one client, configured against the workflowResource. A reference into any other resource needs a client for that resource, and the engine picks one in this order:
resourceClients(parsed), when the resolver passed to the engine returns a client for that reference.- The engine’s own client, when the reference addresses the workflow resource.
- A sibling derived from the engine’s client with
withConfig, cached per resource. The sibling inherits everything except the resource, including the token. A client withoutwithConfigcannot derive one, so the read throws instead.
Writes do not use that fallback. The declared resource surface is the workflow resource plus every resource resourceClients returns a client for, and nothing else: serving a resource is what declares it. The engine checks every reference a write carries against that surface, so a reference to an undeclared resource fails when it is written rather than being read later with whatever credentials happen to reach it.
A resource reached only through the derived sibling is therefore readable but not writable as a reference value. A write carrying a reference to it is rejected with a ref-resource-undeclared error that names the resource and the declared surface, and nothing commits. The check runs when startInstance resolves initial fields and again on every operation that writes a reference.
Two resolver mistakes look like routing bugs. Returning undefined from resourceClients for a resource you meant to declare leaves it off the write surface, while reads still work through the fallback. Comparing assembled resource strings instead of the parsed reference fields makes the resolver miss references it was written to match; resourceFromParsed and sameResource compare the parsed form.
The Workflows CLI builds its surface from the config. Its resolver is deploymentResourceClients({client, deployment}), which serves every resource the deployment binds in resourceAliases through clients derived from the CLI’s own token and API host. So start and instance commands such as fire-action, abort, and diagnose accept references to the workflow resource and to each bound resource, and binding a resource declares it even when no definition uses the alias. Your own code can pass the same resolver to createEngine.
Coordinate content across projects and datasets starts an instance on content in a second dataset and tests both the read and the write side.
Runtime hosting configuration
A runtime calls the engine to advance workflow instances and run effects, the external work a workflow queues. The runtime block declares which host runs that code. The experimental generator uses it to decide which Sanity Functions to create.
A workflow inherits its deployment's runtime.kind. Each effect inherits its workflow's kind. A block on the workflow or effect overrides that inherited value. When no level declares a block, the kind is function. For example, a deployment with runtime: {kind: 'selfHosted'} can keep one workflow on Sanity Functions by giving that workflow runtime: {kind: 'function'}.
The generator handles the three kinds as follows:
Kind | Generated runtime |
|---|---|
| Functions run effects and reevaluate time-based conditions. Effects without a custom budget share a drain function. |
| The plan reports the workflow or effect, but emits no durable execution function. A qualifying autonomous workflow can still receive a document watcher that starts its instances. |
| Your process runs the workflow or effect. The plan reports deadlines to tick, effects to drain, parents to reevaluate after children settle, and autonomous workflows to start. |
Generated runtime resources derive their names from the deployment name. Renaming a deployment that generates functions replaces those functions, so run npx sanity@latest blueprints plan and review every replacement before deploying.
The block is authoring metadata. Deployment removes it before storing the definition, so changing hosting does not create a definition version. Regenerate and redeploy the runtime after changing it. Regeneration preserves your handler implementations; directories no longer referenced by the generated Blueprint are inert and can be removed separately.
Effect blocks can also set a function timeout and memory, which give that effect its own drain function. The runtime block reference lists each key, its range, and where it is accepted.
Hosting is not an execution restriction. Any engine with a registered handler can drain that effect. The generator emits a heartbeat, a scheduled function, when a function-hosted workflow reads $now or spawns children, or when any workflow has a function-hosted effect. Once emitted, it ticks in-flight instances of all function-hosted workflows in the deployment. Recovery of function-hosted effects has a separate scope and can include completed instances and workflows hosted elsewhere. See heartbeat recovery.
Functions and engine operations
A runtime that hosts workflows on Sanity Functions pairs an event with an engine operation. Document Functions run on a content change and Scheduled Functions run on a cron schedule. Inside either, the code calls the engine operation that produces the workflow outcome it is there for:
Workflow outcome | Trigger | Engine operation |
|---|---|---|
Create an instance when a document becomes eligible | Document Function filtered on the change that makes the document eligible |
|
Reevaluate promptly after a field a condition reads changes | Document Function filtered on that field delta |
|
Reevaluate as time passes | Scheduled Function on a cron schedule |
|
Send an explicit action | The Function for the change or schedule that warrants it |
|
Run queued effects | Document Function that runs when the number of unclaimed effects increases, or the Scheduled Function that already ticks |
|
Two engine rules shape these pairings. Time passing creates no document event, so anything that depends on time, such as a condition that reads $now, is reevaluated only when something calls tick(). And an action that declares when has no caller-driven path: fireAction() rejects it, and it fires the next time another operation reevaluates the instance and finds the condition true.
Effects are external work a workflow queues: the engine records the work and a registered handler performs it. drainEffects() selects a handler by effect name and continues until no registered work can be claimed. There is no combined tick-and-drain operation, so a Scheduled Function that ticks can also drain in the same invocation, accepting its interval as the delay before queued work runs. Register handlers in one drainer only when they can share credentials, dependencies, timeout budget, scaling, and ownership, and use separate drainers where any of those differ.
Run Workflows with Sanity Functions builds a Document Function that drains effects and a Scheduled Function that ticks in-flight instances.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?