@sanity/workflow-mcp 0.8.0 → 0.9.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.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 55a54b3: Surfaces understand the new `subject` field kind. Studio plugin: the start dialog's default subject seed lands on a definition's `subject`-kind entry whatever it is named (the doc.ref-named-`subject` convention stays the fallback), subject entries get the document picker / read-only face / state-panel preview / document pill, and mapping validation checks the subject entry's `types` facet. MCP: the subject lookup behind title enrichment prefers the `subject`-kind entry and keeps the name convention as fallback; the authoring guide teaches the kind. Test bench — **BREAKING:** `subjectField` now builds a `subject`-KIND entry value (matching its name and the engine's subject rule); a definition declaring the doc.ref convention needs the new `docRefField(documentId, {name})` instead. `instancesForSubject` matches both single-ref kinds.
8
+ - 4490442: Add inclusive validation.min/max bounds for workflow string, text, and number declarations, with Studio input constraints and MCP authoring guidance.
9
+ - 1540dce: **BREAKING:** Definitions and instances written by this release require a model-2 engine reader; upgrade every reader before deploying its writers.
10
+
11
+ Workflow fields, nested field shapes, effect outputs, and action parameters can declare typed scalar choices with `options.list`. The engine validates authored lists and rejects runtime values outside them, Studio renders one shared select control and displays each selected choice by its title, and the MCP authoring guide explains the syntax.
12
+
13
+ ### Patch Changes
14
+
15
+ - 00c089d: **BREAKING:** Require every definition deployment and workflow deployment configuration to state an `expectedMinReaderModel` literal that exactly matches the installed engine's writer capability.
16
+
17
+ Deploy and definition-diff paths now reject missing or stale acknowledgements before client access or telemetry, with a structured error and readers-first rollout guidance.
18
+
19
+ - Updated dependencies [239d9f6]
20
+ - Updated dependencies [3af2ca0]
21
+ - Updated dependencies [7276702]
22
+ - Updated dependencies [58f8211]
23
+ - Updated dependencies [00c089d]
24
+ - Updated dependencies [71cd58e]
25
+ - Updated dependencies [cd03973]
26
+ - Updated dependencies [d26cc1a]
27
+ - Updated dependencies [9126299]
28
+ - Updated dependencies [72018af]
29
+ - Updated dependencies [5cd8ffd]
30
+ - Updated dependencies [b82bda4]
31
+ - Updated dependencies [55a54b3]
32
+ - Updated dependencies [b82bda4]
33
+ - Updated dependencies [4490442]
34
+ - Updated dependencies [1540dce]
35
+ - @sanity/workflow-engine@0.18.0
36
+
3
37
  ## 0.8.0
4
38
 
5
39
  ### Minor Changes
@@ -120,14 +120,14 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
120
120
  if (problems.length > 0) throw new Error(problems.join(`\n`));
121
121
  const {engine: engine} = await context();
122
122
  return engine.deployDefinitions({
123
+ expectedMinReaderModel: 2,
123
124
  definitions: stored
124
125
  });
125
126
  }
126
127
  }), SUBJECT_ENTRY_NAME = "subject";
127
128
 
128
129
  function getSubjectGdr(instance) {
129
- const entry = instance.fields.find(s => s._type === "doc.ref" && s.name === SUBJECT_ENTRY_NAME);
130
- if (!(entry === void 0 || entry._type !== "doc.ref")) return entry.value?.id;
130
+ return (instance.fields.find(s => s._type === "subject") ?? instance.fields.find(s => s._type === "doc.ref" && s.name === SUBJECT_ENTRY_NAME))?.value?.id;
131
131
  }
132
132
 
133
133
  async function fetchSubjectTitles(engine, instances) {
@@ -541,7 +541,7 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
541
541
  description: "One review stage with a single approve action, then a terminal stage.",
542
542
  initialStage: "review",
543
543
  fields: [ {
544
- type: "doc.ref",
544
+ type: "subject",
545
545
  name: "subject",
546
546
  title: "Document",
547
547
  initialValue: {
@@ -576,7 +576,7 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
576
576
  description: "Draft, then editorial review that approves or rejects back to drafting.",
577
577
  initialStage: "drafting",
578
578
  fields: [ {
579
- type: "doc.ref",
579
+ type: "subject",
580
580
  name: "subject",
581
581
  title: "Document",
582
582
  initialValue: {
@@ -657,7 +657,7 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
657
657
  title: "Approved",
658
658
  description: "Terminal — no transitions out."
659
659
  } ]
660
- }, 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 `validate_workflow_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 `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.\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? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `boolean`, `date` (YYYY-MM-DD), `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`), identities (`actor` — one concrete\n principal; `assignee` / `assignees` — one or many user-or-role 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 By convention a workflow has an `input`-sourced `doc.ref` named `subject` — the\n headline document it is about.\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'`), and `$can`. `$actor`/`$assigned`/`$can`\nbelong in **caller-fired action filters only**. 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`/`$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.) 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({
660
+ }, 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 `validate_workflow_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 `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 `boolean`, `date` (YYYY-MM-DD), `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 and number 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 Choices must satisfy any bounds. The same syntax works on nested field shapes,\n 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'`), and `$can`. `$actor`/`$assigned`/`$can`\nbelong in **caller-fired action filters only**. 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`/`$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.) 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({
661
661
  name: "get_workflow_authoring_guide",
662
662
  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 validate_workflow_definition. Pure read; takes no arguments.",
663
663
  inputSchema: {},
@@ -692,7 +692,7 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
692
692
  }
693
693
  }), getWorkflowStateTool = defineWorkflowTool({
694
694
  name: "get_workflow_state",
695
- description: `Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable \`workflowTitle\`, the current stage, the subject document the workflow is about (when the workflow has a conventional subject — its ref and title), every in-scope activity on the current stage, and the most recent history entries, plus a one-line \`autonomy\` narrative saying whether the workflow runs itself and where it waits on someone. Per activity: its \`classification\` (who fires its actions: interactive, autonomous, off-system, or hybrid), the causal \`completesWithoutCaller\` verdict (yes/no/conditional — a mechanically autonomous activity whose triggers only read caller-written state answers no) with narrated \`waitsOn\` lines when not yes, its invocable \`actions\` (each with an allowed/disabled verdict — these are what fire_action accepts), and its \`automations\` (cascade-fired actions the ENGINE fires on its own when their \`firesWhen\` trigger holds — never invocable via fire_action). Activities and actions that exist in the definition but are scoped out for this visit or actor are simply absent. Use this whenever you need to understand what's possible on an instance before deciding to act. This is a pure read — it does not change anything. ${UNTRUSTED_AUTHORED_DATA_NOTE} If you only need to discover what instances exist, use list_workflow_instances instead; this tool requires you to know the instance id. list_workflow_instances also returns the same \`workflowTitle\` and \`subject\` fields, so prefer it for fan-out discovery rather than polling get_workflow_state per instance.`,
695
+ description: `Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable \`workflowTitle\`, the current stage, the subject document the workflow is about (when the workflow has a declared subject — its ref and title), every in-scope activity on the current stage, and the most recent history entries, plus a one-line \`autonomy\` narrative saying whether the workflow runs itself and where it waits on someone. Per activity: its \`classification\` (who fires its actions: interactive, autonomous, off-system, or hybrid), the causal \`completesWithoutCaller\` verdict (yes/no/conditional — a mechanically autonomous activity whose triggers only read caller-written state answers no) with narrated \`waitsOn\` lines when not yes, its invocable \`actions\` (each with an allowed/disabled verdict — these are what fire_action accepts), and its \`automations\` (cascade-fired actions the ENGINE fires on its own when their \`firesWhen\` trigger holds — never invocable via fire_action). Activities and actions that exist in the definition but are scoped out for this visit or actor are simply absent. Use this whenever you need to understand what's possible on an instance before deciding to act. This is a pure read — it does not change anything. ${UNTRUSTED_AUTHORED_DATA_NOTE} If you only need to discover what instances exist, use list_workflow_instances instead; this tool requires you to know the instance id. list_workflow_instances also returns the same \`workflowTitle\` and \`subject\` fields, so prefer it for fan-out discovery rather than polling get_workflow_state per instance.`,
696
696
  inputSchema: {
697
697
  ...workflowAddressFields,
698
698
  instance_id: instanceIdField
@@ -735,7 +735,7 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
735
735
  }
736
736
  }), LIST_CAP = 25, listWorkflowInstancesTool = defineWorkflowTool({
737
737
  name: "list_workflow_instances",
738
- description: `List workflow instances in one workflow environment. Use this when you need to find a workflow but don't already know its instance id, or to survey what's in flight. Returns a compact summary — id, \`definition\` (the workflow definition's \`name\`) and human-readable \`workflowTitle\`, current stage, whether the instance is done, and (when the workflow has a conventional subject document) a \`subject\` field with the subject doc's ref and title. Use \`workflowTitle\` when the user names the workflow by type (e.g. "article reviews") and \`subject.title\` when they name a specific in-flight instance by what it's about (e.g. "the article-review about pricing"). Capped at ${LIST_CAP} results — the most recently changed matches; every filter applies before the cap. Do NOT use this to inspect a single known instance — use get_workflow_state for that, the response will be richer. ` + UNTRUSTED_AUTHORED_DATA_NOTE,
738
+ description: `List workflow instances in one workflow environment. Use this when you need to find a workflow but don't already know its instance id, or to survey what's in flight. Returns a compact summary — id, \`definition\` (the workflow definition's \`name\`) and human-readable \`workflowTitle\`, current stage, whether the instance is done, and (when the workflow declares a subject document) a \`subject\` field with the subject doc's ref and title. Use \`workflowTitle\` when the user names the workflow by type (e.g. "article reviews") and \`subject.title\` when they name a specific in-flight instance by what it's about (e.g. "the article-review about pricing"). Capped at ${LIST_CAP} results — the most recently changed matches; every filter applies before the cap. Do NOT use this to inspect a single known instance — use get_workflow_state for that, the response will be richer. ` + UNTRUSTED_AUTHORED_DATA_NOTE,
739
739
  inputSchema: {
740
740
  ...workflowAddressFields,
741
741
  definition: z.string().describe("Optional. Restrict to instances of this workflow definition, by its `name` (e.g. 'article-review').").optional(),
package/dist/index.cjs CHANGED
@@ -103,14 +103,14 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
103
103
  if (problems.length > 0) throw new Error(problems.join(`\n`));
104
104
  const {engine: engine} = await context();
105
105
  return engine.deployDefinitions({
106
+ expectedMinReaderModel: 2,
106
107
  definitions: stored
107
108
  });
108
109
  }
109
110
  }), SUBJECT_ENTRY_NAME = "subject";
110
111
 
111
112
  function getSubjectGdr(instance) {
112
- const entry = instance.fields.find(s => s._type === "doc.ref" && s.name === SUBJECT_ENTRY_NAME);
113
- if (!(entry === void 0 || entry._type !== "doc.ref")) return entry.value?.id;
113
+ return (instance.fields.find(s => s._type === "subject") ?? instance.fields.find(s => s._type === "doc.ref" && s.name === SUBJECT_ENTRY_NAME))?.value?.id;
114
114
  }
115
115
 
116
116
  async function fetchSubjectTitles(engine, instances) {
@@ -524,7 +524,7 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
524
524
  description: "One review stage with a single approve action, then a terminal stage.",
525
525
  initialStage: "review",
526
526
  fields: [ {
527
- type: "doc.ref",
527
+ type: "subject",
528
528
  name: "subject",
529
529
  title: "Document",
530
530
  initialValue: {
@@ -559,7 +559,7 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
559
559
  description: "Draft, then editorial review that approves or rejects back to drafting.",
560
560
  initialStage: "drafting",
561
561
  fields: [ {
562
- type: "doc.ref",
562
+ type: "subject",
563
563
  name: "subject",
564
564
  title: "Document",
565
565
  initialValue: {
@@ -640,7 +640,7 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
640
640
  title: "Approved",
641
641
  description: "Terminal — no transitions out."
642
642
  } ]
643
- }, 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 `validate_workflow_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 `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.\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? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `boolean`, `date` (YYYY-MM-DD), `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`), identities (`actor` — one concrete\n principal; `assignee` / `assignees` — one or many user-or-role 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 By convention a workflow has an `input`-sourced `doc.ref` named `subject` — the\n headline document it is about.\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'`), and `$can`. `$actor`/`$assigned`/`$can`\nbelong in **caller-fired action filters only**. 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`/`$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.) 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({
643
+ }, 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 `validate_workflow_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 `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 `boolean`, `date` (YYYY-MM-DD), `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 and number 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 Choices must satisfy any bounds. The same syntax works on nested field shapes,\n 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'`), and `$can`. `$actor`/`$assigned`/`$can`\nbelong in **caller-fired action filters only**. 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`/`$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.) 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({
644
644
  name: "get_workflow_authoring_guide",
645
645
  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 validate_workflow_definition. Pure read; takes no arguments.",
646
646
  inputSchema: {},
@@ -675,7 +675,7 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
675
675
  }
676
676
  }), getWorkflowStateTool = defineWorkflowTool({
677
677
  name: "get_workflow_state",
678
- description: `Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable \`workflowTitle\`, the current stage, the subject document the workflow is about (when the workflow has a conventional subject — its ref and title), every in-scope activity on the current stage, and the most recent history entries, plus a one-line \`autonomy\` narrative saying whether the workflow runs itself and where it waits on someone. Per activity: its \`classification\` (who fires its actions: interactive, autonomous, off-system, or hybrid), the causal \`completesWithoutCaller\` verdict (yes/no/conditional — a mechanically autonomous activity whose triggers only read caller-written state answers no) with narrated \`waitsOn\` lines when not yes, its invocable \`actions\` (each with an allowed/disabled verdict — these are what fire_action accepts), and its \`automations\` (cascade-fired actions the ENGINE fires on its own when their \`firesWhen\` trigger holds — never invocable via fire_action). Activities and actions that exist in the definition but are scoped out for this visit or actor are simply absent. Use this whenever you need to understand what's possible on an instance before deciding to act. This is a pure read — it does not change anything. ${UNTRUSTED_AUTHORED_DATA_NOTE} If you only need to discover what instances exist, use list_workflow_instances instead; this tool requires you to know the instance id. list_workflow_instances also returns the same \`workflowTitle\` and \`subject\` fields, so prefer it for fan-out discovery rather than polling get_workflow_state per instance.`,
678
+ description: `Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable \`workflowTitle\`, the current stage, the subject document the workflow is about (when the workflow has a declared subject — its ref and title), every in-scope activity on the current stage, and the most recent history entries, plus a one-line \`autonomy\` narrative saying whether the workflow runs itself and where it waits on someone. Per activity: its \`classification\` (who fires its actions: interactive, autonomous, off-system, or hybrid), the causal \`completesWithoutCaller\` verdict (yes/no/conditional — a mechanically autonomous activity whose triggers only read caller-written state answers no) with narrated \`waitsOn\` lines when not yes, its invocable \`actions\` (each with an allowed/disabled verdict — these are what fire_action accepts), and its \`automations\` (cascade-fired actions the ENGINE fires on its own when their \`firesWhen\` trigger holds — never invocable via fire_action). Activities and actions that exist in the definition but are scoped out for this visit or actor are simply absent. Use this whenever you need to understand what's possible on an instance before deciding to act. This is a pure read — it does not change anything. ${UNTRUSTED_AUTHORED_DATA_NOTE} If you only need to discover what instances exist, use list_workflow_instances instead; this tool requires you to know the instance id. list_workflow_instances also returns the same \`workflowTitle\` and \`subject\` fields, so prefer it for fan-out discovery rather than polling get_workflow_state per instance.`,
679
679
  inputSchema: {
680
680
  ...workflowAddressFields,
681
681
  instance_id: instanceIdField
@@ -718,7 +718,7 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
718
718
  }
719
719
  }), LIST_CAP = 25, listWorkflowInstancesTool = defineWorkflowTool({
720
720
  name: "list_workflow_instances",
721
- description: `List workflow instances in one workflow environment. Use this when you need to find a workflow but don't already know its instance id, or to survey what's in flight. Returns a compact summary — id, \`definition\` (the workflow definition's \`name\`) and human-readable \`workflowTitle\`, current stage, whether the instance is done, and (when the workflow has a conventional subject document) a \`subject\` field with the subject doc's ref and title. Use \`workflowTitle\` when the user names the workflow by type (e.g. "article reviews") and \`subject.title\` when they name a specific in-flight instance by what it's about (e.g. "the article-review about pricing"). Capped at ${LIST_CAP} results — the most recently changed matches; every filter applies before the cap. Do NOT use this to inspect a single known instance — use get_workflow_state for that, the response will be richer. ` + UNTRUSTED_AUTHORED_DATA_NOTE,
721
+ description: `List workflow instances in one workflow environment. Use this when you need to find a workflow but don't already know its instance id, or to survey what's in flight. Returns a compact summary — id, \`definition\` (the workflow definition's \`name\`) and human-readable \`workflowTitle\`, current stage, whether the instance is done, and (when the workflow declares a subject document) a \`subject\` field with the subject doc's ref and title. Use \`workflowTitle\` when the user names the workflow by type (e.g. "article reviews") and \`subject.title\` when they name a specific in-flight instance by what it's about (e.g. "the article-review about pricing"). Capped at ${LIST_CAP} results — the most recently changed matches; every filter applies before the cap. Do NOT use this to inspect a single known instance — use get_workflow_state for that, the response will be richer. ` + UNTRUSTED_AUTHORED_DATA_NOTE,
722
722
  inputSchema: {
723
723
  ...workflowAddressFields,
724
724
  definition: zod.z.string().describe("Optional. Restrict to instances of this workflow definition, by its `name` (e.g. 'article-review').").optional(),
package/dist/index.d.cts CHANGED
@@ -214,16 +214,12 @@ export declare interface ProjectedInstanceSummary {
214
214
  * The subject document a workflow is "about" — the article under review,
215
215
  * the release being shipped, the KYC application being processed.
216
216
  *
217
- * Sourced by convention from the workflow's `doc.ref` field entry named
218
- * `subject`. Optional because:
219
- * - Some workflows don't have a conventional subject entry.
217
+ * Sourced from the workflow's `subject`-kind field entry (deterministic,
218
+ * whatever the entry is named); a `doc.ref` entry named `subject` is the
219
+ * fallback for definitions that predate the kind. Optional because:
220
+ * - Some workflows declare no subject entry.
220
221
  * - Some subject docs don't carry a `title` field. In that case `ref`
221
222
  * is still populated so the LLM can fall back to it as an identifier.
222
- *
223
- * Known gap: "subject" is a naming convention, not an engine-enforced
224
- * contract. Workflows with multiple `doc.ref` entries have to pick one to
225
- * call `subject` to surface here. The principled fix is engine-side — a
226
- * `headline: true` flag on the entry declaration.
227
223
  */
228
224
  export declare interface ProjectedSubject {
229
225
  /** GDR URI of the subject doc — what `*[_id == extractDocumentId(ref)]` resolves. */
package/dist/index.d.ts CHANGED
@@ -214,16 +214,12 @@ export declare interface ProjectedInstanceSummary {
214
214
  * The subject document a workflow is "about" — the article under review,
215
215
  * the release being shipped, the KYC application being processed.
216
216
  *
217
- * Sourced by convention from the workflow's `doc.ref` field entry named
218
- * `subject`. Optional because:
219
- * - Some workflows don't have a conventional subject entry.
217
+ * Sourced from the workflow's `subject`-kind field entry (deterministic,
218
+ * whatever the entry is named); a `doc.ref` entry named `subject` is the
219
+ * fallback for definitions that predate the kind. Optional because:
220
+ * - Some workflows declare no subject entry.
220
221
  * - Some subject docs don't carry a `title` field. In that case `ref`
221
222
  * is still populated so the LLM can fall back to it as an identifier.
222
- *
223
- * Known gap: "subject" is a naming convention, not an engine-enforced
224
- * contract. Workflows with multiple `doc.ref` entries have to pick one to
225
- * call `subject` to surface here. The principled fix is engine-side — a
226
- * `headline: true` flag on the entry declaration.
227
223
  */
228
224
  export declare interface ProjectedSubject {
229
225
  /** GDR URI of the subject doc — what `*[_id == extractDocumentId(ref)]` resolves. */
package/dist/stdio.js CHANGED
@@ -8,7 +8,7 @@ import { registerWorkflowTools, workflowAddressFromInput, createMcpTelemetry } f
8
8
 
9
9
  import { createClient } from "@sanity/client";
10
10
 
11
- var version = "0.8.0", packageJson = {
11
+ var version = "0.9.0", packageJson = {
12
12
  version: version
13
13
  };
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",
@@ -56,14 +56,14 @@
56
56
  "@sanity/client": "^7.22.1",
57
57
  "@sanity/telemetry": "^1.1.0",
58
58
  "zod": "^4.4.3",
59
- "@sanity/workflow-engine": "0.17.0"
59
+ "@sanity/workflow-engine": "0.18.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@sanity/pkg-utils": "^10.5.2",
63
63
  "@types/node": "^24.12.4",
64
64
  "vitest": "^4.1.8",
65
- "@sanity/workflow-engine-test": "0.12.0",
66
- "@sanity/workflow-examples": "0.7.0"
65
+ "@sanity/workflow-engine-test": "0.13.0",
66
+ "@sanity/workflow-examples": "0.8.0"
67
67
  },
68
68
  "engines": {
69
69
  "node": ">=20"