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

# 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](https://www.sanity.io/docs/workflows/getting-started): `article-review` deployed, and a `sanity.workflow.ts` that exports the `dev` deployment.
- A second dataset named `marketing` in the same project, with one published `article` document. Note its `_id`. Create the dataset with `npx sanity@latest dataset create marketing`.
- To start instances from your own code: the `scripts/engine.ts` module from [Run the engine from your own code](https://www.sanity.io/docs/workflows/run-the-engine).

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](https://www.sanity.io/docs/workflows/deployments-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`. The `dev` deployment keeps its instances there.
- Editorial content: the `marketing` dataset, 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](https://www.sanity.io/docs/workflows/global-document-references) 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`:

**sanity.workflow.ts**

```typescript
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](https://www.sanity.io/docs/workflows/deploy-definitions).

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

**npm**

```shell
npx sanity-workflows start article-review \
  --field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'
```

**pnpm**

```shell
pnpm dlx sanity-workflows start article-review \
  --field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'
```

**yarn**

```shell
yarn dlx sanity-workflows start article-review \
  --field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'
```

**bun**

```shell
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:

```text
Started dev.wf-instance.a1b2c3d4e5f6 — now at drafting
```

Fire the first action. The subject field is required, so the engine reads the marketing article before it lets the action fire:

**npm**

```shell
npx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
```

**pnpm**

```shell
pnpm dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
```

**yarn**

```shell
yarn dlx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
```

**bun**

```shell
bunx sanity-workflows fire-action INSTANCE_ID --activity write --action submit
```

Confirm the result:

**npm**

```shell
npx sanity-workflows show INSTANCE_ID
```

**pnpm**

```shell
pnpm dlx sanity-workflows show INSTANCE_ID
```

**yarn**

```shell
yarn dlx sanity-workflows show INSTANCE_ID
```

**bun**

```shell
bunx sanity-workflows show INSTANCE_ID
```

`show` 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](https://www.sanity.io/docs/workflows/deployments-and-resources) 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](https://www.sanity.io/docs/workflows/run-the-engine) with `deploymentResourceClients`, which serves every resource the deployment binds in `resourceAliases`:

**scripts/engine.ts**

```typescript
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:

**scripts/engine.ts**

```typescript
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:

**scripts/start-marketing-review.ts**

```typescript
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:

```sh
SANITY_AUTH_TOKEN=YOUR_TOKEN npx tsx scripts/start-marketing-review.ts DOCUMENT_ID
```

The script prints the new instance and its subject:

```text
Started dev.wf-instance.a1b2c3d4e5f6 at drafting
Subject: dataset:PROJECT_ID:marketing:DOCUMENT_ID
```

[Fields](https://www.sanity.io/docs/workflows/fields) covers the field types reference values land in, and [Effects and runtimes](https://www.sanity.io/docs/workflows/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](https://www.sanity.io/docs/workflows/deployments-and-resources)). Prove the write side by removing the binding. Comment out the `resourceAliases` line in `sanity.workflow.ts` and run the start command again:

**npm**

```shell
npx sanity-workflows start article-review \
  --field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'
```

**pnpm**

```shell
pnpm dlx sanity-workflows start article-review \
  --field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'
```

**yarn**

```shell
yarn dlx sanity-workflows start article-review \
  --field subject='{"id":"dataset:PROJECT_ID:marketing:DOCUMENT_ID","type":"article"}'
```

**bun**

```shell
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:

```text
✖ 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.

