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. Instances and their history sit in one resource. The documents they coordinate can sit in other datasets, other projects, a Media Library, or Canvas. This guide starts an article-review instance on an article in a second dataset, 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.
You need:
- The finished quick start:
article-reviewdeployed, and asanity.workflow.tsthat exports thedevdeployment. - A second dataset named
marketingin the same project, with one publishedarticledocument. Note its_id. Create the dataset withnpx sanity@latest dataset create marketing. - To start instances from your own code: the
scripts/engine.tsmodule from Run the engine from your own code.
For the model underneath the config on this page, how a deployment binds a workflowResource, what a partition partitions, and how each @<handle>: reference resolves at deploy, see Deployments, partitions, 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.
In this guide, article-review spans two datasets in one project:
- Workflow data: the quick start dataset,
DATASET_NAME. Thedevdeployment keeps its instances there. - Editorial content: the
marketingdataset, which holds the article to review.
The engine is configured against the workflow resource and reaches the marketing dataset through references. It never copies content: the instance's subject field holds a global document reference that names the marketing dataset, so the content stays where its own editors and permissions already are. A dataset in another project, a Media Library, or Canvas works the same way.
Bind the content dataset with a resource alias
A global document reference names an exact project and dataset, or an exact resource. Baked into a definition, that pins the definition to one environment. A resource alias avoids that. 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. resourceAliases is a list of bindings on a deployment, and a duplicate name is rejected when the config parses. Add a binding for the marketing dataset to the dev deployment in sanity.workflow.ts:
import type {WorkflowDeploymentInput} from '@sanity/workflow-engine'
import {defineWorkflowConfig} from '@sanity/workflow-engine/define'
import {articleReview} from './workflows/article-review'
export const dev = {
name: 'dev',
partition: 'dev',
expectedMinReaderModel: 10,
workflowResource: {type: 'dataset', id: 'PROJECT_ID.DATASET_NAME'},
resourceAliases: [{name: 'marketing', resource: {type: 'dataset', id: 'PROJECT_ID.marketing'}}],
definitions: [articleReview],
} satisfies WorkflowDeploymentInput
export default defineWorkflowConfig({deployments: [dev]})Aliases are expanded at deploy, so a deployed definition holds only physical references. To bind the same alias to a different dataset in each environment, see Run the same workflow in more than one place.
article-review has no @marketing: references, so there is nothing to redeploy. The CLI reads the binding from sanity.workflow.ts each time it runs, and expectedMinReaderModel stays at 10. The binding is also what lets the CLI accept a reference into marketing, even though no definition uses the alias.
Start an instance on the marketing article
Start article-review with a subject in the marketing dataset. Replace DOCUMENT_ID with the _id of the marketing article:
npx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'pnpm dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'yarn dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'bunx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'The instance is created in the quick start dataset, under dev, and its subject points at the marketing dataset. The start passes the write-time check because dev binds that dataset:
Started dev.wf-instance.a1b2c3d4e5f6 — now at draftingFire the first action. The subject field is required, so the engine reads the marketing article before it lets the action fire:
npx sanity-workflows fire-action INSTANCE_ID --activity write --action submitpnpm dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submityarn dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submitbunx sanity-workflows fire-action INSTANCE_ID --activity write --action submitConfirm the result:
npx sanity-workflows show INSTANCE_IDpnpm dlx sanity-workflows show INSTANCE_IDyarn dlx sanity-workflows show INSTANCE_IDbunx sanity-workflows show INSTANCE_IDshow reports stage review and partition dev. The instance could only leave drafting by reading the article in marketing, so the read routed correctly. Add --json to see the stored subject, dataset:PROJECT_ID:marketing:DOCUMENT_ID.
Route foreign resources to their own clients
The CLI builds its clients from your config. An engine you create in your own code reads a document in another resource through a client derived from its own, with the same token. It accepts a reference in a write only when its resourceClients resolver serves that resource. Read routing and the write surface explains why the two differ.
Starting an instance on the marketing article writes a reference. When one token can reach every resource, extend scripts/engine.ts from Run the engine from your own code with deploymentResourceClients, which serves every resource the deployment binds in resourceAliases:
import {
clientConfigFromResource,
createEngine,
deploymentResourceClients,
ENGINE_API_VERSION,
} from '@sanity/workflow-engine'
// ...the client stays as it is...
export const engine = createEngine({
client,
workflowResource: dev.workflowResource,
partition: dev.partition,
resourceClients: deploymentResourceClients({client, deployment: dev}),
})Write your own resolver when the derived sibling is not enough: a foreign resource that needs 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. Returning undefined leaves that resource off the write surface. This version gives the marketing dataset its own token, and matches on the parsed reference with sameResource rather than comparing assembled strings:
import {
clientConfigFromResource,
createEngine,
ENGINE_API_VERSION,
resourceFromParsed,
sameResource,
} from '@sanity/workflow-engine'
// ...the client stays as it is...
const marketing = {type: 'dataset', id: 'PROJECT_ID.marketing'} as const
// The marketing dataset needs its own token, so a derived sibling is not enough.
const marketingClient = client.withConfig({
...clientConfigFromResource(marketing),
token: process.env.MARKETING_TOKEN,
})
export const engine = createEngine({
client,
workflowResource: dev.workflowResource,
partition: dev.partition,
resourceClients: (parsed) =>
sameResource(resourceFromParsed(parsed), marketing) ? marketingClient : 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. Create scripts/start-marketing-review.ts. It imports the engine you extended above and starts an instance on the marketing article:
import {refDataset} from '@sanity/workflow-engine'
import {engine} from './engine'
async function main() {
const documentId = process.argv[2]
if (!documentId) {
throw new Error('Pass the article _id: npx tsx scripts/start-marketing-review.ts DOCUMENT_ID')
}
const subject = refDataset({
projectId: 'PROJECT_ID',
dataset: 'marketing',
documentId,
type: 'article',
})
const {instance} = await engine.startInstance({
definition: 'article-review',
initialFields: [{type: 'subject', name: 'subject', value: subject}],
})
console.log(`Started ${instance._id} at ${instance.currentStage}`)
console.log(`Subject: ${subject.id}`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})Run it with the marketing article's published _id, as in the quick start:
SANITY_AUTH_TOKEN=YOUR_TOKEN npx tsx scripts/start-marketing-review.ts DOCUMENT_IDThe script prints the new instance and its subject:
Started dev.wf-instance.a1b2c3d4e5f6 at drafting
Subject: dataset:PROJECT_ID:marketing:DOCUMENT_IDFields covers the field types reference values land in, and Effects and runtimes covers reaching them from an effect handler.
Verify the routing
You proved the read side when fire-action moved the instance to review. Writes are checked separately: a reference is accepted only into the workflow resource or a resource the deployment declares (see the write surface). Prove the write side by removing the binding. Comment out the resourceAliases line in sanity.workflow.ts and run the start command again:
npx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'pnpm dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'yarn dlx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'bunx sanity-workflows start article-review \
--field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'The start is rejected before anything is written:
✖ Start rejected
✖ start error:
Field entry "subject" (subject) rejected: ref "dataset:PROJECT_ID:marketing:DOCUMENT_ID" targets dataset "PROJECT_ID.marketing", which this deployment does not declare — declared surface: the workflow resource (dataset "PROJECT_ID.DATASET_NAME"). Add a resourceClients entry for the target resource, or fix the ref.Restore the binding. A resource that reads fine and fails on write is the signature of a missing alias binding or resourceClients entry.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?