@sanity/workflow-mcp 0.7.0 → 0.8.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 +24 -0
- package/README.md +16 -9
- package/dist/_chunks-es/index.js +73 -21
- package/dist/index.cjs +72 -20
- package/dist/index.d.cts +21 -3
- package/dist/index.d.ts +21 -3
- package/dist/stdio.js +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @sanity/workflow-mcp
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f9389e5: `get_workflow_state` (and `fire_action`'s shared state projection) now carries the causal-autonomy insight: per activity a `completesWithoutCaller` verdict (`yes`/`no`/`conditional`) with narrated `waitsOn` lines when not `yes`, and a workflow-level `autonomy` one-liner ("this workflow runs itself except at …").
|
|
8
|
+
- e3a7ba2: `start_workflow` gains `instance_id` — for retries: the id is the start's idempotency key, so an agent retrying a start that errored can pass the same id and resume it instead of minting a duplicate instance. A start whose first auto-advance fails after the instance was created and primed no longer surfaces as a failed tool call — the run exists, so the tool returns its projected state with a `startNotSettled` caveat naming the cause and the retry id. A start that fails before priming (`StartNotPrimedError`) stays a failed call, its message naming the `instance_id` to retry with (or `abort_workflow` to discard).
|
|
9
|
+
- 5a1a9fe: Direct reads of engine-owned documents adopt the data-model gate (`assertReadableModel`): the CLI's instance and definition reads (`show`, `tail`, `list`, `definition show`, `definition list`, deploy lookups, the share read-back) and the MCP tools that interpret rows from the ungated `engine.query` escape hatch (`list_workflow_definitions`, `list_workflow_instances`, the definition lookup behind `start_workflow` / `get_workflow_definition`). A document written by a newer engine data model now fails with the governed "upgrade `@sanity/workflow-engine`" error instead of rendering unchecked; list projections carry the stamp pair so projected rows gate too.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 30fed9e: The authoring guide states the definition-name grammar (`^[a-z0-9][a-z0-9-]*$`) and the condition dot-path shape rule (reference envelopes carry `id`/`type`/…, never `_id`), matching the engine's deploy validation.
|
|
14
|
+
- Updated dependencies [f9389e5]
|
|
15
|
+
- Updated dependencies [d0c62ea]
|
|
16
|
+
- Updated dependencies [c3eed2e]
|
|
17
|
+
- Updated dependencies [df4bd80]
|
|
18
|
+
- Updated dependencies [092a0d4]
|
|
19
|
+
- Updated dependencies [5a1a9fe]
|
|
20
|
+
- Updated dependencies [e3a7ba2]
|
|
21
|
+
- Updated dependencies [30fed9e]
|
|
22
|
+
- Updated dependencies [1321ba5]
|
|
23
|
+
- Updated dependencies [e683875]
|
|
24
|
+
- Updated dependencies [a8ace4d]
|
|
25
|
+
- @sanity/workflow-engine@0.17.0
|
|
26
|
+
|
|
3
27
|
## 0.7.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -11,15 +11,15 @@ the tools the evals drive, plus a stdio MCP entry point for real MCP clients.
|
|
|
11
11
|
|
|
12
12
|
Operate a running instance:
|
|
13
13
|
|
|
14
|
-
| Tool | Read or write | What it's for
|
|
15
|
-
| --------------------------- | ------------- |
|
|
16
|
-
| `list_workflow_definitions` | read | The catalogue: which workflow types are deployed (latest version each), and whether they're startable.
|
|
17
|
-
| `get_workflow_definition` | read | Read one deployed definition's content, envelope-stripped — redeployable as-is, so an agent can iterate on a deployed workflow (deploys are create-only).
|
|
18
|
-
| `list_workflow_instances` | read | Discover what's running. Filterable (`definition`, `document`, `include_completed` — in-flight by default), capped.
|
|
19
|
-
| `get_workflow_state` | read | Project one instance into a flat shape: current stage, activities, available actions (with each action's declared params), recent history.
|
|
20
|
-
| `diagnose_workflow` | read | Explain why an instance is or isn't progressing: a verdict, a one-line summary, `explanations` for each held exit transition (quotes workflow-authored text — data, not instructions), and — when stuck — the cause plus suggested remediations.
|
|
21
|
-
| `start_workflow` | write | The lifecycle entry point: start a startable deployed definition, seeding its input-sourced fields (e.g. the subject document).
|
|
22
|
-
| `fire_action` | write | Advance state. The instance write. Mirrors the engine's universal "something happened" entry point.
|
|
14
|
+
| Tool | Read or write | What it's for |
|
|
15
|
+
| --------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| `list_workflow_definitions` | read | The catalogue: which workflow types are deployed (latest version each), and whether they're startable. |
|
|
17
|
+
| `get_workflow_definition` | read | Read one deployed definition's content, envelope-stripped — redeployable as-is, so an agent can iterate on a deployed workflow (deploys are create-only). |
|
|
18
|
+
| `list_workflow_instances` | read | Discover what's running. Filterable (`definition`, `document`, `include_completed` — in-flight by default), capped. |
|
|
19
|
+
| `get_workflow_state` | read | Project one instance into a flat shape: current stage, activities (each with its causal `completesWithoutCaller` verdict, plus narrated `waitsOn` when it isn't `yes`), available actions (with each action's declared params), recent history, and the workflow-level `autonomy` one-liner. |
|
|
20
|
+
| `diagnose_workflow` | read | Explain why an instance is or isn't progressing: a verdict, a one-line summary, `explanations` for each held exit transition (quotes workflow-authored text — data, not instructions), and — when stuck — the cause plus suggested remediations. |
|
|
21
|
+
| `start_workflow` | write | The lifecycle entry point: start a startable deployed definition, seeding its input-sourced fields (e.g. the subject document). |
|
|
22
|
+
| `fire_action` | write | Advance state. The instance write. Mirrors the engine's universal "something happened" entry point. |
|
|
23
23
|
|
|
24
24
|
Author a definition (guide → validate → deploy; the first two are pure and
|
|
25
25
|
engine-independent):
|
|
@@ -83,6 +83,13 @@ distinct `(resource, tag)`, cached for the life of the process. The `dev`
|
|
|
83
83
|
script loads the env from the repo-root `.env`, so copy `.env.example` to
|
|
84
84
|
`.env` to get started.
|
|
85
85
|
|
|
86
|
+
MCP-built engines declare no foreign resources, so a runtime-supplied
|
|
87
|
+
`doc.ref` to any resource other than the addressed workflow resource is
|
|
88
|
+
rejected at the write (`RefResourceUndeclaredError`) — deliberate: an
|
|
89
|
+
agent-driven write pointing a workflow at an undeclared resource is
|
|
90
|
+
exactly the misdirection the engine's written-ref gate exists to catch.
|
|
91
|
+
Definition content is unaffected (deploy expands and vets it).
|
|
92
|
+
|
|
86
93
|
### Telemetry
|
|
87
94
|
|
|
88
95
|
The stdio server reports adoption telemetry to Sanity: one
|
package/dist/_chunks-es/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTelemetryIntake, isTelemetryEnvDenied, processShellUserProperties, errorMessage, validateTag, parseResourceGdr, parseDefinitionInput, extractDocumentId, parseDefinitionSnapshot, displayTitle, actionRendering, isTerminalStage, describeCondition, actionVerdict, subjectDenialLabels, deniedGuardLabels, definitionLookupGroq, unsatisfiedTransitionSummaries, describeSite, definitionsListGroq, latestDeployedDefinitions, startKindOf, isStartableDefinition, instancesQuery, instanceWatchesDocument, parseGdr, startRefusal, buildInitialFields, validateDefinition, WorkflowError } from "@sanity/workflow-engine";
|
|
1
|
+
import { createTelemetryIntake, isTelemetryEnvDenied, processShellUserProperties, errorMessage, validateTag, parseResourceGdr, parseDefinitionInput, extractDocumentId, parseDefinitionSnapshot, displayTitle, autonomySummary, actionRendering, isTerminalStage, describeCondition, actionVerdict, narrateAutonomyWaits, subjectDenialLabels, deniedGuardLabels, definitionLookupGroq, assertReadableModel, unsatisfiedTransitionSummaries, describeSite, definitionsListGroq, latestDeployedDefinitions, startKindOf, isStartableDefinition, instancesQuery, instanceWatchesDocument, parseGdr, startRefusal, buildInitialFields, StartNotPrimedError, StartNotSettledError, validateDefinition, WorkflowError } from "@sanity/workflow-engine";
|
|
2
2
|
|
|
3
3
|
import { defineEvent, createBatchedStore, createSessionId } from "@sanity/telemetry";
|
|
4
4
|
|
|
@@ -211,6 +211,9 @@ function projectState({instance: instance, evaluation: evaluation, subjectTitles
|
|
|
211
211
|
subject: subject
|
|
212
212
|
} : {},
|
|
213
213
|
activities: activities,
|
|
214
|
+
autonomy: autonomySummary(evaluation.autonomy, {
|
|
215
|
+
definition: definition
|
|
216
|
+
}).text,
|
|
214
217
|
recentHistory: recentHistory
|
|
215
218
|
};
|
|
216
219
|
}
|
|
@@ -235,6 +238,7 @@ function projectActivity(te, definition) {
|
|
|
235
238
|
} : {},
|
|
236
239
|
status: te.status,
|
|
237
240
|
classification: te.classification,
|
|
241
|
+
...autonomyFields(te, definition),
|
|
238
242
|
actions: actions,
|
|
239
243
|
...automations.length > 0 ? {
|
|
240
244
|
automations: automations
|
|
@@ -242,6 +246,22 @@ function projectActivity(te, definition) {
|
|
|
242
246
|
};
|
|
243
247
|
}
|
|
244
248
|
|
|
249
|
+
function autonomyFields(te, definition) {
|
|
250
|
+
const {completesWithoutCaller: completesWithoutCaller} = te.autonomy;
|
|
251
|
+
if (completesWithoutCaller === "yes") return {
|
|
252
|
+
completesWithoutCaller: completesWithoutCaller
|
|
253
|
+
};
|
|
254
|
+
const waitsOn = narrateAutonomyWaits(te.autonomy.waitsOn, {
|
|
255
|
+
definition: definition
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
completesWithoutCaller: completesWithoutCaller,
|
|
259
|
+
...waitsOn.length > 0 ? {
|
|
260
|
+
waitsOn: waitsOn
|
|
261
|
+
} : {}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
245
265
|
function automationWhen(ae) {
|
|
246
266
|
const when = ae.action.when;
|
|
247
267
|
if (when === void 0) throw new Error(`action "${ae.action.name}" verdicts as automation but declares no \`when\` — the evaluation and the pinned definition disagree`);
|
|
@@ -308,7 +328,7 @@ async function fetchDeployedDefinition({engine: engine, definition: definition,
|
|
|
308
328
|
const label = version !== void 0 ? ` v${version}` : "";
|
|
309
329
|
throw new Error(`no deployed definition "${definition}"${label} in this workflow environment — list_workflow_definitions shows what is deployed`);
|
|
310
330
|
}
|
|
311
|
-
return deployed;
|
|
331
|
+
return assertReadableModel(deployed);
|
|
312
332
|
}
|
|
313
333
|
|
|
314
334
|
function isInstanceDone(instance) {
|
|
@@ -637,7 +657,7 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
|
|
|
637
657
|
title: "Approved",
|
|
638
658
|
description: "Terminal — no transitions out."
|
|
639
659
|
} ]
|
|
640
|
-
}, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (triggers, filters, predicates, guards) is a GROQ *string*. Generate\nthe JSON, then call `validate_workflow_definition` (it takes a `definitions`\narray — validate a parent and its child workflows together) to check it; fix\nthe reported errors and re-validate until it returns `valid: true`. Each\n`results` entry pairs with your input and carries the *desugared* definition\nunder `definition` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, title, description?, initialStage, fields?, stages[], predicates? }`.\n `initialStage` must be the `name` of a declared stage. Do NOT include a\n `version` — definitions are immutable and content-addressed; deploy assigns\n the version from the content, the author never writes one.\n- **Stage**: `{ name, title?, description?, activities?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: `{ name, title?, filter?, actions?, fields? }` — a unit of work\n that carries NO payload of its own; everything that DOES anything lives on\n its actions. Every in-scope activity is **active from the moment its stage\n is entered** (there is no activation step) and stays active until an action\n resolves it `done`/`skipped`/`failed`. `filter` is existence, evaluated\n once at stage entry: a definite `false` skips the activity for this visit.\n- **Action**: `{ name, title?, when?, status?, params?, ops? }` — actions are\n the ONLY payload mechanism. Two firing modes:\n - **No `when`** — invoked by a caller (a person or agent calling\n `fire_action`).\n - **With `when`** — CASCADE-FIRED: the engine fires it on its own the\n moment the GROQ trigger is true (at most once per stage visit), and no\n caller can ever invoke it. Fires-on-entry work is `when: 'true'`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that resolves the *firing\n activity* to that status when the action fires. Status is a health axis, not\n a decision: a routine decision (reject, send back, decline, hold) resolves\n `'done'` and writes the decision into a field the transition triggers read —\n reserve `'failed'` for work that genuinely could not complete (e.g. a missed\n deadline via `{name: 'deadline', when: '$now > $fields.dueBy', status: 'failed'}`).\n `ops` are mutations applied when the action fires (e.g.\n `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values a caller-fired action collects from the caller\n (referenced by `{type:'param', param:'<name>'}` sources); a `when` action\n has no caller, so `params` is rejected there.\n- **Transition**: `{ name, title?, to, when? }` — a pure edge; transitions\n carry no ops or effects (only actions do). `to` must name a declared stage.\n `when` is the GROQ trigger gating the exit; omit it and it defaults to\n `$allActivitiesDone`. The first transition whose `when` is true (in\n declaration order) fires.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, initialValue? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `boolean`, `date` (YYYY-MM-DD), `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`), identities (`actor` — one concrete\n principal; `assignee` / `assignees` — one or many user-or-role assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) — `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the field once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: `{type:'input'}` (the caller supplies it when the\n instance starts), `{type:'query', query:'<groq>'}` (computed from the lake),\n `{type:'literal', value:<json>}`, or `{type:'fieldRead', field:'<name>'}`.\n By convention a workflow has an `input`-sourced `doc.ref` named `subject` — the\n headline document it is about.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work — rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log — rows\n `{ body, actor, at }`; pairs with the `audit` op, which stamps `actor`/`at`).\n\n## GROQ in conditions\n\nBuilt-in variables available in triggers/filters/predicates (the engine's\nexported `CONDITION_VARS` inventory is the source of truth):\n`$allActivitiesDone`, `$anyActivityFailed` (booleans over the current stage's\nactivities), `$activities` (the activity list), `$fields` (field values),\n`$context` (the start-time context bag — values seeded when the instance\nstarted; written once, never mutated), `$now`,\n`$self`/`$stage`/`$parent`/`$ancestors` (instance identity + position),\n`$effectStatus` (effect name → `'done'`/`'failed'` of the run queued during\nthe CURRENT stage entry — the re-entry-safe way to gate on an effect having\ndrained, e.g. a trigger action\n`{name: 'settled', when: \"defined($effectStatus['my.effect'])\", status: 'done'}`),\nand the caller-scoped vars `$actor` (the acting user), `$assigned` (whether\nthe caller is the activity's assignee — the idiomatic permission gate, used as\nan action `filter: '$assigned'`), and `$can`. `$actor`/`$assigned`/`$can`\nbelong in **caller-fired action filters only**. The cascade is deliberately\ncaller-blind: transition `when`s, activity `filter`s, and a cascade-fired\naction's `when`/`filter` re-evaluate on every trigger (another editor's\naction, an effect draining, a tick) and must resolve the same way regardless\nof whose token that is, so deploy rejects `$actor`/`$assigned`/`$can`/`$params`\nthere — route on instance state an action wrote instead. `$params` (the firing\naction's args) is not usable in **any** filter — action filters included: a\nfilter decides whether the action is enabled before the caller supplies args,\nso deploy rejects it there too. Collect caller input with the action's\n`params` and consume it in the action's `ops` (a `{type:'param'}` value).\n(Identity still gates every move: the commit rides the caller's token, and the\nlake's ACL accepts or rejects the write wholesale.) Define reusable named\nconditions under top-level `predicates: { name: '<groq>' }` and reference\nthem as `$name` (e.g.\n`predicates: { ready: \"count($activities[status != 'done']) == 0\" }` → `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n`$fields.<name>` must name a declared field entry visible at the reading\nsite: workflow fields everywhere, plus the enclosing stage's fields at that\nstage's sites, plus the enclosing activity's fields inside that activity.\nTransition triggers cannot see activity fields — put a decision a transition\nroutes on at stage scope (Example 2). The validator rejects reads of\nundeclared names, stages no transition path reaches, and `fieldRead` value\nsources whose target entry or dot-path doesn't resolve.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` — resolves the firing activity (shown above).\n- Omitted transition `when` — defaults to `$allActivitiesDone`.\n- Action `roles: ['editor', ...]` — on a caller-fired action, folds a\n role-membership check into its `filter`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review → drafting` gated\n on a `decision` field the reject action wrote), not to a terminal stage.\n Reserve terminal stages for completion and for explicit\n cancellation/abandonment — a workflow should not dead-end just because\n something was declined.\n- **Decisions are fields, not failures.** When a stage branches on a human\n decision, declare a stage-scoped `string` field (stage scope resets on\n re-entry, so loop-backs start clean), have each deciding action `field.set`\n it, and gate every outbound transition on its value (Example 2). Do not\n encode a decision as `status: 'failed'` + `$anyActivityFailed` — reporting\n would count healthy loops as failures.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Every activity must have a path to a terminal status — some action in the\n stage (its own, or a sibling's via a `status.set` op) resolves it\n `done`/`skipped`/`failed`; an activity nothing can ever resolve is rejected.\n- A terminal stage (no transitions) declares no activities — entering it\n completes the instance, so they could never run.\n- Custom `predicates` must not shadow a built-in (e.g. `allActivitiesDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\`\`\`json\n${JSON.stringify(minimalExample, null, 2)}\n\`\`\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\`\`\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\`\`\`\n`, getWorkflowAuthoringGuideTool = defineWorkflowTool({
|
|
660
|
+
}, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (triggers, filters, predicates, guards) is a GROQ *string*. Generate\nthe JSON, then call `validate_workflow_definition` (it takes a `definitions`\narray — validate a parent and its child workflows together) to check it; fix\nthe reported errors and re-validate until it returns `valid: true`. Each\n`results` entry pairs with your input and carries the *desugared* definition\nunder `definition` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, title, description?, initialStage, fields?, stages[], predicates? }`.\n `name` must match `^[a-z0-9][a-z0-9-]*$` (lowercase + digits + dashes — it\n interpolates into every deployed document id, so spaces, uppercase, dots,\n and underscores are rejected). `initialStage` must be the `name` of a\n declared stage. Do NOT include a `version` — definitions are immutable and\n content-addressed; deploy assigns the version from the content, the author\n never writes one.\n- **Stage**: `{ name, title?, description?, activities?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: `{ name, title?, filter?, actions?, fields? }` — a unit of work\n that carries NO payload of its own; everything that DOES anything lives on\n its actions. Every in-scope activity is **active from the moment its stage\n is entered** (there is no activation step) and stays active until an action\n resolves it `done`/`skipped`/`failed`. `filter` is existence, evaluated\n once at stage entry: a definite `false` skips the activity for this visit.\n- **Action**: `{ name, title?, when?, status?, params?, ops? }` — actions are\n the ONLY payload mechanism. Two firing modes:\n - **No `when`** — invoked by a caller (a person or agent calling\n `fire_action`).\n - **With `when`** — CASCADE-FIRED: the engine fires it on its own the\n moment the GROQ trigger is true (at most once per stage visit), and no\n caller can ever invoke it. Fires-on-entry work is `when: 'true'`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that resolves the *firing\n activity* to that status when the action fires. Status is a health axis, not\n a decision: a routine decision (reject, send back, decline, hold) resolves\n `'done'` and writes the decision into a field the transition triggers read —\n reserve `'failed'` for work that genuinely could not complete (e.g. a missed\n deadline via `{name: 'deadline', when: '$now > $fields.dueBy', status: 'failed'}`).\n `ops` are mutations applied when the action fires (e.g.\n `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values a caller-fired action collects from the caller\n (referenced by `{type:'param', param:'<name>'}` sources); a `when` action\n has no caller, so `params` is rejected there.\n- **Transition**: `{ name, title?, to, when? }` — a pure edge; transitions\n carry no ops or effects (only actions do). `to` must name a declared stage.\n `when` is the GROQ trigger gating the exit; omit it and it defaults to\n `$allActivitiesDone`. The first transition whose `when` is true (in\n declaration order) fires.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, initialValue? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `boolean`, `date` (YYYY-MM-DD), `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`), identities (`actor` — one concrete\n principal; `assignee` / `assignees` — one or many user-or-role assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) — `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the field once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: `{type:'input'}` (the caller supplies it when the\n instance starts), `{type:'query', query:'<groq>'}` (computed from the lake),\n `{type:'literal', value:<json>}`, or `{type:'fieldRead', field:'<name>'}`.\n By convention a workflow has an `input`-sourced `doc.ref` named `subject` — the\n headline document it is about.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work — rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log — rows\n `{ body, actor, at }`; pairs with the `audit` op, which stamps `actor`/`at`).\n\n## GROQ in conditions\n\nBuilt-in variables available in triggers/filters/predicates (the engine's\nexported `CONDITION_VARS` inventory is the source of truth):\n`$allActivitiesDone`, `$anyActivityFailed` (booleans over the current stage's\nactivities), `$activities` (the activity list), `$fields` (field values),\n`$context` (the start-time context bag — values seeded when the instance\nstarted; written once, never mutated), `$now`,\n`$self`/`$stage`/`$parent`/`$ancestors` (instance identity + position),\n`$effectStatus` (effect name → `'done'`/`'failed'` of the run queued during\nthe CURRENT stage entry — the re-entry-safe way to gate on an effect having\ndrained, e.g. a trigger action\n`{name: 'settled', when: \"defined($effectStatus['my.effect'])\", status: 'done'}`),\nand the caller-scoped vars `$actor` (the acting user), `$assigned` (whether\nthe caller is the activity's assignee — the idiomatic permission gate, used as\nan action `filter: '$assigned'`), and `$can`. `$actor`/`$assigned`/`$can`\nbelong in **caller-fired action filters only**. The cascade is deliberately\ncaller-blind: transition `when`s, activity `filter`s, and a cascade-fired\naction's `when`/`filter` re-evaluate on every trigger (another editor's\naction, an effect draining, a tick) and must resolve the same way regardless\nof whose token that is, so deploy rejects `$actor`/`$assigned`/`$can`/`$params`\nthere — route on instance state an action wrote instead. `$params` (the firing\naction's args) is not usable in **any** filter — action filters included: a\nfilter decides whether the action is enabled before the caller supplies args,\nso deploy rejects it there too. Collect caller input with the action's\n`params` and consume it in the action's `ops` (a `{type:'param'}` value).\n(Identity still gates every move: the commit rides the caller's token, and the\nlake's ACL accepts or rejects the write wholesale.) Define reusable named\nconditions under top-level `predicates: { name: '<groq>' }` and reference\nthem as `$name` (e.g.\n`predicates: { ready: \"count($activities[status != 'done']) == 0\" }` → `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n`$fields.<name>` must name a declared field entry visible at the reading\nsite: workflow fields everywhere, plus the enclosing stage's fields at that\nstage's sites, plus the enclosing activity's fields inside that activity.\nTransition triggers cannot see activity fields — put a decision a transition\nroutes on at stage scope (Example 2). A read's dot-path must also fit the\nentry's declared value shape — reference envelopes especially: a\n`release.ref` value carries `id`/`type`/`releaseName` (never `_id`), a\n`doc.refs` element `id`/`type`. The validator rejects reads of\nundeclared names, condition dot-paths that don't fit the declared shape,\nstages no transition path reaches, and `fieldRead` value sources whose\ntarget entry or dot-path doesn't resolve.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` — resolves the firing activity (shown above).\n- Omitted transition `when` — defaults to `$allActivitiesDone`.\n- Action `roles: ['editor', ...]` — on a caller-fired action, folds a\n role-membership check into its `filter`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review → drafting` gated\n on a `decision` field the reject action wrote), not to a terminal stage.\n Reserve terminal stages for completion and for explicit\n cancellation/abandonment — a workflow should not dead-end just because\n something was declined.\n- **Decisions are fields, not failures.** When a stage branches on a human\n decision, declare a stage-scoped `string` field (stage scope resets on\n re-entry, so loop-backs start clean), have each deciding action `field.set`\n it, and gate every outbound transition on its value (Example 2). Do not\n encode a decision as `status: 'failed'` + `$anyActivityFailed` — reporting\n would count healthy loops as failures.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Every activity must have a path to a terminal status — some action in the\n stage (its own, or a sibling's via a `status.set` op) resolves it\n `done`/`skipped`/`failed`; an activity nothing can ever resolve is rejected.\n- A terminal stage (no transitions) declares no activities — entering it\n completes the instance, so they could never run.\n- Custom `predicates` must not shadow a built-in (e.g. `allActivitiesDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\`\`\`json\n${JSON.stringify(minimalExample, null, 2)}\n\`\`\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\`\`\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\`\`\`\n`, getWorkflowAuthoringGuideTool = defineWorkflowTool({
|
|
641
661
|
name: "get_workflow_authoring_guide",
|
|
642
662
|
description: "Get the guide for authoring a workflow definition: the DSL shape, the GROQ condition built-ins, the sugars, the rules the validator enforces, and two worked JSON examples. Call this BEFORE writing a definition from a description, then generate the JSON and check it with validate_workflow_definition. Pure read; takes no arguments.",
|
|
643
663
|
inputSchema: {},
|
|
@@ -672,7 +692,7 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
|
|
|
672
692
|
}
|
|
673
693
|
}), getWorkflowStateTool = defineWorkflowTool({
|
|
674
694
|
name: "get_workflow_state",
|
|
675
|
-
description: `Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable \`workflowTitle\`, the current stage, the subject document the workflow is about (when the workflow has a conventional subject — its ref and title), every in-scope activity on the current stage, and the most recent history entries. Per activity: its \`classification\` (who
|
|
695
|
+
description: `Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable \`workflowTitle\`, the current stage, the subject document the workflow is about (when the workflow has a conventional subject — its ref and title), every in-scope activity on the current stage, and the most recent history entries, plus a one-line \`autonomy\` narrative saying whether the workflow runs itself and where it waits on someone. Per activity: its \`classification\` (who fires its actions: interactive, autonomous, off-system, or hybrid), the causal \`completesWithoutCaller\` verdict (yes/no/conditional — a mechanically autonomous activity whose triggers only read caller-written state answers no) with narrated \`waitsOn\` lines when not yes, its invocable \`actions\` (each with an allowed/disabled verdict — these are what fire_action accepts), and its \`automations\` (cascade-fired actions the ENGINE fires on its own when their \`firesWhen\` trigger holds — never invocable via fire_action). Activities and actions that exist in the definition but are scoped out for this visit or actor are simply absent. Use this whenever you need to understand what's possible on an instance before deciding to act. This is a pure read — it does not change anything. ${UNTRUSTED_AUTHORED_DATA_NOTE} If you only need to discover what instances exist, use list_workflow_instances instead; this tool requires you to know the instance id. list_workflow_instances also returns the same \`workflowTitle\` and \`subject\` fields, so prefer it for fan-out discovery rather than polling get_workflow_state per instance.`,
|
|
676
696
|
inputSchema: {
|
|
677
697
|
...workflowAddressFields,
|
|
678
698
|
instance_id: instanceIdField
|
|
@@ -697,9 +717,9 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
|
|
|
697
717
|
readOnlyHint: !0
|
|
698
718
|
},
|
|
699
719
|
run: async context => {
|
|
700
|
-
const {engine: engine} = await context(), deployed = await engine.query({
|
|
720
|
+
const {engine: engine} = await context(), deployed = (await engine.query({
|
|
701
721
|
groq: definitionsListGroq("desc")
|
|
702
|
-
});
|
|
722
|
+
})).map(assertReadableModel);
|
|
703
723
|
return {
|
|
704
724
|
definitions: latestDeployedDefinitions(deployed).map(def => ({
|
|
705
725
|
name: def.name,
|
|
@@ -737,22 +757,55 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
|
|
|
737
757
|
}, {query: query, params: params} = instancesQuery({
|
|
738
758
|
tag: engine.tag,
|
|
739
759
|
filter: filter
|
|
740
|
-
}), recency = `${query} | order(lastChangedAt desc)`, docs = await engine.query({
|
|
760
|
+
}), recency = `${query} | order(lastChangedAt desc)`, docs = (await engine.query({
|
|
741
761
|
groq: document === void 0 ? `${recency}[0...${LIST_CAP}]` : recency,
|
|
742
762
|
params: params
|
|
743
|
-
}), capped = (document === void 0 ? docs : docs.filter(doc => instanceWatchesDocument(doc, document))).slice(0, LIST_CAP), subjectTitles = await fetchSubjectTitles(engine, capped);
|
|
763
|
+
})).map(assertReadableModel), capped = (document === void 0 ? docs : docs.filter(doc => instanceWatchesDocument(doc, document))).slice(0, LIST_CAP), subjectTitles = await fetchSubjectTitles(engine, capped);
|
|
744
764
|
return {
|
|
745
765
|
instances: capped.map(doc => projectSummary(doc, subjectTitles))
|
|
746
766
|
};
|
|
747
767
|
}
|
|
748
|
-
})
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
async function startResolvingRetries(args) {
|
|
771
|
+
const {engine: engine, input: input, initialFields: initialFields} = args;
|
|
772
|
+
try {
|
|
773
|
+
const {instance: instance} = await engine.startInstance({
|
|
774
|
+
definition: input.definition,
|
|
775
|
+
...input.version !== void 0 ? {
|
|
776
|
+
version: input.version
|
|
777
|
+
} : {},
|
|
778
|
+
...initialFields.length > 0 ? {
|
|
779
|
+
initialFields: initialFields
|
|
780
|
+
} : {},
|
|
781
|
+
...input.instance_id !== void 0 ? {
|
|
782
|
+
instanceId: input.instance_id
|
|
783
|
+
} : {}
|
|
784
|
+
});
|
|
785
|
+
return {
|
|
786
|
+
instanceId: instance._id
|
|
787
|
+
};
|
|
788
|
+
} catch (err) {
|
|
789
|
+
if (err instanceof StartNotPrimedError) throw new Error(`${errorMessage(err)} Retry start_workflow with instance_id "${err.instanceId}" to resume, or abort_workflow to discard it.`, {
|
|
790
|
+
cause: err
|
|
791
|
+
});
|
|
792
|
+
if (!(err instanceof StartNotSettledError)) throw err;
|
|
793
|
+
return {
|
|
794
|
+
instanceId: err.instanceId,
|
|
795
|
+
startNotSettled: `The workflow started (created and primed) but its first auto-advance failed: ${errorMessage(err.cause)}. It settles on the next engine tick, or retry start_workflow with instance_id "${err.instanceId}".`
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const startWorkflowTool = defineWorkflowTool({
|
|
749
801
|
name: "start_workflow",
|
|
750
802
|
description: "Start a new workflow instance from a deployed definition — the lifecycle entry point. Use list_workflow_definitions first: `startable: true` marks what this tool can start (child workflows are spawn-only — a parent workflow's activity creates them, never this tool). Supply values for the workflow's input-sourced fields via `initial_fields` — e.g. the subject document the workflow is about. Returns the started instance, same shape as get_workflow_state; the engine's cascade (triggers and transitions) has already run, so it may land past the initial stage. Do NOT use this to advance an existing instance — that is fire_action. " + UNTRUSTED_AUTHORED_DATA_NOTE,
|
|
751
803
|
inputSchema: {
|
|
752
804
|
...workflowAddressFields,
|
|
753
805
|
definition: z.string().min(1).describe("The workflow definition `name` to start (as listed by list_workflow_definitions)."),
|
|
754
806
|
version: z.number().int().min(1).describe("Optional. The deployed definition version to start from. Defaults to the highest.").optional(),
|
|
755
|
-
initial_fields: z.record(z.string(), z.unknown()).describe('Optional. Values for the workflow\'s input-sourced field entries, keyed by field name (e.g. {"subject": {"id": "dataset:proj:ds:article-1", "type": "article"}}). doc.ref values take an object with a GDR `id` and doc `type`. get_workflow_definition shows a workflow\'s declared fields; only input-sourced entries accept a value here.').optional()
|
|
807
|
+
initial_fields: z.record(z.string(), z.unknown()).describe('Optional. Values for the workflow\'s input-sourced field entries, keyed by field name (e.g. {"subject": {"id": "dataset:proj:ds:article-1", "type": "article"}}). doc.ref values take an object with a GDR `id` and doc `type`. get_workflow_definition shows a workflow\'s declared fields; only input-sourced entries accept a value here.').optional(),
|
|
808
|
+
instance_id: z.string().min(1).describe("Optional. Start under this instance id — for retries. The id is the start's idempotency key: pass the SAME id when retrying a start that errored and the engine resumes that start instead of creating a duplicate instance (an already-settled start replays as a no-op). A failed start names the id to retry with in its error message. Omit to mint a fresh id.").optional()
|
|
756
809
|
},
|
|
757
810
|
annotations: {
|
|
758
811
|
readOnlyHint: !1,
|
|
@@ -771,19 +824,18 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
|
|
|
771
824
|
const initialFields = buildInitialFields({
|
|
772
825
|
declared: deployed.fields ?? [],
|
|
773
826
|
values: input.initial_fields ?? {}
|
|
774
|
-
}), {
|
|
775
|
-
definition: input.definition,
|
|
776
|
-
...input.version !== void 0 ? {
|
|
777
|
-
version: input.version
|
|
778
|
-
} : {},
|
|
779
|
-
...initialFields.length > 0 ? {
|
|
780
|
-
initialFields: initialFields
|
|
781
|
-
} : {}
|
|
782
|
-
});
|
|
783
|
-
return getInstanceState({
|
|
827
|
+
}), {instanceId: instanceId, startNotSettled: startNotSettled} = await startResolvingRetries({
|
|
784
828
|
engine: engine,
|
|
785
|
-
|
|
829
|
+
input: input,
|
|
830
|
+
initialFields: initialFields
|
|
831
|
+
}), state = await getInstanceState({
|
|
832
|
+
engine: engine,
|
|
833
|
+
instanceId: instanceId
|
|
786
834
|
});
|
|
835
|
+
return startNotSettled !== void 0 ? {
|
|
836
|
+
...state,
|
|
837
|
+
startNotSettled: startNotSettled
|
|
838
|
+
} : state;
|
|
787
839
|
}
|
|
788
840
|
}), validateWorkflowDefinitionTool = defineWorkflowTool({
|
|
789
841
|
name: "validate_workflow_definition",
|
package/dist/index.cjs
CHANGED
|
@@ -194,6 +194,9 @@ function projectState({instance: instance, evaluation: evaluation, subjectTitles
|
|
|
194
194
|
subject: subject
|
|
195
195
|
} : {},
|
|
196
196
|
activities: activities,
|
|
197
|
+
autonomy: workflowEngine.autonomySummary(evaluation.autonomy, {
|
|
198
|
+
definition: definition
|
|
199
|
+
}).text,
|
|
197
200
|
recentHistory: recentHistory
|
|
198
201
|
};
|
|
199
202
|
}
|
|
@@ -218,6 +221,7 @@ function projectActivity(te, definition) {
|
|
|
218
221
|
} : {},
|
|
219
222
|
status: te.status,
|
|
220
223
|
classification: te.classification,
|
|
224
|
+
...autonomyFields(te, definition),
|
|
221
225
|
actions: actions,
|
|
222
226
|
...automations.length > 0 ? {
|
|
223
227
|
automations: automations
|
|
@@ -225,6 +229,22 @@ function projectActivity(te, definition) {
|
|
|
225
229
|
};
|
|
226
230
|
}
|
|
227
231
|
|
|
232
|
+
function autonomyFields(te, definition) {
|
|
233
|
+
const {completesWithoutCaller: completesWithoutCaller} = te.autonomy;
|
|
234
|
+
if (completesWithoutCaller === "yes") return {
|
|
235
|
+
completesWithoutCaller: completesWithoutCaller
|
|
236
|
+
};
|
|
237
|
+
const waitsOn = workflowEngine.narrateAutonomyWaits(te.autonomy.waitsOn, {
|
|
238
|
+
definition: definition
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
completesWithoutCaller: completesWithoutCaller,
|
|
242
|
+
...waitsOn.length > 0 ? {
|
|
243
|
+
waitsOn: waitsOn
|
|
244
|
+
} : {}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
228
248
|
function automationWhen(ae) {
|
|
229
249
|
const when = ae.action.when;
|
|
230
250
|
if (when === void 0) throw new Error(`action "${ae.action.name}" verdicts as automation but declares no \`when\` — the evaluation and the pinned definition disagree`);
|
|
@@ -291,7 +311,7 @@ async function fetchDeployedDefinition({engine: engine, definition: definition,
|
|
|
291
311
|
const label = version !== void 0 ? ` v${version}` : "";
|
|
292
312
|
throw new Error(`no deployed definition "${definition}"${label} in this workflow environment — list_workflow_definitions shows what is deployed`);
|
|
293
313
|
}
|
|
294
|
-
return deployed;
|
|
314
|
+
return workflowEngine.assertReadableModel(deployed);
|
|
295
315
|
}
|
|
296
316
|
|
|
297
317
|
function isInstanceDone(instance) {
|
|
@@ -620,7 +640,7 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
|
|
|
620
640
|
title: "Approved",
|
|
621
641
|
description: "Terminal — no transitions out."
|
|
622
642
|
} ]
|
|
623
|
-
}, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (triggers, filters, predicates, guards) is a GROQ *string*. Generate\nthe JSON, then call `validate_workflow_definition` (it takes a `definitions`\narray — validate a parent and its child workflows together) to check it; fix\nthe reported errors and re-validate until it returns `valid: true`. Each\n`results` entry pairs with your input and carries the *desugared* definition\nunder `definition` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, title, description?, initialStage, fields?, stages[], predicates? }`.\n `initialStage` must be the `name` of a declared stage. Do NOT include a\n `version` — definitions are immutable and content-addressed; deploy assigns\n the version from the content, the author never writes one.\n- **Stage**: `{ name, title?, description?, activities?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: `{ name, title?, filter?, actions?, fields? }` — a unit of work\n that carries NO payload of its own; everything that DOES anything lives on\n its actions. Every in-scope activity is **active from the moment its stage\n is entered** (there is no activation step) and stays active until an action\n resolves it `done`/`skipped`/`failed`. `filter` is existence, evaluated\n once at stage entry: a definite `false` skips the activity for this visit.\n- **Action**: `{ name, title?, when?, status?, params?, ops? }` — actions are\n the ONLY payload mechanism. Two firing modes:\n - **No `when`** — invoked by a caller (a person or agent calling\n `fire_action`).\n - **With `when`** — CASCADE-FIRED: the engine fires it on its own the\n moment the GROQ trigger is true (at most once per stage visit), and no\n caller can ever invoke it. Fires-on-entry work is `when: 'true'`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that resolves the *firing\n activity* to that status when the action fires. Status is a health axis, not\n a decision: a routine decision (reject, send back, decline, hold) resolves\n `'done'` and writes the decision into a field the transition triggers read —\n reserve `'failed'` for work that genuinely could not complete (e.g. a missed\n deadline via `{name: 'deadline', when: '$now > $fields.dueBy', status: 'failed'}`).\n `ops` are mutations applied when the action fires (e.g.\n `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values a caller-fired action collects from the caller\n (referenced by `{type:'param', param:'<name>'}` sources); a `when` action\n has no caller, so `params` is rejected there.\n- **Transition**: `{ name, title?, to, when? }` — a pure edge; transitions\n carry no ops or effects (only actions do). `to` must name a declared stage.\n `when` is the GROQ trigger gating the exit; omit it and it defaults to\n `$allActivitiesDone`. The first transition whose `when` is true (in\n declaration order) fires.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, initialValue? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `boolean`, `date` (YYYY-MM-DD), `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`), identities (`actor` — one concrete\n principal; `assignee` / `assignees` — one or many user-or-role assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) — `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the field once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: `{type:'input'}` (the caller supplies it when the\n instance starts), `{type:'query', query:'<groq>'}` (computed from the lake),\n `{type:'literal', value:<json>}`, or `{type:'fieldRead', field:'<name>'}`.\n By convention a workflow has an `input`-sourced `doc.ref` named `subject` — the\n headline document it is about.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work — rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log — rows\n `{ body, actor, at }`; pairs with the `audit` op, which stamps `actor`/`at`).\n\n## GROQ in conditions\n\nBuilt-in variables available in triggers/filters/predicates (the engine's\nexported `CONDITION_VARS` inventory is the source of truth):\n`$allActivitiesDone`, `$anyActivityFailed` (booleans over the current stage's\nactivities), `$activities` (the activity list), `$fields` (field values),\n`$context` (the start-time context bag — values seeded when the instance\nstarted; written once, never mutated), `$now`,\n`$self`/`$stage`/`$parent`/`$ancestors` (instance identity + position),\n`$effectStatus` (effect name → `'done'`/`'failed'` of the run queued during\nthe CURRENT stage entry — the re-entry-safe way to gate on an effect having\ndrained, e.g. a trigger action\n`{name: 'settled', when: \"defined($effectStatus['my.effect'])\", status: 'done'}`),\nand the caller-scoped vars `$actor` (the acting user), `$assigned` (whether\nthe caller is the activity's assignee — the idiomatic permission gate, used as\nan action `filter: '$assigned'`), and `$can`. `$actor`/`$assigned`/`$can`\nbelong in **caller-fired action filters only**. The cascade is deliberately\ncaller-blind: transition `when`s, activity `filter`s, and a cascade-fired\naction's `when`/`filter` re-evaluate on every trigger (another editor's\naction, an effect draining, a tick) and must resolve the same way regardless\nof whose token that is, so deploy rejects `$actor`/`$assigned`/`$can`/`$params`\nthere — route on instance state an action wrote instead. `$params` (the firing\naction's args) is not usable in **any** filter — action filters included: a\nfilter decides whether the action is enabled before the caller supplies args,\nso deploy rejects it there too. Collect caller input with the action's\n`params` and consume it in the action's `ops` (a `{type:'param'}` value).\n(Identity still gates every move: the commit rides the caller's token, and the\nlake's ACL accepts or rejects the write wholesale.) Define reusable named\nconditions under top-level `predicates: { name: '<groq>' }` and reference\nthem as `$name` (e.g.\n`predicates: { ready: \"count($activities[status != 'done']) == 0\" }` → `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n`$fields.<name>` must name a declared field entry visible at the reading\nsite: workflow fields everywhere, plus the enclosing stage's fields at that\nstage's sites, plus the enclosing activity's fields inside that activity.\nTransition triggers cannot see activity fields — put a decision a transition\nroutes on at stage scope (Example 2). The validator rejects reads of\nundeclared names, stages no transition path reaches, and `fieldRead` value\nsources whose target entry or dot-path doesn't resolve.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` — resolves the firing activity (shown above).\n- Omitted transition `when` — defaults to `$allActivitiesDone`.\n- Action `roles: ['editor', ...]` — on a caller-fired action, folds a\n role-membership check into its `filter`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review → drafting` gated\n on a `decision` field the reject action wrote), not to a terminal stage.\n Reserve terminal stages for completion and for explicit\n cancellation/abandonment — a workflow should not dead-end just because\n something was declined.\n- **Decisions are fields, not failures.** When a stage branches on a human\n decision, declare a stage-scoped `string` field (stage scope resets on\n re-entry, so loop-backs start clean), have each deciding action `field.set`\n it, and gate every outbound transition on its value (Example 2). Do not\n encode a decision as `status: 'failed'` + `$anyActivityFailed` — reporting\n would count healthy loops as failures.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Every activity must have a path to a terminal status — some action in the\n stage (its own, or a sibling's via a `status.set` op) resolves it\n `done`/`skipped`/`failed`; an activity nothing can ever resolve is rejected.\n- A terminal stage (no transitions) declares no activities — entering it\n completes the instance, so they could never run.\n- Custom `predicates` must not shadow a built-in (e.g. `allActivitiesDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\`\`\`json\n${JSON.stringify(minimalExample, null, 2)}\n\`\`\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\`\`\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\`\`\`\n`, getWorkflowAuthoringGuideTool = defineWorkflowTool({
|
|
643
|
+
}, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (triggers, filters, predicates, guards) is a GROQ *string*. Generate\nthe JSON, then call `validate_workflow_definition` (it takes a `definitions`\narray — validate a parent and its child workflows together) to check it; fix\nthe reported errors and re-validate until it returns `valid: true`. Each\n`results` entry pairs with your input and carries the *desugared* definition\nunder `definition` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: `{ name, title, description?, initialStage, fields?, stages[], predicates? }`.\n `name` must match `^[a-z0-9][a-z0-9-]*$` (lowercase + digits + dashes — it\n interpolates into every deployed document id, so spaces, uppercase, dots,\n and underscores are rejected). `initialStage` must be the `name` of a\n declared stage. Do NOT include a `version` — definitions are immutable and\n content-addressed; deploy assigns the version from the content, the author\n never writes one.\n- **Stage**: `{ name, title?, description?, activities?, transitions? }`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: `{ name, title?, filter?, actions?, fields? }` — a unit of work\n that carries NO payload of its own; everything that DOES anything lives on\n its actions. Every in-scope activity is **active from the moment its stage\n is entered** (there is no activation step) and stays active until an action\n resolves it `done`/`skipped`/`failed`. `filter` is existence, evaluated\n once at stage entry: a definite `false` skips the activity for this visit.\n- **Action**: `{ name, title?, when?, status?, params?, ops? }` — actions are\n the ONLY payload mechanism. Two firing modes:\n - **No `when`** — invoked by a caller (a person or agent calling\n `fire_action`).\n - **With `when`** — CASCADE-FIRED: the engine fires it on its own the\n moment the GROQ trigger is true (at most once per stage visit), and no\n caller can ever invoke it. Fires-on-entry work is `when: 'true'`.\n `status: 'done' | 'skipped' | 'failed'` is sugar that resolves the *firing\n activity* to that status when the action fires. Status is a health axis, not\n a decision: a routine decision (reject, send back, decline, hold) resolves\n `'done'` and writes the decision into a field the transition triggers read —\n reserve `'failed'` for work that genuinely could not complete (e.g. a missed\n deadline via `{name: 'deadline', when: '$now > $fields.dueBy', status: 'failed'}`).\n `ops` are mutations applied when the action fires (e.g.\n `{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}`).\n `params` are values a caller-fired action collects from the caller\n (referenced by `{type:'param', param:'<name>'}` sources); a `when` action\n has no caller, so `params` is rejected there.\n- **Transition**: `{ name, title?, to, when? }` — a pure edge; transitions\n carry no ops or effects (only actions do). `to` must name a declared stage.\n `when` is the GROQ trigger gating the exit; omit it and it defaults to\n `$allActivitiesDone`. The first transition whose `when` is true (in\n declaration order) fires.\n- **Field** (workflow- or stage-scoped persistent state): `{ type, name, title?, initialValue? }`.\n Scalar `type`s mirror Sanity: `string`, `text` (multiline), `number`,\n `boolean`, `date` (YYYY-MM-DD), `datetime` (ISO), `url`. Plus references\n (`doc.ref`, `doc.refs`, `release.ref`), identities (`actor` — one concrete\n principal; `assignee` / `assignees` — one or many user-or-role assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) — `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the field once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: `{type:'input'}` (the caller supplies it when the\n instance starts), `{type:'query', query:'<groq>'}` (computed from the lake),\n `{type:'literal', value:<json>}`, or `{type:'fieldRead', field:'<name>'}`.\n By convention a workflow has an `input`-sourced `doc.ref` named `subject` — the\n headline document it is about.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work — rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log — rows\n `{ body, actor, at }`; pairs with the `audit` op, which stamps `actor`/`at`).\n\n## GROQ in conditions\n\nBuilt-in variables available in triggers/filters/predicates (the engine's\nexported `CONDITION_VARS` inventory is the source of truth):\n`$allActivitiesDone`, `$anyActivityFailed` (booleans over the current stage's\nactivities), `$activities` (the activity list), `$fields` (field values),\n`$context` (the start-time context bag — values seeded when the instance\nstarted; written once, never mutated), `$now`,\n`$self`/`$stage`/`$parent`/`$ancestors` (instance identity + position),\n`$effectStatus` (effect name → `'done'`/`'failed'` of the run queued during\nthe CURRENT stage entry — the re-entry-safe way to gate on an effect having\ndrained, e.g. a trigger action\n`{name: 'settled', when: \"defined($effectStatus['my.effect'])\", status: 'done'}`),\nand the caller-scoped vars `$actor` (the acting user), `$assigned` (whether\nthe caller is the activity's assignee — the idiomatic permission gate, used as\nan action `filter: '$assigned'`), and `$can`. `$actor`/`$assigned`/`$can`\nbelong in **caller-fired action filters only**. The cascade is deliberately\ncaller-blind: transition `when`s, activity `filter`s, and a cascade-fired\naction's `when`/`filter` re-evaluate on every trigger (another editor's\naction, an effect draining, a tick) and must resolve the same way regardless\nof whose token that is, so deploy rejects `$actor`/`$assigned`/`$can`/`$params`\nthere — route on instance state an action wrote instead. `$params` (the firing\naction's args) is not usable in **any** filter — action filters included: a\nfilter decides whether the action is enabled before the caller supplies args,\nso deploy rejects it there too. Collect caller input with the action's\n`params` and consume it in the action's `ops` (a `{type:'param'}` value).\n(Identity still gates every move: the commit rides the caller's token, and the\nlake's ACL accepts or rejects the write wholesale.) Define reusable named\nconditions under top-level `predicates: { name: '<groq>' }` and reference\nthem as `$name` (e.g.\n`predicates: { ready: \"count($activities[status != 'done']) == 0\" }` → `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by `_type` (e.g. `*[_type==\"article\"]`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a `doc.ref` field for it.\n\n`$fields.<name>` must name a declared field entry visible at the reading\nsite: workflow fields everywhere, plus the enclosing stage's fields at that\nstage's sites, plus the enclosing activity's fields inside that activity.\nTransition triggers cannot see activity fields — put a decision a transition\nroutes on at stage scope (Example 2). A read's dot-path must also fit the\nentry's declared value shape — reference envelopes especially: a\n`release.ref` value carries `id`/`type`/`releaseName` (never `_id`), a\n`doc.refs` element `id`/`type`. The validator rejects reads of\nundeclared names, condition dot-paths that don't fit the declared shape,\nstages no transition path reaches, and `fieldRead` value sources whose\ntarget entry or dot-path doesn't resolve.\n\n## Sugars worth knowing\n\n- Action `status: 'done' | 'skipped' | 'failed'` — resolves the firing activity (shown above).\n- Omitted transition `when` — defaults to `$allActivitiesDone`.\n- Action `roles: ['editor', ...]` — on a caller-fired action, folds a\n role-membership check into its `filter`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. `review → drafting` gated\n on a `decision` field the reject action wrote), not to a terminal stage.\n Reserve terminal stages for completion and for explicit\n cancellation/abandonment — a workflow should not dead-end just because\n something was declined.\n- **Decisions are fields, not failures.** When a stage branches on a human\n decision, declare a stage-scoped `string` field (stage scope resets on\n re-entry, so loop-backs start clean), have each deciding action `field.set`\n it, and gate every outbound transition on its value (Example 2). Do not\n encode a decision as `status: 'failed'` + `$anyActivityFailed` — reporting\n would count healthy loops as failures.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition `to` and `initialStage` names a declared stage.\n- Every activity must have a path to a terminal status — some action in the\n stage (its own, or a sibling's via a `status.set` op) resolves it\n `done`/`skipped`/`failed`; an activity nothing can ever resolve is rejected.\n- A terminal stage (no transitions) declares no activities — entering it\n completes the instance, so they could never run.\n- Custom `predicates` must not shadow a built-in (e.g. `allActivitiesDone`).\n- Every GROQ string must parse and must not be a `_type` discovery scan.", AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\`\`\`json\n${JSON.stringify(minimalExample, null, 2)}\n\`\`\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\`\`\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\`\`\`\n`, getWorkflowAuthoringGuideTool = defineWorkflowTool({
|
|
624
644
|
name: "get_workflow_authoring_guide",
|
|
625
645
|
description: "Get the guide for authoring a workflow definition: the DSL shape, the GROQ condition built-ins, the sugars, the rules the validator enforces, and two worked JSON examples. Call this BEFORE writing a definition from a description, then generate the JSON and check it with validate_workflow_definition. Pure read; takes no arguments.",
|
|
626
646
|
inputSchema: {},
|
|
@@ -655,7 +675,7 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
|
|
|
655
675
|
}
|
|
656
676
|
}), getWorkflowStateTool = defineWorkflowTool({
|
|
657
677
|
name: "get_workflow_state",
|
|
658
|
-
description: `Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable \`workflowTitle\`, the current stage, the subject document the workflow is about (when the workflow has a conventional subject — its ref and title), every in-scope activity on the current stage, and the most recent history entries. Per activity: its \`classification\` (who
|
|
678
|
+
description: `Get the current state of a single workflow instance, projected for action. Returns the workflow's id and human-readable \`workflowTitle\`, the current stage, the subject document the workflow is about (when the workflow has a conventional subject — its ref and title), every in-scope activity on the current stage, and the most recent history entries, plus a one-line \`autonomy\` narrative saying whether the workflow runs itself and where it waits on someone. Per activity: its \`classification\` (who fires its actions: interactive, autonomous, off-system, or hybrid), the causal \`completesWithoutCaller\` verdict (yes/no/conditional — a mechanically autonomous activity whose triggers only read caller-written state answers no) with narrated \`waitsOn\` lines when not yes, its invocable \`actions\` (each with an allowed/disabled verdict — these are what fire_action accepts), and its \`automations\` (cascade-fired actions the ENGINE fires on its own when their \`firesWhen\` trigger holds — never invocable via fire_action). Activities and actions that exist in the definition but are scoped out for this visit or actor are simply absent. Use this whenever you need to understand what's possible on an instance before deciding to act. This is a pure read — it does not change anything. ${UNTRUSTED_AUTHORED_DATA_NOTE} If you only need to discover what instances exist, use list_workflow_instances instead; this tool requires you to know the instance id. list_workflow_instances also returns the same \`workflowTitle\` and \`subject\` fields, so prefer it for fan-out discovery rather than polling get_workflow_state per instance.`,
|
|
659
679
|
inputSchema: {
|
|
660
680
|
...workflowAddressFields,
|
|
661
681
|
instance_id: instanceIdField
|
|
@@ -680,9 +700,9 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
|
|
|
680
700
|
readOnlyHint: !0
|
|
681
701
|
},
|
|
682
702
|
run: async context => {
|
|
683
|
-
const {engine: engine} = await context(), deployed = await engine.query({
|
|
703
|
+
const {engine: engine} = await context(), deployed = (await engine.query({
|
|
684
704
|
groq: workflowEngine.definitionsListGroq("desc")
|
|
685
|
-
});
|
|
705
|
+
})).map(workflowEngine.assertReadableModel);
|
|
686
706
|
return {
|
|
687
707
|
definitions: workflowEngine.latestDeployedDefinitions(deployed).map(def => ({
|
|
688
708
|
name: def.name,
|
|
@@ -720,22 +740,55 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
|
|
|
720
740
|
}, {query: query, params: params} = workflowEngine.instancesQuery({
|
|
721
741
|
tag: engine.tag,
|
|
722
742
|
filter: filter
|
|
723
|
-
}), recency = `${query} | order(lastChangedAt desc)`, docs = await engine.query({
|
|
743
|
+
}), recency = `${query} | order(lastChangedAt desc)`, docs = (await engine.query({
|
|
724
744
|
groq: document === void 0 ? `${recency}[0...${LIST_CAP}]` : recency,
|
|
725
745
|
params: params
|
|
726
|
-
}), capped = (document === void 0 ? docs : docs.filter(doc => workflowEngine.instanceWatchesDocument(doc, document))).slice(0, LIST_CAP), subjectTitles = await fetchSubjectTitles(engine, capped);
|
|
746
|
+
})).map(workflowEngine.assertReadableModel), capped = (document === void 0 ? docs : docs.filter(doc => workflowEngine.instanceWatchesDocument(doc, document))).slice(0, LIST_CAP), subjectTitles = await fetchSubjectTitles(engine, capped);
|
|
727
747
|
return {
|
|
728
748
|
instances: capped.map(doc => projectSummary(doc, subjectTitles))
|
|
729
749
|
};
|
|
730
750
|
}
|
|
731
|
-
})
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
async function startResolvingRetries(args) {
|
|
754
|
+
const {engine: engine, input: input, initialFields: initialFields} = args;
|
|
755
|
+
try {
|
|
756
|
+
const {instance: instance} = await engine.startInstance({
|
|
757
|
+
definition: input.definition,
|
|
758
|
+
...input.version !== void 0 ? {
|
|
759
|
+
version: input.version
|
|
760
|
+
} : {},
|
|
761
|
+
...initialFields.length > 0 ? {
|
|
762
|
+
initialFields: initialFields
|
|
763
|
+
} : {},
|
|
764
|
+
...input.instance_id !== void 0 ? {
|
|
765
|
+
instanceId: input.instance_id
|
|
766
|
+
} : {}
|
|
767
|
+
});
|
|
768
|
+
return {
|
|
769
|
+
instanceId: instance._id
|
|
770
|
+
};
|
|
771
|
+
} catch (err) {
|
|
772
|
+
if (err instanceof workflowEngine.StartNotPrimedError) throw new Error(`${workflowEngine.errorMessage(err)} Retry start_workflow with instance_id "${err.instanceId}" to resume, or abort_workflow to discard it.`, {
|
|
773
|
+
cause: err
|
|
774
|
+
});
|
|
775
|
+
if (!(err instanceof workflowEngine.StartNotSettledError)) throw err;
|
|
776
|
+
return {
|
|
777
|
+
instanceId: err.instanceId,
|
|
778
|
+
startNotSettled: `The workflow started (created and primed) but its first auto-advance failed: ${workflowEngine.errorMessage(err.cause)}. It settles on the next engine tick, or retry start_workflow with instance_id "${err.instanceId}".`
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const startWorkflowTool = defineWorkflowTool({
|
|
732
784
|
name: "start_workflow",
|
|
733
785
|
description: "Start a new workflow instance from a deployed definition — the lifecycle entry point. Use list_workflow_definitions first: `startable: true` marks what this tool can start (child workflows are spawn-only — a parent workflow's activity creates them, never this tool). Supply values for the workflow's input-sourced fields via `initial_fields` — e.g. the subject document the workflow is about. Returns the started instance, same shape as get_workflow_state; the engine's cascade (triggers and transitions) has already run, so it may land past the initial stage. Do NOT use this to advance an existing instance — that is fire_action. " + UNTRUSTED_AUTHORED_DATA_NOTE,
|
|
734
786
|
inputSchema: {
|
|
735
787
|
...workflowAddressFields,
|
|
736
788
|
definition: zod.z.string().min(1).describe("The workflow definition `name` to start (as listed by list_workflow_definitions)."),
|
|
737
789
|
version: zod.z.number().int().min(1).describe("Optional. The deployed definition version to start from. Defaults to the highest.").optional(),
|
|
738
|
-
initial_fields: zod.z.record(zod.z.string(), zod.z.unknown()).describe('Optional. Values for the workflow\'s input-sourced field entries, keyed by field name (e.g. {"subject": {"id": "dataset:proj:ds:article-1", "type": "article"}}). doc.ref values take an object with a GDR `id` and doc `type`. get_workflow_definition shows a workflow\'s declared fields; only input-sourced entries accept a value here.').optional()
|
|
790
|
+
initial_fields: zod.z.record(zod.z.string(), zod.z.unknown()).describe('Optional. Values for the workflow\'s input-sourced field entries, keyed by field name (e.g. {"subject": {"id": "dataset:proj:ds:article-1", "type": "article"}}). doc.ref values take an object with a GDR `id` and doc `type`. get_workflow_definition shows a workflow\'s declared fields; only input-sourced entries accept a value here.').optional(),
|
|
791
|
+
instance_id: zod.z.string().min(1).describe("Optional. Start under this instance id — for retries. The id is the start's idempotency key: pass the SAME id when retrying a start that errored and the engine resumes that start instead of creating a duplicate instance (an already-settled start replays as a no-op). A failed start names the id to retry with in its error message. Omit to mint a fresh id.").optional()
|
|
739
792
|
},
|
|
740
793
|
annotations: {
|
|
741
794
|
readOnlyHint: !1,
|
|
@@ -754,19 +807,18 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
|
|
|
754
807
|
const initialFields = workflowEngine.buildInitialFields({
|
|
755
808
|
declared: deployed.fields ?? [],
|
|
756
809
|
values: input.initial_fields ?? {}
|
|
757
|
-
}), {
|
|
758
|
-
definition: input.definition,
|
|
759
|
-
...input.version !== void 0 ? {
|
|
760
|
-
version: input.version
|
|
761
|
-
} : {},
|
|
762
|
-
...initialFields.length > 0 ? {
|
|
763
|
-
initialFields: initialFields
|
|
764
|
-
} : {}
|
|
765
|
-
});
|
|
766
|
-
return getInstanceState({
|
|
810
|
+
}), {instanceId: instanceId, startNotSettled: startNotSettled} = await startResolvingRetries({
|
|
767
811
|
engine: engine,
|
|
768
|
-
|
|
812
|
+
input: input,
|
|
813
|
+
initialFields: initialFields
|
|
814
|
+
}), state = await getInstanceState({
|
|
815
|
+
engine: engine,
|
|
816
|
+
instanceId: instanceId
|
|
769
817
|
});
|
|
818
|
+
return startNotSettled !== void 0 ? {
|
|
819
|
+
...state,
|
|
820
|
+
startNotSettled: startNotSettled
|
|
821
|
+
} : state;
|
|
770
822
|
}
|
|
771
823
|
}), validateWorkflowDefinitionTool = defineWorkflowTool({
|
|
772
824
|
name: "validate_workflow_definition",
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionParam } from "@sanity/workflow-engine";
|
|
2
|
+
import type { AutonomyVerdict } from "@sanity/workflow-engine";
|
|
2
3
|
import type { Diagnosis } from "@sanity/workflow-engine";
|
|
3
4
|
import type { Engine } from "@sanity/workflow-engine";
|
|
4
5
|
import type { ExecutorClassification } from "@sanity/workflow-engine";
|
|
@@ -63,11 +64,25 @@ export declare interface ProjectedActivity {
|
|
|
63
64
|
* projection, so `skipped` here always means an action resolved it so. */
|
|
64
65
|
status: "active" | "done" | "skipped" | "failed";
|
|
65
66
|
/**
|
|
66
|
-
* Who, if anyone, the activity
|
|
67
|
-
* actions), `autonomous` (only cascade-fired automation),
|
|
68
|
-
* (work performed outside the system), or `hybrid` (both
|
|
67
|
+
* Who, if anyone, fires the activity's actions — `interactive` (only
|
|
68
|
+
* invocable actions), `autonomous` (only cascade-fired automation),
|
|
69
|
+
* `off-system` (work performed outside the system), or `hybrid` (both
|
|
70
|
+
* kinds). Shape-only; `completesWithoutCaller` is the causal answer.
|
|
69
71
|
*/
|
|
70
72
|
classification: ExecutorClassification;
|
|
73
|
+
/**
|
|
74
|
+
* The causal refinement of `classification`: whether the activity resolves
|
|
75
|
+
* with no caller acting, following its triggers' reads to their producers
|
|
76
|
+
* (a mechanically autonomous trigger that only reads caller-written state
|
|
77
|
+
* still answers `no`). `conditional` means it depends on runtime values
|
|
78
|
+
* the static analysis can't see.
|
|
79
|
+
*/
|
|
80
|
+
completesWithoutCaller: AutonomyVerdict;
|
|
81
|
+
/** What the activity waits on when it doesn't complete on its own
|
|
82
|
+
* ("waits on caller action …"), narrated in the workflow's vocabulary.
|
|
83
|
+
* Present only when `completesWithoutCaller` isn't `yes` AND something is
|
|
84
|
+
* narratable (a bare `conditional`, e.g. a cyclic gate, carries none). */
|
|
85
|
+
waitsOn?: string[];
|
|
71
86
|
/** Invocable actions on this activity, each with its allowed/disabled
|
|
72
87
|
* verdict. See {@link ProjectedActionVerdict} for what never appears. */
|
|
73
88
|
actions: ProjectedActionVerdict[];
|
|
@@ -164,6 +179,9 @@ export declare interface ProjectedInstanceState {
|
|
|
164
179
|
subject?: ProjectedSubject;
|
|
165
180
|
/** Activities on the current stage, with available actions. */
|
|
166
181
|
activities: ProjectedActivity[];
|
|
182
|
+
/** The workflow-level causal-autonomy narrative — "this workflow runs
|
|
183
|
+
* itself", or the stages where a run waits on someone. */
|
|
184
|
+
autonomy: string;
|
|
167
185
|
/** Most recent history entries (newest first), capped to keep payload tight. */
|
|
168
186
|
recentHistory: ProjectedHistoryEntry[];
|
|
169
187
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionParam } from "@sanity/workflow-engine";
|
|
2
|
+
import type { AutonomyVerdict } from "@sanity/workflow-engine";
|
|
2
3
|
import type { Diagnosis } from "@sanity/workflow-engine";
|
|
3
4
|
import type { Engine } from "@sanity/workflow-engine";
|
|
4
5
|
import type { ExecutorClassification } from "@sanity/workflow-engine";
|
|
@@ -63,11 +64,25 @@ export declare interface ProjectedActivity {
|
|
|
63
64
|
* projection, so `skipped` here always means an action resolved it so. */
|
|
64
65
|
status: "active" | "done" | "skipped" | "failed";
|
|
65
66
|
/**
|
|
66
|
-
* Who, if anyone, the activity
|
|
67
|
-
* actions), `autonomous` (only cascade-fired automation),
|
|
68
|
-
* (work performed outside the system), or `hybrid` (both
|
|
67
|
+
* Who, if anyone, fires the activity's actions — `interactive` (only
|
|
68
|
+
* invocable actions), `autonomous` (only cascade-fired automation),
|
|
69
|
+
* `off-system` (work performed outside the system), or `hybrid` (both
|
|
70
|
+
* kinds). Shape-only; `completesWithoutCaller` is the causal answer.
|
|
69
71
|
*/
|
|
70
72
|
classification: ExecutorClassification;
|
|
73
|
+
/**
|
|
74
|
+
* The causal refinement of `classification`: whether the activity resolves
|
|
75
|
+
* with no caller acting, following its triggers' reads to their producers
|
|
76
|
+
* (a mechanically autonomous trigger that only reads caller-written state
|
|
77
|
+
* still answers `no`). `conditional` means it depends on runtime values
|
|
78
|
+
* the static analysis can't see.
|
|
79
|
+
*/
|
|
80
|
+
completesWithoutCaller: AutonomyVerdict;
|
|
81
|
+
/** What the activity waits on when it doesn't complete on its own
|
|
82
|
+
* ("waits on caller action …"), narrated in the workflow's vocabulary.
|
|
83
|
+
* Present only when `completesWithoutCaller` isn't `yes` AND something is
|
|
84
|
+
* narratable (a bare `conditional`, e.g. a cyclic gate, carries none). */
|
|
85
|
+
waitsOn?: string[];
|
|
71
86
|
/** Invocable actions on this activity, each with its allowed/disabled
|
|
72
87
|
* verdict. See {@link ProjectedActionVerdict} for what never appears. */
|
|
73
88
|
actions: ProjectedActionVerdict[];
|
|
@@ -164,6 +179,9 @@ export declare interface ProjectedInstanceState {
|
|
|
164
179
|
subject?: ProjectedSubject;
|
|
165
180
|
/** Activities on the current stage, with available actions. */
|
|
166
181
|
activities: ProjectedActivity[];
|
|
182
|
+
/** The workflow-level causal-autonomy narrative — "this workflow runs
|
|
183
|
+
* itself", or the stages where a run waits on someone. */
|
|
184
|
+
autonomy: string;
|
|
167
185
|
/** Most recent history entries (newest first), capped to keep payload tight. */
|
|
168
186
|
recentHistory: ProjectedHistoryEntry[];
|
|
169
187
|
}
|
package/dist/stdio.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/workflow-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "MCP server exposing Sanity workflow tools to agents — operate running workflow instances and author new definitions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
"@sanity/client": "^7.22.1",
|
|
57
57
|
"@sanity/telemetry": "^1.1.0",
|
|
58
58
|
"zod": "^4.4.3",
|
|
59
|
-
"@sanity/workflow-engine": "0.
|
|
59
|
+
"@sanity/workflow-engine": "0.17.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@sanity/pkg-utils": "^10.5.2",
|
|
63
63
|
"@types/node": "^24.12.4",
|
|
64
64
|
"vitest": "^4.1.8",
|
|
65
|
-
"@sanity/workflow-engine-test": "0.
|
|
66
|
-
"@sanity/workflow-examples": "0.
|
|
65
|
+
"@sanity/workflow-engine-test": "0.12.0",
|
|
66
|
+
"@sanity/workflow-examples": "0.7.0"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": ">=20"
|