@sanity/workflow-mcp 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @sanity/workflow-mcp
2
2
 
3
+ ## 0.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 90207dc: **BREAKING:** `assertReaderModelAcknowledgement` now requires `{requiredMinReaderModel, context?}` instead of a context string, and `ReaderModelAcknowledgementError` accepts its optional context through `{context}`. The Blueprint package's `AcknowledgedDeployment` type moves to the engine as `AcknowledgedWorkflowDeployment`; update imports and use that shared name. Assignment-valued fields can now declare a non-empty `roles` list that limits newly written assignees. Existing fields without `roles` stay unconstrained, retain reader floor 4, and continue deploying with `expectedMinReaderModel: 4`; installing this release requires no coordinated rollout or configuration change.
8
+
9
+ Before adding `roles`, upgrade every Studio, CLI, MCP server, Function, and other runtime that shares the affected workflow resource, then change that deployment's literal `expectedMinReaderModel` to `8`. MCP deployments pass the same reviewed literal as the optional `expectedMinReaderModel` tool argument. Definitions and instances carrying the facet require reader model 8. User assignees qualify through direct roles or the definition's role aliases, while collective role assignees must literally match one configured role. Studio assignment controls, including manual start, auto-start, and assignees nested in to-do rows, offer only eligible choices while retaining stale current values so editors can remove or replace them. Constrained pickers name the active eligible roles and distinguish an empty eligible set from a search with no matches. Top-level plural and composite edits retain no more occurrences of a stale assignment at each declared assignment location than the previous value contained; removal and reordering therefore work, while an added copy is a new assignment and must qualify. Direct singular replacements and every assignment present in an `updateWhere` fragment remain strict. Each engine verb uses one member-directory snapshot across retries, cascade hops, and spawned children, then the next verb reads current membership.
10
+
11
+ Malformed project-member and project-user directory responses now fail loudly instead of treating bad rows or role records as missing users, and actor resolution reports those failures as `inaccessible`, so integrations can distinguish directory failures from absent membership.
12
+
13
+ CLI `start` and `definition delete` no longer apply the definition-submission acknowledgement gate; deploy, check, dry-run, definition diff, Blueprint provision, and engine definition APIs still validate the selected definitions' required floor.
14
+
15
+ No reader-fleet rollout or deployment-literal change is required for existing definitions that do not use `roles`.
16
+
17
+ **Docs impact:** Update the assignment field reference and authoring examples for `roles`, distinguish eligibility from editability, action permission, and literal queue ownership, and add the model-8 conditional adoption sequence to the prerelease rollout guide.
18
+
19
+ - b0b1550: **BREAKING:** Product-prefixed telemetry events now use `Workflows <event>` instead of `Editorial Workflows <event>`; `EditorialWorkflowsResource` is replaced by `WorkflowsResource`; and `defineWorkflows` now generates `workflows-<deployment name>` resource names instead of `editorial-workflows-<deployment name>`. Telemetry consumers matching the previous literals stop receiving events until updated, TypeScript consumers importing the previous Blueprint interface no longer compile, and an existing Blueprint stack that relies on the generated name treats the new default as a different resource identity.
20
+
21
+ Update telemetry dashboards, schemas, alerts, and queries to match the `Workflows` prefix; event suffixes, payloads, and versions are unchanged. Replace Blueprint imports with `WorkflowsResource`. Before deploying an existing Blueprint stack, either pass its recorded resource name explicitly or plan the detach-and-create migration to the new default. Workflows request-tag families and billing classification are unchanged.
22
+
23
+ Other user-facing package copy now uses the Workflows product name. No upgrade action is required for consumers that do not match telemetry event names or use the Blueprint package.
24
+
25
+ **Docs impact:** Rename the product throughout the public documentation, move the docs section to `/docs/workflows/*` with permanent redirects from every `/docs/editorial-workflows/*` path, and document the telemetry-prefix, `WorkflowsResource`, and Blueprint resource-name migrations in the telemetry reference, Blueprint guide, and release notes.
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies [210ca63]
30
+ - Updated dependencies [a3a8dc0]
31
+ - Updated dependencies [a3a8dc0]
32
+ - Updated dependencies [a3a8dc0]
33
+ - Updated dependencies [90207dc]
34
+ - Updated dependencies [b0b1550]
35
+ - @sanity/workflow-engine@0.30.0
36
+
3
37
  ## 0.29.0
4
38
 
5
39
  ### Minor Changes
package/README.md CHANGED
@@ -106,7 +106,7 @@ Definition content is unaffected (deploy expands and vets it).
106
106
  ### Telemetry
107
107
 
108
108
  The stdio server reports adoption telemetry to Sanity: one
109
- `Editorial Workflows MCP Tool Called` event per tool invocation carrying the
109
+ `Workflows MCP Tool Called` event per tool invocation carrying the
110
110
  tool name, a success flag, and whether a list cursor was supplied — from every
111
111
  tool it registers, `workflows_list_tags` included, through one shared outcome
112
112
  path. Cursor
@@ -72,7 +72,7 @@ function workflowErrorText(error) {
72
72
  }
73
73
 
74
74
  const WorkflowMcpToolCalled = defineEvent({
75
- name: "Editorial Workflows MCP Tool Called",
75
+ name: "Workflows MCP Tool Called",
76
76
  version: 2,
77
77
  description: "An MCP tool was invoked — payload is the tool name, success, and list-cursor usage; never arguments, cursor values, or results"
78
78
  });
@@ -141,7 +141,8 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
141
141
  name: "workflows_deploy_definition",
142
142
  description: "Deploy workflow definitions you have authored into one workflow environment. Call workflows_validate_definition first and deploy only after it returns valid:true — deploy runs the same checks but errors instead of returning the problem list. Pass a parent and the child workflows it spawns in ONE call — the engine deploys children before the parents that reference them. Deploys are create-only and content-addressed: content identical to the latest deployed version is a no-op (status 'unchanged'), any change mints the next version (status 'created'; a new name starts at version 1), and a deployed version is never patched — running instances keep the definition version they started under. Returns {results, deployId} with one {name, version, status} per definition, in the resolved deploy order (children first). Do NOT use this to check a definition (workflows_validate_definition) or to see what is already deployed (workflows_list_definitions / workflows_get_definition).",
143
143
  inputSchema: {
144
- definitions: z.array(z.record(z.string(), z.unknown())).min(1).describe("The workflow definitions to deploy, as JSON objects in authoring shape — the same values workflows_validate_definition takes. A single workflow is a one-element array; a parent and its child workflows belong in one call. See workflows_get_authoring_guide for the shape and examples.")
144
+ definitions: z.array(z.record(z.string(), z.unknown())).min(1).describe("The workflow definitions to deploy, as JSON objects in authoring shape — the same values workflows_validate_definition takes. A single workflow is a one-element array; a parent and its child workflows belong in one call. See workflows_get_authoring_guide for the shape and examples."),
145
+ expectedMinReaderModel: z.number().int().min(0).optional().describe("Highest reader model already verified across every runtime sharing this workflow resource. Omit to retain the model-4 baseline. Pass 6 only after upgrading all shared Studio, CLI, MCP, Function, and other runtimes.")
145
146
  },
146
147
  requiresAddress: !0,
147
148
  annotations: {
@@ -161,7 +162,7 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
161
162
  if (problems.length > 0) throw new Error(problems.join(`\n`));
162
163
  const {engine: engine} = await context();
163
164
  return engine.deployDefinitions({
164
- expectedMinReaderModel: 4,
165
+ expectedMinReaderModel: input.expectedMinReaderModel ?? 4,
165
166
  definitions: stored
166
167
  });
167
168
  }
@@ -698,7 +699,7 @@ const diagnoseWorkflowTool = defineWorkflowTool({
698
699
  title: "Approved",
699
700
  description: "Terminal — no transitions out."
700
701
  } ]
701
- }, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (triggers, filters, predicates, guards) is a GROQ *string*. Generate\nthe JSON, then call `workflows_validate_definition` (it takes a `definitions`\narray — validate a parent and its child workflows together) to check it; fix\nthe reported errors and re-validate until it returns `valid: true`. Each\n`results` entry pairs with your input and carries the *desugared* definition\nunder `definition` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, title, description?, initialStage, fields?, stages[], predicates? }`.\n `name` must match `^[a-z0-9][a-z0-9-]*$` (lowercase + digits + dashes — it\n interpolates into every deployed document id, so spaces, uppercase, dots,\n and underscores are rejected). `initialStage` must be the `name` of a\n declared stage. Do NOT include a `version` — definitions are immutable and\n content-addressed; deploy assigns the version from the content, the author\n never writes one.\n- **Stage**: `{ name, title?, description?, activities?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: `{ name, title?, filter?, actions?, fields? }` — a unit of work\n that carries NO payload of its own; everything that DOES anything lives on\n its actions. Every in-scope activity is **active from the moment its stage\n is entered** (there is no activation step) and stays active until an action\n resolves it `done`/`skipped`/`failed`. `filter` is existence, evaluated\n once at stage entry: a definite `false` skips the activity for this visit.\n- **Action**: `{ name, title?, when?, status?, params?, ops? }` — actions are\n the ONLY payload mechanism. Two firing modes:\n - **No `when`** — invoked by a caller (a person or agent calling\n `workflows_fire_action`).\n - **With `when`** — CASCADE-FIRED: the engine fires it on its own the\n moment the GROQ trigger is true (at most once per stage visit), and no\n caller can ever invoke it. Fires-on-entry work is `when: 'true'`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that resolves the *firing\n activity* to that status when the action fires. Status is a health axis, not\n a decision: a routine decision (reject, send back, decline, hold) resolves\n `'done'` and writes the decision into a field the transition triggers read —\n reserve `'failed'` for work that genuinely could not complete (e.g. a missed\n deadline via `{name: 'deadline', when: '$now > $fields.dueBy', status: 'failed'}`).\n `ops` are mutations applied when the action fires (e.g.\n `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values a caller-fired action collects from the caller\n (referenced by `{type:'param', param:'<name>'}` sources); a `when` action\n has no caller, so `params` is rejected there. Scalar params may constrain\n callers to titled choices with\n `options:{list:[{title:'Approve', value:'approve'}]}`, and string/text or\n number params may declare inclusive `validation:{min,max}` bounds.\n- **Transition**: `{ name, title?, to, when? }` — a pure edge; transitions\n carry no ops or effects (only actions do). `to` must name a declared stage.\n `when` is the GROQ trigger gating the exit; omit it and it defaults to\n `$allActivitiesDone`. The first transition whose `when` is true (in\n declaration order) fires.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, initialValue?, options?, validation? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `progress` (a number elevated to mean 0–100 completion; always finite and\n within 0–100 inclusive, fractions allowed), `boolean`, `date` (YYYY-MM-DD),\n `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`, and `subject` — a single `doc.ref`\n elevated to name THE document the workflow is about; workflow scope only, at\n most one, and what document pickers match against), identities (`actor` — one\n concrete principal; `assignee` / `assignees` — one or many user-or-role\n assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) — `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the field once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: `{type:'input'}` (the caller supplies it when the\n instance starts), `{type:'query', query:'<groq>'}` (computed from the lake),\n `{type:'literal', value:<json>}`, or `{type:'fieldRead', field:'<name>'}`.\n String/text, number, URL, date, and datetime fields may declare a non-empty\n `options.list` of `{title, value}` choices. Values must match the field kind,\n be unique, and every non-null runtime value must be listed. The same syntax\n works on nested field shapes, effect outputs, and action params (where datetime\n is spelled `dateTime`).\n String/text, number, and progress declarations may also use inclusive\n `validation:{min,max}` bounds. String/text bounds measure character length\n and must be non-negative integers; number bounds measure the numeric value;\n progress bounds may only narrow the kind's intrinsic 0–100 (each declared\n bound must itself sit within 0–100). Choices must satisfy any bounds. The\n same syntax works on nested field shapes, effect outputs, and action params.\n A workflow about a document declares an `input`-sourced `subject` entry — the\n runtime and every surface identify the subject by that kind.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work — rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log — rows\n `{ body, actor, at }`; pairs with the `audit` op, which stamps `actor`/`at`).\n\n## GROQ in conditions\n\nBuilt-in variables available in triggers/filters/predicates (the engine's\nexported `CONDITION_VARS` inventory is the source of truth):\n`$allActivitiesDone`, `$anyActivityFailed` (booleans over the current stage's\nactivities), `$activities` (the activity list), `$fields` (field values),\n`$context` (the start-time context bag — values seeded when the instance\nstarted; written once, never mutated), `$now`,\n`$self`/`$stage`/`$parent`/`$ancestors` (instance identity + position),\n`$effectStatus` (effect name → `'done'`/`'failed'` of the run queued during\nthe CURRENT stage entry — the re-entry-safe way to gate on an effect having\ndrained, e.g. a trigger action\n`{name: 'settled', when: \"defined($effectStatus['my.effect'])\", status: 'done'}`),\nand the caller-scoped vars `$actor` (the acting user), `$assigned` (whether\nthe caller is the activity's assignee — the idiomatic permission gate, used as\nan action `filter: '$assigned'`), `$can`, and `$attributes` (the caller's\norg-level User Attributes when available — Enterprise; same values lake filters\nread via `user::attributes()`, e.g. `$attributes.department == \"politics\"`;\nunbound and fail-closed on expected absence; unexpected fetch failures throw). `$actor`/`$assigned`/`$can`/`$attributes`\nbelong in the **caller-bound projection**: fireAction action filters, activity\nrequirements, and editable predicates. The cascade is deliberately\ncaller-blind: transition `when`s, activity `filter`s, and a cascade-fired\naction's `when`/`filter` re-evaluate on every trigger (another editor's\naction, an effect draining, a tick) and must resolve the same way regardless\nof whose token that is, so deploy rejects `$actor`/`$assigned`/`$can`/`$attributes`/`$params`\nthere — route on instance state an action wrote instead. `$params` (the firing\naction's args) is not usable in **any** filter — action filters included: a\nfilter decides whether the action is enabled before the caller supplies args,\nso deploy rejects it there too. Collect caller input with the action's\n`params` and consume it in the action's `ops` (a `{type:'param'}` value).\n(Identity still gates every move: the commit rides the caller's token, and the\nlake's ACL accepts or rejects the write wholesale. Engine checks including\n`$attributes` are advisory — not a security boundary.) Define reusable named\nconditions under top-level `predicates: { name: '<groq>' }` and reference\nthem as `$name` (e.g.\n`predicates: { ready: \"count($activities[status != 'done']) == 0\" }` → `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n`$fields.<name>` must name a declared field entry visible at the reading\nsite: workflow fields everywhere, plus the enclosing stage's fields at that\nstage's sites, plus the enclosing activity's fields inside that activity.\nTransition triggers cannot see activity fields — put a decision a transition\nroutes on at stage scope (Example 2). A read's dot-path must also fit the\nentry's declared value shape — reference envelopes especially: a\n`release.ref` value carries `id`/`type`/`releaseName` (never `_id`), a\n`doc.refs` element `id`/`type`. The validator rejects reads of\nundeclared names, condition dot-paths that don't fit the declared shape,\nstages no transition path reaches, and `fieldRead` value sources whose\ntarget entry or dot-path doesn't resolve.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` — resolves the firing activity (shown above).\n- Omitted transition `when` — defaults to `$allActivitiesDone`.\n- Action `roles: ['editor', ...]` — on a caller-fired action, folds a\n role-membership check into its `filter`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review → drafting` gated\n on a `decision` field the reject action wrote), not to a terminal stage.\n Reserve terminal stages for completion and for explicit\n cancellation/abandonment — a workflow should not dead-end just because\n something was declined.\n- **Decisions are fields, not failures.** When a stage branches on a human\n decision, declare a stage-scoped `string` field (stage scope resets on\n re-entry, so loop-backs start clean), have each deciding action `field.set`\n it, and gate every outbound transition on its value (Example 2). Do not\n encode a decision as `status: 'failed'` + `$anyActivityFailed` — reporting\n would count healthy loops as failures.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Every activity must have a path to a terminal status — some action in the\n stage (its own, or a sibling's via a `status.set` op) resolves it\n `done`/`skipped`/`failed`; an activity nothing can ever resolve is rejected.\n- A terminal stage (no transitions) declares no activities — entering it\n completes the instance, so they could never run.\n- Custom `predicates` must not shadow a built-in (e.g. `allActivitiesDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\`\`\`json\n${JSON.stringify(minimalExample, null, 2)}\n\`\`\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\`\`\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\`\`\`\n`, getWorkflowAuthoringGuideTool = defineWorkflowTool({
702
+ }, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (triggers, filters, predicates, guards) is a GROQ *string*. Generate\nthe JSON, then call `workflows_validate_definition` (it takes a `definitions`\narray — validate a parent and its child workflows together) to check it; fix\nthe reported errors and re-validate until it returns `valid: true`. Each\n`results` entry pairs with your input and carries the *desugared* definition\nunder `definition` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, title, description?, initialStage, fields?, stages[], predicates? }`.\n `name` must match `^[a-z0-9][a-z0-9-]*$` (lowercase + digits + dashes — it\n interpolates into every deployed document id, so spaces, uppercase, dots,\n and underscores are rejected). `initialStage` must be the `name` of a\n declared stage. Do NOT include a `version` — definitions are immutable and\n content-addressed; deploy assigns the version from the content, the author\n never writes one.\n- **Stage**: `{ name, title?, description?, activities?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: `{ name, title?, filter?, actions?, fields? }` — a unit of work\n that carries NO payload of its own; everything that DOES anything lives on\n its actions. Every in-scope activity is **active from the moment its stage\n is entered** (there is no activation step) and stays active until an action\n resolves it `done`/`skipped`/`failed`. `filter` is existence, evaluated\n once at stage entry: a definite `false` skips the activity for this visit.\n- **Action**: `{ name, title?, when?, status?, params?, ops? }` — actions are\n the ONLY payload mechanism. Two firing modes:\n - **No `when`** — invoked by a caller (a person or agent calling\n `workflows_fire_action`).\n - **With `when`** — CASCADE-FIRED: the engine fires it on its own the\n moment the GROQ trigger is true (at most once per stage visit), and no\n caller can ever invoke it. Fires-on-entry work is `when: 'true'`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that resolves the *firing\n activity* to that status when the action fires. Status is a health axis, not\n a decision: a routine decision (reject, send back, decline, hold) resolves\n `'done'` and writes the decision into a field the transition triggers read —\n reserve `'failed'` for work that genuinely could not complete (e.g. a missed\n deadline via `{name: 'deadline', when: '$now > $fields.dueBy', status: 'failed'}`).\n `ops` are mutations applied when the action fires (e.g.\n `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values a caller-fired action collects from the caller\n (referenced by `{type:'param', param:'<name>'}` sources); a `when` action\n has no caller, so `params` is rejected there. Scalar params may constrain\n callers to titled choices with\n `options:{list:[{title:'Approve', value:'approve'}]}`, and string/text or\n number params may declare inclusive `validation:{min,max}` bounds.\n- **Transition**: `{ name, title?, to, when? }` — a pure edge; transitions\n carry no ops or effects (only actions do). `to` must name a declared stage.\n `when` is the GROQ trigger gating the exit; omit it and it defaults to\n `$allActivitiesDone`. The first transition whose `when` is true (in\n declaration order) fires.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, initialValue?, options?, validation? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `progress` (a number elevated to mean 0–100 completion; always finite and\n within 0–100 inclusive, fractions allowed), `boolean`, `date` (YYYY-MM-DD),\n `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`, and `subject` — a single `doc.ref`\n elevated to name THE document the workflow is about; workflow scope only, at\n most one, and what document pickers match against), identities (`actor` — one\n concrete principal; `assignee` / `assignees` — one or many user-or-role\n assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) — `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the field once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: `{type:'input'}` (the caller supplies it when the\n instance starts), `{type:'query', query:'<groq>'}` (computed from the lake),\n `{type:'literal', value:<json>}`, or `{type:'fieldRead', field:'<name>'}`.\n String/text, number, URL, date, and datetime fields may declare a non-empty\n `options.list` of `{title, value}` choices. Values must match the field kind,\n be unique, and every non-null runtime value must be listed. The same syntax\n works on nested field shapes, effect outputs, and action params (where datetime\n is spelled `dateTime`).\n String/text, number, and progress declarations may also use inclusive\n `validation:{min,max}` bounds. String/text bounds measure character length\n and must be non-negative integers; number bounds measure the numeric value;\n progress bounds may only narrow the kind's intrinsic 0–100 (each declared\n bound must itself sit within 0–100). Choices must satisfy any bounds. The\n same syntax works on nested field shapes, effect outputs, and action params.\n Assignment fields may declare a non-empty `roles:['editor', ...]` list.\n It limits newly written users to project members who hold or fulfill one of\n those roles, and limits collective role values to literal listed roles. The\n facet works on `assignee` / `assignees` at every field-shape level,\n including nested shapes and effect outputs. Before deploying such a\n definition, upgrade every runtime sharing the workflow resource and pass\n `expectedMinReaderModel:6` to `workflows_deploy_definition`; otherwise\n omit that tool argument and retain the model-4 baseline.\n A workflow about a document declares an `input`-sourced `subject` entry — the\n runtime and every surface identify the subject by that kind.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work — rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log — rows\n `{ body, actor, at }`; pairs with the `audit` op, which stamps `actor`/`at`).\n\n## GROQ in conditions\n\nBuilt-in variables available in triggers/filters/predicates (the engine's\nexported `CONDITION_VARS` inventory is the source of truth):\n`$allActivitiesDone`, `$anyActivityFailed` (booleans over the current stage's\nactivities), `$activities` (the activity list), `$fields` (field values),\n`$context` (the start-time context bag — values seeded when the instance\nstarted; written once, never mutated), `$now`,\n`$self`/`$stage`/`$parent`/`$ancestors` (instance identity + position),\n`$effectStatus` (effect name → `'done'`/`'failed'` of the run queued during\nthe CURRENT stage entry — the re-entry-safe way to gate on an effect having\ndrained, e.g. a trigger action\n`{name: 'settled', when: \"defined($effectStatus['my.effect'])\", status: 'done'}`),\nand the caller-scoped vars `$actor` (the acting user), `$assigned` (whether\nthe caller is the activity's assignee — the idiomatic permission gate, used as\nan action `filter: '$assigned'`), `$can`, and `$attributes` (the caller's\norg-level User Attributes when available — Enterprise; same values lake filters\nread via `user::attributes()`, e.g. `$attributes.department == \"politics\"`;\nunbound and fail-closed on expected absence; unexpected fetch failures throw). `$actor`/`$assigned`/`$can`/`$attributes`\nbelong in the **caller-bound projection**: fireAction action filters, activity\nrequirements, and editable predicates. The cascade is deliberately\ncaller-blind: transition `when`s, activity `filter`s, and a cascade-fired\naction's `when`/`filter` re-evaluate on every trigger (another editor's\naction, an effect draining, a tick) and must resolve the same way regardless\nof whose token that is, so deploy rejects `$actor`/`$assigned`/`$can`/`$attributes`/`$params`\nthere — route on instance state an action wrote instead. `$params` (the firing\naction's args) is not usable in **any** filter — action filters included: a\nfilter decides whether the action is enabled before the caller supplies args,\nso deploy rejects it there too. Collect caller input with the action's\n`params` and consume it in the action's `ops` (a `{type:'param'}` value).\n(Identity still gates every move: the commit rides the caller's token, and the\nlake's ACL accepts or rejects the write wholesale. Engine checks including\n`$attributes` are advisory — not a security boundary.) Define reusable named\nconditions under top-level `predicates: { name: '<groq>' }` and reference\nthem as `$name` (e.g.\n`predicates: { ready: \"count($activities[status != 'done']) == 0\" }` → `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n`$fields.<name>` must name a declared field entry visible at the reading\nsite: workflow fields everywhere, plus the enclosing stage's fields at that\nstage's sites, plus the enclosing activity's fields inside that activity.\nTransition triggers cannot see activity fields — put a decision a transition\nroutes on at stage scope (Example 2). A read's dot-path must also fit the\nentry's declared value shape — reference envelopes especially: a\n`release.ref` value carries `id`/`type`/`releaseName` (never `_id`), a\n`doc.refs` element `id`/`type`. The validator rejects reads of\nundeclared names, condition dot-paths that don't fit the declared shape,\nstages no transition path reaches, and `fieldRead` value sources whose\ntarget entry or dot-path doesn't resolve.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` — resolves the firing activity (shown above).\n- Omitted transition `when` — defaults to `$allActivitiesDone`.\n- Action `roles: ['editor', ...]` — on a caller-fired action, folds a\n role-membership check into its `filter`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review → drafting` gated\n on a `decision` field the reject action wrote), not to a terminal stage.\n Reserve terminal stages for completion and for explicit\n cancellation/abandonment — a workflow should not dead-end just because\n something was declined.\n- **Decisions are fields, not failures.** When a stage branches on a human\n decision, declare a stage-scoped `string` field (stage scope resets on\n re-entry, so loop-backs start clean), have each deciding action `field.set`\n it, and gate every outbound transition on its value (Example 2). Do not\n encode a decision as `status: 'failed'` + `$anyActivityFailed` — reporting\n would count healthy loops as failures.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Every activity must have a path to a terminal status — some action in the\n stage (its own, or a sibling's via a `status.set` op) resolves it\n `done`/`skipped`/`failed`; an activity nothing can ever resolve is rejected.\n- A terminal stage (no transitions) declares no activities — entering it\n completes the instance, so they could never run.\n- Custom `predicates` must not shadow a built-in (e.g. `allActivitiesDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\`\`\`json\n${JSON.stringify(minimalExample, null, 2)}\n\`\`\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\`\`\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\`\`\`\n`, getWorkflowAuthoringGuideTool = defineWorkflowTool({
702
703
  name: "workflows_get_authoring_guide",
703
704
  description: "Get the guide for authoring a workflow definition: the DSL shape, the GROQ condition built-ins, the sugars, the rules the validator enforces, and two worked JSON examples. Call this BEFORE writing a definition from a description, then generate the JSON and check it with workflows_validate_definition. Pure read; takes no arguments.",
704
705
  inputSchema: {},
package/dist/index.cjs CHANGED
@@ -66,7 +66,7 @@ function workflowErrorText(error) {
66
66
  }
67
67
 
68
68
  const WorkflowMcpToolCalled = telemetry.defineEvent({
69
- name: "Editorial Workflows MCP Tool Called",
69
+ name: "Workflows MCP Tool Called",
70
70
  version: 2,
71
71
  description: "An MCP tool was invoked — payload is the tool name, success, and list-cursor usage; never arguments, cursor values, or results"
72
72
  });
@@ -117,7 +117,8 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
117
117
  name: "workflows_deploy_definition",
118
118
  description: "Deploy workflow definitions you have authored into one workflow environment. Call workflows_validate_definition first and deploy only after it returns valid:true — deploy runs the same checks but errors instead of returning the problem list. Pass a parent and the child workflows it spawns in ONE call — the engine deploys children before the parents that reference them. Deploys are create-only and content-addressed: content identical to the latest deployed version is a no-op (status 'unchanged'), any change mints the next version (status 'created'; a new name starts at version 1), and a deployed version is never patched — running instances keep the definition version they started under. Returns {results, deployId} with one {name, version, status} per definition, in the resolved deploy order (children first). Do NOT use this to check a definition (workflows_validate_definition) or to see what is already deployed (workflows_list_definitions / workflows_get_definition).",
119
119
  inputSchema: {
120
- definitions: v3.z.array(v3.z.record(v3.z.string(), v3.z.unknown())).min(1).describe("The workflow definitions to deploy, as JSON objects in authoring shape — the same values workflows_validate_definition takes. A single workflow is a one-element array; a parent and its child workflows belong in one call. See workflows_get_authoring_guide for the shape and examples.")
120
+ definitions: v3.z.array(v3.z.record(v3.z.string(), v3.z.unknown())).min(1).describe("The workflow definitions to deploy, as JSON objects in authoring shape — the same values workflows_validate_definition takes. A single workflow is a one-element array; a parent and its child workflows belong in one call. See workflows_get_authoring_guide for the shape and examples."),
121
+ expectedMinReaderModel: v3.z.number().int().min(0).optional().describe("Highest reader model already verified across every runtime sharing this workflow resource. Omit to retain the model-4 baseline. Pass 6 only after upgrading all shared Studio, CLI, MCP, Function, and other runtimes.")
121
122
  },
122
123
  requiresAddress: !0,
123
124
  annotations: {
@@ -138,7 +139,7 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
138
139
  if (problems.length > 0) throw new Error(problems.join(`\n`));
139
140
  const {engine: engine} = await context();
140
141
  return engine.deployDefinitions({
141
- expectedMinReaderModel: 4,
142
+ expectedMinReaderModel: input.expectedMinReaderModel ?? 4,
142
143
  definitions: stored
143
144
  });
144
145
  }
@@ -675,7 +676,7 @@ const diagnoseWorkflowTool = defineWorkflowTool({
675
676
  title: "Approved",
676
677
  description: "Terminal — no transitions out."
677
678
  } ]
678
- }, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (triggers, filters, predicates, guards) is a GROQ *string*. Generate\nthe JSON, then call `workflows_validate_definition` (it takes a `definitions`\narray — validate a parent and its child workflows together) to check it; fix\nthe reported errors and re-validate until it returns `valid: true`. Each\n`results` entry pairs with your input and carries the *desugared* definition\nunder `definition` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, title, description?, initialStage, fields?, stages[], predicates? }`.\n `name` must match `^[a-z0-9][a-z0-9-]*$` (lowercase + digits + dashes — it\n interpolates into every deployed document id, so spaces, uppercase, dots,\n and underscores are rejected). `initialStage` must be the `name` of a\n declared stage. Do NOT include a `version` — definitions are immutable and\n content-addressed; deploy assigns the version from the content, the author\n never writes one.\n- **Stage**: `{ name, title?, description?, activities?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: `{ name, title?, filter?, actions?, fields? }` — a unit of work\n that carries NO payload of its own; everything that DOES anything lives on\n its actions. Every in-scope activity is **active from the moment its stage\n is entered** (there is no activation step) and stays active until an action\n resolves it `done`/`skipped`/`failed`. `filter` is existence, evaluated\n once at stage entry: a definite `false` skips the activity for this visit.\n- **Action**: `{ name, title?, when?, status?, params?, ops? }` — actions are\n the ONLY payload mechanism. Two firing modes:\n - **No `when`** — invoked by a caller (a person or agent calling\n `workflows_fire_action`).\n - **With `when`** — CASCADE-FIRED: the engine fires it on its own the\n moment the GROQ trigger is true (at most once per stage visit), and no\n caller can ever invoke it. Fires-on-entry work is `when: 'true'`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that resolves the *firing\n activity* to that status when the action fires. Status is a health axis, not\n a decision: a routine decision (reject, send back, decline, hold) resolves\n `'done'` and writes the decision into a field the transition triggers read —\n reserve `'failed'` for work that genuinely could not complete (e.g. a missed\n deadline via `{name: 'deadline', when: '$now > $fields.dueBy', status: 'failed'}`).\n `ops` are mutations applied when the action fires (e.g.\n `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values a caller-fired action collects from the caller\n (referenced by `{type:'param', param:'<name>'}` sources); a `when` action\n has no caller, so `params` is rejected there. Scalar params may constrain\n callers to titled choices with\n `options:{list:[{title:'Approve', value:'approve'}]}`, and string/text or\n number params may declare inclusive `validation:{min,max}` bounds.\n- **Transition**: `{ name, title?, to, when? }` — a pure edge; transitions\n carry no ops or effects (only actions do). `to` must name a declared stage.\n `when` is the GROQ trigger gating the exit; omit it and it defaults to\n `$allActivitiesDone`. The first transition whose `when` is true (in\n declaration order) fires.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, initialValue?, options?, validation? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `progress` (a number elevated to mean 0–100 completion; always finite and\n within 0–100 inclusive, fractions allowed), `boolean`, `date` (YYYY-MM-DD),\n `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`, and `subject` — a single `doc.ref`\n elevated to name THE document the workflow is about; workflow scope only, at\n most one, and what document pickers match against), identities (`actor` — one\n concrete principal; `assignee` / `assignees` — one or many user-or-role\n assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) — `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the field once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: `{type:'input'}` (the caller supplies it when the\n instance starts), `{type:'query', query:'<groq>'}` (computed from the lake),\n `{type:'literal', value:<json>}`, or `{type:'fieldRead', field:'<name>'}`.\n String/text, number, URL, date, and datetime fields may declare a non-empty\n `options.list` of `{title, value}` choices. Values must match the field kind,\n be unique, and every non-null runtime value must be listed. The same syntax\n works on nested field shapes, effect outputs, and action params (where datetime\n is spelled `dateTime`).\n String/text, number, and progress declarations may also use inclusive\n `validation:{min,max}` bounds. String/text bounds measure character length\n and must be non-negative integers; number bounds measure the numeric value;\n progress bounds may only narrow the kind's intrinsic 0–100 (each declared\n bound must itself sit within 0–100). Choices must satisfy any bounds. The\n same syntax works on nested field shapes, effect outputs, and action params.\n A workflow about a document declares an `input`-sourced `subject` entry — the\n runtime and every surface identify the subject by that kind.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work — rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log — rows\n `{ body, actor, at }`; pairs with the `audit` op, which stamps `actor`/`at`).\n\n## GROQ in conditions\n\nBuilt-in variables available in triggers/filters/predicates (the engine's\nexported `CONDITION_VARS` inventory is the source of truth):\n`$allActivitiesDone`, `$anyActivityFailed` (booleans over the current stage's\nactivities), `$activities` (the activity list), `$fields` (field values),\n`$context` (the start-time context bag — values seeded when the instance\nstarted; written once, never mutated), `$now`,\n`$self`/`$stage`/`$parent`/`$ancestors` (instance identity + position),\n`$effectStatus` (effect name → `'done'`/`'failed'` of the run queued during\nthe CURRENT stage entry — the re-entry-safe way to gate on an effect having\ndrained, e.g. a trigger action\n`{name: 'settled', when: \"defined($effectStatus['my.effect'])\", status: 'done'}`),\nand the caller-scoped vars `$actor` (the acting user), `$assigned` (whether\nthe caller is the activity's assignee — the idiomatic permission gate, used as\nan action `filter: '$assigned'`), `$can`, and `$attributes` (the caller's\norg-level User Attributes when available — Enterprise; same values lake filters\nread via `user::attributes()`, e.g. `$attributes.department == \"politics\"`;\nunbound and fail-closed on expected absence; unexpected fetch failures throw). `$actor`/`$assigned`/`$can`/`$attributes`\nbelong in the **caller-bound projection**: fireAction action filters, activity\nrequirements, and editable predicates. The cascade is deliberately\ncaller-blind: transition `when`s, activity `filter`s, and a cascade-fired\naction's `when`/`filter` re-evaluate on every trigger (another editor's\naction, an effect draining, a tick) and must resolve the same way regardless\nof whose token that is, so deploy rejects `$actor`/`$assigned`/`$can`/`$attributes`/`$params`\nthere — route on instance state an action wrote instead. `$params` (the firing\naction's args) is not usable in **any** filter — action filters included: a\nfilter decides whether the action is enabled before the caller supplies args,\nso deploy rejects it there too. Collect caller input with the action's\n`params` and consume it in the action's `ops` (a `{type:'param'}` value).\n(Identity still gates every move: the commit rides the caller's token, and the\nlake's ACL accepts or rejects the write wholesale. Engine checks including\n`$attributes` are advisory — not a security boundary.) Define reusable named\nconditions under top-level `predicates: { name: '<groq>' }` and reference\nthem as `$name` (e.g.\n`predicates: { ready: \"count($activities[status != 'done']) == 0\" }` → `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n`$fields.<name>` must name a declared field entry visible at the reading\nsite: workflow fields everywhere, plus the enclosing stage's fields at that\nstage's sites, plus the enclosing activity's fields inside that activity.\nTransition triggers cannot see activity fields — put a decision a transition\nroutes on at stage scope (Example 2). A read's dot-path must also fit the\nentry's declared value shape — reference envelopes especially: a\n`release.ref` value carries `id`/`type`/`releaseName` (never `_id`), a\n`doc.refs` element `id`/`type`. The validator rejects reads of\nundeclared names, condition dot-paths that don't fit the declared shape,\nstages no transition path reaches, and `fieldRead` value sources whose\ntarget entry or dot-path doesn't resolve.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` — resolves the firing activity (shown above).\n- Omitted transition `when` — defaults to `$allActivitiesDone`.\n- Action `roles: ['editor', ...]` — on a caller-fired action, folds a\n role-membership check into its `filter`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review → drafting` gated\n on a `decision` field the reject action wrote), not to a terminal stage.\n Reserve terminal stages for completion and for explicit\n cancellation/abandonment — a workflow should not dead-end just because\n something was declined.\n- **Decisions are fields, not failures.** When a stage branches on a human\n decision, declare a stage-scoped `string` field (stage scope resets on\n re-entry, so loop-backs start clean), have each deciding action `field.set`\n it, and gate every outbound transition on its value (Example 2). Do not\n encode a decision as `status: 'failed'` + `$anyActivityFailed` — reporting\n would count healthy loops as failures.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Every activity must have a path to a terminal status — some action in the\n stage (its own, or a sibling's via a `status.set` op) resolves it\n `done`/`skipped`/`failed`; an activity nothing can ever resolve is rejected.\n- A terminal stage (no transitions) declares no activities — entering it\n completes the instance, so they could never run.\n- Custom `predicates` must not shadow a built-in (e.g. `allActivitiesDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\`\`\`json\n${JSON.stringify(minimalExample, null, 2)}\n\`\`\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\`\`\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\`\`\`\n`, getWorkflowAuthoringGuideTool = defineWorkflowTool({
679
+ }, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (triggers, filters, predicates, guards) is a GROQ *string*. Generate\nthe JSON, then call `workflows_validate_definition` (it takes a `definitions`\narray — validate a parent and its child workflows together) to check it; fix\nthe reported errors and re-validate until it returns `valid: true`. Each\n`results` entry pairs with your input and carries the *desugared* definition\nunder `definition` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, title, description?, initialStage, fields?, stages[], predicates? }`.\n `name` must match `^[a-z0-9][a-z0-9-]*$` (lowercase + digits + dashes — it\n interpolates into every deployed document id, so spaces, uppercase, dots,\n and underscores are rejected). `initialStage` must be the `name` of a\n declared stage. Do NOT include a `version` — definitions are immutable and\n content-addressed; deploy assigns the version from the content, the author\n never writes one.\n- **Stage**: `{ name, title?, description?, activities?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: `{ name, title?, filter?, actions?, fields? }` — a unit of work\n that carries NO payload of its own; everything that DOES anything lives on\n its actions. Every in-scope activity is **active from the moment its stage\n is entered** (there is no activation step) and stays active until an action\n resolves it `done`/`skipped`/`failed`. `filter` is existence, evaluated\n once at stage entry: a definite `false` skips the activity for this visit.\n- **Action**: `{ name, title?, when?, status?, params?, ops? }` — actions are\n the ONLY payload mechanism. Two firing modes:\n - **No `when`** — invoked by a caller (a person or agent calling\n `workflows_fire_action`).\n - **With `when`** — CASCADE-FIRED: the engine fires it on its own the\n moment the GROQ trigger is true (at most once per stage visit), and no\n caller can ever invoke it. Fires-on-entry work is `when: 'true'`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that resolves the *firing\n activity* to that status when the action fires. Status is a health axis, not\n a decision: a routine decision (reject, send back, decline, hold) resolves\n `'done'` and writes the decision into a field the transition triggers read —\n reserve `'failed'` for work that genuinely could not complete (e.g. a missed\n deadline via `{name: 'deadline', when: '$now > $fields.dueBy', status: 'failed'}`).\n `ops` are mutations applied when the action fires (e.g.\n `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values a caller-fired action collects from the caller\n (referenced by `{type:'param', param:'<name>'}` sources); a `when` action\n has no caller, so `params` is rejected there. Scalar params may constrain\n callers to titled choices with\n `options:{list:[{title:'Approve', value:'approve'}]}`, and string/text or\n number params may declare inclusive `validation:{min,max}` bounds.\n- **Transition**: `{ name, title?, to, when? }` — a pure edge; transitions\n carry no ops or effects (only actions do). `to` must name a declared stage.\n `when` is the GROQ trigger gating the exit; omit it and it defaults to\n `$allActivitiesDone`. The first transition whose `when` is true (in\n declaration order) fires.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, initialValue?, options?, validation? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `progress` (a number elevated to mean 0–100 completion; always finite and\n within 0–100 inclusive, fractions allowed), `boolean`, `date` (YYYY-MM-DD),\n `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`, and `subject` — a single `doc.ref`\n elevated to name THE document the workflow is about; workflow scope only, at\n most one, and what document pickers match against), identities (`actor` — one\n concrete principal; `assignee` / `assignees` — one or many user-or-role\n assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) — `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the field once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: `{type:'input'}` (the caller supplies it when the\n instance starts), `{type:'query', query:'<groq>'}` (computed from the lake),\n `{type:'literal', value:<json>}`, or `{type:'fieldRead', field:'<name>'}`.\n String/text, number, URL, date, and datetime fields may declare a non-empty\n `options.list` of `{title, value}` choices. Values must match the field kind,\n be unique, and every non-null runtime value must be listed. The same syntax\n works on nested field shapes, effect outputs, and action params (where datetime\n is spelled `dateTime`).\n String/text, number, and progress declarations may also use inclusive\n `validation:{min,max}` bounds. String/text bounds measure character length\n and must be non-negative integers; number bounds measure the numeric value;\n progress bounds may only narrow the kind's intrinsic 0–100 (each declared\n bound must itself sit within 0–100). Choices must satisfy any bounds. The\n same syntax works on nested field shapes, effect outputs, and action params.\n Assignment fields may declare a non-empty `roles:['editor', ...]` list.\n It limits newly written users to project members who hold or fulfill one of\n those roles, and limits collective role values to literal listed roles. The\n facet works on `assignee` / `assignees` at every field-shape level,\n including nested shapes and effect outputs. Before deploying such a\n definition, upgrade every runtime sharing the workflow resource and pass\n `expectedMinReaderModel:6` to `workflows_deploy_definition`; otherwise\n omit that tool argument and retain the model-4 baseline.\n A workflow about a document declares an `input`-sourced `subject` entry — the\n runtime and every surface identify the subject by that kind.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work — rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log — rows\n `{ body, actor, at }`; pairs with the `audit` op, which stamps `actor`/`at`).\n\n## GROQ in conditions\n\nBuilt-in variables available in triggers/filters/predicates (the engine's\nexported `CONDITION_VARS` inventory is the source of truth):\n`$allActivitiesDone`, `$anyActivityFailed` (booleans over the current stage's\nactivities), `$activities` (the activity list), `$fields` (field values),\n`$context` (the start-time context bag — values seeded when the instance\nstarted; written once, never mutated), `$now`,\n`$self`/`$stage`/`$parent`/`$ancestors` (instance identity + position),\n`$effectStatus` (effect name → `'done'`/`'failed'` of the run queued during\nthe CURRENT stage entry — the re-entry-safe way to gate on an effect having\ndrained, e.g. a trigger action\n`{name: 'settled', when: \"defined($effectStatus['my.effect'])\", status: 'done'}`),\nand the caller-scoped vars `$actor` (the acting user), `$assigned` (whether\nthe caller is the activity's assignee — the idiomatic permission gate, used as\nan action `filter: '$assigned'`), `$can`, and `$attributes` (the caller's\norg-level User Attributes when available — Enterprise; same values lake filters\nread via `user::attributes()`, e.g. `$attributes.department == \"politics\"`;\nunbound and fail-closed on expected absence; unexpected fetch failures throw). `$actor`/`$assigned`/`$can`/`$attributes`\nbelong in the **caller-bound projection**: fireAction action filters, activity\nrequirements, and editable predicates. The cascade is deliberately\ncaller-blind: transition `when`s, activity `filter`s, and a cascade-fired\naction's `when`/`filter` re-evaluate on every trigger (another editor's\naction, an effect draining, a tick) and must resolve the same way regardless\nof whose token that is, so deploy rejects `$actor`/`$assigned`/`$can`/`$attributes`/`$params`\nthere — route on instance state an action wrote instead. `$params` (the firing\naction's args) is not usable in **any** filter — action filters included: a\nfilter decides whether the action is enabled before the caller supplies args,\nso deploy rejects it there too. Collect caller input with the action's\n`params` and consume it in the action's `ops` (a `{type:'param'}` value).\n(Identity still gates every move: the commit rides the caller's token, and the\nlake's ACL accepts or rejects the write wholesale. Engine checks including\n`$attributes` are advisory — not a security boundary.) Define reusable named\nconditions under top-level `predicates: { name: '<groq>' }` and reference\nthem as `$name` (e.g.\n`predicates: { ready: \"count($activities[status != 'done']) == 0\" }` → `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n`$fields.<name>` must name a declared field entry visible at the reading\nsite: workflow fields everywhere, plus the enclosing stage's fields at that\nstage's sites, plus the enclosing activity's fields inside that activity.\nTransition triggers cannot see activity fields — put a decision a transition\nroutes on at stage scope (Example 2). A read's dot-path must also fit the\nentry's declared value shape — reference envelopes especially: a\n`release.ref` value carries `id`/`type`/`releaseName` (never `_id`), a\n`doc.refs` element `id`/`type`. The validator rejects reads of\nundeclared names, condition dot-paths that don't fit the declared shape,\nstages no transition path reaches, and `fieldRead` value sources whose\ntarget entry or dot-path doesn't resolve.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` — resolves the firing activity (shown above).\n- Omitted transition `when` — defaults to `$allActivitiesDone`.\n- Action `roles: ['editor', ...]` — on a caller-fired action, folds a\n role-membership check into its `filter`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review → drafting` gated\n on a `decision` field the reject action wrote), not to a terminal stage.\n Reserve terminal stages for completion and for explicit\n cancellation/abandonment — a workflow should not dead-end just because\n something was declined.\n- **Decisions are fields, not failures.** When a stage branches on a human\n decision, declare a stage-scoped `string` field (stage scope resets on\n re-entry, so loop-backs start clean), have each deciding action `field.set`\n it, and gate every outbound transition on its value (Example 2). Do not\n encode a decision as `status: 'failed'` + `$anyActivityFailed` — reporting\n would count healthy loops as failures.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Every activity must have a path to a terminal status — some action in the\n stage (its own, or a sibling's via a `status.set` op) resolves it\n `done`/`skipped`/`failed`; an activity nothing can ever resolve is rejected.\n- A terminal stage (no transitions) declares no activities — entering it\n completes the instance, so they could never run.\n- Custom `predicates` must not shadow a built-in (e.g. `allActivitiesDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\`\`\`json\n${JSON.stringify(minimalExample, null, 2)}\n\`\`\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\`\`\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\`\`\`\n`, getWorkflowAuthoringGuideTool = defineWorkflowTool({
679
680
  name: "workflows_get_authoring_guide",
680
681
  description: "Get the guide for authoring a workflow definition: the DSL shape, the GROQ condition built-ins, the sugars, the rules the validator enforces, and two worked JSON examples. Call this BEFORE writing a definition from a description, then generate the JSON and check it with workflows_validate_definition. Pure read; takes no arguments.",
681
682
  inputSchema: {},
package/dist/index.d.cts CHANGED
@@ -301,9 +301,10 @@ export declare interface ProjectedSubject {
301
301
  }
302
302
 
303
303
  /**
304
- * Register every workflow tool onto `server`, wired through `getContext`.
305
- * A host that owns a telemetry shell passes its logger to log one
306
- * adoption event per tool call; without it, calls log nothing.
304
+ * Register every workflow tool onto `server`, wired through `getContext`. Hosts with
305
+ * their own wrappers (error handling, telemetry, response helpers) should iterate
306
+ * {@link WORKFLOW_TOOLS} directly instead. A host that owns a telemetry shell passes
307
+ * its logger to log one adoption event per tool call; without it, calls log nothing.
307
308
  */
308
309
  export declare function registerWorkflowTools(
309
310
  server: McpServer,
@@ -377,6 +378,16 @@ export declare const validateWorkflowDefinitionTool: WorkflowToolDef;
377
378
  */
378
379
  export declare const WORKFLOW_TAG_DESCRIPTION: string;
379
380
 
381
+ /**
382
+ * Every tool def this package ships, in the order an agent should read them: operate a
383
+ * running instance, then author a definition (see {@link WorkflowToolDef}).
384
+ *
385
+ * Each tool's own description is written for an agent, not a human: what it does, when
386
+ * to use it, what not to use it for. The write tool spells out that it's the only way to
387
+ * advance state — without that line, an LLM looks for a `complete_activity` or
388
+ * `set_stage` tool that doesn't exist. Read tools cap their output rather than burn
389
+ * context on the rest.
390
+ */
380
391
  export declare const WORKFLOW_TOOLS: readonly WorkflowToolDef[];
381
392
 
382
393
  /**
@@ -418,17 +429,20 @@ declare interface WorkflowClientPolicy {
418
429
  perspective: "published";
419
430
  }
420
431
 
421
- /** Where one tool call reads/writes workflow data: resource + tag. */
432
+ /**
433
+ * Where one tool call reads/writes workflow data: resource + tag. The server is
434
+ * org-scoped — auth says who may act, never where — so there is no default; a call
435
+ * without an address fails schema validation rather than guessing one.
436
+ */
422
437
  export declare interface WorkflowEnvironmentAddress {
423
438
  workflowResource: WorkflowResource;
424
439
  tag: string;
425
440
  }
426
441
 
427
442
  /**
428
- * One rendering of a failed tool call, shared by every host. A structured
429
- * error's stable `kind` leads the text so the model can branch on the failure
430
- * without parsing the human-readable message — a host that re-derives this
431
- * prefix drifts from the wording the descriptions and evals were tuned against.
443
+ * One rendering of a failed tool call, shared by every host. A structured error's
444
+ * stable `kind` leads the text so the model can branch on the failure without parsing
445
+ * the message — call this rather than re-deriving the prefix, which would drift.
432
446
  */
433
447
  export declare function workflowErrorText(error: unknown): string;
434
448
 
@@ -448,10 +462,13 @@ declare interface WorkflowResourceAddressing {
448
462
  }
449
463
 
450
464
  /**
451
- * Everything a tool call needs from its host: the engine to operate on.
452
- * Identity is the token behind the engine's client (`/users/me`) — a host
453
- * that should act as someone else supplies an engine over their token, and
454
- * declares itself via the engine's `executionContext`.
465
+ * Everything a tool call needs from its host: the engine to operate on. The engine is
466
+ * the boundary — nothing in this module or the tool defs constructs one, reads the
467
+ * environment, or assumes an identity or transport; that all lives in the host layer.
468
+ *
469
+ * Identity is the token behind the engine's client (`/users/me`) — a host that should
470
+ * act as someone else supplies an engine over their token, and declares itself via the
471
+ * engine's `executionContext`.
455
472
  */
456
473
  export declare interface WorkflowToolContext {
457
474
  engine: Engine;
package/dist/index.d.ts CHANGED
@@ -301,9 +301,10 @@ export declare interface ProjectedSubject {
301
301
  }
302
302
 
303
303
  /**
304
- * Register every workflow tool onto `server`, wired through `getContext`.
305
- * A host that owns a telemetry shell passes its logger to log one
306
- * adoption event per tool call; without it, calls log nothing.
304
+ * Register every workflow tool onto `server`, wired through `getContext`. Hosts with
305
+ * their own wrappers (error handling, telemetry, response helpers) should iterate
306
+ * {@link WORKFLOW_TOOLS} directly instead. A host that owns a telemetry shell passes
307
+ * its logger to log one adoption event per tool call; without it, calls log nothing.
307
308
  */
308
309
  export declare function registerWorkflowTools(
309
310
  server: McpServer,
@@ -377,6 +378,16 @@ export declare const validateWorkflowDefinitionTool: WorkflowToolDef;
377
378
  */
378
379
  export declare const WORKFLOW_TAG_DESCRIPTION: string;
379
380
 
381
+ /**
382
+ * Every tool def this package ships, in the order an agent should read them: operate a
383
+ * running instance, then author a definition (see {@link WorkflowToolDef}).
384
+ *
385
+ * Each tool's own description is written for an agent, not a human: what it does, when
386
+ * to use it, what not to use it for. The write tool spells out that it's the only way to
387
+ * advance state — without that line, an LLM looks for a `complete_activity` or
388
+ * `set_stage` tool that doesn't exist. Read tools cap their output rather than burn
389
+ * context on the rest.
390
+ */
380
391
  export declare const WORKFLOW_TOOLS: readonly WorkflowToolDef[];
381
392
 
382
393
  /**
@@ -418,17 +429,20 @@ declare interface WorkflowClientPolicy {
418
429
  perspective: "published";
419
430
  }
420
431
 
421
- /** Where one tool call reads/writes workflow data: resource + tag. */
432
+ /**
433
+ * Where one tool call reads/writes workflow data: resource + tag. The server is
434
+ * org-scoped — auth says who may act, never where — so there is no default; a call
435
+ * without an address fails schema validation rather than guessing one.
436
+ */
422
437
  export declare interface WorkflowEnvironmentAddress {
423
438
  workflowResource: WorkflowResource;
424
439
  tag: string;
425
440
  }
426
441
 
427
442
  /**
428
- * One rendering of a failed tool call, shared by every host. A structured
429
- * error's stable `kind` leads the text so the model can branch on the failure
430
- * without parsing the human-readable message — a host that re-derives this
431
- * prefix drifts from the wording the descriptions and evals were tuned against.
443
+ * One rendering of a failed tool call, shared by every host. A structured error's
444
+ * stable `kind` leads the text so the model can branch on the failure without parsing
445
+ * the message — call this rather than re-deriving the prefix, which would drift.
432
446
  */
433
447
  export declare function workflowErrorText(error: unknown): string;
434
448
 
@@ -448,10 +462,13 @@ declare interface WorkflowResourceAddressing {
448
462
  }
449
463
 
450
464
  /**
451
- * Everything a tool call needs from its host: the engine to operate on.
452
- * Identity is the token behind the engine's client (`/users/me`) — a host
453
- * that should act as someone else supplies an engine over their token, and
454
- * declares itself via the engine's `executionContext`.
465
+ * Everything a tool call needs from its host: the engine to operate on. The engine is
466
+ * the boundary — nothing in this module or the tool defs constructs one, reads the
467
+ * environment, or assumes an identity or transport; that all lives in the host layer.
468
+ *
469
+ * Identity is the token behind the engine's client (`/users/me`) — a host that should
470
+ * act as someone else supplies an engine over their token, and declares itself via the
471
+ * engine's `executionContext`.
455
472
  */
456
473
  export declare interface WorkflowToolContext {
457
474
  engine: Engine;
package/dist/stdio.d.ts CHANGED
@@ -1,27 +1,13 @@
1
1
  /**
2
- * stdio MCP host — boots an {@link McpServer} from
3
- * `@modelcontextprotocol/sdk`, binds the workflow tools, and listens on
4
- * stdio.
2
+ * Boots an {@link McpServer}, binds the workflow tools, and listens on stdio; resolves
3
+ * once the transport connects (the process then stays alive on the open streams).
5
4
  *
6
- * The server is org-authed: boot config is the org-level
7
- * `SANITY_AUTH_TOKEN` (plus an optional `SANITY_API_HOST`) and nothing
8
- * else — no project, dataset, resource, or tag. Every instance-operating
9
- * tool call names its own workflow environment (`workflow_resource` +
10
- * `tag`, see `./address.ts`), and the host builds one engine per
11
- * distinct environment, cached for the life of the process (see
12
- * `./engine-cache.ts`). Same model as the Sanity MCP: auth says who may
13
- * act, the call says where.
5
+ * Org-authed: boot config is just the org token — no project, dataset, resource, or
6
+ * tag. Every tool call names its own workflow environment (`workflow_resource` + `tag`),
7
+ * and the host builds/caches one engine per distinct environment for the process's life.
14
8
  *
15
- * Kept as its own package entry (not inline in `bin/`) so it can be
16
- * compiled to `dist/stdio.js` by pkg-utils and re-used by both the
17
- * published bin (`bin/run.js`) and the dev entry (`bin/workflow-mcp.ts`)
18
- * — one boot path, no duplication. Keeping it out of the `.` entry also
19
- * keeps the MCP transport deps out of the library import graph.
20
- */
21
- /**
22
- * Read the org token, register the tools, and serve on stdio. Resolves
23
- * when the transport is connected (the process then stays alive on the
24
- * open stdio streams).
9
+ * Kept as its own package entry, not inline in `bin/`, so both the published bin and the
10
+ * dev entry share one boot path, and the MCP transport deps stay out of the library's `.` entry.
25
11
  */
26
12
  export declare function runStdioServer(): Promise<void>;
27
13
 
package/dist/stdio.js CHANGED
@@ -6,7 +6,7 @@ import { deployedTagsGroq, parseResourceGdr, datasetResourceParts, EXECUTION_KIN
6
6
 
7
7
  import { LIST_WORKFLOW_TAGS_TOOL_NAME, workflowAddressFields, LIST_WORKFLOW_TAGS_DESCRIPTION, withToolTelemetry, clientForResource, registerWorkflowTools, workflowAddressFromInput, createMcpTelemetry, createEngineCache } from "./_chunks-es/index.js";
8
8
 
9
- var version = "0.29.0", packageJson = {
9
+ var version = "0.30.0", packageJson = {
10
10
  version: version
11
11
  };
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-mcp",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "MCP server exposing Sanity workflow tools to agents — operate running workflow instances and author new definitions.",
5
5
  "keywords": [
6
6
  "agent",
@@ -62,14 +62,14 @@
62
62
  "@types/node": "^24.12.4",
63
63
  "vitest": "^4.1.8",
64
64
  "zod": "^4.4.3",
65
- "@sanity/workflow-engine": "0.29.0",
66
- "@sanity/workflow-engine-test": "0.29.0",
67
- "@sanity/workflow-examples": "0.10.8"
65
+ "@sanity/workflow-engine": "0.30.0",
66
+ "@sanity/workflow-engine-test": "0.30.0",
67
+ "@sanity/workflow-examples": "0.11.0"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@modelcontextprotocol/sdk": "^1.29.0",
71
71
  "zod": "^3.25.28 || ^4",
72
- "@sanity/workflow-engine": "0.29.0"
72
+ "@sanity/workflow-engine": "0.30.0"
73
73
  },
74
74
  "engines": {
75
75
  "node": ">=20"