> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# 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.

> [!WARNING]
> Early access
> Workflows is in early access, built in public. Read [How early access works](https://www.sanity.io/docs/workflows/prerelease) 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](https://www.sanity.io/docs/workflows/deploy-definitions). Every key, type, default, and validation error is in the [sanity.workflow.ts configuration reference](https://www.sanity.io/docs/workflows/workflow-config-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](https://www.sanity.io/docs/workflows/cli-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.

> [!NOTE]
> 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](https://www.sanity.io/docs/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](https://www.sanity.io/docs/workflows/deploy-definitions). 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](https://www.sanity.io/docs/workflows/global-document-references). [Coordinate content across projects and datasets](https://www.sanity.io/docs/workflows/cross-resource-workflows) 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](https://www.sanity.io/docs/workflows/deploy-definitions).

## 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:

1. `resourceClients(parsed)`, when the resolver passed to the engine returns a client for that reference.
2. The engine’s own client, when the reference addresses the workflow resource.
3. 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 without `withConfig` cannot 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](https://www.sanity.io/docs/workflows/cross-resource-workflows) 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](https://www.sanity.io/docs/workflows/generate-functions) 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 |
| --- | --- |
| `function` | Functions run effects and reevaluate time-based conditions. Effects without a custom budget share a drain function. |
| `durableFunction` | 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. |
| `selfHosted` | 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](https://www.sanity.io/docs/workflows/workflow-config-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](https://www.sanity.io/docs/workflows/generate-functions).

## Functions and engine operations

A runtime that hosts workflows on Sanity Functions pairs an event with an engine operation. [Document Functions](https://www.sanity.io/docs/functions/function-quickstart) run on a content change and [Scheduled Functions](https://www.sanity.io/docs/functions/scheduled-function-quickstart) run on a cron schedule. Inside either, the code calls the [engine operation](https://www.sanity.io/docs/workflows/engine) 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 | `startInstance()`. Nothing else creates an instance, so a missed change is not recovered by a later `tick()`. |
| Reevaluate promptly after a field a condition reads changes | Document Function filtered on that field delta | `tick()` once per affected instance. Optional when the scheduled delay is acceptable. |
| Reevaluate as time passes | Scheduled Function on a cron schedule | `tick()` once per in-flight instance. The cron interval is the longest delay before a time-based condition is reevaluated. |
| Send an explicit action | The Function for the change or schedule that warrants it | `fireAction()`, for actions that declare no `when`. |
| Run queued effects | Document Function that runs when the number of unclaimed effects increases, or the Scheduled Function that already ticks | `drainEffects()`. |

Two engine rules shape these pairings. Time passing creates no document event, so anything that depends on time, such as a [condition](https://www.sanity.io/docs/workflows/conditions) that reads `$now`, is reevaluated only when something calls `tick()`. And an [action](https://www.sanity.io/docs/workflows/activities-and-actions) 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](https://www.sanity.io/docs/workflows/effects-and-runtimes) 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](https://www.sanity.io/docs/workflows/sanity-functions) builds a Document Function that drains effects and a Scheduled Function that ticks in-flight instances.

