New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@sanity/workflows-cli

Package Overview
Dependencies
Maintainers
120
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sanity/workflows-cli

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

latest
bootstrap
Source
npmnpm
Version
0.0.0
Version published
Weekly downloads
505
Maintainers
120
Weekly downloads
 
Created
Source

@sanity/workflows-cli

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

The package is in prerelease and is publicly available on npm. Commands that access Sanity use the resources in sanity.workflow.ts and authenticate with a login session or an API token. deploy --check validates definitions and checks the generated runtime offline, and definitions validate checks definition JSON files without a config.

Run

Use Node.js 20.12 or later. Install the CLI and its engine peer from the same Workflows release in your project:

npm install --save-dev @sanity/workflows-cli @sanity/workflows
npx @sanity/workflows-cli --help

After authentication and configuration, run commands through the project-local package:

npx @sanity/workflows-cli deploy --dry-run
npx @sanity/workflows-cli list --include-completed
npx @sanity/workflows-cli show INSTANCE_ID

Replace INSTANCE_ID with the workflow instance ID returned by list.

The package is an oclif plugin: every command's canonical id nests under the workflows topic (sanity-workflows workflows deploy), so the sanity CLI runs the same commands as sanity workflows … without colliding with its own root commands (the sanity CLI already has a deploy). The short forms used throughout this README (deploy, definitions list, …) are the standalone binary's stable surface — its entrypoint rewrites 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 with npx sanity@latest login; the CLI reads that session token. For CI, set SANITY_AUTH_TOKEN instead. Create a sanity.workflow.ts in your project, as described in 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.

Configuration

The CLI is configured by a sanity.workflow.ts (or .js/.mjs). It looks in the directory you run from, then in each parent directory, and uses the first one it finds. Pass --path to name a config file, or a directory to start the search from. Wherever you run the command, blueprints generate writes the generated Blueprint files next to the config file, and deploy checks them there.

The file exports a config built with defineWorkflowConfig, declaring your deployments (typically one per environment):

import type {WorkflowDeploymentInput} from '@sanity/workflows'
import {defineWorkflowConfig} from '@sanity/workflows/define'

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

// Export each deployment under its own `name`, then list the exports in the
// config. `blueprints generate` requires these named exports, and so does
// `deploy` once a generated runtime sits next to this file; nothing else does.
export const production = {
  expectedMinReaderModel: 4,
  name: 'production', // the deployment's unique identity (lowercase letters, digits, dashes)
  partition: '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
} satisfies WorkflowDeploymentInput

export default defineWorkflowConfig({deployments: [production]})

name is the deployment's identity: unique across the config and constrained to the same grammar as partition. Partitions group deployments by environment and may repeat, as long as no two deployments share both a workflowResource and a partition. The config rejects that collision and names both deployments.

For deploy, start, definitions diff, and definitions delete, select a deployment with --deployment <name>. With one deployment configured, you can omit the selector. --partition <partition> selects one deployment when the partition is unique; on deploy, it targets every deployment carrying the partition. With several deployments and no selector, these commands prompt for a deployment in an interactive terminal. In a non-interactive shell, they fail asking for --deployment or --partition; deploy also suggests --all-partitions.

The instance-targeted commands abort, reset-activity, fire-action, diagnose, show, and tail locate the instance by ID across configured resources. Their optional --deployment and --partition flags narrow the resources searched; the operation's partition comes from the loaded instance.

list, definitions list, and definitions show read every configured resource and partition by default. --partition filters to one partition, and --deployment reads only that deployment's resource and partition.

The read commands list, show, tail, diagnose, definitions list, definitions show, and whoami can also read one resource you name, with or without a sanity.workflow.ts. Use the same flags as the Sanity CLI: --project-id <id> and --dataset <name> (-p and -d) together for a dataset, or --media-library-id <id> for a media library. The CLI then reads only that resource and loads no config, so --deployment and --path can't be combined with them. --partition still filters. Commands that write, such as start, abort, and deploy, always act through a deployment in sanity.workflow.ts.

While the experimental SANITY_STUDIO_WORKFLOWS_SERVICE=true is set in your shell, start, fire-action, and abort send their write to the project's Workflows service when your token can read workflow instances but not write them, and the project has a service. Otherwise they write directly with your token.

sanity workflows definitions list --project-id abc123 --dataset production
sanity workflows show prod.wf-instance.x7k2 -p abc123 -d production

When no sanity.workflow.ts is found and you pass none of these flags or --path or --deployment, a read in an interactive terminal asks you to pick a project, then one of its datasets, from those your login can access. It then prints the --project-id and --dataset flags that read the same dataset, so you can script the next run. The picker offers datasets only; pass --media-library-id to read a media library. With --json or in a non-interactive shell, the read fails and names the flags instead.

When an instance ID or definition name is found in more than one deployment, for example two deployments on different resources that both use default, an interactive terminal prompts you to pick one. Each choice shows the deployment name with its dataset. With --json or in a non-interactive shell, the command fails, lists each candidate as name (dataset), and asks for --deployment.

A deployment alone on its configured resource may omit partition; it resolves to default, independent of its name. If several deployments share that resource, give every one an explicit, distinct partition. The engine stores the partition on every definition and instance the deployment writes, so a deployment only reaches documents written under its own partition value.

--all-partitions 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. @<handle>: references exist at deploy time only: deploy expands each one 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 or any resource the deployment binds in resourceAliases, and anything else is rejected at the write with RefResourceUndeclaredError. To start a workflow on content in another dataset or project, bind that resource even when no definition references the alias. The CLI reaches each bound resource with the same token and API host it uses for workflowResource, so that login must be a member of every bound project. Instance commands use the bindings of the configured deployment that owns the instance's partition on the resource it was found in. When no configured deployment owns that partition, refs may target only the workflow resource.

For a release.ref field, start --field also takes a bare Content Release name, such as --field release=rA1b2C3d4. The CLI builds the release ref on the one resource the deployment binds in resourceAliases other than workflowResource, or on workflowResource when it binds no other. When it binds several other resources, name one with --release-resource <alias> or pass the full object instead.

VarPurpose
SANITY_AUTH_TOKENExplicit token for CI / scripted use — wins over the sanity login session (editor role for writes)
SANITY_API_HOSTOptional API host override (defaults to the prod API)

Your sanity.workflow.ts loads straight from TypeScript source: the CLI transpiles the config file on the fly with jiti. Definitions you author inline in the config, or import by relative path, transpile the same way — no build step. A definition imported from an installed package loads that package's published JavaScript.

Command list

Invoke as sanity workflows <command> in the Sanity CLI, or sanity-workflows <command> through the standalone binary. Suggested next commands in the output use whichever form you ran.

CommandDescription
deployValidate the definitions, check any generated runtime still matches them and, with the experimental service switch set, that the Workflows service can read them, then deploy the selected deployment.
deploy --all-partitionsDeploy every deployment in the config. A failure in one does not stop the rest.
deploy --checkValidate local definitions and check the generated runtime, without contacting the dataset or the Workflows service.
deploy --dry-runCheck the generated runtime and, with the experimental service switch set, the Workflows service's data model, then show a diff against what is already deployed, without writing.
deploy --verbosePrint the full report of the functions the selected deployments need when no generated runtime is here, in place of the summary.
deploy --only <name>Limit deploy, check, or dry-run to one definition. The generated-runtime check still covers the whole config.
start <name>Start an instance from a deployed definition. Pass --field for input values.
listList 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.
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.
whoamiShow your user ID as Workflows records it. Put the Sanity user ID in user assignee fields. --json prints an object.
blueprints generateWrite the Sanity Blueprints runtime the definitions require next to sanity.workflow.ts.
blueprints generate --checkVerify that runtime still matches the definitions. Writes nothing; exits non-zero on any difference.
blueprints generate --verbosePrint the full runtime-needs report, every file with its status, and the heartbeat stack's instructions on every run, in place of the summary.
definitions listList deployed definitions.
definitions show <name>Show a deployed definition (latest version, or --version).
definitions validate <file...>Validate definitions in JSON files without a config, login, or network. --json prints the machine-readable report.
definitions diff <name>Diff the in-code definition against what is deployed.
definitions delete <name>Delete a deployed definition. Refuses while live instances exist unless --cascade.
nuke --deployment <name> / nuke --partition <partition>Dev reset: delete every engine-owned document for that partition (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 definitions and blueprints commands also answer to the singular definition and blueprint, so definition list runs definitions list.

Generating the Blueprints runtime

A definition that declares an effect, reads $now, or starts autonomously needs server-side code: something has to run the effect handlers, sample deadlines, and start instances from document writes. blueprints generate derives that runtime from the definitions and writes it next to sanity.workflow.ts, so npx sanity@latest blueprints deploy has something to deploy. A purely interactive workflow needs none of it, and the command says so.

Current gaps:

  • A start watcher in a dataset starts a definition on the write that makes its start.filter true, even for a document that already has an instance from an earlier time it became true. A start filter that reads $now or another document, reads $tag or $definition and a field of the document other than _type (or the whole document), or is not an expression the generator recognizes as boolean, starts on every matching write instead, and the generator warns about it and names which of these applies. Only the definition's own start requirements refuse a start, such as a singleSubject requirement.
  • A start watcher in a Media Library starts an asset only when the asset is created. When a deployment binds a Media Library, blueprints generate fails for an autonomous asset definition whose start.filter reads anything of the asset other than _type.
  • Definitions and functions deploy in two steps, definitions first, until the Blueprints service registers the sanity.workflow resource.
  • A generated file cannot be taken over: editing one is drift, and the next generation rewrites it.
  • A retry policy is not checked against its function's timeout at generation, so an effect can be configured to retry for longer than its function may run.

Blueprint generation parses sanity.blueprint.ts without loading your project's TypeScript installation. The CLI works beside TypeScript 6 or 7 and does not require TypeScript for this command.

Five prerequisites:

  • Each deployment is exported under its own name, and the export is the deployment object itself, as the Configuration example does: every generated module imports its deployment from ./sanity.workflow by that name. The command refuses to write anything until each name is exported, and names the export to add. It checks the name only, so make sure each export is the deployment your config lists. A typecheck of the generated modules catches a wrong export only where a generated function uses it, and a deployment that needs no functions emits none.
  • sanity.workflow.ts is importable inside a deployed function. Every generated module imports it, and each function bundles it, so the file runs where your shell and your .env do not exist. Reading an absent variable there yields undefined; what fails is a config that throws when a variable is missing, or requires one to build the deployment, because that runs on init before your handler is reached. Keep the deployment's resource coordinates literal in the file and read secrets inside your handlers.
  • The Blueprints packages the generated modules import are installed, alongside the CLI and its peers.
  • Your definitions are deployed before the functions. The generated workflows.blueprint.ts carries the definitions entry commented out, because the Blueprints service does not accept the sanity.workflow resource type yet, so npx sanity@latest blueprints deploy does not deploy them. Run npx @sanity/workflows-cli deploy first: a function that starts or advances an instance resolves its definition from the Content Lake, and finds nothing until that deploy has run.
  • A generated heartbeat deploys from its own directory, with an organization deploy token. The heartbeat is a scheduled function, and the platform only accepts one on a stack in organization scope. The command writes it into workflows-heartbeat/sanity.blueprint.ts, a blueprint of its own, so the document functions in your sanity.blueprint.ts deploy to a project-scoped stack with a token that can deploy to the project. Each heartbeat stack counts toward your organization's limit of 10 organization-scoped stacks. How often it may run is a plan limit rather than something the generator chooses — see the schedule below.
npm install --save-dev @sanity/blueprints @sanity/functions @sanity/workflows
npx @sanity/workflows-cli blueprints generate
npx @sanity/workflows-cli deploy
# Project stack, with a token that can deploy to the project
npx sanity@latest blueprints init . --project-id <projectId>   # first deploy only
npx sanity@latest blueprints deploy
# Heartbeat stack, with an organization deploy token, when one was generated
cd workflows-heartbeat
npx sanity@latest blueprints init . --organization-id <organizationId>   # first deploy only
npx sanity@latest blueprints deploy

Each generated function's package.json, and the heartbeat stack's workflows-heartbeat/package.json, copies the specifications your project declares for @sanity/functions, @sanity/workflows/blueprints, and @sanity/workflows. A package you declare nowhere fails the command rather than being stamped with a guessed range. The Functions build installs a function's dependencies from the package.json beside the blueprint it deploys from, so the heartbeat built from workflows-heartbeat/ runs the versions your project pins rather than the latest ones.

The command prints a short summary: one line per deployment saying how many functions it needs, the files that need you (a new handler scaffold, a change to your own sanity.blueprint.ts, anything it could not resolve), a count of the files it wrote, and a Deploy: section naming which stack to deploy with which token. The project stack is always named when there are document functions, because each function bundles sanity.workflow.ts; the heartbeat stack is named only when this run created or updated one of its files. On the first run:

Runtime needs:
  prod (prod) needs 3 Sanity Functions (1 effect drain, 1 heartbeat, 1 start watcher)

Generated tree:
  created    effect-handlers/send-notification.ts  yours now — implement the effect
  created    sanity.blueprint.ts  the workflow resources spread

✔ 12 file(s) written (12 created), 0 left untouched

Deploy:
  project stack: `npx sanity@latest blueprints deploy` here, with a token that can deploy to the project. It holds every document function; redeploy it after any definition change.
  heartbeat stack: `npx sanity@latest blueprints deploy` in workflows-heartbeat/, with an organization deploy token. It holds only the scheduled heartbeat.
  ⚠ Without the heartbeat stack nothing checks these on a schedule, and nothing reports it:
      $now deadlines take effect only when another engine call evaluates them in article-review
      an effect queued before its drain existed, or whose drain timed out, waits until a later drain picks it up: send-notification

Pass --verbose for the full report.

When a later run changes a heartbeat-stack file, the command cannot tell whether you deployed that stack, so it names the files and asks conditionally:

  ⚠ workflows-heartbeat/sanity.blueprint.ts changed: if you have deployed the heartbeat stack, redeploy it with the organization deploy token.

--verbose prints the full needs report, every file with its status, and the heartbeat stack's instructions whether or not this run changed its files.

The heartbeat stack's blueprint lists, in its header, the partition, the datasets, the 'function'-hosted workflows the heartbeat ticks and the 'function'-hosted effects it re-queues. Editing an existing workflow leaves it unchanged, so the default Deploy: section does not name the heartbeat stack, and --verbose lists the file as unchanged. Adding or removing a 'function'-hosted workflow or effect changes it: redeploy the heartbeat stack too. So does moving the declared specification of @sanity/functions, @sanity/workflows/blueprints, or @sanity/workflows, which changes workflows-heartbeat/package.json, the manifest the heartbeat stack's build installs from; the Deploy: section names every heartbeat-stack file that changed. The heartbeat bundles the installed runtime when it is deployed, so after upgrading the Workflows packages redeploy both stacks even when every file is unchanged. blueprints generate --check reports a changed file as differs until you regenerate, like any other generated file.

Fill in each effect-handlers/<effect name>.ts: they are yours, scaffolded once and never overwritten. Everything marked created, updated, or unchanged under workflows.blueprint.ts, workflows-heartbeat/, functions/, and effect-handlers/all.ts is regenerated on every run, so edits there do not survive. Your own sanity.blueprint.ts receives the workflowResources import and spread once; after that the command leaves it alone. The command never deletes a file. A handler for an effect you removed, a scaffold blocked by a path that differs only in case, and a blueprint it could not wire are reported for you to resolve. So is a stale function directory: one under functions/ whose index.ts carries the generator's header but which the definitions no longer produce, such as the directory a renamed function leaves behind. The generated blueprint does not deploy it. A directory whose index.ts has no generator header is yours, and is never reported. When no deployment needs a heartbeat any more, a workflows-heartbeat/sanity.blueprint.ts left from an earlier run is reported too: destroy its stack with the organization deploy token, then delete the directory.

The command covers every deployment in the config, because the emitted workflows.blueprint.ts declares them all, and workflows-heartbeat/sanity.blueprint.ts declares every one that needs a heartbeat.

Where each workflow runs

The --verbose report opens with the hosting each runtime block resolved to, after every level has inherited from the one above it. It matters because a name listed under durableFunction or selfHosted has no emitted function to point at:

  • function — a drain and the start watchers on the project stack, and the heartbeat on its own stack.
  • durableFunction — nothing is emitted yet. The rest of the deployment emits normally around it.
  • selfHosted — nothing is emitted, and the report ends with what your own process must do instead: which deadlines to tick, which effects to drain, which parents to re-evaluate after they miss a child settling, and which autonomous definitions to start. The heartbeat ticks only the 'function'-hosted workflows.

The generated heartbeat's schedule

The heartbeat is emitted on * * * * *, one run a minute, and how often the platform will actually run it is a plan limit rather than something the generator can choose: Sanity documents the threshold as Free daily, Growth hourly, Enterprise minutely. A workflow whose deadlines depend on the heartbeat advances no faster than the heartbeat runs. An effect no drain picked up becomes eligible to re-queue five minutes after it was queued, and a later heartbeat run re-queues it; each run re-queues on at most 30 instances, so a large backlog takes several runs. Check your plan's cadence before relying on a $now deadline in a deployed workflow.

Checking for drift in CI

blueprints generate --check renders the same runtime in memory and compares it with the files on disk. It writes nothing and exits zero when they match:

$ npx @sanity/workflows-cli blueprints generate --check
✔ the generated tree matches the definitions

Otherwise it exits 1 and names every file, so the log alone says what to fix:

$ npx @sanity/workflows-cli blueprints generate --check
✖ The generated tree does not match the definitions (1):
    differs    effect-handlers/all.ts
  Run `sanity-workflows blueprints generate` to bring it back in line.
$ echo $?
1

A file reports missing when nothing is at its path, differs when the file there has other contents, and a handler for a declared effect reports missing when you have not written it yet. A stale function directory also fails the check, and the report names it. Add the command to the job that builds your blueprint and a definition change can no longer ship without the runtime it needs.

Every deploy runs the same check

deploy validates the definitions, then looks for workflows.blueprint.ts next to sanity.workflow.ts. When that file is there, the deploy runs the same comparison as Checking for drift in CI, and a difference stops it:

$ npx @sanity/workflows-cli deploy --partition prod
✖ The generated tree does not match the definitions (1):
    differs    workflows.blueprint.ts
  Run `sanity-workflows blueprints generate` to bring it back in line.
$ echo $?
1

Nothing is written when that happens: the check runs before the first definition reaches the dataset, and before the CLI resolves a token. A stale function directory does not stop the deploy, because the generated blueprint does not deploy it. The deploy prints a warning naming the directory and continues. Run blueprints generate, review what it wrote, and deploy again.

Definitions deploy first and the functions follow: sanity-workflows deploy writes the definitions, then npx sanity@latest blueprints deploy ships the runtime that serves them. That order is why the deploy is where drift is caught. It is the step that would otherwise put definitions in front of a runtime that cannot serve them.

When no workflows.blueprint.ts is there, the deploy summarises the functions the selected deployments need and continues, so a project that has not generated one yet still deploys:

$ npx @sanity/workflows-cli deploy --partition prod
Runtime needs:
  prod (prod) needs 3 Sanity Functions (1 effect drain, 1 heartbeat, 1 start watcher)

⚠ No generated runtime is here. Run `sanity-workflows blueprints generate` to emit one, unless a runtime you maintain yourself already serves these definitions. Pass --verbose for the full report.

deploy --verbose prints the full report in place of the summary: each function, what it runs, and why the definitions need it, the same report blueprints generate --verbose prints before it writes. Nothing is written either way. The summary and the report cover the deployments the run selects, so a deployment needing no functions prints nothing about the runtime even when a sibling needs some. Definitions no runtime can be derived from at all print a warning saying so. Either way the deploy continues.

The check has no off switch, by design: the definition deploy is the one moment every deployment passes through, and a flag would be reached for in exactly the situation the check exists to catch, which is shipping a definition change whose runtime was never regenerated. Nothing runs for a hand-written runtime, because a project that never generated a tree has no workflows.blueprint.ts for the check to compare.

Two consequences worth knowing:

  • The check covers the whole config, because the generated tree declares every deployment in it. deploy --deployment one still holds the tree to every deployment's definitions, so a sibling deployment stops it by drifting or by failing validation.
  • deploy --check and deploy --dry-run run it too, and the check itself is offline in both. --check is the complete credential-free pre-flight: definitions valid, runtime in line, dataset untouched. --dry-run goes on to authenticate and read the deployed state to build its diff.

Telemetry

The CLI collects usage telemetry through Sanity's standard pipeline. 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. A session that isn't logged in sends nothing. A logged-in run sends whether or not a sanity.workflow.ts is found. A one-time notice on stderr discloses collection on first use.

What may be collected is governed by Sanity's telemetry policy. This package sends a subset of it.

Collected:

  • Project id, dataset, and organization id, as session properties. The organization id is best effort: it needs a lookup that can resolve to nothing, and the event sends either way.
  • Operating system, CPU architecture, JavaScript runtime and version, and the CLI version.
  • The command invoked, and the flags it was given. Each is normalised to its canonical long form, so --partition=prod, --partition prod, and the deprecated --tag prod all report --partition=prod. A flag the command declared, and did not redact, reports its value; because values ship, a flag naming a definition, stage, deployment, document, or user reports that name or id verbatim. Redact a flag whose value should not leave the machine.
  • Structural counts and closed vocabulary values derived from a definition or instance: stage and action counts, activity and field kinds, lifecycle, status enums, and durations.
  • Content fingerprints of definitions, and random run ids.
  • instanceId, the instance document _id. Engine-minted ids embed the deployment partition (<partition>.wf-instance.<random>), so the partition reaches telemetry through this field. A caller-supplied instance id is sent verbatim.
  • The author-chosen effect name, on effect events.

Never collected, because the policy excludes them:

  • Environment variables, file paths, and file contents.
  • Logs and serialized JavaScript stack traces.

Never collected, because this package goes further than the policy requires. The policy does not prohibit any of these, so their absence here is this package's choice and not a company-wide guarantee:

  • The value of a redacted flag. A flag whose value is text a person typed reports its name alone: --reason on abort and definitions delete, --field on start, and --param on fire-action.
  • The value of any flag the command did not declare, including a misspelling of a redacted flag. Only the name is reported, and the token after it is withheld unless the flag carries its value in the same token (--resaon=x, -zx). The sanity CLI sends these whole; withholding them is what keeps a mistyped --reason from sending the text redaction exists to withhold.
  • Anything after the -- terminator. Those tokens are not reported at all.
  • Error messages, and stack traces of any language. The policy permits error information that excludes sensitive data, but this package's errors quote definition names, partitions, and argv, so none is sent. A failure is reported as a success boolean and nothing else.
  • Document content, GROQ, and field values.
  • Definition names and stage names in the engine's own event payloads. A stage transition reports positions in stages[], never names. This does not extend to flag values, which ship verbatim as described above.

Note that a dataset name, a deployment partition, and an effect name are all customer-authored. They are collected deliberately, as identifiers and as a closed label, not as free text.

Definition sharing sends the definition document to a first-party feedback endpoint, not through this pipeline; telemetry only records the content-free markers below.

Each event includes context.surface: 'cli', the @sanity/workflows-cli package version in context.packageVersion (also when run as sanity workflows), and its process execution mode in context.environment. NODE_ENV=production reports production; development and test report development. Unset, empty, and unrecognized values default to production for installed CLI and MCP runs. The SDK reports build mode and defaults to development instead.

When comparing activity across surfaces, group or filter by context.surface alongside context.environment. Environment does not identify a production dataset or deployment; the API host, dataset name, and deployment partition do not set it. Existing telemetry consent and opt-out settings still apply.

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.

Session properties

The built-in store attaches these user properties once per invocation (joined to events by session id). User identity is not in the payload; the intake service resolves the sender from the authenticated session.

PropertyWhat it is
surfaceAlways 'cli'.
machinePlatformNode process.platform (for example darwin, linux).
cpuArchitectureNode process.arch.
runtimeAlways 'node' for this CLI.
runtimeVersionNode process.version.
cliVersionThe CLI package version, when oclif supplied it.
projectIdThe --project-id the run names, else the first dataset-backed deployment's project in sanity.workflow.ts. Absent when neither exists.
datasetThe --dataset the run names, else that deployment's dataset. Absent when neither exists.
orgIdOrganization id for that project, when the lookup succeeds before the command flushes. Optional.

CLI events

These events are defined by this package.

Workflows CLI Command Executed (version 2)

One trace per workflows command, shaped like the sanity CLI's own CLI Command Executed so both join on the same columns. The built-in store fixes the invocation's identity as the trace context in the prerun hook and completes the trace in the finally hook; the context rides every event the trace emits. Host commands outside the workflows topic do not emit.

The context carries:

FieldTypeWhat is collected
groupOrCommandstringAlways 'workflows'.
commandArgumentsstring[]The oclif command path below the topic: ['deploy'], ['definitions', 'list']. Empty for the bare topic and for an invocation oclif resolved to no command. Never argv free-text.
extraArgumentsstring[]Each flag on the invocation, in the order given and repeated if repeated, in its canonical --name=value form: --partition prod, --partition=prod, and the deprecated --tag prod all report --partition=prod. A boolean reports --name or --no-name. A redacted flag reports --name alone, and an undeclared flag reports its name as typed (--resaon, -z) alone. Tokens after -- are not reported.
coreOptionsobject{debug, help, version}. debug comes from a set DEBUG variable, help from a literal help token, version from --version.

The payload carries one field, on trace.log:

FieldTypeWhat is collected
successbooleantrue when the command finished without an oclif error.

A config-supplied logger has no trace, so it receives the four context fields and success together as one log payload.

Workflows CLI Definition Shared (version 1)

One content-free marker per newly created definition version that actually reached Sanity's definition-feedback endpoint. Unchanged redeploys, dry runs, and --check never emit it. The document itself is not in this event.

FieldTypeWhat is collected
contentHashstringContent fingerprint of the donated definition.
deployIdstringRandom run id shared with that invocation's Workflows Engine Definition Deployed events.
stageCountnumberDeclared stages.
activityCountnumberActivities across those stages.
actionCountnumberActions across those activities.
transitionCountnumberDeclared transitions.
fieldCountnumberDeclared field entries at workflow, stage, and activity scope.
activityKindsstring[]Distinct effective activity kinds, sorted: user, service, script, manual, receive.
fieldKindsstring[]Distinct declared field kinds, sorted (engine field kinds such as string, subject, assignees).
guardCountnumberGuards declared on stages.
effectCountnumberEffects declared on actions.
subworkflowCountnumberActions that carry a spawn block.
lifecyclestring'standalone' or 'child'.

Workflows CLI Definition Sharing Decided (version 2)

One event per deploy that created at least one new definition version, whether or not anything was donated. Measures opt-out rate without carrying the document.

FieldTypeWhat is collected
decisionstring'opt-in' (--share-defs), 'opt-out' (--no-share-defs), or 'default' (neither flag).
sharedbooleanWhether a donation POST reached the endpoint. false for any non-sharing decision, and for a chosen share whose POST failed.
definitionCountnumberNewly created definition versions this invocation could have donated.

Engine events the CLI records

Write commands pass the CLI logger into the engine, so a successful operation that writes directly also records the engine's adoption events on the same session. A start, fire-action, or abort sent to the project's Workflows service records none here, and createWorkflowService records no product telemetry for it either. Read commands (list, show, diagnose, tail, definitions list / show / diff) do not emit engine events. nuke deletes engine-owned documents outside those verbs and does not emit them either.

Every engine event includes:

FieldTypeWhat is collected
executionRuntimestringThe JavaScript runtime the engine detected around itself. Always 'node' for this CLI.
executionKindstringThe host the engine was told it is running in. Always 'cli' for this CLI.

Every instance-scoped engine event additionally includes:

FieldTypeWhat is collected
definitionContentHashstringContent fingerprint of the pinned definition. Omitted when the definition predates fingerprinting.
instanceIdstringInstance document _id.
EventVersionWhen the CLI emits itAdditional payload
Workflows Engine Definition Deployed2deploy for each definition (created or unchanged)Same structural counts as Workflows CLI Definition Shared, plus status: 'created' or 'unchanged'.
Workflows Engine Instance Started2start, and a fire-action that spawns a childinitialFieldCount (number of caller-supplied initial fields), viaSpawn (true only for a spawned child), lifecycle.
Workflows Engine Stage Transitioned2A committed hop from fire-action, start, or reset-activityfromStageIndex, toStageIndex (indexes into stages[], never names), toIsTerminal, isRevisit, dwellMs (milliseconds in the exited stage, omitted if that entry is missing), via: 'transition'. Unsampled.
Workflows Engine Action Fired2fire-actionactivityKind (optional; derived kind of the fired activity), hasParams (whether params were supplied — not the params), cascaded (auto-transitions in the post-action cascade).
Workflows Engine Instance Aborted2abortchanged: false if the instance was already terminal.
Workflows Engine Activity Reset2reset-activitychanged: false if already at the target status, or the instance was terminal.
Workflows Engine Definition Deleted2definitions deletedeletedVersionCount, cascadeAbortedCount, deletedGuardCount (guards only when the last version goes).
Workflows Engine Effect Completed2abort cancelling a pending effecteffect (author-chosen name), status ('done', 'failed', or 'cancelled'), origin ('action'), cascaded.

The engine also defines Workflows Engine Field Edited, Workflows Engine Instance Ticked, Workflows Engine Effects Drained, and Workflows Engine Effect State Reported. This CLI does not call those verbs, so it does not record those events. Field-edit and tick events are sampled at most once per 60 seconds when some other shell does emit them.

Definition sharing

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.

Known gaps

  • Not listed in sanity --help yet. Every command runs as sanity workflows <command>, but the Sanity CLI's help output does not list the workflows topic. Run npx @sanity/workflows-cli --help to see the commands.

Identity

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. Run whoami to see that user: its Sanity user ID is the value user assignee fields take. whoami asks about one resource; when the config names several, pass --deployment or --project-id with --dataset. Alongside that "who", the CLI declares an advisory "via what": every verb carries executionContext: { kind: "cli", id: "workflow-cli" }, stamped on the history entries of the writes it commits directly. A write it sends to the project's Workflows service is stamped { kind: "server", id: "workflows-service" }.

Tech stack

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.

Keywords

cli

FAQs

Package last updated on 09 Nov 2026

Related posts