Sanity Library Reference Docs
    Preparing search index...

    Module @sanity/workflow-cli - v0.36.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.

    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 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 with npx sanity@latest login; the CLI reads that session token. For CI, set SANITY_AUTH_TOKEN instead. Create a sanity.workflow.ts in the directory you run from, 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) discovered in the directory you run from. It 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. `blueprint 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)
    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
    } satisfies WorkflowDeploymentInput

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

    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.

    For deploy, start, definition diff, and definition delete, select a deployment with --deployment <name>. With one deployment configured, you can omit the selector. --tag <tag> selects one deployment when the tag is unique; on deploy, it targets every deployment carrying the tag. 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 --tag; deploy also suggests --all-tags.

    The instance-targeted commands abort, set-stage, reset-activity, fire-action, and diagnose locate the instance by ID across configured resources. They do not prompt for a deployment. Their optional --deployment and --tag flags narrow the resources searched; the operation's tag comes from the loaded instance.

    --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 the definitions, check any generated runtime still matches them, then deploy 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 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 --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.
    blueprint generate Write the Sanity Blueprints runtime the definitions require next to sanity.workflow.ts.
    blueprint generate --check Verify that runtime still matches the definitions. Writes nothing; exits non-zero on any difference.
    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).

    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:

    • The start watcher still wakes for documents that already have an instance or match no definition: its trigger narrows to creates of a watched type and to updates that changed a field a start.filter reads, or to every update when a filter's truth can change without a write to its subject, and the handler decides the rest. The heartbeat is scheduled every minute, ticks the in-flight instances of the 'function'-hosted workflows it names, and recovers the effect queues of the workflows its catch-up names; how often the platform runs it is the plan limit described below.
    • 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. blueprint 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. The CLI supplies its own compiler API for editing sanity.blueprint.ts, so your project can use TypeScript 6 or 7 without changing versions and does not need to install 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 blueprint 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 and why, then every file it wrote:

    $ npx @sanity/workflow-cli blueprint generate
    Runtime needs:

    ▸ prod (prod)
    hosting: this deployment declares function, and each level below inherits it
    → function workflow(s): article-review
    → function effect(s): notify-reviewer
    effects: 1 declared — notify-reviewer
    → drain function wf-prod-drain-effects runs notify-reviewer
    triggered by an instance write matching _type == "sanity.workflow.instance" && tag == "prod" && (delta::operation() == "create" || delta::changedAny(pendingEffects)) && count(pendingEffects[!defined(claim) && name in ["notify-reviewer"]]) > 0
    clock: samples $now deadlines; drains effects no drain invocation claimed and sweeps the claims a dead drain left
    article-review · working · action-when · $fields.subject.embargoAt <= $now
    → scheduled function wf-prod-heartbeat on * * * * *
    ticks instances of article-review
    drains notify-reviewer on any instance of article-review holding one unclaimed for longer than 60s, or under a lapsed claim, completed instances included
    so deploying definitions before these functions loses nothing on a plan whose schedule runs the beat: an effect queued in the gap is dispatched by a later beat
    one beat drains the instances it selected in sequence; budgeted dispatches are capped at one per beat and unbudgeted ones are not, and a backlog past the function's budget continues on the next beat
    a scheduled function runs at most as often as your organization's plan allows — every minute on Enterprise, hourly on Growth, daily on Free (https://www.sanity.io/docs/functions/functions-introduction) — and a schedule below that threshold is not guaranteed to run at the rate it asks for
    autonomous starts: 1 definition(s) — article-review
    → start watcher wf-prod-start-workflows-creates on acme.workflows matching _type in ["article"] && delta::operation() == "create" — article-review
    creates only: the lake rejects a change of _type, so a document cannot become a watched type later, and these definitions read nothing of the subject, the dataset or the clock, so each document starts once, at its creation
    a document that existed before this watcher deployed starts only when you start it explicitly (sanity-workflows start, or a script), or once the definition gains a start filter reading the subject or the dataset, which makes its later writes start candidates

    Generated tree:
    created workflows.blueprint.ts
    created functions/wf-prod-drain-effects/index.ts
    created functions/wf-prod-drain-effects/package.json
    created functions/wf-prod-heartbeat/index.ts
    created functions/wf-prod-heartbeat/package.json
    created functions/wf-prod-start-workflows-creates/index.ts
    created functions/wf-prod-start-workflows-creates/package.json
    created effect-handlers/all.ts
    created effect-handlers/notify-reviewer.ts yours now — implement the effect
    created sanity.blueprint.ts the workflow resources spread

    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.

    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 — the drain, heartbeat, and start watcher above.
    • 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. A self-hosted deadline is never a reason to emit a heartbeat, so it is listed there and not under clock; an emitted heartbeat ticks only the in-flight instances of the 'function'-hosted workflows it names. A durable or self-hosted workflow that declares a 'function'-hosted effect gets a beat that recovers its effect queue.

    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 beat runs, and an effect no drain claimed waits for the next beat or the next write that changes its instance's queue. Check your plan's cadence before relying on a $now deadline in a deployed workflow.

    blueprint 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 blueprint 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 blueprint generate --check
    ✖ The generated tree does not match the definitions (1):
    differs effect-handlers/all.ts
    Run `sanity-workflows blueprint 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. 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 --tag prod
    ✖ The generated tree does not match the definitions (1):
    differs workflows.blueprint.ts
    Run `sanity-workflows blueprint 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. Run blueprint 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 prints the runtime the definitions need and continues, so a project that has not generated one yet still deploys:

    $ npx @sanity/workflow-cli deploy --tag prod
    Runtime needs:
    …
    ⚠ No generated runtime is here. Run `sanity-workflows blueprint generate` to emit one, unless a runtime you maintain yourself already serves these definitions.

    Definitions that need no functions at all print nothing about the runtime, and definitions no runtime can be derived from at all print a warning saying so. Either way the deploy continues. blueprint generate is what tells you which functions a given set of definitions needs, and why.

    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 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 --tag=prod, --tag prod, and -t prod all report --tag=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 tag (<tag>.wf-instance.<random>), so the tag 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 definition 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, tags, 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 tag, 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' 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 tag 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 Project id of the first dataset-backed deployment in sanity.workflow.ts.
    dataset Dataset of that same deployment.
    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'], ['definition', '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: --tag prod, --tag=prod, and -t prod all report --tag=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, definition 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 definition 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 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.