@sanity/workflow-engine 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,328 @@
1
1
  # @sanity/workflow-engine
2
2
 
3
+ ## 0.33.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8874c50: **BREAKING:** A `subject`, `doc.ref`, or `doc.refs` field marked `required: true`
8
+ now requires its selected documents to remain readable after initialization.
9
+ Previously, the flag checked only that an initial value was supplied. Missing
10
+ required targets now produce a fault and prevent normal actions, triggered
11
+ actions, and transitions from advancing, even without an activity requirement
12
+ reading those targets. Abort and direct edits to editable fields remain
13
+ available. Optional references fault only when an unmet runtime condition needs
14
+ their content; completed workflows remain completed.
15
+
16
+ `StuckCause` adds `document-missing`. Update exhaustive handlers before upgrading,
17
+ or their typechecks fail and renderers have no matching branch. Evaluation
18
+ identifies the affected field and reference in `missingDocuments`, with its title
19
+ and completed availability evidence; `blockingMissingDocuments` selects the
20
+ references that prevent progress. The engine checks draft, published, and release
21
+ representations before distinguishing deletion from inaccessible content or
22
+ content outside the workflow perspective. Studio, CLI, and MCP use that same
23
+ result. Failed availability checks report unreadable content and log the cause;
24
+ optional references still permit unrelated actions. Incomplete reads do not
25
+ claim deletion, and an existing task fault keeps
26
+ its recovery controls. The fault clears when the required content becomes
27
+ readable; this release adds no undo-delete operation and does not automatically
28
+ abort workflows.
29
+
30
+ Upgrade every Workflows runtime sharing affected data, including Studio, CLI,
31
+ MCP servers, and deployed Functions, before relying on continued required-target
32
+ availability. Then set the deployment's reviewed `expectedMinReaderModel` to
33
+ `10` before deploying definitions with required content references. Writers stamp
34
+ model 10; this feature requires reader model 10, while documents without it keep
35
+ the floor required by their other features, normally 4, 8, or 9. Existing
36
+ instances remain readable without a backfill and adopt the rule under upgraded
37
+ engines. Their stored floor rises on the next full write, so an older runtime
38
+ can still advance an unstamped existing instance until the fleet is upgraded.
39
+ See `packages/workflow-engine/DATAMODEL.md` for the complete rollout contract.
40
+
41
+ The rendered CLI `show` command evaluates running instances. If evaluation fails,
42
+ it warns and displays stored state; terminal instances and `show --json` retain
43
+ their stored-state behavior.
44
+
45
+ **Docs impact:** Update field requiredness and initialization guidance, required
46
+ reference repair examples, the model-10 readers-first rollout, the diagnostics
47
+ reference for `StuckCause` and `MissingDocument`, CLI `show` and `diagnose`, MCP
48
+ workflow-state guidance, and the Workflows tool and document-view guides for
49
+ loading, deletion, permissions, and perspectives.
50
+
51
+ - 0555271: An effect node can now declare a bounded retry policy the engine enforces on every runtime:
52
+ `retry: {kind: 'engine', attempts, backoff?: {kind: 'fixed' | 'exponential', delayMs}, expiryMs?}`.
53
+ The block's `kind` names who runs the policy and `engine` is the only accepted member; authoring may
54
+ omit it, as with `start.kind`, and `defineWorkflow` fills it in while desugaring, so a stored block
55
+ without it fails to parse at deploy. `attempts` is the total number of attempts
56
+ including the first, `backoff` is the wait between two attempts
57
+ (`delayMs` every time under `fixed`, doubled per attempt already made under `exponential`), and
58
+ `expiryMs` decides whether a further attempt may start: the engine stops rather than begin a wait
59
+ that would carry the run past it, so an attempt can go unused. It is not a handler timeout: an
60
+ attempt already running is never interrupted, so a run can finish after the window and a success
61
+ then still counts.
62
+
63
+ The whole policy runs inside one `drainEffects` call, under one claim whose lease is renewed across
64
+ each wait, and **every attempt invokes the handler again**. That is a third cause of the
65
+ at-least-once repetition handlers already tolerate, and no `effectHistory` row sits between two
66
+ attempts, so derive external-system identifiers from `ctx.effectKey` rather than checking history
67
+ to deduplicate. Two
68
+ things stop a run without settling it, both reported in the drain's `lost` bucket. A lease this
69
+ drainer can no longer renew leaves the entry pending, for a later drain to pick up once the lease
70
+ lapses. An entry another party completed or cancelled during a backoff is already settled by that
71
+ party, and the handler is not invoked again.
72
+ Otherwise the run settles in the single `effectCompleted` row the drain has always written: `failed`
73
+ with a `detail` naming the attempts used and the last error (`failed after 3 of 3 attempts: gateway
74
+ timeout`), or `done` with a `detail` naming the attempt that succeeded when a later one did. A
75
+ policy changes no routing rule, since `$effectStatus['<name>'] == 'failed'` routes the instance
76
+ exactly as a single failure does. It does change which outcome those rules see, because an attempt
77
+ after the first that succeeds makes the status `done`. Adding a policy to an effect a `'failed'`
78
+ branch already watches is therefore a behaviour change, not only a latency change. Nothing
79
+ about an attempt is persisted, so no scheduler is involved and no per-attempt history rows appear.
80
+ An effect that declares no `retry` is unchanged: its handler's first failure settles it.
81
+
82
+ The waits are the engine's only deliberate pause, so `createEngine({sleep})` now takes a `Sleeper`
83
+ that decides how they are spent. Production omits it and spends real time; a deterministic harness
84
+ passes one that moves its `clock` forward instead, which is what the engine test bench does, so a
85
+ paced policy runs in a test without waiting.
86
+
87
+ **No upgrade action required** to keep existing definitions working; `sweepStaleClaims` and every
88
+ history shape are unchanged, and `drainEffects` keeps its signature and result shape. Two things to
89
+ weigh before adopting it. The
90
+ waits are real time inside the drain's invocation, so on a Functions host the policy has to fit
91
+ that function's timeout. Handing the policy to a Durable Functions host's own retry strategy
92
+ instead is future work in the generated runtime, not part of this contract; today every host waits
93
+ inside the invocation. And **declaring `retry` raises the definition's reader floor to model 10**:
94
+ `DATA_MODEL_VERSION` and `DATA_MODEL_MAX_READER` both move to 10, so upgrade every Studio, CLI, MCP
95
+ server, Function, and other runtime sharing that workflow resource, then raise the reviewed
96
+ `expectedMinReaderModel` literal to `10`, then deploy. A pre-model-10 reader would ignore the
97
+ policy and settle the effect on its first failure. Deploy refuses a `retry` definition against an
98
+ acknowledgement below 10, one whose backoff cannot fit its declared `attempts` inside `expiryMs`,
99
+ and one whose `expiryMs` or accumulated backoff exceeds **366 days**, since the whole policy runs
100
+ inside one `drainEffects` invocation and no host offers a single execution longer than a year.
101
+
102
+ **Docs impact:** Update the effects concept in the Workflows concepts guide to describe retry as an
103
+ in-invocation loop and name the per-host cost (the functions timeout, and durables delegation as future work), add `retry`
104
+ and its one-year span ceiling to the effect-node reference, note the loop in the `drainEffects` protocol entry along with what
105
+ the `lost` bucket now covers, list the `sleep` option in the `createEngine` reference, and add the retry
106
+ feature to the crossing-to-model-10 section of the reader-model rollout guide. A cookbook recipe for a bounded
107
+ external call should show the `retry` node instead of the counter-field retry loop.
108
+
109
+ - b3b2797: Where the generated unattended runtime hosts a workflow or an effect is declared as one `runtime` block on three authoring nodes: the deployment in `sanity.workflow.ts`, the workflow in `defineWorkflow`, and the effect node. `kind` is `'function'` (a plain Sanity Function), `'durableFunction'` (a durable one), or `'selfHosted'` (a process you run yourself). Each level inherits from the one above, so a workflow that declares no block takes its deployment's kind and an effect takes its workflow's. Omit it everywhere and everything hosts on `'function'`. `kind` is the whole block at the deployment and workflow levels; an effect's `'function'` block also carries the function budget, `timeout` in seconds and `memory` in megabytes, and an effect that declares either gets its own drain function so an expensive handler's budget does not size every other handler's. An unknown kind fails at `defineWorkflow` or `defineWorkflowConfig`, before any deploy runs, naming the three accepted members; a budget on any other kind is rejected there too.
110
+
111
+ `@sanity/workflow-blueprint`'s emission plan groups by resolved kind. A `'function'` workflow gets the drain, the heartbeat, and the start watcher it always got. A `'durableFunction'` workflow or effect is listed under `planned.hosting` and emits nothing yet, while the rest of the deployment emits normally, with no error and no whole-deployment refusal. A `'selfHosted'` workflow or effect emits nothing, is skipped by the generated drains, is left out of the generated handler registry, and is reported under `planned.selfHosted`, which names the deadlines that process must tick, the effects it must drain, the parents it must re-evaluate when a child settles, and the autonomous definitions it must start. Two definitions in one deployment that host the same effect name differently fail generation, because one effect name has one handler and one budget. A start watcher is emitted for a durable workflow as well as a function-hosted one, because starting an instance does not depend on the host.
112
+
113
+ The block is authoring-only. `defineWorkflow` returns the stored definition plus the `runtime` the generator reads, and the deploy strips it: it never reaches the Content Lake, the stored definition schema rejects it, and two definitions that differ only in a hosting kind fingerprint identically, so changing a kind never mints a definition version. It does change the emitted tree, so regenerate afterwards and delete the function directories the new tree no longer contains.
114
+
115
+ **No upgrade action required.** Nothing this release replaces has been published: the deployment `runtime` block and the `@sanity/workflow-blueprint/generate` entry point that exposed the earlier planning helpers both ship for the first time here. Declare a block only where you want to move a workflow or an effect off the default.
116
+
117
+ **Docs impact:** Document the `runtime` block once, at all three levels, in the deployment configuration reference beside `tag`, `workflowResource`, and `resourceAliases`: each kind, what the generator emits for it, and the effect-level `timeout` and `memory` units. Add the four hosting shapes (function hosted, durable hosted, self hosted with one workflow kept on functions, and mixed inside one workflow) to the generated-runtime guide, and state there that hosting decides whether a `$now` deadline causes a heartbeat to be emitted rather than which instances an emitted heartbeat ticks. State in the data-model concepts that `runtime` is the authoring-only block tooling reads and the deploy never stores.
118
+
119
+ - 225e0fb: **BREAKING:** Workflows App SDK and Studio integrations now require `@sanity/sdk` 3.1 or later in the 3.x line, and `@sanity/workflow-sdk` requires the matching `@sanity/sdk-react` 3.1 line when its React entry is used. The previous SDK 2 peer contract is no longer supported. The exported `WorkflowClient`, `TelemetryIntakeClient`, `ProjectUserProfileClient`, and `StudioUserClient` request contracts now pass their target in `url`; the previous `uri` request target is no longer used. The engine's effective client config also accepts the broader `{type: string, id: string}` resource descriptors returned by Sanity client 8.
120
+
121
+ Before upgrading Workflows, upgrade `@sanity/sdk` and `@sanity/sdk-react` together to 3.1 or later. Applications that stay on SDK 2 must stay on an earlier Workflows release. If you implement any of the request contracts named above, update it to read the request target from `url` instead of `uri`; otherwise its request-backed reads will fail. Existing `WorkflowClient.config()` implementations need no change when their resource descriptor already has string `type` and `id` fields. No separate CLI upgrade action is required; its definition-sharing and telemetry requests adopt `url` internally.
122
+
123
+ Before installing, override SDK 3's `@sanity/mutate` dependency to `0.18.2` in your application's root package-manager configuration, reinstall, and commit the updated lockfile. SDK 3.1.0 allows Mutate 0.18.1, which can leave document reads pending with client 8. For npm, set `overrides["@sanity/sdk"]["@sanity/mutate"]` to `"0.18.2"` in `package.json`. For pnpm, set `overrides['@sanity/sdk@3>@sanity/mutate']` to `0.18.2` in `pnpm-workspace.yaml`. Keep the override until your SDK release requires Mutate 0.18.2 or later. Complete examples and verification steps are in the Installation section of the published `@sanity/workflow-sdk` README.
124
+
125
+ Malformed project-member responses now report an inaccessible directory and can recover on a later lookup, instead of being cached as a missing user. No additional upgrade action is required for this correction.
126
+
127
+ **Docs impact:** Update the App SDK and Studio integration installation guidance, package compatibility references, and examples to require Sanity App SDK 3.1 and the consumer Mutate override described in the published `@sanity/workflow-sdk` README; update the client API references for `request({url})`, effective resource descriptors, and directory-response failures.
128
+
129
+ ### Patch Changes
130
+
131
+ - 393ac71: CLI and MCP telemetry now includes `context.surface` (`cli` or `mcp`) and
132
+ execution mode in each event's `context.environment`. Dashboards can include
133
+ shell and engine activity while distinguishing it from SDK events marked
134
+ `sdk`. Existing command trace context is preserved.
135
+
136
+ All three surfaces report `production` for `NODE_ENV=production`, and
137
+ `development` for `development` or `test`. Unset, empty, and unrecognized values
138
+ default to `production` for CLI and MCP execution mode, and `development` for
139
+ SDK build mode. SDK environment classification is unchanged. When comparing
140
+ activity across surfaces, group or filter by surface alongside environment.
141
+ Environment does not identify a production dataset or deployment; API host,
142
+ dataset name, and workflow tag do not set it.
143
+
144
+ **No upgrade action required.** Existing telemetry consent and opt-out
145
+ settings still apply. Historical events are unchanged.
146
+
147
+ **Docs impact:** Update CLI, MCP, and SDK telemetry guidance to explain surface
148
+ context, build versus execution mode, the explicit defaults, and how to
149
+ interpret environment when comparing activity across surfaces.
150
+
151
+ - 7eb9eca: Correct the API references for field initialization and edits, start requirements,
152
+ transitions, reference IDs, effect handling, reactive state, member controls,
153
+ Studio mappings, test helpers, and GROQ condition outcomes. The references state
154
+ caller constraints and defaults that were missing or incorrect. Package setup
155
+ guidance identifies the public npm packages and supported deployment command;
156
+ the MCP validation description distinguishes validation from deployment checks.
157
+ Runtime behavior and API signatures are unchanged.
158
+
159
+ **No upgrade action required.**
160
+
161
+ **Docs impact:** After release and reference sync, reconcile the modeling,
162
+ runtime, reactive UI, Studio, testing, deployment, MCP, and evaluation-insight
163
+ guides and references with the corrected contracts. Fix affected examples and
164
+ replace redundant API inventories with verified symbol links while preserving
165
+ useful teaching and the CLI/MCP reference material not exposed by TypeDoc.
166
+
167
+ - 2cef086: Internal maintenance consolidates engine action/edit handling and Studio document
168
+ and workflow title sorting. Action and field-edit calls retain their inputs and
169
+ results, and document and workflow titles retain their ordering. Action
170
+ availability, telemetry, and persisted document formats are unchanged.
171
+
172
+ **No upgrade action required.**
173
+
174
+ **Docs impact: None** because public APIs, configuration, and workflow behavior
175
+ are unchanged.
176
+
177
+ - 232f811: Workflows can resolve reviewer attributes from self-hosted studios without
178
+ sending the attributes request to a global API host that rejects their origin.
179
+ Actor resolution uses the account-global identity carried by the project user
180
+ response when available, avoiding an unnecessary global request.
181
+
182
+ Custom clients without `withConfig` can also resolve organization attributes
183
+ when they already serve the engine's required API version.
184
+
185
+ **No upgrade action required.** Existing project CORS settings and workflow
186
+ definitions continue to apply.
187
+
188
+ **Docs impact:** Update Workflows prerelease troubleshooting guidance to describe
189
+ the fix for assignee loading on self-hosted studios and advise upgrading the
190
+ Workflows packages together. Document the version requirement for custom clients
191
+ in the engine client reference.
192
+
193
+ - Updated dependencies [7eb9eca]
194
+ - @sanity/groq-condition-describe@0.5.1
195
+
196
+ ## 0.32.0
197
+
198
+ ### Minor Changes
199
+
200
+ - 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.
201
+
202
+ 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.
203
+
204
+ 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.
205
+
206
+ `@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.
207
+
208
+ **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.
209
+
210
+ - 2de38fd: **BREAKING:** `createEngine` takes its effect settings as one `effects` group. The top-level `effectHandlers`, `effectLeaseMs`, and `missingHandler` options are removed, with no alias and no deprecation shim, so everyone who builds an engine — a Sanity Function drainer, a hosted runtime, a script, a test — must move them into `effects: {handlers, leaseMs, missingHandler}`. An unmigrated TypeScript call stops compiling. An unmigrated plain-JavaScript call still builds an engine, but one with no handlers and the default `fail` policy, so its first drain throws `MissingHandlerError` instead of dispatching.
211
+
212
+ Migrate every construction site: `createEngine({client, workflowResource, tag, effectHandlers: H, effectLeaseMs: L, missingHandler: P})` becomes `createEngine({client, workflowResource, tag, effects: {handlers: H, leaseMs: L, missingHandler: P}})`. The resolved values read back off the engine under the same word, so `engine.effectHandlers` and `engine.missingHandler` become `engine.effects.handlers` and `engine.effects.missingHandler`. Nothing about draining changes: the same `fail` default, the same five-minute default lease, and the same claim, dispatch, and deploy-verification semantics. `@sanity/workflow-engine-test`'s `createBenchEngine(bench, overrides)` forwards `CreateEngineArgs` unchanged, so bench engines move their handler and policy overrides into the same group.
213
+
214
+ `@sanity/workflow-studio`'s `useWorkflowEngine` follows the engine: its `effectHandlers` and `missingHandler` props are replaced by one `effects` prop taking the engine's exported `EngineEffectsArgs`, so a Studio-side drainer passes `effects: {handlers, missingHandler}`. Keep that object referentially stable, at module scope or through `useMemo` — the hook memoizes the engine on it, and a new object every render rebuilds the engine every render. The `@sanity/workflow-studio-plugin` `effectHandlers` config key is unchanged; the plugin translates it into the group where it builds the engine.
215
+
216
+ **Docs impact:** Update the `createEngine` options reference and the effects concepts page for the new group, the missing-handler and claim-lease reference entries that named the old keys, the `useWorkflowEngine` adapter reference, the Studio plugin README's drain-function example, and every cookbook or runtime example that constructs an engine with handlers; carry the migration into the release notes.
217
+
218
+ - a2ce4a7: **BREAKING:** Mutation guards now have one public compilation path:
219
+ `compileGuards` replaces the singular `compileGuard` export and returns every
220
+ Lake document required for the authored guard. Callers using `compileGuard`
221
+ must switch to `compileGuards` and persist every returned document; retaining
222
+ the old call would either fail to compile after upgrade or omit required
223
+ ID-space siblings. Literal field-seed document IDs now also enforce the Lake's
224
+ 128-character limit and reject double dots; replace an invalid seed with a
225
+ valid resource-local document ID before upgrading. `MutationContext.action` now admits only the
226
+ Lake's `create`, `update`, and `delete` operations, and
227
+ `documentActionDenials` takes their concrete `before` / `after` mutation image
228
+ instead of a prospective document plus authored lifecycle action. Callers of
229
+ that helper must construct the same mutation the Lake will evaluate; otherwise
230
+ their preview can disagree with enforcement. Pure `evaluateFromSnapshot`
231
+ callers that supply dereference-bearing guards must also pass a
232
+ `guardDereference` resolver for stored resource reads; the engine and reactive
233
+ session supply their token-bound client resolver automatically.
234
+
235
+ Mutation guards now deploy edit locks against draft IDs and publish or
236
+ unpublish gates against published IDs using the create, update, and delete
237
+ operations Content Lake evaluates. A guard combining both action classes
238
+ emits separate temporary guard documents, preventing either match from leaking
239
+ into the other ID space. Guard previews bind `document.before` and
240
+ `document.after`, follow stored resource-local references, continue to
241
+ understand temporary guards emitted with the older lifecycle-action vocabulary,
242
+ and reject invalid actions, ID patterns, or predicates before deployment.
243
+
244
+ Existing guard definitions keep their authored shape. Definitions whose guards
245
+ stay within one emitted ID space require no rollout change and retain their
246
+ existing reader floor. A guard that combines direct create/delete actions,
247
+ content updates, or publish/unpublish actions across ID spaces is a detectable
248
+ model-9 feature on both its definition and instances.
249
+
250
+ Before deploying such a definition, upgrade every Studio, CLI, MCP server,
251
+ Function, and other engine runtime sharing its workflow resource, then change
252
+ that deployment's reviewed `expectedMinReaderModel` literal to `9`. This
253
+ readers-first order is required because pre-model-9 engines only know the base
254
+ guard document ID: after a newer engine emits an ID-space sibling, an older
255
+ engine could otherwise advance or abort the instance, retract only the base,
256
+ and leave an advisory lock behind. The affected instance commits reader floor
257
+ 9 before split guards deploy, so an old runtime fails explicitly instead of
258
+ stranding a sibling. Existing affected instances need no data migration; their
259
+ first model-9 commit raises the floor before it can create a split guard.
260
+
261
+ **Docs impact:** Update `docs/reference.md` and the public mutation-guard
262
+ reference to explain publish and unpublish action translation, predicate
263
+ document bindings and dereferencing,
264
+ deploy-time validation errors, and why mixed-ID-space guards emit separate
265
+ documents. Update the prerelease reader-model rollout guide with the conditional
266
+ model-9 adoption sequence, including every shared runtime, the
267
+ `expectedMinReaderModel: 9` change, the old-retractor failure mode, and the fact
268
+ that unaffected definitions retain their existing floor and require no data
269
+ migration. Keep the `@sanity/workflow-engine-test` README's draft-ID examples
270
+ and the root live-parity credential guidance aligned with those contracts.
271
+
272
+ - 2ddd3d7: **BREAKING:** `WorkflowErrorKind` can now be `concurrent-cascade`, paired with
273
+ the new exported `ConcurrentCascadeError`, when an automatic cascade exhausts
274
+ its revision-conflict retry budget. Consumers with exhaustive switches or
275
+ `Record<WorkflowErrorKind, …>` mappings over the previous union must add the
276
+ new member before upgrading; otherwise their TypeScript build will fail.
277
+
278
+ Automatic cascade hops now recover from concurrent same-instance commits by
279
+ reloading and re-evaluating state before retrying. Bounded retries for actions,
280
+ field edits, and effect writes now also classify conflicts only at the parent
281
+ instance commit, so a later guard or child-settlement 409 surfaces immediately
282
+ instead of being mislabeled as instance contention. Update exhaustive error
283
+ handling as described above; workflow definitions and stored instances require
284
+ no migration.
285
+
286
+ **Docs impact:** Add `ConcurrentCascadeError` to the engine error reference and
287
+ describe automatic cascade conflict recovery.
288
+
289
+ - 6035672: Every mutating verb's `OperationResult` and `evaluate`'s `WorkflowEvaluation` now carry `nextEvaluationAt`, an optional ISO 8601 instant: the earliest future moment at which the passage of time alone could change what an instance evaluates to. It is the nearest `$now` boundary across every clock-reading condition site in the instance's current stage — transitions, activity filters, requirements, action filters and triggers, and editable-field gates — computed from the instance's current field values and verified by re-evaluating each site at the candidate instant. A runtime that polls on a cron can wait exactly instead (`tick`, read `nextEvaluationAt`, sleep until it, `tick` again), and a live session can re-render on the same instant.
290
+
291
+ Read the absence of the field precisely: it is absent for two reasons and no others. Either no clock-reading site the engine can judge there has a boundary — the stage declares none, or none has one the instance's current values pin, since date arithmetic and dataset scans report nothing rather than a guess — or the instance is terminal and will never re-evaluate. A result with `changed: false` still carries it, which is exactly when a scheduler needs it: a tick that moved nothing because the deadline has not arrived yet still reports when it will. What the engine can judge differs between the two surfaces: it derives the operation-side copy caller-free, so an action filter, requirement, or editable predicate combining `$now` with `$actor`, `$assigned`, `$can`, or `$attributes` is unevaluable there and contributes nothing to a verb result. Such a site answers per actor and a verb result carries no actor, so `evaluate` — projected for one — is the surface that answers it; deploy already keeps caller-bound reads out of the cascade gates the engine acts on.
292
+
293
+ The instant is a pure function of the evaluated instance state, its definition, and the operation's clock reading. It is never persisted and never derived after a write: a cascading verb derives it from the settled in-memory state before that hop persists, a verb returning the instance unchanged derives it from the state it loaded, and a derivation failure fails the call rather than following a commit that already landed.
294
+
295
+ **No upgrade action required.** The field is optional and additive; existing callers, the reactive session, and the Studio and App SDK adapters compile and behave unchanged, and consumers that already return these result types expose the new field without any change of their own.
296
+
297
+ **Docs impact:** The repository reference (`docs/reference.md`) is updated in this change; the published documentation is not. Update the published engine reference where it lists the `OperationResult` and `WorkflowEvaluation` shapes and where the verb entries describe what `tick`, `fireAction`, and `evaluate` return, so both shapes name `nextEvaluationAt` and carry the absence rule above, and correct the `createEngine.clock` entry, which says one reading per operation where the engine takes one per pass. Update the published concepts page where it explains that the engine never runs a clock and that a caller must `tick` when a deadline passes, so the scheduling story says how a runtime learns when that is.
298
+
299
+ - 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.
300
+
301
+ **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.
302
+
303
+ **Docs impact:** Refresh the generated API references for the affected packages; no conceptual guides, examples, or migration guidance need changes.
304
+
305
+ - b04580d: **BREAKING:** The in-memory bench now exposes `GuardDeniedError` when Content Lake-style mutation guards reject client writes, replacing the engine-specific `MutationGuardDeniedError` previously thrown by the fake seam. Bench consumers that catch or assert the old class must import `GuardDeniedError` from `@sanity/workflow-engine-test` and match it instead; without this migration, denial assertions will fail even though the write remains rejected.
306
+
307
+ Publish and unpublish tests must also express guards in terms of the lake mutations they commit: publish updates or creates the published document and deletes its draft, while unpublish creates the draft and deletes the published document. Replace synthetic `publish` and `unpublish` guard-action expectations with the applicable `create`, `update`, and `delete` actions.
308
+
309
+ The engine's `DocumentValuePermission` and `Grant` types now include the lake's `manage` permission so access configurations can represent identities allowed to author system documents. `DOCUMENT_VALUE_PERMISSIONS` is now exported for consumers that need the complete runtime vocabulary.
310
+
311
+ The bench also re-exports `WriteAccessDeniedError`, giving access-control denial assertions the same stable package entry point as mutation-guard denial assertions. No upgrade action is required. Consumers may move existing imports of this class from `@sanity-labs/client-fake-for-test` to `@sanity/workflow-engine-test`.
312
+
313
+ **Docs impact:** Update the engine authorization API reference for the `manage` permission and `DOCUMENT_VALUE_PERMISSIONS`, plus the engine test-bench guard and access-denial guidance for the re-exported error classes and underlying publish/unpublish mutations.
314
+
315
+ ### Patch Changes
316
+
317
+ - 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.
318
+
319
+ **No upgrade action required.** Runtime behavior and public TypeScript contracts are unchanged.
320
+
321
+ **Docs impact:** Regenerate the API reference entries for these packages so their corrected TSDoc and README content is visible.
322
+
323
+ - Updated dependencies [26dd4e5]
324
+ - @sanity/groq-condition-describe@0.5.0
325
+
3
326
  ## 0.31.0
4
327
 
5
328
  ### Patch Changes
package/DATAMODEL.md CHANGED
@@ -6,7 +6,7 @@ The engine owns three standalone document types in the Content Lake:
6
6
  | ---------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------- |
7
7
  | `sanity.workflow.definition` | `planDefinitionDeploy` (`src/api/deploy.ts`) | never — deploys are create-only |
8
8
  | `sanity.workflow.instance` | `buildInstanceBase` (`src/instance.ts`) | every persist writes the canonical state via `instanceStateFields` (`src/engine/mutation.ts`) |
9
- | `temp.system.guard` | `compileGuard` (`src/core/guards.ts`) | full-body refresh on stage re-entry; revision-conditional delete on retract |
9
+ | `temp.system.guard` | `compileGuards` (`src/core/guards.ts`) | full-body refresh on stage re-entry; revision-conditional delete on retract |
10
10
 
11
11
  These field trees are **our contract with every past and future engine
12
12
  version reading the same dataset**. This file is where changes to them are
@@ -66,7 +66,9 @@ never matches the new type).
66
66
 
67
67
  - **`modelVersion` — provenance ("conforms-to").** The value of
68
68
  `DATA_MODEL_VERSION` at write time: "this document conforms to model N
69
- now". Bumps on **every** declared shape change, additive included.
69
+ now". Advances for **every** declared shape change, additive included,
70
+ though several changes merged into one unreleased model share its number
71
+ (rule 9).
70
72
  Instances are re-stamped on every full persist (and rollback restore).
71
73
  Partial patches deliberately leave the pair alone — they don't normalize
72
74
  the shape, so restamping there would lie.
@@ -210,6 +212,13 @@ these principles before the change-process rules below apply.
210
212
  definition grammar has no kebab-case values, and no addition introduces
211
213
  the first.
212
214
 
215
+ One block sits outside the persisted language entirely: `runtime` declares where
216
+ the generated unattended runtime hosts a workflow or an effect, tooling reads it
217
+ at authoring and generation time, and the deploy strips it so nothing reaches the
218
+ Content Lake. One rule decides the category for any new property: it is stored
219
+ if the engine reads it while running, and authoring-only if only tooling reads it
220
+ before deploy.
221
+
213
222
  ## Rules for changing the model
214
223
 
215
224
  `DATA_MODEL_CHANGES` is the append-only, machine-readable counterpart of the
@@ -234,11 +243,12 @@ retained model-4 baseline.
234
243
  log below, and decide — in the same change — whether `DATA_MODEL_VERSION`
235
244
  moves **and whether the reader floor moves with it**. Nothing lands
236
245
  "incidentally".
237
- 2. **Additive optional fields are allowed** without machinery: bump
238
- `DATA_MODEL_VERSION`, leave the reader constants untouched, and
239
- declare the tolerant read (what an absent value means on docs written
240
- before the field existed) plus why the field survives an old writer's
241
- round-trip.
246
+ 2. **Additive optional fields are allowed** without machinery: land on the
247
+ current model per rule 9 — bumping `DATA_MODEL_VERSION` when that model has
248
+ shipped and joining it when it has not leave the reader constants
249
+ untouched, and declare the tolerant read (what an absent value means on
250
+ docs written before the field existed) plus why the field survives an old
251
+ writer's round-trip.
242
252
  3. **Renames, removals, and semantic changes are not allowed yet.** They
243
253
  require versioned upgrade machinery (per-model normalization steps applied
244
254
  on read) that does not exist; until it does, the model may only grow
@@ -246,8 +256,9 @@ retained model-4 baseline.
246
256
  documents' reader floor — through `DATA_MODEL_MIN_READER` when no reliable
247
257
  marker exists — and ships only after the fleet story is written.
248
258
  4. **`DATA_MODEL_VERSION` moves when compatibility reasoning changes** — when
249
- a reader would need to know which shape it is looking at. A purely
250
- internal refactor that provably writes an identical tree does not bump.
259
+ a reader would need to know which shape it is looking at, and that model has
260
+ already shipped (rule 9). A purely internal refactor that provably writes an
261
+ identical tree does not move it.
251
262
  5. **Classify by misinterpretation, not parseability.** The snapshot gate
252
263
  catches structural drift only. A change can be structurally additive yet
253
264
  change the MEANING of existing fields — old readers parse it fine and
@@ -272,6 +283,11 @@ retained model-4 baseline.
272
283
  existing slot, value, or variable could carry the behavior. "The existing
273
284
  mechanism is illegible or undiscoverable to surfaces" is an argument for
274
285
  restructuring that mechanism, not for a sibling property beside it.
286
+ 9. **A model number is assigned per release, not per merged change.** A change
287
+ merged before its model has shipped joins that model's log entry and its
288
+ ledger instead of opening a new number, so one release carries one model.
289
+ Reworking a ledger that has not shipped is the one exception to
290
+ append-only; once a model is released its entry and ledger are frozen.
275
291
 
276
292
  ## Model log
277
293
 
@@ -854,6 +870,204 @@ facet requires acknowledgement 8; a set without it may retain acknowledgement 4.
854
870
  instances whose canonical field tree carries a non-empty `roles` list stamp
855
871
  floor 8.
856
872
 
873
+ ### Model 9 — split guard ID spaces and singular assignment member lists (reader floor: 9)
874
+
875
+ #### Split guard ID spaces
876
+
877
+ A guard whose actions span more than one emitted ID space produces separate
878
+ temporary guard documents: direct create/delete actions preserve authored IDs,
879
+ content updates lock draft IDs, and publish/unpublish gates target published
880
+ IDs. The split prevents one match from applying every action to every ID, but
881
+ it also means retraction must reconstruct more than the historical base guard
882
+ ID. The definition and instance field trees do not grow; model 9 records the
883
+ new compatibility meaning of the existing guard action list, as required by
884
+ rule 4.
885
+
886
+ Manifest feature: `split-guard-id-spaces` (definition + instance,
887
+ reader-floor, detectable, floor 9). The stable marker is a guard whose actions
888
+ occupy at least two of the direct, edit, and lifecycle groups. Definitions and
889
+ instances without such a guard retain the floor derived from their other
890
+ features, normally model 4 or model 8.
891
+
892
+ Would an old reader misread (rule 5)? Yes. A pre-model-9 engine can read the
893
+ unchanged guard definition but only knows the base temporary-guard ID. If it
894
+ advances or aborts an instance after a model-9 engine emitted the sibling
895
+ document, it retracts the base and silently strands the sibling. That stale
896
+ advisory guard can keep an action locked after its stage exited. The instance's
897
+ embedded `definitionSnapshot` makes the feature detectable, so the commit that
898
+ can first deploy split guards stamps floor 9 before deployment; an older engine
899
+ then fails at the instance reader gate, and a concurrent old write loses its
900
+ revision compare-and-swap before it can retract anything. Full persists retain
901
+ the raise-only floor, so a newer engine cannot reopen the old-writer path.
902
+
903
+ Roll out readers first: upgrade every Studio, CLI, MCP server, Function, and
904
+ other engine runtime sharing the workflow resource, then raise the deployment's
905
+ reviewed `expectedMinReaderModel` literal to 9 before deploying a definition
906
+ with a split guard. Existing definitions and instances are safe to leave in
907
+ place: until a model-9 engine commits an affected instance, no split sibling
908
+ exists; that first commit establishes the floor before deploying one. The guard
909
+ document remains the unstamped foreign Content Lake shape—the compatibility
910
+ marker belongs to the engine-owned definition and instance trees.
911
+
912
+ The model-9 vocabulary ledger also records `document` beside `guard` and
913
+ `mutation` as a guard-predicate root. This pins the already-authored foreign
914
+ predicate dialect to the evaluator: `document.before` and `document.after`
915
+ must not disappear from a later reader while stored guard predicates still
916
+ reference them.
917
+
918
+ #### Singular assignment member lists
919
+
920
+ The singular `assignee` value becomes the same ordered user-or-role member
921
+ list as `assignees`, with one cardinality rule: it admits at most one user
922
+ member while role members do not consume the limit. This lets a singular slot
923
+ retain one or more routing roles while one person holds it. An empty value is
924
+ `[]`; append and remove-where ops apply to the list.
925
+
926
+ Like plural and composite assignment values, a singular replacement validates
927
+ new members against the current project directory while retaining already
928
+ stored members that have since left it. Taking or releasing routed work must
929
+ not become impossible merely because another retained member is now stale.
930
+
931
+ Readers permanently accept the model-8-and-earlier object/null representation
932
+ and normalize it to a zero-or-one-element list in memory. Writers partition by
933
+ the frozen definition snapshot's model stamp: instances pinned to definitions
934
+ below model 9 re-encode singular values as object/null for their whole lives,
935
+ while instances pinned to model-9 definitions write lists. A legacy instance
936
+ therefore never mixes representations, and no stored document is backfilled.
937
+ Redeploying is the opt-in migration for future instances; already-running
938
+ instances finish under their pinned definition model.
939
+
940
+ An older reader would interpret the list as an invalid singular value rather
941
+ than the assignment it represents, so this is a reader-floor change. The
942
+ definition tree gains no property: the existing `assignee` kind is the right
943
+ slot and its persisted value evolves. Definitions that declare that kind and
944
+ their instances require model 9. Definitions without it retain the highest
945
+ floor required by their other features, including the unconditional model-4
946
+ instance floor.
947
+
948
+ Manifest feature: `singular-assignee-lists` (definition + instance,
949
+ reader-floor, detectable, floor 9).
950
+
951
+ ### Model 10 — required content-reference availability and bounded effect retry (reader floor: 10)
952
+
953
+ #### Required content-reference availability
954
+
955
+ A workflow-scope `subject`, `doc.ref`, or `doc.refs` field marked `required: true`
956
+ requires its selected targets to remain readable in the workflow's perspective
957
+ after initialization. Missing targets produce a derived fault and prevent normal
958
+ actions, triggered actions, and transitions from advancing. Abort and direct
959
+ field repair remain available. Other field kinds retain the input-presence
960
+ contract; optional references retain their authored runtime requirements.
961
+
962
+ This is an explicitly approved extension of the existing `required` contract.
963
+ It changes interpretation without changing stored field shapes. Existing
964
+ reference values and frozen definition snapshots remain readable without
965
+ normalization or backfill. The behavior applies when the upgraded engine reads
966
+ an existing instance as well as when it starts a new one. The same word and
967
+ shape carry requiredness; no parallel definition property is introduced.
968
+
969
+ Manifest feature: `required-content-references` (definition + instance,
970
+ reader-floor, detectable, floor 10). The marker is a workflow-scope required
971
+ content-reference declaration, including one in an instance's frozen definition
972
+ snapshot. New writes stamp model 10. Documents without this feature retain the
973
+ floor required by their other features, normally 4, 8, or 9.
974
+
975
+ Would an old reader misread (rule 5)? Yes. Older engines interpret `required`
976
+ only at initialization and can advance after a required target disappears.
977
+ Upgrade all runtimes sharing affected workflows before relying on this rule,
978
+ then acknowledge reader model 10 before deploying affected definitions. A new
979
+ engine's next full instance write raises the stored floor to 10. Existing
980
+ instances are not backfilled, so until that write the stored floor alone cannot
981
+ prevent an older runtime from advancing them; the readers-first rollout is
982
+ required for those existing workflows too. Unknown fields and frozen snapshots
983
+ survive full persists unchanged, and the floor remains raise-only.
984
+
985
+ #### Bounded effect retry policy
986
+
987
+ An effect node may declare `retry: {kind, attempts, backoff?: {kind, delayMs},
988
+ expiryMs?}`. The block's `kind` says who runs the policy, and `engine` is its
989
+ only member: the stored schema requires the discriminator and rejects any
990
+ other value, and an authored block may omit it because desugar fills `engine`
991
+ in, the same shape `start.kind` has. `attempts` is the total number of
992
+ attempts, the first included. `backoff.kind` is `fixed` or `exponential`, and
993
+ `delayMs` is the wait between two attempts: the same every time under `fixed`,
994
+ doubled per attempt already made under `exponential`. `expiryMs` decides
995
+ whether a further attempt may start rather than bounding the run, so an
996
+ attempt already running is never interrupted.
997
+
998
+ The accepted range is bounded on one side: `expiryMs` and the backoff a policy
999
+ accumulates are each capped at 366 days. The whole policy runs inside one
1000
+ `drainEffects` invocation and no host offers a single execution longer than a
1001
+ year, so a policy declaring more than that could never run as written wherever
1002
+ it ran. The caps weigh declared waits alone, because a handler's
1003
+ duration is unknown at deploy; at runtime every elapsed millisecond counts
1004
+ against `expiryMs`, handler time included. This
1005
+ is a tightening of the accepted values in a new field, not a reshape: no
1006
+ previously valid stored tree becomes invalid, because nothing before model 10
1007
+ could carry a `retry` block at all. Bounding the span also keeps every deadline
1008
+ the engine stamps from it inside the range a timestamp can carry.
1009
+
1010
+ The discriminator is there from the start because who runs the policy is the
1011
+ one thing about it that will vary, and a later member is then a new value in
1012
+ this slot rather than a second mechanism beside it. A `caller` member — the
1013
+ engine holding the claim and the expiry ceiling while surfacing each failure
1014
+ to whoever called `drainEffects` — is a separate, additive change under its
1015
+ own ticket.
1016
+
1017
+ This is a definition-tree addition only. The policy runs inside one
1018
+ `drainEffects` call: the drain dispatches, waits the declared backoff on a
1019
+ failure that has attempts left, renews the claim's lease across the wait, and
1020
+ dispatches again, stopping when `attempts` are used up, or rather than start
1021
+ a wait that would carry the run past `expiryMs`. Nothing about an attempt is
1022
+ persisted, because nothing needs to survive the call — the run is one dispatch
1023
+ as far as the instance is concerned, and it ends in the `effectCompleted` row
1024
+ the engine already writes.
1025
+ That row's `detail` names how the run ended: the attempts used and the last
1026
+ error on a failure, or the attempt that succeeded when a later one did. No
1027
+ instance field, history row, or vocabulary value is added or changed.
1028
+
1029
+ An absent `retry` preserves the model-9 meaning exactly: a failing handler
1030
+ completes the effect as failed on its first attempt, and a claim abandoned by
1031
+ a dead drainer is redispatched without limit under `effects.leaseMs`. That
1032
+ recovery path is also unchanged for an effect that declares a policy — a
1033
+ drainer that dies mid-run leaves a lapsed claim, the sweep releases it, and
1034
+ the next drain starts a fresh run.
1035
+
1036
+ Why the existing vocabulary could not carry this (rule 8). The engine's only
1037
+ retry control was `effects.leaseMs`, a runtime construction option and one
1038
+ global number for every effect a deployment drains; it cannot say that a
1039
+ payout gets five paced attempts and a notification gets one. The definition
1040
+ could already express a retry LOOP — a counter field, an incrementing op, and
1041
+ transitions reading `$effectStatus` — but that re-enters a stage and queues a
1042
+ new effect entry each pass, which is a different thing: it retries the
1043
+ workflow step, not the dispatch. This policy governs how one queued entry is
1044
+ dispatched, and no existing slot speaks about a dispatch. `retry` is a new
1045
+ value in the existing effect node rather than a sibling mechanism: it
1046
+ introduces no error type, no ordering rule against an existing gate, and no
1047
+ verdict leg. Its failure is the effect-failure outcome that already exists,
1048
+ and its routing is the `$effectStatus` variable that already exists.
1049
+
1050
+ Would an old reader misread (rule 5)? Yes. A pre-model-10 engine reading a
1051
+ definition preserves the unknown `retry` property and ignores it, so a handler
1052
+ failure completes the effect on the first attempt: an author who declared five
1053
+ attempts silently gets one, and the instance takes its failure branch four
1054
+ attempts early. That is a silent wrong outcome rather than a refused read,
1055
+ which is exactly what the floor exists for. Instances need no separate
1056
+ judgment — the policy writes no instance shape a model-9 reader has not seen.
1057
+
1058
+ The marker is an effect node carrying `retry`, detectable in a definition
1059
+ document's own tree and in an instance's embedded `definitionSnapshot`. An
1060
+ instance derives the floor from its pinned definition, so an instance of a
1061
+ policy-bearing definition is held to the same reader as the definition itself.
1062
+ Definitions and instances without a `retry` effect retain the floor their
1063
+ other features derive, normally model 4, 8 or 9, or model 10 through the
1064
+ required-reference feature beside it.
1065
+
1066
+ Manifest feature: `effect-retry-policy` (definition + instance, reader-floor,
1067
+ detectable, floor 10). The instance document type is listed because an
1068
+ instance inherits its pinned definition's floor, not because the policy adds
1069
+ an instance shape.
1070
+
857
1071
  ## Pending governed changes
858
1072
 
859
1073
  - **`temp.system.guard` → `system.guard`** — the guard doc type's `temp.`
package/README.md CHANGED
@@ -4,8 +4,8 @@ Workflow / BPM engine for Sanity content. Define workflows as data, run them as
4
4
  instances against a Sanity client, gate transitions on GROQ filters, and queue
5
5
  effects for runtimes to drain.
6
6
 
7
- > **Status:** 0.x, internal. Restricted-access package; the API may change
8
- > between minor versions.
7
+ > **Status:** Pre-1.0 and publicly available on npm. The API may change between
8
+ > minor versions.
9
9
 
10
10
  ## Installation
11
11