@sanity/workflow-mcp 0.30.0 → 0.32.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 +51 -0
- package/README.md +2 -2
- package/dist/_chunks-es/index.js +120 -41
- package/dist/index.cjs +119 -40
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/stdio.d.ts +1 -2
- package/dist/stdio.js +1 -1
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @sanity/workflow-mcp
|
|
2
2
|
|
|
3
|
+
## 0.32.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a8ed312: **BREAKING:** Assignment is now one ordered user-or-role member-list model: a direct user holder shadows every role in the same activity, role-only values route work to a pool, and singular `assignee` fields allow at most one user while retaining any number of roles. The `claim` field and action sugars have been removed from the authoring DSL; authors using them must replace each pair with an `assignee` field, a literal role seed where the work starts in a pool, ordinary field edits for take/release, a guarded `editable` predicate when second-taker exclusion is required, and `$assigned` on holder-only actions. Definitions that still submit `type: 'claim'` now fail validation.
|
|
8
|
+
|
|
9
|
+
Upgrade every engine, Studio, CLI, MCP, and adapter runtime sharing a workflow resource before deploying a definition containing a singular `assignee`, then acknowledge reader model 9 on deployment. Existing model-8-and-earlier instances remain readable and keep their object/null singular representation; no stored-document backfill is required. Deploy now rejects project-role references absent from the target project's live role catalog and warns when a referenced role has no current human holder, so the deploying identity must be able to read the project role and member directories.
|
|
10
|
+
|
|
11
|
+
Studio assignment matching and holder explanations now follow direct-user shadowing. The CLI adds viewer-scoped assignment list flags and counts, MCP instance listing adds corresponding assignment inputs and counts, and the public waiting `Diagnosis` adds a required `waitingFor` discriminant that distinguishes caller-actionable, manual-but-unavailable, and automation waits without implying that unassigned work is freely actionable.
|
|
12
|
+
|
|
13
|
+
`@sanity/workflow-components` now exports `roleMemberCount`, which reports the distinct people who can fulfill any supplied project role using the workflow definition's aliases. Pass the current member directory, required role names, and normalized `roleAliases`; literal members and alias-only fulfillers are deduplicated by account-global user id. No upgrade action is required unless a custom assignment surface wants alias-aware pool sizes.
|
|
14
|
+
|
|
15
|
+
**Docs impact:** Update the assignment model and authoring references, the reader-model rollout guide, guarded role-pool and take/release examples, the `@sanity/workflow-components` member-selection reference for `roleMemberCount`, Studio task-holder explanations, CLI instance-list flags, MCP list tool reference, deploy role-validation requirements, and migration guidance from removed claim sugar.
|
|
16
|
+
|
|
17
|
+
- 26dd4e5: Generated API references now include every type used by a public signature and link cross-package symbols to their authoritative package entry. Caller-facing helper contracts such as field mutation operations, engine operation context types, test-bench argument types, member avatar data, MCP client policy, and Studio user clients are now available as named exports where their public APIs already expose those shapes. GROQ condition consumers can import the documented `COMPARISON_OPS` list alongside its `ComparisonOp` type, while the shared cross-package exhaustiveness helper is an unsupported `@internal` export and is omitted from the reference.
|
|
18
|
+
|
|
19
|
+
**No upgrade action required.** Existing imports and runtime behavior remain compatible; consumers may adopt the new named type exports instead of reconstructing those shapes locally.
|
|
20
|
+
|
|
21
|
+
**Docs impact:** Refresh the generated API references for the affected packages; no conceptual guides, examples, or migration guidance need changes.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 2ba0c09: Generated API references keep navigation between public authoring, resource-alias, and observer helpers while no longer presenting private implementation helpers or private-package README targets as broken links.
|
|
26
|
+
|
|
27
|
+
**No upgrade action required.** Runtime behavior and public TypeScript contracts are unchanged.
|
|
28
|
+
|
|
29
|
+
**Docs impact:** Regenerate the API reference entries for these packages so their corrected TSDoc and README content is visible.
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [a8ed312]
|
|
32
|
+
- Updated dependencies [2ba0c09]
|
|
33
|
+
- Updated dependencies [2de38fd]
|
|
34
|
+
- Updated dependencies [a2ce4a7]
|
|
35
|
+
- Updated dependencies [2ddd3d7]
|
|
36
|
+
- Updated dependencies [6035672]
|
|
37
|
+
- Updated dependencies [26dd4e5]
|
|
38
|
+
- Updated dependencies [b04580d]
|
|
39
|
+
- @sanity/workflow-engine@0.32.0
|
|
40
|
+
|
|
41
|
+
## 0.31.0
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- 36c0aca: The MCP authoring guide and `workflows_deploy_definition` schema now report the engine-required reader-model floor for role-constrained assignment fields instead of stale deployment guidance.
|
|
46
|
+
|
|
47
|
+
Upgrade the MCP server to receive the corrected guidance. Before deploying a definition with assignment `roles`, upgrade every runtime sharing the workflow resource and then acknowledge the model reported by the guide; an older acknowledgement is rejected.
|
|
48
|
+
|
|
49
|
+
**Docs impact:** Update the MCP tool reference and authoring examples to source reader-model requirements from the engine; the readers-first rollout remains canonical in the Workflows prerelease guide.
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [ff72ece]
|
|
52
|
+
- @sanity/workflow-engine@0.31.0
|
|
53
|
+
|
|
3
54
|
## 0.30.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
MCP tool surface for the workflows engine. Two jobs, no fluff: operate running
|
|
4
4
|
instances, and author new definitions.
|
|
5
5
|
|
|
6
|
-
The tool surface is developed against the
|
|
7
|
-
|
|
6
|
+
The tool surface is developed against the private `@sanity/workflow-mcp-evals`
|
|
7
|
+
eval harness — this package exposes
|
|
8
8
|
the tools the evals drive, plus a stdio MCP entry point for real MCP clients.
|
|
9
9
|
|
|
10
10
|
## What the tools do
|
package/dist/_chunks-es/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errorMessage, validateTag, parseResourceGdr, WorkflowError, createTelemetryIntake, isTelemetryEnvDenied, processShellUserProperties, parseDefinitionInput, extractDocumentId, parseDefinitionSnapshot, displayTitle, autonomySummary, actionRendering, isTerminalStage, describeCondition, actionVerdict, narrateAutonomyWaits, subjectDenialLabels, deniedGuardLabels, definitionLookupGroq, assertReadableModel, unsatisfiedTransitionSummaries, describeSite, latestDefinitionsGroq, startKindOf, isStartableDefinition,
|
|
1
|
+
import { errorMessage, validateTag, parseResourceGdr, WorkflowError, createTelemetryIntake, isTelemetryEnvDenied, processShellUserProperties, parseDefinitionInput, DATA_MODEL_MIN_READER, requiredDefinitionReaderModel, extractDocumentId, parseDefinitionSnapshot, displayTitle, autonomySummary, actionRendering, isTerminalStage, describeCondition, actionVerdict, narrateAutonomyWaits, subjectDenialLabels, deniedGuardLabels, definitionLookupGroq, assertReadableModel, unsatisfiedTransitionSummaries, describeSite, latestDefinitionsGroq, startKindOf, isStartableDefinition, parseGdr, instanceWatchesDocument, instanceAssignmentStateCounts, resourceGdr, instancesQuery, startRefusal, buildInitialFields, StartNotPrimedError, StartNotSettledError, validateDefinition, createEngine, clientConfigFromResource, ENGINE_API_VERSION } from "@sanity/workflow-engine";
|
|
2
2
|
|
|
3
3
|
import { z } from "zod/v3";
|
|
4
4
|
|
|
@@ -107,6 +107,23 @@ function parseWorkflowDefinition(definition) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
const roleConstrainedAssignmentDefinition = {
|
|
111
|
+
fields: [ {
|
|
112
|
+
type: "assignees",
|
|
113
|
+
name: "reviewers",
|
|
114
|
+
roles: [ "editor" ]
|
|
115
|
+
} ]
|
|
116
|
+
}, singularAssignmentDefinition = {
|
|
117
|
+
fields: [ {
|
|
118
|
+
type: "assignee",
|
|
119
|
+
name: "reviewer"
|
|
120
|
+
} ]
|
|
121
|
+
}, READER_MODEL_GUIDANCE = Object.freeze({
|
|
122
|
+
baseline: DATA_MODEL_MIN_READER,
|
|
123
|
+
roleConstrainedAssignments: requiredDefinitionReaderModel([ roleConstrainedAssignmentDefinition ]),
|
|
124
|
+
singularAssignments: requiredDefinitionReaderModel([ singularAssignmentDefinition ])
|
|
125
|
+
});
|
|
126
|
+
|
|
110
127
|
function defineWorkflowTool(def) {
|
|
111
128
|
const {run: run, ...rest} = def;
|
|
112
129
|
return {
|
|
@@ -142,7 +159,7 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
|
|
|
142
159
|
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
160
|
inputSchema: {
|
|
144
161
|
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(
|
|
162
|
+
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-${READER_MODEL_GUIDANCE.baseline} baseline. Definitions with role-constrained assignment fields require model ${READER_MODEL_GUIDANCE.roleConstrainedAssignments}; singular assignment fields require model ${READER_MODEL_GUIDANCE.singularAssignments}. Pass the highest applicable floor only after upgrading all shared Studio, CLI, MCP, Function, and other runtimes.`)
|
|
146
163
|
},
|
|
147
164
|
requiresAddress: !0,
|
|
148
165
|
annotations: {
|
|
@@ -162,7 +179,7 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
|
|
|
162
179
|
if (problems.length > 0) throw new Error(problems.join(`\n`));
|
|
163
180
|
const {engine: engine} = await context();
|
|
164
181
|
return engine.deployDefinitions({
|
|
165
|
-
expectedMinReaderModel: input.expectedMinReaderModel ??
|
|
182
|
+
expectedMinReaderModel: input.expectedMinReaderModel ?? READER_MODEL_GUIDANCE.baseline,
|
|
166
183
|
definitions: stored
|
|
167
184
|
});
|
|
168
185
|
}
|
|
@@ -472,7 +489,7 @@ function diagnosisSummary(diagnosis) {
|
|
|
472
489
|
return "This instance will advance on its own.";
|
|
473
490
|
|
|
474
491
|
case "waiting":
|
|
475
|
-
return diagnosis.
|
|
492
|
+
return diagnosis.waitingFor === "caller" ? `Waiting for action on activity "${diagnosis.activity}": ${diagnosis.actions.join(" or ")}. This is the normal in-flight state — it advances when someone acts.` : diagnosis.waitingFor === "automation" ? `Waiting on automation on activity "${diagnosis.activity}" — the engine fires its trigger(s) on its own when their conditions hold; there is nothing for a caller to fire.` : `Activity "${diagnosis.activity}" is waiting for a manual action, but none is available to this caller right now.`;
|
|
476
493
|
|
|
477
494
|
case "blocked":
|
|
478
495
|
return `Activity "${diagnosis.activity}" is visible but not yet executable — unmet requirement(s): ${diagnosis.requirements.join(", ")}. It will not advance until those are satisfied.`;
|
|
@@ -699,7 +716,7 @@ const diagnoseWorkflowTool = defineWorkflowTool({
|
|
|
699
716
|
title: "Approved",
|
|
700
717
|
description: "Terminal — no transitions out."
|
|
701
718
|
} ]
|
|
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({
|
|
719
|
+
}, 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\` — ordered lists of user-or-role\n members, with \`assignee\` allowing at most one user but any number of role\n members), 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 This is an eligibility facet: it limits newly written users to project members\n who hold or fulfill one of those roles, and limits collective role values to\n literal listed roles. It never adds a role member to the field's value. The\n facet works on \`assignee\` / \`assignees\` at every field-shape level,\n including nested shapes and effect outputs. It requires reader model\n ${READER_MODEL_GUIDANCE.roleConstrainedAssignments}. Separately, every singular\n \`assignee\` kind — role-constrained or not, including the \`todoList\` sugar —\n uses member-list storage and requires reader model\n ${READER_MODEL_GUIDANCE.singularAssignments}. Before deploying either feature,\n upgrade every runtime sharing the workflow resource and pass the highest required\n \`expectedMinReaderModel:${READER_MODEL_GUIDANCE.roleConstrainedAssignments}\` to\n \`workflows_deploy_definition\` (use ${READER_MODEL_GUIDANCE.singularAssignments}\n when singular assignment appears). Definitions using neither feature retain the\n model-${READER_MODEL_GUIDANCE.baseline} baseline.\n Route work to a pool by placing a literal role member in the value. Taking\n work is an ordinary field edit that adds the caller's literal user id;\n releasing it removes that user and leaves the role pool in place. Any user\n member shadows every role member, so held work appears only in that user's\n inbox. A one-holder guarded pool uses an \`assignee\` field with a role seed\n and an activity-scoped \`editable:\` predicate such as\n \`"count($fields.seat[@.type == 'user']) == 0 || $assigned"\`. An open\n editable field intentionally permits reassignment. Do not invent a claim or\n take action type: assignment changes use the generic field edit contract.\n \`roleAliases\` may widen advisory action/edit authorization, but never\n changes which literal role pool routes into a viewer's inbox.\n The three derived states are **unrouted** (the list is empty), **routed**\n (roles but no user), and **held** (a user is present and shadows the roles).\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 matches the activity's assignment after direct-user shadowing — the\nidiomatic declared holder gate, used as an action \`filter: '$assigned'\`),\n\`$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({
|
|
703
720
|
name: "workflows_get_authoring_guide",
|
|
704
721
|
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.",
|
|
705
722
|
inputSchema: {},
|
|
@@ -783,7 +800,7 @@ const diagnoseWorkflowTool = defineWorkflowTool({
|
|
|
783
800
|
});
|
|
784
801
|
|
|
785
802
|
function cursorScope(engine, input) {
|
|
786
|
-
return JSON.stringify([ resourceGdr(engine.workflowResource), engine.tag, input.definition ?? null, input.document ?? null, input.include_completed ]);
|
|
803
|
+
return JSON.stringify([ resourceGdr(engine.workflowResource), engine.tag, input.definition ?? null, input.document ?? null, input.include_completed, input.assignment_user_id ?? null, [ ...input.assignment_roles ?? [] ].sort(), [ ...input.assignment_states ?? [] ].sort() ]);
|
|
787
804
|
}
|
|
788
805
|
|
|
789
806
|
function encodeCursor(payload) {
|
|
@@ -803,13 +820,107 @@ function decodeCursor(cursor, expectedScope) {
|
|
|
803
820
|
return parsed.data;
|
|
804
821
|
}
|
|
805
822
|
|
|
806
|
-
const SUMMARY_PROJECTION = `{\n _type,\n _id,\n modelVersion,\n minReaderModel,\n workflowResource,\n definition,\n definitionSnapshot,\n fields,\n ancestors,\n subworkflows,\n stages,\n currentStage,\n lastChangedAt,\n completedAt,\n abortedAt\n}
|
|
823
|
+
const SUMMARY_PROJECTION = `{\n _type,\n _id,\n modelVersion,\n minReaderModel,\n workflowResource,\n definition,\n definitionSnapshot,\n fields,\n ancestors,\n subworkflows,\n stages,\n currentStage,\n lastChangedAt,\n completedAt,\n abortedAt\n}`;
|
|
824
|
+
|
|
825
|
+
function assignmentIdentity(input) {
|
|
826
|
+
if (input.assignment_user_id !== void 0) return {
|
|
827
|
+
userId: input.assignment_user_id,
|
|
828
|
+
roles: input.assignment_roles ?? []
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function assignmentQueryFilter(input) {
|
|
833
|
+
if (input.assignment_user_id === void 0) {
|
|
834
|
+
if (input.assignment_roles !== void 0 || input.assignment_states !== void 0) throw new Error("workflows_list_instances: assignment_roles and assignment_states require assignment_user_id");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
userId: input.assignment_user_id,
|
|
839
|
+
roles: input.assignment_roles,
|
|
840
|
+
states: input.assignment_states
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function listQueryFilter(input, document) {
|
|
845
|
+
const assignment = assignmentQueryFilter(input);
|
|
846
|
+
return {
|
|
847
|
+
includeCompleted: input.include_completed,
|
|
848
|
+
...input.definition === void 0 ? {} : {
|
|
849
|
+
definition: input.definition
|
|
850
|
+
},
|
|
851
|
+
...document === void 0 ? {} : {
|
|
852
|
+
document: document
|
|
853
|
+
},
|
|
854
|
+
...assignment === void 0 ? {} : {
|
|
855
|
+
assignment: assignment
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function cursorQueryParts(cursor) {
|
|
861
|
+
return cursor === void 0 ? {
|
|
862
|
+
filter: "",
|
|
863
|
+
params: {}
|
|
864
|
+
} : {
|
|
865
|
+
filter: "[lastChangedAt < $cursorLastChangedAt || (lastChangedAt == $cursorLastChangedAt && _id > $cursorInstanceId)]",
|
|
866
|
+
params: {
|
|
867
|
+
cursorLastChangedAt: cursor.lastChangedAt,
|
|
868
|
+
cursorInstanceId: cursor.instanceId
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async function readInstancePage(args) {
|
|
874
|
+
const {query: query, params: params} = instancesQuery({
|
|
875
|
+
tag: args.engine.tag,
|
|
876
|
+
filter: listQueryFilter(args.input, args.document)
|
|
877
|
+
}), cursorParts = cursorQueryParts(args.cursor), recency = `${query}${cursorParts.filter} | order(lastChangedAt desc, _id asc)`;
|
|
878
|
+
return (await args.engine.query({
|
|
879
|
+
groq: `${recency}[0...$pageSize]${SUMMARY_PROJECTION}`,
|
|
880
|
+
params: {
|
|
881
|
+
...params,
|
|
882
|
+
...cursorParts.params,
|
|
883
|
+
pageSize: args.input.limit + 1
|
|
884
|
+
}
|
|
885
|
+
})).map(assertReadableModel);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async function listInstances(engine, input) {
|
|
889
|
+
const document = input.document, identity = assignmentIdentity(input), scope = cursorScope(engine, input), cursor = input.cursor === void 0 ? void 0 : decodeCursor(input.cursor, scope), docs = await readInstancePage({
|
|
890
|
+
engine: engine,
|
|
891
|
+
input: input,
|
|
892
|
+
document: document,
|
|
893
|
+
cursor: cursor
|
|
894
|
+
}), rawPage = docs.slice(0, input.limit), page = document === void 0 ? rawPage : rawPage.filter(instance => instanceWatchesDocument(instance, document)), subjectTitles = await fetchSubjectTitles(engine, page), boundary = rawPage.at(-1), hasMore = docs.length > input.limit;
|
|
895
|
+
return {
|
|
896
|
+
instances: page.map(instance => ({
|
|
897
|
+
...projectSummary(instance, subjectTitles),
|
|
898
|
+
...identity === void 0 ? {} : {
|
|
899
|
+
assignment: instanceAssignmentStateCounts(instance, identity)
|
|
900
|
+
}
|
|
901
|
+
})),
|
|
902
|
+
has_more: hasMore,
|
|
903
|
+
...hasMore && boundary !== void 0 ? {
|
|
904
|
+
next_cursor: encodeCursor({
|
|
905
|
+
version: 1,
|
|
906
|
+
lastChangedAt: boundary.lastChangedAt,
|
|
907
|
+
instanceId: boundary._id,
|
|
908
|
+
scope: scope
|
|
909
|
+
})
|
|
910
|
+
} : {}
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const listWorkflowInstancesTool = defineWorkflowTool({
|
|
807
915
|
name: "workflows_list_instances",
|
|
808
916
|
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"). Returns up to ${DEFAULT_LIST_LIMIT} results per page by default, ordered by most recently changed; set \`limit\` up to ${MAX_LIST_LIMIT}. Defensive document verification can leave a page underfilled. When \`has_more\` is true, call this tool again with the same filters and \`next_cursor\` as \`cursor\`; never claim the list is complete until \`has_more\` is false. Do NOT use this to inspect a single known instance — use workflows_get_state for that, the response will be richer. ` + UNTRUSTED_AUTHORED_DATA_NOTE,
|
|
809
917
|
inputSchema: {
|
|
810
918
|
definition: z.string().describe("Optional. Restrict to instances of this workflow definition, by its `name` (e.g. 'article-review').").optional(),
|
|
811
919
|
document: z.string().describe(`Optional. Only instances that reference this document — the workflow's subject or any other doc its fields point at — as a resource-qualified GDR URI (e.g. "dataset:proj:ds:article-1"). Use this to answer "which workflows are about this document?".`).superRefine(zodCheck(parseGdr)).optional(),
|
|
812
920
|
include_completed: z.boolean().describe("Optional. `true` includes completed/aborted instances. Defaults to false — in-flight instances only.").default(!1),
|
|
921
|
+
assignment_user_id: z.string().min(1).describe("Optional account-global user id for viewer-scoped assignment filtering and per-instance unrouted/routed/held counts.").optional(),
|
|
922
|
+
assignment_roles: z.array(z.string().min(1)).describe("Optional literal project roles held by assignment_user_id. Role aliases do not change inbox ownership.").optional(),
|
|
923
|
+
assignment_states: z.array(z.enum([ "unrouted", "routed", "held" ])).min(1).describe("Optional assignment states to include: unrouted work, routed work offered through assignment_roles, or work held directly by assignment_user_id.").optional(),
|
|
813
924
|
limit: z.number().int().min(1).max(MAX_LIST_LIMIT).describe(`Optional. Results per page, from 1 to ${MAX_LIST_LIMIT}. Defaults to ${DEFAULT_LIST_LIMIT}.`).default(DEFAULT_LIST_LIMIT),
|
|
814
925
|
cursor: z.string().describe("Optional. Opaque `next_cursor` from the previous response. Reuse the same filters and workflow environment when continuing.").optional()
|
|
815
926
|
},
|
|
@@ -818,40 +929,8 @@ const SUMMARY_PROJECTION = `{\n _type,\n _id,\n modelVersion,\n minReaderMod
|
|
|
818
929
|
readOnlyHint: !0
|
|
819
930
|
},
|
|
820
931
|
run: async (context, input) => {
|
|
821
|
-
const {engine: engine} = await context()
|
|
822
|
-
|
|
823
|
-
...input.definition !== void 0 ? {
|
|
824
|
-
definition: input.definition
|
|
825
|
-
} : {},
|
|
826
|
-
...document !== void 0 ? {
|
|
827
|
-
document: document
|
|
828
|
-
} : {}
|
|
829
|
-
}, {query: query, params: params} = instancesQuery({
|
|
830
|
-
tag: engine.tag,
|
|
831
|
-
filter: filter
|
|
832
|
-
}), scope = cursorScope(engine, input), cursor = input.cursor === void 0 ? void 0 : decodeCursor(input.cursor, scope), recency = `${query}${cursor === void 0 ? "" : "[lastChangedAt < $cursorLastChangedAt || (lastChangedAt == $cursorLastChangedAt && _id > $cursorInstanceId)]"} | order(lastChangedAt desc, _id asc)`, docs = (await engine.query({
|
|
833
|
-
groq: `${recency}[0...$pageSize]${SUMMARY_PROJECTION}`,
|
|
834
|
-
params: {
|
|
835
|
-
...params,
|
|
836
|
-
pageSize: input.limit + 1,
|
|
837
|
-
...cursor === void 0 ? {} : {
|
|
838
|
-
cursorLastChangedAt: cursor.lastChangedAt,
|
|
839
|
-
cursorInstanceId: cursor.instanceId
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
})).map(assertReadableModel), rawPage = docs.slice(0, input.limit), page = document === void 0 ? rawPage : rawPage.filter(doc => instanceWatchesDocument(doc, document)), hasMore = docs.length > input.limit, subjectTitles = await fetchSubjectTitles(engine, page), boundary = rawPage.at(-1);
|
|
843
|
-
return {
|
|
844
|
-
instances: page.map(doc => projectSummary(doc, subjectTitles)),
|
|
845
|
-
has_more: hasMore,
|
|
846
|
-
...hasMore && boundary !== void 0 ? {
|
|
847
|
-
next_cursor: encodeCursor({
|
|
848
|
-
version: 1,
|
|
849
|
-
lastChangedAt: boundary.lastChangedAt,
|
|
850
|
-
instanceId: boundary._id,
|
|
851
|
-
scope: scope
|
|
852
|
-
})
|
|
853
|
-
} : {}
|
|
854
|
-
};
|
|
932
|
+
const {engine: engine} = await context();
|
|
933
|
+
return listInstances(engine, input);
|
|
855
934
|
}
|
|
856
935
|
});
|
|
857
936
|
|
package/dist/index.cjs
CHANGED
|
@@ -83,6 +83,23 @@ function parseWorkflowDefinition(definition) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const roleConstrainedAssignmentDefinition = {
|
|
87
|
+
fields: [ {
|
|
88
|
+
type: "assignees",
|
|
89
|
+
name: "reviewers",
|
|
90
|
+
roles: [ "editor" ]
|
|
91
|
+
} ]
|
|
92
|
+
}, singularAssignmentDefinition = {
|
|
93
|
+
fields: [ {
|
|
94
|
+
type: "assignee",
|
|
95
|
+
name: "reviewer"
|
|
96
|
+
} ]
|
|
97
|
+
}, READER_MODEL_GUIDANCE = Object.freeze({
|
|
98
|
+
baseline: workflowEngine.DATA_MODEL_MIN_READER,
|
|
99
|
+
roleConstrainedAssignments: workflowEngine.requiredDefinitionReaderModel([ roleConstrainedAssignmentDefinition ]),
|
|
100
|
+
singularAssignments: workflowEngine.requiredDefinitionReaderModel([ singularAssignmentDefinition ])
|
|
101
|
+
});
|
|
102
|
+
|
|
86
103
|
function defineWorkflowTool(def) {
|
|
87
104
|
const {run: run, ...rest} = def;
|
|
88
105
|
return {
|
|
@@ -118,7 +135,7 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
|
|
|
118
135
|
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
136
|
inputSchema: {
|
|
120
137
|
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(
|
|
138
|
+
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-${READER_MODEL_GUIDANCE.baseline} baseline. Definitions with role-constrained assignment fields require model ${READER_MODEL_GUIDANCE.roleConstrainedAssignments}; singular assignment fields require model ${READER_MODEL_GUIDANCE.singularAssignments}. Pass the highest applicable floor only after upgrading all shared Studio, CLI, MCP, Function, and other runtimes.`)
|
|
122
139
|
},
|
|
123
140
|
requiresAddress: !0,
|
|
124
141
|
annotations: {
|
|
@@ -139,7 +156,7 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
|
|
|
139
156
|
if (problems.length > 0) throw new Error(problems.join(`\n`));
|
|
140
157
|
const {engine: engine} = await context();
|
|
141
158
|
return engine.deployDefinitions({
|
|
142
|
-
expectedMinReaderModel: input.expectedMinReaderModel ??
|
|
159
|
+
expectedMinReaderModel: input.expectedMinReaderModel ?? READER_MODEL_GUIDANCE.baseline,
|
|
143
160
|
definitions: stored
|
|
144
161
|
});
|
|
145
162
|
}
|
|
@@ -449,7 +466,7 @@ function diagnosisSummary(diagnosis) {
|
|
|
449
466
|
return "This instance will advance on its own.";
|
|
450
467
|
|
|
451
468
|
case "waiting":
|
|
452
|
-
return diagnosis.
|
|
469
|
+
return diagnosis.waitingFor === "caller" ? `Waiting for action on activity "${diagnosis.activity}": ${diagnosis.actions.join(" or ")}. This is the normal in-flight state — it advances when someone acts.` : diagnosis.waitingFor === "automation" ? `Waiting on automation on activity "${diagnosis.activity}" — the engine fires its trigger(s) on its own when their conditions hold; there is nothing for a caller to fire.` : `Activity "${diagnosis.activity}" is waiting for a manual action, but none is available to this caller right now.`;
|
|
453
470
|
|
|
454
471
|
case "blocked":
|
|
455
472
|
return `Activity "${diagnosis.activity}" is visible but not yet executable — unmet requirement(s): ${diagnosis.requirements.join(", ")}. It will not advance until those are satisfied.`;
|
|
@@ -676,7 +693,7 @@ const diagnoseWorkflowTool = defineWorkflowTool({
|
|
|
676
693
|
title: "Approved",
|
|
677
694
|
description: "Terminal — no transitions out."
|
|
678
695
|
} ]
|
|
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({
|
|
696
|
+
}, 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\` — ordered lists of user-or-role\n members, with \`assignee\` allowing at most one user but any number of role\n members), 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 This is an eligibility facet: it limits newly written users to project members\n who hold or fulfill one of those roles, and limits collective role values to\n literal listed roles. It never adds a role member to the field's value. The\n facet works on \`assignee\` / \`assignees\` at every field-shape level,\n including nested shapes and effect outputs. It requires reader model\n ${READER_MODEL_GUIDANCE.roleConstrainedAssignments}. Separately, every singular\n \`assignee\` kind — role-constrained or not, including the \`todoList\` sugar —\n uses member-list storage and requires reader model\n ${READER_MODEL_GUIDANCE.singularAssignments}. Before deploying either feature,\n upgrade every runtime sharing the workflow resource and pass the highest required\n \`expectedMinReaderModel:${READER_MODEL_GUIDANCE.roleConstrainedAssignments}\` to\n \`workflows_deploy_definition\` (use ${READER_MODEL_GUIDANCE.singularAssignments}\n when singular assignment appears). Definitions using neither feature retain the\n model-${READER_MODEL_GUIDANCE.baseline} baseline.\n Route work to a pool by placing a literal role member in the value. Taking\n work is an ordinary field edit that adds the caller's literal user id;\n releasing it removes that user and leaves the role pool in place. Any user\n member shadows every role member, so held work appears only in that user's\n inbox. A one-holder guarded pool uses an \`assignee\` field with a role seed\n and an activity-scoped \`editable:\` predicate such as\n \`"count($fields.seat[@.type == 'user']) == 0 || $assigned"\`. An open\n editable field intentionally permits reassignment. Do not invent a claim or\n take action type: assignment changes use the generic field edit contract.\n \`roleAliases\` may widen advisory action/edit authorization, but never\n changes which literal role pool routes into a viewer's inbox.\n The three derived states are **unrouted** (the list is empty), **routed**\n (roles but no user), and **held** (a user is present and shadows the roles).\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 matches the activity's assignment after direct-user shadowing — the\nidiomatic declared holder gate, used as an action \`filter: '$assigned'\`),\n\`$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({
|
|
680
697
|
name: "workflows_get_authoring_guide",
|
|
681
698
|
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.",
|
|
682
699
|
inputSchema: {},
|
|
@@ -760,7 +777,7 @@ const diagnoseWorkflowTool = defineWorkflowTool({
|
|
|
760
777
|
});
|
|
761
778
|
|
|
762
779
|
function cursorScope(engine, input) {
|
|
763
|
-
return JSON.stringify([ workflowEngine.resourceGdr(engine.workflowResource), engine.tag, input.definition ?? null, input.document ?? null, input.include_completed ]);
|
|
780
|
+
return JSON.stringify([ workflowEngine.resourceGdr(engine.workflowResource), engine.tag, input.definition ?? null, input.document ?? null, input.include_completed, input.assignment_user_id ?? null, [ ...input.assignment_roles ?? [] ].sort(), [ ...input.assignment_states ?? [] ].sort() ]);
|
|
764
781
|
}
|
|
765
782
|
|
|
766
783
|
function encodeCursor(payload) {
|
|
@@ -780,13 +797,107 @@ function decodeCursor(cursor, expectedScope) {
|
|
|
780
797
|
return parsed.data;
|
|
781
798
|
}
|
|
782
799
|
|
|
783
|
-
const SUMMARY_PROJECTION = `{\n _type,\n _id,\n modelVersion,\n minReaderModel,\n workflowResource,\n definition,\n definitionSnapshot,\n fields,\n ancestors,\n subworkflows,\n stages,\n currentStage,\n lastChangedAt,\n completedAt,\n abortedAt\n}
|
|
800
|
+
const SUMMARY_PROJECTION = `{\n _type,\n _id,\n modelVersion,\n minReaderModel,\n workflowResource,\n definition,\n definitionSnapshot,\n fields,\n ancestors,\n subworkflows,\n stages,\n currentStage,\n lastChangedAt,\n completedAt,\n abortedAt\n}`;
|
|
801
|
+
|
|
802
|
+
function assignmentIdentity(input) {
|
|
803
|
+
if (input.assignment_user_id !== void 0) return {
|
|
804
|
+
userId: input.assignment_user_id,
|
|
805
|
+
roles: input.assignment_roles ?? []
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function assignmentQueryFilter(input) {
|
|
810
|
+
if (input.assignment_user_id === void 0) {
|
|
811
|
+
if (input.assignment_roles !== void 0 || input.assignment_states !== void 0) throw new Error("workflows_list_instances: assignment_roles and assignment_states require assignment_user_id");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
userId: input.assignment_user_id,
|
|
816
|
+
roles: input.assignment_roles,
|
|
817
|
+
states: input.assignment_states
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function listQueryFilter(input, document) {
|
|
822
|
+
const assignment = assignmentQueryFilter(input);
|
|
823
|
+
return {
|
|
824
|
+
includeCompleted: input.include_completed,
|
|
825
|
+
...input.definition === void 0 ? {} : {
|
|
826
|
+
definition: input.definition
|
|
827
|
+
},
|
|
828
|
+
...document === void 0 ? {} : {
|
|
829
|
+
document: document
|
|
830
|
+
},
|
|
831
|
+
...assignment === void 0 ? {} : {
|
|
832
|
+
assignment: assignment
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function cursorQueryParts(cursor) {
|
|
838
|
+
return cursor === void 0 ? {
|
|
839
|
+
filter: "",
|
|
840
|
+
params: {}
|
|
841
|
+
} : {
|
|
842
|
+
filter: "[lastChangedAt < $cursorLastChangedAt || (lastChangedAt == $cursorLastChangedAt && _id > $cursorInstanceId)]",
|
|
843
|
+
params: {
|
|
844
|
+
cursorLastChangedAt: cursor.lastChangedAt,
|
|
845
|
+
cursorInstanceId: cursor.instanceId
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function readInstancePage(args) {
|
|
851
|
+
const {query: query, params: params} = workflowEngine.instancesQuery({
|
|
852
|
+
tag: args.engine.tag,
|
|
853
|
+
filter: listQueryFilter(args.input, args.document)
|
|
854
|
+
}), cursorParts = cursorQueryParts(args.cursor), recency = `${query}${cursorParts.filter} | order(lastChangedAt desc, _id asc)`;
|
|
855
|
+
return (await args.engine.query({
|
|
856
|
+
groq: `${recency}[0...$pageSize]${SUMMARY_PROJECTION}`,
|
|
857
|
+
params: {
|
|
858
|
+
...params,
|
|
859
|
+
...cursorParts.params,
|
|
860
|
+
pageSize: args.input.limit + 1
|
|
861
|
+
}
|
|
862
|
+
})).map(workflowEngine.assertReadableModel);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function listInstances(engine, input) {
|
|
866
|
+
const document = input.document, identity = assignmentIdentity(input), scope = cursorScope(engine, input), cursor = input.cursor === void 0 ? void 0 : decodeCursor(input.cursor, scope), docs = await readInstancePage({
|
|
867
|
+
engine: engine,
|
|
868
|
+
input: input,
|
|
869
|
+
document: document,
|
|
870
|
+
cursor: cursor
|
|
871
|
+
}), rawPage = docs.slice(0, input.limit), page = document === void 0 ? rawPage : rawPage.filter(instance => workflowEngine.instanceWatchesDocument(instance, document)), subjectTitles = await fetchSubjectTitles(engine, page), boundary = rawPage.at(-1), hasMore = docs.length > input.limit;
|
|
872
|
+
return {
|
|
873
|
+
instances: page.map(instance => ({
|
|
874
|
+
...projectSummary(instance, subjectTitles),
|
|
875
|
+
...identity === void 0 ? {} : {
|
|
876
|
+
assignment: workflowEngine.instanceAssignmentStateCounts(instance, identity)
|
|
877
|
+
}
|
|
878
|
+
})),
|
|
879
|
+
has_more: hasMore,
|
|
880
|
+
...hasMore && boundary !== void 0 ? {
|
|
881
|
+
next_cursor: encodeCursor({
|
|
882
|
+
version: 1,
|
|
883
|
+
lastChangedAt: boundary.lastChangedAt,
|
|
884
|
+
instanceId: boundary._id,
|
|
885
|
+
scope: scope
|
|
886
|
+
})
|
|
887
|
+
} : {}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const listWorkflowInstancesTool = defineWorkflowTool({
|
|
784
892
|
name: "workflows_list_instances",
|
|
785
893
|
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"). Returns up to ${DEFAULT_LIST_LIMIT} results per page by default, ordered by most recently changed; set \`limit\` up to ${MAX_LIST_LIMIT}. Defensive document verification can leave a page underfilled. When \`has_more\` is true, call this tool again with the same filters and \`next_cursor\` as \`cursor\`; never claim the list is complete until \`has_more\` is false. Do NOT use this to inspect a single known instance — use workflows_get_state for that, the response will be richer. ` + UNTRUSTED_AUTHORED_DATA_NOTE,
|
|
786
894
|
inputSchema: {
|
|
787
895
|
definition: v3.z.string().describe("Optional. Restrict to instances of this workflow definition, by its `name` (e.g. 'article-review').").optional(),
|
|
788
896
|
document: v3.z.string().describe(`Optional. Only instances that reference this document — the workflow's subject or any other doc its fields point at — as a resource-qualified GDR URI (e.g. "dataset:proj:ds:article-1"). Use this to answer "which workflows are about this document?".`).superRefine(zodCheck(workflowEngine.parseGdr)).optional(),
|
|
789
897
|
include_completed: v3.z.boolean().describe("Optional. `true` includes completed/aborted instances. Defaults to false — in-flight instances only.").default(!1),
|
|
898
|
+
assignment_user_id: v3.z.string().min(1).describe("Optional account-global user id for viewer-scoped assignment filtering and per-instance unrouted/routed/held counts.").optional(),
|
|
899
|
+
assignment_roles: v3.z.array(v3.z.string().min(1)).describe("Optional literal project roles held by assignment_user_id. Role aliases do not change inbox ownership.").optional(),
|
|
900
|
+
assignment_states: v3.z.array(v3.z.enum([ "unrouted", "routed", "held" ])).min(1).describe("Optional assignment states to include: unrouted work, routed work offered through assignment_roles, or work held directly by assignment_user_id.").optional(),
|
|
790
901
|
limit: v3.z.number().int().min(1).max(MAX_LIST_LIMIT).describe(`Optional. Results per page, from 1 to ${MAX_LIST_LIMIT}. Defaults to ${DEFAULT_LIST_LIMIT}.`).default(DEFAULT_LIST_LIMIT),
|
|
791
902
|
cursor: v3.z.string().describe("Optional. Opaque `next_cursor` from the previous response. Reuse the same filters and workflow environment when continuing.").optional()
|
|
792
903
|
},
|
|
@@ -795,40 +906,8 @@ const SUMMARY_PROJECTION = `{\n _type,\n _id,\n modelVersion,\n minReaderMod
|
|
|
795
906
|
readOnlyHint: !0
|
|
796
907
|
},
|
|
797
908
|
run: async (context, input) => {
|
|
798
|
-
const {engine: engine} = await context()
|
|
799
|
-
|
|
800
|
-
...input.definition !== void 0 ? {
|
|
801
|
-
definition: input.definition
|
|
802
|
-
} : {},
|
|
803
|
-
...document !== void 0 ? {
|
|
804
|
-
document: document
|
|
805
|
-
} : {}
|
|
806
|
-
}, {query: query, params: params} = workflowEngine.instancesQuery({
|
|
807
|
-
tag: engine.tag,
|
|
808
|
-
filter: filter
|
|
809
|
-
}), scope = cursorScope(engine, input), cursor = input.cursor === void 0 ? void 0 : decodeCursor(input.cursor, scope), recency = `${query}${cursor === void 0 ? "" : "[lastChangedAt < $cursorLastChangedAt || (lastChangedAt == $cursorLastChangedAt && _id > $cursorInstanceId)]"} | order(lastChangedAt desc, _id asc)`, docs = (await engine.query({
|
|
810
|
-
groq: `${recency}[0...$pageSize]${SUMMARY_PROJECTION}`,
|
|
811
|
-
params: {
|
|
812
|
-
...params,
|
|
813
|
-
pageSize: input.limit + 1,
|
|
814
|
-
...cursor === void 0 ? {} : {
|
|
815
|
-
cursorLastChangedAt: cursor.lastChangedAt,
|
|
816
|
-
cursorInstanceId: cursor.instanceId
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
})).map(workflowEngine.assertReadableModel), rawPage = docs.slice(0, input.limit), page = document === void 0 ? rawPage : rawPage.filter(doc => workflowEngine.instanceWatchesDocument(doc, document)), hasMore = docs.length > input.limit, subjectTitles = await fetchSubjectTitles(engine, page), boundary = rawPage.at(-1);
|
|
820
|
-
return {
|
|
821
|
-
instances: page.map(doc => projectSummary(doc, subjectTitles)),
|
|
822
|
-
has_more: hasMore,
|
|
823
|
-
...hasMore && boundary !== void 0 ? {
|
|
824
|
-
next_cursor: encodeCursor({
|
|
825
|
-
version: 1,
|
|
826
|
-
lastChangedAt: boundary.lastChangedAt,
|
|
827
|
-
instanceId: boundary._id,
|
|
828
|
-
scope: scope
|
|
829
|
-
})
|
|
830
|
-
} : {}
|
|
831
|
-
};
|
|
909
|
+
const {engine: engine} = await context();
|
|
910
|
+
return listInstances(engine, input);
|
|
832
911
|
}
|
|
833
912
|
});
|
|
834
913
|
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionParam } from "@sanity/workflow-engine";
|
|
2
|
+
import type { AssignmentStateCounts } from "@sanity/workflow-engine";
|
|
2
3
|
import type { AutonomyVerdict } from "@sanity/workflow-engine";
|
|
3
4
|
import { DeclaredExecutionContext } from "@sanity/workflow-engine";
|
|
4
5
|
import type { Diagnosis } from "@sanity/workflow-engine";
|
|
@@ -279,6 +280,8 @@ export declare interface ProjectedInstanceSummary {
|
|
|
279
280
|
done: boolean;
|
|
280
281
|
/** What this workflow is about. See {@link ProjectedSubject}. */
|
|
281
282
|
subject?: ProjectedSubject;
|
|
283
|
+
/** Viewer-scoped assignment slots when the list call supplied an assignment identity. */
|
|
284
|
+
assignment?: AssignmentStateCounts;
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
/**
|
|
@@ -422,7 +425,7 @@ export declare function workflowClientConfig<Base extends object>(args: {
|
|
|
422
425
|
* The client settings engine traffic pins, whatever base a host layers them over.
|
|
423
426
|
* Literal types so the result stays assignable to a host's own client config.
|
|
424
427
|
*/
|
|
425
|
-
declare interface WorkflowClientPolicy {
|
|
428
|
+
export declare interface WorkflowClientPolicy {
|
|
426
429
|
apiVersion: string;
|
|
427
430
|
useCdn: false;
|
|
428
431
|
requestTagPrefix: string;
|
|
@@ -455,7 +458,7 @@ export declare function workflowErrorText(error: unknown): string;
|
|
|
455
458
|
* `@sanity/client` prefers `resource` over `projectId`/`dataset` whenever both
|
|
456
459
|
* are present, which would otherwise silently address the wrong environment.
|
|
457
460
|
*/
|
|
458
|
-
declare interface WorkflowResourceAddressing {
|
|
461
|
+
export declare interface WorkflowResourceAddressing {
|
|
459
462
|
projectId?: string;
|
|
460
463
|
dataset?: string;
|
|
461
464
|
resource?: WorkflowResource;
|
|
@@ -495,7 +498,7 @@ export declare interface WorkflowToolDef {
|
|
|
495
498
|
readonly description: string;
|
|
496
499
|
/**
|
|
497
500
|
* Raw zod shape — the single schema source of truth: MCP servers
|
|
498
|
-
* register it directly
|
|
501
|
+
* register it directly; the runtime parser wraps it for
|
|
499
502
|
* parsing, and {@link toolInputJsonSchema} derives the JSON-schema
|
|
500
503
|
* descriptor for non-MCP consumers.
|
|
501
504
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionParam } from "@sanity/workflow-engine";
|
|
2
|
+
import type { AssignmentStateCounts } from "@sanity/workflow-engine";
|
|
2
3
|
import type { AutonomyVerdict } from "@sanity/workflow-engine";
|
|
3
4
|
import { DeclaredExecutionContext } from "@sanity/workflow-engine";
|
|
4
5
|
import type { Diagnosis } from "@sanity/workflow-engine";
|
|
@@ -279,6 +280,8 @@ export declare interface ProjectedInstanceSummary {
|
|
|
279
280
|
done: boolean;
|
|
280
281
|
/** What this workflow is about. See {@link ProjectedSubject}. */
|
|
281
282
|
subject?: ProjectedSubject;
|
|
283
|
+
/** Viewer-scoped assignment slots when the list call supplied an assignment identity. */
|
|
284
|
+
assignment?: AssignmentStateCounts;
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
/**
|
|
@@ -422,7 +425,7 @@ export declare function workflowClientConfig<Base extends object>(args: {
|
|
|
422
425
|
* The client settings engine traffic pins, whatever base a host layers them over.
|
|
423
426
|
* Literal types so the result stays assignable to a host's own client config.
|
|
424
427
|
*/
|
|
425
|
-
declare interface WorkflowClientPolicy {
|
|
428
|
+
export declare interface WorkflowClientPolicy {
|
|
426
429
|
apiVersion: string;
|
|
427
430
|
useCdn: false;
|
|
428
431
|
requestTagPrefix: string;
|
|
@@ -455,7 +458,7 @@ export declare function workflowErrorText(error: unknown): string;
|
|
|
455
458
|
* `@sanity/client` prefers `resource` over `projectId`/`dataset` whenever both
|
|
456
459
|
* are present, which would otherwise silently address the wrong environment.
|
|
457
460
|
*/
|
|
458
|
-
declare interface WorkflowResourceAddressing {
|
|
461
|
+
export declare interface WorkflowResourceAddressing {
|
|
459
462
|
projectId?: string;
|
|
460
463
|
dataset?: string;
|
|
461
464
|
resource?: WorkflowResource;
|
|
@@ -495,7 +498,7 @@ export declare interface WorkflowToolDef {
|
|
|
495
498
|
readonly description: string;
|
|
496
499
|
/**
|
|
497
500
|
* Raw zod shape — the single schema source of truth: MCP servers
|
|
498
|
-
* register it directly
|
|
501
|
+
* register it directly; the runtime parser wraps it for
|
|
499
502
|
* parsing, and {@link toolInputJsonSchema} derives the JSON-schema
|
|
500
503
|
* descriptor for non-MCP consumers.
|
|
501
504
|
*/
|
package/dist/stdio.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Boots an {@link
|
|
2
|
+
* Boots an {@link https://github.com/modelcontextprotocol/typescript-sdk | MCP server}, binds the workflow tools, and listens on stdio; resolves
|
|
3
3
|
* once the transport connects (the process then stays alive on the open streams).
|
|
4
4
|
*
|
|
5
5
|
* Org-authed: boot config is just the org token — no project, dataset, resource, or
|
|
6
6
|
* tag. Every tool call names its own workflow environment (`workflow_resource` + `tag`),
|
|
7
7
|
* and the host builds/caches one engine per distinct environment for the process's life.
|
|
8
|
-
*
|
|
9
8
|
* Kept as its own package entry, not inline in `bin/`, so both the published bin and the
|
|
10
9
|
* dev entry share one boot path, and the MCP transport deps stay out of the library's `.` entry.
|
|
11
10
|
*/
|
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.
|
|
9
|
+
var version = "0.32.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.
|
|
3
|
+
"version": "0.32.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,20 +62,21 @@
|
|
|
62
62
|
"@types/node": "^24.12.4",
|
|
63
63
|
"vitest": "^4.1.8",
|
|
64
64
|
"zod": "^4.4.3",
|
|
65
|
-
"@sanity/workflow-engine": "0.
|
|
66
|
-
"@sanity/workflow-engine-test": "0.
|
|
67
|
-
"@sanity/workflow-examples": "0.
|
|
65
|
+
"@sanity/workflow-engine": "0.32.0",
|
|
66
|
+
"@sanity/workflow-engine-test": "0.32.0",
|
|
67
|
+
"@sanity/workflow-examples": "0.12.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.
|
|
72
|
+
"@sanity/workflow-engine": "0.32.0"
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|
|
75
75
|
"node": ">=20"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"build": "pkg-utils build --clean",
|
|
79
|
+
"docs": "node ../../scripts/run-typedoc.mjs",
|
|
79
80
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
80
81
|
"test": "vitest run",
|
|
81
82
|
"test:watch": "vitest",
|