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.
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:
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.sanity.workflow resource.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:
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.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.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:
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:
--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.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.Never collected, because the policy excludes them:
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:
--reason on abort, set-stage, and
definition delete, --field on start, and --param on fire-action.--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.-- terminator. Those tokens are not reported at all.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.
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:
| 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.
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.
| 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'. |
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.
| 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.
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.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.