Sanity Library Reference Docs
    Preparing search index...

    Module @sanity/workflow-cli - v0.32.0

    @sanity/workflow-cli

    Command-line tool for deploying, inspecting, and administering Sanity workflow definitions and instances.

    Warning

    Early access, restricted. The commands below talk to a real Sanity dataset, configured by a sanity.workflow.ts file and authenticated with sanity login.

    pnpm --filter @sanity/workflow-cli dev <command> [...args]

    # Examples:
    pnpm --filter @sanity/workflow-cli dev --help
    pnpm --filter @sanity/workflow-cli dev deploy --dry-run
    pnpm --filter @sanity/workflow-cli dev list --include-completed
    pnpm --filter @sanity/workflow-cli dev show wf-instance.abc123

    The package is an oclif plugin: every command's canonical id nests under the workflows topic (sanity-workflows workflows deploy), so mounting the package into the sanity CLI's plugin list surfaces the same commands as sanity workflows … without colliding with the host's own root commands (the sanity CLI already has a deploy). The short forms used throughout this README (deploy, definition list, …) are the standalone binary's stable surface — its production and development entrypoints (bin/run.js and bin/dev.js) rewrite them onto the topic before oclif resolves the command. Those short forms are not registered as oclif aliases, so a host mount does not pollute the host root.

    Authenticate once with sanity login (the CLI reads that session token); for CI, set SANITY_AUTH_TOKEN instead. Then create a sanity.workflow.ts in the directory you run from — see Configuration.

    The token must span every resource the workflow references, not just the workflow resource. Runtime commands build one client from the deployment descriptor; when a workflow's subject or doc.ref fields point into other datasets (or a canvas / media library), the engine reaches them through sibling clients derived from that same token. A sanity login session or an org-capable token covers this out of the box; a token scoped to a single project will fail on cross-resource reads.

    The CLI is configured by a sanity.workflow.ts (or .js/.mjs) discovered in the directory you run from. It exports a config built with defineWorkflowConfig, declaring your deployments (typically one per environment):

    import {defineWorkflowConfig} from '@sanity/workflow-engine/define'

    import {articleReview, urlDraft} from './src/workflows.ts'

    export default defineWorkflowConfig({
    deployments: [
    {
    expectedMinReaderModel: 4,
    name: 'production', // the deployment's unique identity (lowercase letters, digits, dashes)
    tag: 'prod', // the environment partition the engine's docs are scoped to
    workflowResource: {type: 'dataset', id: 'acme.workflows'}, // where those docs live
    resourceAliases: [
    // binds each `@<handle>:` a definition references to a physical resource
    // in a DIFFERENT dataset from `workflowResource` (same-resource content
    // needs no alias — see below)
    {name: 'content', resource: {type: 'dataset', id: 'acme.content'}},
    {name: 'assets', resource: {type: 'dataset', id: 'acme.assets'}},
    ],
    definitions: [articleReview, urlDraft], // the batch this deployment ships
    },
    ],
    })

    name is the deployment's identity: unique across the config and constrained to the same grammar as tag. Tags group deployments by environment and may repeat, as long as no two deployments share both a workflowResource and a tag — that pair is the storage partition, and the config rejects the collision naming both entries.

    Pick a deployment with --deployment <name>; with a single deployment configured you can omit it. --tag <tag> also works on single-deployment commands while the tag names exactly one deployment — on deploy it targets every deployment carrying the tag (a tag is an environment group). With several configured, a bare interactive run presents a keyboard-driven deployment selector (by name, tag alongside) — on deploy, start, and definition diff/delete, and on the instance-targeted commands (abort, set-stage, reset-activity, fire-action, diagnose) when the config spans several resources. In CI or another non-interactive shell, the command fails asking for --deployment or --tag (deploy also suggests --all-tags) instead of blocking for input. --all-tags deploys every deployment in the config in one run: a failure in one doesn't stop the rest — the run continues, prints a summary of what failed, and exits non-zero. The client's project + dataset are derived from the deployment's workflowResource, and deploy expands each definition's @<handle>: reference to the bound physical resource. An invalid config fails with a clean, path-prefixed error before the command runs.

    resourceAliases is only for content that lives in a different resource from workflowResource. A definition can reference a document in the same resource by bare id, no alias needed — bare ids root at workflowResource at runtime. Bindings exist at deploy time only: deploy expands each @<handle>: reference to its bound physical GDR, and nothing alias-shaped survives into the deployed definition or the runtime. Rule of thumb: different resource → bind an alias; same resource → use a bare id and omit resourceAliases entirely. So the simplest single-dataset setup is just a workflowResource and definitions, with no resourceAliases at all.

    Refs supplied at runtime (start --field, fire-action ref params) are gated separately: they may target the workflowResource itself, and anything else is rejected at the write with RefResourceUndeclaredError. The CLI wires no resourceClients resolver, so it has no way to widen that surface — a runtime foreign-resource ref goes through an engine consumer that serves the target resource, not through the CLI.

    Var Purpose
    SANITY_AUTH_TOKEN Explicit token for CI / scripted use — wins over the sanity login session (editor role for writes)
    SANITY_API_HOST Optional API host override (defaults to the prod API)

    Run through the dev / cli scripts and the CLI itself and your sanity.workflow.ts load straight from TypeScript source: the scripts set NODE_OPTIONS='--conditions=development' under tsx, and the config file is transpiled on the fly by jiti. Definitions you author inline in the config, or import by relative path, transpile the same way — no build step.

    One caveat: a definition imported from a separate workspace package (e.g. @sanity/workflow-examples) resolves to that package's built dist/. jiti applies its own module resolution and does not honour --conditions=development, so it never picks up a package's src/ export condition — rebuild that package after editing it, or the config loads its stale compiled output.

    Note

    pnpm intercepts a handful of its own flag names (--check, --filter, --recursive, …) when they appear after a script name. To invoke deploy --check, run from inside the package or via pnpm exec:

    # from inside packages/workflow-cli
    pnpm dev deploy --check

    # or, from anywhere
    pnpm --filter @sanity/workflow-cli exec \
    env NODE_OPTIONS='--conditions=development' \
    tsx --env-file-if-exists=../../.env bin/dev.js deploy --check

    --dry-run and --only don't collide with pnpm flags and work through the normal pnpm --filter … dev … invocation.

    Invoke as sanity-workflows <command> (or sanity workflows <command> once the host CLI mount lands).

    Command Description
    deploy Validate and deploy the definitions in the selected deployment.
    deploy --all-tags Deploy every deployment in the config. A failure in one does not stop the rest.
    deploy --check Validate local definitions without contacting the dataset.
    deploy --dry-run Show a diff against what is already deployed, without writing.
    deploy --only <name> Limit deploy, check, or dry-run to one definition.
    start <name> Start an instance from a deployed definition. Pass --field for input values.
    list List instances (in-flight by default). Filter with --definition.
    show <instance-id> Show an instance's state, activities, and effects.
    diagnose <instance-id> Explain why an instance is or isn't progressing, and what would unstick it.
    tail <instance-id> Stream new history entries as they land.
    abort <instance-id> Stop an in-flight instance. Pending effects cancel; the instance stays where it is.
    set-stage <instance-id> --to <stage> Move an instance to a stage, skipping declared transitions. The target stage's enter lifecycle still runs.
    reset-activity <instance-id> <activity> Re-run a failed activity, or --skip it so a gated transition can fire.
    fire-action <instance-id> Fire an action on a waiting activity. Omit --action to list what can be fired.
    definition list List deployed definitions.
    definition show <name> Show a deployed definition (latest version, or --version).
    definition diff <name> Diff the in-code definition against what is deployed.
    definition delete <name> Delete a deployed definition. Refuses while live instances exist unless --cascade.
    nuke --deployment <name> / nuke --tag <tag> Dev reset: delete every engine-owned document for that tag (instances, definitions, guards). Prints a plan, then asks for confirmation. Content documents are never touched.
    nuke --instance <id> Dev reset: delete one finished instance and its guards. Refuses in-flight instances (abort first).

    The CLI collects usage telemetry through Sanity's standard pipeline: a per-command trace (Workflows CLI Command Executed — the command id, the names of declared flags used, never their values, and a success flag) plus the engine's adoption events from the operations it drives. Consent is the account-wide status managed by npx sanity telemetry enable|disable|status; CI and trueish DO_NOT_TRACK suppress everything (except a deploy that may share new definitions, which forces that deploy's telemetry unless --no-share-defs is passed — see Definition sharing), and a session that isn't logged in sends nothing. A one-time notice on stderr discloses collection on first use.

    One extension point: supply your own logger as telemetry in sanity.workflow.ts and the built-in pipeline is not constructed at all — every event flows to your implementation unconditionally (CI included), and consent, suppression, and destination become its business.

    Separate from telemetry, a deploy that creates new definition versions donates those documents to Sanity — verbatim (structure, names, titles, GROQ filters, effect configuration, seeded values), plus their deployment coordinates (project and dataset, or resource id) — never content documents, instances, or your auth token. The documents go to Sanity's first-party definition-feedback endpoint, not the telemetry pipeline; telemetry carries only a content-free marker (content hash plus structural counts) per shared definition and a per-invocation decision event.

    Sharing is opt-out in every environment. Pass --no-share-defs to decline for one invocation. With neither flag, the first interactive deploy prints a full notice before sending and remembers that the notice was shown; every later flagless deploy prints a status block showing that sharing is on by default and showing runnable shell examples for the default, explicit opt-in, and opt-out forms; the explicit flags suppress the status block. Unattended runs (CI, DO_NOT_TRACK, or a non-TTY pipe) also share by default and print that status block in their logs. A flagless deploy or explicit --share-defs forces that deploy's telemetry through the environment gate so donated definitions retain their content-free markers; account-level sanity telemetry disable still applies. A failed share only warns, never failing a completed deploy.

    • Not mounted in the sanity CLI yet. The package side is mount-ready — commands nest under the workflows oclif topic with no root aliases (see Run) — but the sanity CLI does not list this package in its plugin array, so the commands ship only through the standalone sanity-workflows binary today. The host-side mount is a separate piece of work.
    • No true bypass. set-stage is the engine's setStage admin override: it skips the definition's declared transitions and filters, but the target stage's enter lifecycle and the post-move cascade still run. A true force-set that also bypasses guards + enter effects does not exist yet — it needs engine work that hasn't landed.

    Identity is the token behind the configured client: the engine resolves the acting user (and their roles) from /users/me, so every write is attributed to whoever the token authenticates — there is no identity injection and no --as impersonation. Alongside that "who", the CLI declares an advisory "via what": every verb carries executionContext: { kind: "cli", id: "workflow-cli" }, stamped on history entries as provenance.

    Mirrors the Sanity CLI's stack so the eventual plugin port is small:

    • @oclif/core for command definitions, flags, help.
    • @oclif/plugin-help for help output.
    • ora, boxen, log-symbols, console-table-printer for terminal UX; color comes from node:util's styleText (no separate color dep).
    • @sanity/cli-core for timestamp formatting (absolute for detail views, relative "… ago" for list tables).
    • diff for the colored JSON unified diff in deploy --dry-run.