Sanity Library Reference Docs
    Preparing search index...

    Module @sanity/workflow-cli - v0.37.0

    @sanity/workflow-cli

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

    The package is in early access 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.

    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/workflow-cli @sanity/workflow-engine
    npx @sanity/workflow-cli --help

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

    npx @sanity/workflow-cli deploy --dry-run
    npx @sanity/workflow-cli list --include-completed
    npx @sanity/workflow-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 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 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.

    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/workflow-engine'
    import {defineWorkflowConfig} from '@sanity/workflow-engine/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, set-stage, 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, and definitions show 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.

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

    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.

    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> in the Sanity CLI, or sanity-workflows <command> through the standalone binary. Suggested next commands in the output use whichever form you ran.

    Command Description
    deploy Validate the definitions, check any generated runtime still matches them, then deploy the selected deployment.
    deploy --all-partitions Deploy every deployment in the config. A failure in one does not stop the rest.
    deploy --check Validate local definitions and check the generated runtime, without contacting the dataset.
    deploy --dry-run Check the generated runtime, then show a diff against what is already deployed, without writing.
    deploy --verbose Print 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.
    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.
    blueprints generate Write the Sanity Blueprints runtime the definitions require next to sanity.workflow.ts.
    blueprints generate --check Verify that runtime still matches the definitions. Writes nothing; exits non-zero on any difference.
    definitions list List 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.

    Experimental, and not ready for production use in this release. Both commands are available so you can try the generated runtime and give feedback; their flags and their output may still change. What you would hit today:

    • A start watcher runs for every matching write, and each write starts a new instance of every applicable definition, even for a document that already has one. Only the definition's own start requirements refuse it, such as a singleSubject requirement.
    • 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.

    All of these are tracked, and the next release is the bar for using this in production.

    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. 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/workflow-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 needs an organization-scoped stack. The heartbeat is a scheduled function, and the platform only accepts one on a stack in organization scope. A project-scoped stack refuses it, and with it the whole deploy. See Promote a stack to organization scope. 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/workflow-blueprint
    npx @sanity/workflow-cli blueprints generate
    npx @sanity/workflow-cli deploy
    npx sanity@latest blueprints deploy

    Each generated function's package.json copies the specifications your project declares for @sanity/functions, @sanity/workflow-blueprint, and @sanity/workflow-engine. A package you declare nowhere fails the command rather than being stamped with a guessed range.

    The command prints what the definitions need, then every file it wrote.

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

    The command covers every deployment in the config, because the emitted workflows.blueprint.ts declares them all.

    The 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, the heartbeat, and the start watchers.
    • 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 when a child settles, and which autonomous definitions to start. The heartbeat ticks only the 'function'-hosted workflows.

    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.

    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/workflow-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/workflow-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.

    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/workflow-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/workflow-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 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.

    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, set-stage, 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/workflow-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.

    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.

    Property What it is
    surface Always 'cli'.
    machinePlatform Node process.platform (for example darwin, linux).
    cpuArchitecture Node process.arch.
    runtime Always 'node' for this CLI.
    runtimeVersion Node process.version.
    cliVersion The CLI package version, when oclif supplied it.
    projectId The --project-id the run names, else the first dataset-backed deployment's project in sanity.workflow.ts. Absent when neither exists.
    dataset The --dataset the run names, else that deployment's dataset. Absent when neither exists.
    orgId Organization id for that project, when the lookup succeeds before the command flushes. Optional.

    These events are defined by this package.

    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:

    Field Type What is collected
    groupOrCommand string Always 'workflows'.
    commandArguments string[] 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.
    extraArguments string[] 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.
    coreOptions object {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:

    Field Type What is collected
    success boolean true 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.

    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.

    Field Type What is collected
    contentHash string Content fingerprint of the donated definition.
    deployId string Random run id shared with that invocation's Workflows Engine Definition Deployed events.
    stageCount number Declared stages.
    activityCount number Activities across those stages.
    actionCount number Actions across those activities.
    transitionCount number Declared transitions.
    fieldCount number Declared field entries at workflow, stage, and activity scope.
    activityKinds string[] Distinct effective activity kinds, sorted: user, service, script, manual, receive.
    fieldKinds string[] Distinct declared field kinds, sorted (engine field kinds such as string, subject, assignees).
    guardCount number Guards declared on stages.
    effectCount number Effects declared on actions.
    subworkflowCount number Actions that carry a spawn block.
    lifecycle string 'standalone' or 'child'.

    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.

    Field Type What is collected
    decision string 'opt-in' (--share-defs), 'opt-out' (--no-share-defs), or 'default' (neither flag).
    shared boolean Whether a donation POST reached the endpoint. false for any non-sharing decision, and for a chosen share whose POST failed.
    definitionCount number Newly created definition versions this invocation could have donated.

    Write commands pass the CLI logger into the engine, so a successful operation also records the engine's adoption events on the same session. 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:

    Field Type What is collected
    executionRuntime string The JavaScript runtime the engine detected around itself. Always 'node' for this CLI.
    executionKind string The host the engine was told it is running in. Always 'cli' for this CLI.

    Every instance-scoped engine event additionally includes:

    Field Type What is collected
    definitionContentHash string Content fingerprint of the pinned definition. Omitted when the definition predates fingerprinting.
    instanceId string Instance document _id.
    Event Version When the CLI emits it Additional payload
    Workflows Engine Definition Deployed 2 deploy for each definition (created or unchanged) Same structural counts as Workflows CLI Definition Shared, plus status: 'created' or 'unchanged'.
    Workflows Engine Instance Started 2 start, and a fire-action that spawns a child initialFieldCount (number of caller-supplied initial fields), viaSpawn (true only for a spawned child), lifecycle.
    Workflows Engine Stage Transitioned 2 A committed hop from fire-action, start cascade, or set-stage fromStageIndex, toStageIndex (indexes into stages[], never names), toIsTerminal, isRevisit, dwellMs (milliseconds in the exited stage, omitted if that entry is missing), via: 'transition' or 'setStage'. Unsampled.
    Workflows Engine Action Fired 2 fire-action activityKind (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 Aborted 2 abort changed: false if the instance was already terminal.
    Workflows Engine Stage Set 2 set-stage changed: false if already at the target stage.
    Workflows Engine Activity Reset 2 reset-activity changed: false if already at the target status, or the instance was terminal.
    Workflows Engine Definition Deleted 2 definitions delete deletedVersionCount, cascadeAbortedCount, deletedGuardCount (guards only when the last version goes).
    Workflows Engine Effect Completed 2 abort cancelling a pending effect effect (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.

    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 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/workflow-cli --help to see the commands.
    • 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.