Workflows

Coordinate content across projects and datasets

Run one workflow over content that lives in other projects, datasets, Media Libraries, or Canvas, and prove the routing before you rely on it.

One workflow can govern content that lives somewhere other than the workflow itself. The instance and its guards sit in one resource. The documents they coordinate can sit in other projects, other datasets, a Media Library, or Canvas. This guide sets that up end to end, and shows how to prove the routing works before you rely on it.

The failure this guide is written against is a quiet one: a reference that names a resource your deployment never declared. Reads and writes treat that case differently, so a setup that looks correct while you browse can still reject a write later. Verify the routing shows you which one you have.

For the model underneath the config on this page, how a deployment binds a workflowResource, what a tag partitions, and how each @<handle>: reference resolves at deploy, see Deployments, tags, and resources.

Decide where workflow data lives and what it acts on

Two decisions come first, and they are independent. The workflow resource is where instances, history, and guards are stored. The content resources are where the documents a workflow governs already live. Nothing requires them to be the same place, and picking one workflow resource for an organization is a reasonable way to keep operational data out of editorial datasets.

A launch review might spread across three resources:

  • Workflow data: the workflows dataset in the opsproject project.
  • Editorial content: the production dataset in the webproject project.
  • Campaign assets: the mlcampaign Media Library.

The engine is configured against the workflow resource and reaches the other two through references. It never copies content: an instance stores a reference that carries its own location, so the content stays where its own editors and permissions already are. Global document references describes the reference shape itself.

Bind resource aliases per environment

A global document reference is physical: it names an exact project and dataset, or an exact resource. Baked into a definition, that pins the definition to one environment. A resource alias is the indirection that avoids it. A definition references content as @<alias>:<documentId>, and each deployment binds the alias to a real resource.

Alias names are lowercase letters, digits, and dashes, with no leading dash. Bind them per deployment in sanity.workflow.ts, where resourceAliases is a list of bindings and a duplicate name is rejected when the config parses.

Aliases are expanded at deploy, not at runtime. Every @content: reference becomes a physical reference in the deployed definition, so an instance never sees an alias and nothing has to resolve one while a workflow runs. Moving to another environment is deploying the same source against a different binding list. A single-resource setup needs no aliases at all: bare document ids root at the workflow resource. See Configure and deploy workflow definitions for the rest of the deployment shape.

Route foreign resources to their own clients

The engine holds one client, configured against the workflow resource. To read a document in another resource, it needs a client for that resource. It resolves one in three steps, in order:

  • resourceClients(parsed), when the resolver you passed 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. If the client has no withConfig, this throws instead.

Step three is why a cross-resource setup often needs no resolver at all. A @sanity/client has withConfig, and a derived sibling inherits everything except the resource, including the token. When one token can read every resource involved, the derived sibling is enough and resourceClients adds nothing.

Pass resourceClients when that is not true: a foreign resource needing a different token, a different API host, or any config the workflow client cannot supply by changing resource alone. The resolver receives the parsed reference and returns a client or undefined.

Pass references that carry their resource

Build a reference with the constructor for its scheme: refDataset, refMediaLibrary, refCanvas, or refDashboard. Each produces a reference that names its own resource, so a value stays unambiguous wherever it travels.

Pass the stable document id for a dataset reference, such as launch-article. Do not pass drafts.launch-article or versions.<release>.launch-article. A dataset reference identifies the logical document, and the workflow perspective decides whether the draft, the published document, or a release version is what gets read. Fields covers the field types these values land in, and Effects and runtimes covers reaching them from an effect handler.

Verify the routing

Read routing and write-time validation do not admit the same set of resources, and this is the part worth testing before you rely on it. Reads fall back to a derived sibling client for any resource the workflow client can reach. Writes do not: the declared resource surface is the workflow resource plus every resource resourceClients returns a client for, and nothing else.

So a resource served only by the derived-sibling fallback is readable but not writable as a reference value. A field write carrying a reference to it is rejected with a ref-resource-undeclared error naming the resource and the declared surface, and the write does not commit. The engine checks this when it resolves initial fields at start and again on every operation that writes a reference.

Prove both directions against your real configuration before you deploy. Start an instance with a reference into each foreign resource and confirm it commits, which exercises the write surface, then read a field back through a condition or an effect binding, which exercises the router. A resource that reads fine and fails on write is the signature of a missing resourceClients entry.

Two mistakes produce a confusing version of this. Returning undefined from resourceClients for a resource you meant to declare leaves it off the write surface even though reads still work through the fallback. Comparing assembled resource-qualified strings instead of comparing the parsed reference fields makes the resolver miss references it was written to match.

Was this page helpful?