@sanity/workflow-engine 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,206 @@
1
1
  # @sanity/workflow-engine
2
2
 
3
+ ## 0.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a3a8dc0: The engine gains a list-projection read for instance dashboards. `instancePreviewsQuery` compiles the same filter arms as `instancesQuery` into a `WorkflowInstancePreview` projection — the open stage alone, field lists reduced to identity, date, document-reference, and release-reference kinds, and `claimedEffects`/`failedEffects` facts in place of the unbounded audit trails — kilobytes lighter per row than the full document. `readInstancePreviewDoc` is the matching gate-then-parse funnel (`WorkflowInstancePreviewSchema`/`parseInstancePreviewDocument` underneath), and the internal identity normalizer accepts any host carrying the identity slots, rows included. `InstancesQueryFilter` adds a keyset `before` cursor (requires `limit`) for paging the newest-first order without skipping or repeating rows across pages; both list orders now break `startedAt` ties on `_id`. **No upgrade action required** — existing reads are unchanged apart from the deterministic tie order.
8
+
9
+ **Docs impact:** The engine reference's instance-query section documents `instancePreviewsQuery`, `WorkflowInstancePreview`, and the `before` cursor in this release's pass — the Workflows tool ships reading them in the same release.
10
+
11
+ - a3a8dc0: **BREAKING:** the exported `WorkflowObserver` contract (workflow-react) gains two required members, `fetchInstancePreviews` and `listenInstanceChanges` — a custom observer no longer compiles until it implements both; the shipped SDK and Studio adapters already do, so their consumers need no action.
12
+
13
+ The reactive layer gains a progressively-hydrated preview list. `useInstancePreviews` (workflow-react) drains the engine's preview projection page by page until the whole filtered set is held — `drained: true` means counts over it are totals — then keeps every row fresh from ONE shared change listener per engine tag: an event refetches the touched previews alone, so update cost is per commit, never per list size. Page size is caller-supplied. The engine adds `instanceChangesQuery` (the tag partition as a listen filter), and the `WorkflowObserver` contract adds `fetchInstancePreviews` and `listenInstanceChanges`, implemented by the SDK observer (the Studio adapter inherits them). A change-feed failure never silently freezes a list: the SDK observer reconnects with exponential backoff and then emits a `reset` event, on which `useInstancePreviews` re-drains the whole partition — gap-missed additions, edits, and deletions all land.
14
+
15
+ **Docs impact:** The reactive-core reference documents `useInstancePreviews` and the observer contract's two new required members in this release's pass — the Workflows tool ships reading them in the same release, and the custom-adapter guidance must name both members beside the BREAKING note.
16
+
17
+ - 90207dc: **BREAKING:** `assertReaderModelAcknowledgement` now requires `{requiredMinReaderModel, context?}` instead of a context string, and `ReaderModelAcknowledgementError` accepts its optional context through `{context}`. The Blueprint package's `AcknowledgedDeployment` type moves to the engine as `AcknowledgedWorkflowDeployment`; update imports and use that shared name. Assignment-valued fields can now declare a non-empty `roles` list that limits newly written assignees. Existing fields without `roles` stay unconstrained, retain reader floor 4, and continue deploying with `expectedMinReaderModel: 4`; installing this release requires no coordinated rollout or configuration change.
18
+
19
+ Before adding `roles`, upgrade every Studio, CLI, MCP server, Function, and other runtime that shares the affected workflow resource, then change that deployment's literal `expectedMinReaderModel` to `8`. MCP deployments pass the same reviewed literal as the optional `expectedMinReaderModel` tool argument. Definitions and instances carrying the facet require reader model 8. User assignees qualify through direct roles or the definition's role aliases, while collective role assignees must literally match one configured role. Studio assignment controls, including manual start, auto-start, and assignees nested in to-do rows, offer only eligible choices while retaining stale current values so editors can remove or replace them. Constrained pickers name the active eligible roles and distinguish an empty eligible set from a search with no matches. Top-level plural and composite edits retain no more occurrences of a stale assignment at each declared assignment location than the previous value contained; removal and reordering therefore work, while an added copy is a new assignment and must qualify. Direct singular replacements and every assignment present in an `updateWhere` fragment remain strict. Each engine verb uses one member-directory snapshot across retries, cascade hops, and spawned children, then the next verb reads current membership.
20
+
21
+ Malformed project-member and project-user directory responses now fail loudly instead of treating bad rows or role records as missing users, and actor resolution reports those failures as `inaccessible`, so integrations can distinguish directory failures from absent membership.
22
+
23
+ CLI `start` and `definition delete` no longer apply the definition-submission acknowledgement gate; deploy, check, dry-run, definition diff, Blueprint provision, and engine definition APIs still validate the selected definitions' required floor.
24
+
25
+ No reader-fleet rollout or deployment-literal change is required for existing definitions that do not use `roles`.
26
+
27
+ **Docs impact:** Update the assignment field reference and authoring examples for `roles`, distinguish eligibility from editability, action permission, and literal queue ownership, and add the model-8 conditional adoption sequence to the prerelease rollout guide.
28
+
29
+ - b0b1550: **BREAKING:** Product-prefixed telemetry events now use `Workflows <event>` instead of `Editorial Workflows <event>`; `EditorialWorkflowsResource` is replaced by `WorkflowsResource`; and `defineWorkflows` now generates `workflows-<deployment name>` resource names instead of `editorial-workflows-<deployment name>`. Telemetry consumers matching the previous literals stop receiving events until updated, TypeScript consumers importing the previous Blueprint interface no longer compile, and an existing Blueprint stack that relies on the generated name treats the new default as a different resource identity.
30
+
31
+ Update telemetry dashboards, schemas, alerts, and queries to match the `Workflows` prefix; event suffixes, payloads, and versions are unchanged. Replace Blueprint imports with `WorkflowsResource`. Before deploying an existing Blueprint stack, either pass its recorded resource name explicitly or plan the detach-and-create migration to the new default. Workflows request-tag families and billing classification are unchanged.
32
+
33
+ Other user-facing package copy now uses the Workflows product name. No upgrade action is required for consumers that do not match telemetry event names or use the Blueprint package.
34
+
35
+ **Docs impact:** Rename the product throughout the public documentation, move the docs section to `/docs/workflows/*` with permanent redirects from every `/docs/editorial-workflows/*` path, and document the telemetry-prefix, `WorkflowsResource`, and Blueprint resource-name migrations in the telemetry reference, Blueprint guide, and release notes.
36
+
37
+ ### Patch Changes
38
+
39
+ - 210ca63: The API documentation shipped in every package's `.d.ts` files is rewritten to be substantially terser: multi-paragraph doc comments are cut to the facts a consumer needs, and internal narration no longer appears in editor hover text. Runtime behavior, exports, and type signatures are unchanged across the whole package group.
40
+
41
+ **No upgrade action required.**
42
+
43
+ **Docs impact: None** — the published documentation site is authored separately; only in-editor `.d.ts` hover text changes.
44
+
45
+ - a3a8dc0: `readInstancePreviewDoc` now takes an untyped lake row. A preview page slot without a string `_id` fails as a shape error named `(unknown id)` instead of being assumed to have an id for the model gate.
46
+
47
+ **No upgrade action required.** Callers that already passed `{_id: string}` continue to type-check.
48
+
49
+ **Docs impact:** None. The preview-row funnel is unchanged for well-formed rows; only the untyped input is honest.
50
+
51
+ - Updated dependencies [210ca63]
52
+ - @sanity/groq-condition-describe@0.4.1
53
+
54
+ ## 0.29.0
55
+
56
+ ### Minor Changes
57
+
58
+ - f44fc33: Workflow conditions can now read the caller's org-level User Attributes (Enterprise) through the advisory `$attributes` variable — for example `$attributes.department == "politics"` on a fireAction action filter, activity requirement, or editable predicate. Values match what lake role filters read via `user::attributes()`, keyed by attribute name with each active scalar or array value. Soft-gate paths (`evaluate`, fireAction filter re-check, editField) fetch the bag; ticks and drainers do not. Expected absences (401/402/403/404 — non-Enterprise, missing feature, forbidden) leave `$attributes` unbound so conditions that read it fail closed; unexpected fetch failures (5xx, network) throw on those soft-gate paths only and are not cached so the next call retries. An empty successful page binds `{}`. When the Management API envelope reports `hasMore: true`, the engine still binds the partial bag and warns (`nextCursor` makes follow-up pagination a small engine change). Engine checks remain advisory; the Content Lake is still the only enforcement point. `$attributes` is never persisted on `Actor` / history. The MCP authoring guide names the new variable alongside the other caller-bound vars. The test bench accepts an `attributes` bag (construction shortcuts and per-call overrides) the same way it accepts `grants`.
59
+
60
+ **No upgrade action required.** Existing definitions are unchanged. Authors who want attribute-gated actions opt in by writing `$attributes` in caller-bound condition sites; cascade gates continue to reject caller-bound vars at deploy.
61
+
62
+ **Docs impact:** Update the condition-variables reference (`docs/reference.md` § context-bound variables) and any guide that lists `$actor` / `$can` / `$assigned` so `$attributes` and the Enterprise / fail-closed semantics are named; note the spelling difference vs lake `user::attributes()`.
63
+
64
+ - 26f25e0: Definition authors and effect runtimes can now use `field.inc`, `field.dec`, and
65
+ `field.setIfMissing` operations. Arithmetic operations accept an optional
66
+ numeric value expression that defaults to `1`, validate number-field targets,
67
+ reject non-finite results, and participate in the same atomic engine commits as
68
+ existing field operations. A retry is deduplicated only when the caller supplies
69
+ an idempotency key; the arithmetic operation itself is not idempotent.
70
+ `field.setIfMissing` targets nullable fields; list fields are always arrays and
71
+ are rejected. When the target already has a value, `field.setIfMissing` does
72
+ nothing and writes no `opApplied` history row. Like Sanity Content Lake patches,
73
+ `field.inc` and `field.dec` require the target to already hold a number: a
74
+ declared number without an initial value is `null`, so arithmetic rejects without
75
+ committing. Initialize counters with `field.setIfMissing` set to `0` before
76
+ incrementing when the value may be absent.
77
+
78
+ Persisted-model version 6 adds the three operation discriminators. The change is
79
+ additive and the reader floor remains 4, so existing definitions and instances
80
+ require no migration. Before deploying a definition that uses these operations,
81
+ upgrade every Studio, CLI, MCP server, Function, and other runtime that can
82
+ process workflow data in the shared environment. The unchanged reader floor
83
+ does not fence out an older engine, which rejects or aborts when it encounters a
84
+ new operation rather than executing it with different semantics.
85
+
86
+ **Docs impact:** Update the Operations guide and operation reference with all
87
+ three forms, their defaults and validation, effect-completion availability, the
88
+ Sanity-compatible `field.setIfMissing(0)` then `field.inc` initialization
89
+ pattern, the warning that arithmetic never assumes a missing value is zero, and
90
+ the bounded-retry counting pattern that loops below a fixed total-attempt limit
91
+ and routes exhausted work to manual recovery. Note that no-op
92
+ `field.setIfMissing` attempts write no `opApplied` history row, and include the
93
+ readers-first rollout guidance for model 6 operations.
94
+
95
+ - 25bd1fb: Each card in the Studio plugin's Workflows tab now carries what the definition
96
+ is doing, not just its name: how many of its documents have work in flight, when
97
+ one of them last changed stage, how many hold a task past its due date, and one
98
+ alert glyph when something is wrong. The card reports live state only — what a
99
+ workflow runs on and what it says about itself change on a deploy, and that
100
+ workflow's own Definition page already names both. The
101
+ grid bands into "Assigned to you" and "Other workflows", so whether the reader
102
+ has work in a workflow is carried by where its card sits; rows keep the
103
+ catalog's alphabetical order inside each band. A card wears one alert, and it
104
+ means a run of that workflow cannot continue without help. The engine's
105
+ `blocked` readiness hold earns none, because it is an expected transient state
106
+ and flagging it would mark most cards most of the time; a rejected
107
+ document-type binding earns none either, because it belongs to the definition
108
+ rather than to any of its runs and is reported on that workflow's Definition
109
+ page.
110
+
111
+ The age on a card is a STAGE ARRIVAL, not a last-modified stamp. Every engine
112
+ commit moves an instance's change stamp — an assignment, a date, a drained
113
+ effect — and a workflow can absorb a great deal of that without advancing, so
114
+ the arrival is the event a catalog can read as progress. The consequence is
115
+ deliberate: a busy workflow held inside one stage reads as old, which is the
116
+ pathology worth seeing. Hovering that age names the move — which document went
117
+ to which stage, and the exact time — resolving the document's title through the
118
+ preview store only once the tooltip opens.
119
+
120
+ Overdue is the only signal drawn in a tone — caution, where the alert glyph
121
+ takes critical — and the only one that disappears when it reads zero: it reports
122
+ that something is wrong rather than how things stand, so a card with nothing
123
+ late spends no room saying so. A
124
+ document counts as overdue while an unsettled task on it is past its due date;
125
+ a finished task's date, however old, is not a debt. Overdue is measured on one
126
+ shared rule with the todo rows, so a card's count and a row's face can no
127
+ longer disagree about a single date.
128
+
129
+ The grid counts DOCUMENTS where it used to count instances, because its reader
130
+ is an editor and the document is what they open. Several instances on one
131
+ document count once. An instance whose chain references no document counts as
132
+ one unit of its own, so a workflow that runs on no document does not read as
133
+ idle. The workflow's own page still reports instances, in those words.
134
+
135
+ Hovering that count also says how many of those documents have yet to leave the
136
+ stage their workflow starts from — on a line of its own, so the two facts can be
137
+ scanned rather than read through, and folded into the first line when the answer
138
+ is all of them, where a second line would repeat one number. A document counts
139
+ there while any of its runs still waits at the start, judged against that run's
140
+ own pinned snapshot, so a run begun under an earlier version answers to where
141
+ that version started. The count stays in the hint rather than taking a slot on
142
+ the rule: it is a part of a number the card already shows, meaningless without
143
+ that total beside it, and no glyph in the set says "waiting at the start".
144
+
145
+ The page no longer opens on a project-wide total. The band headings count the
146
+ workflows, so a total above them stated the same figure twice, and a total of
147
+ documents invited a comparison against the cards that can never add up — one
148
+ document running two workflows is one document in a project total and one on
149
+ each of their cards.
150
+
151
+ The engine gains `documentStuckCause`, which classifies an instance as stuck
152
+ from its persisted document and pinned snapshot alone — no evaluation, no
153
+ reads, no actor. It answers for the effect- and activity-level causes
154
+ `diagnoseInstance` reports (`failed-effect`, `failed-activity`,
155
+ `hung-effect`) and returns `undefined` for the transition-level ones, which
156
+ need GROQ results. It is sound but incomplete by construction: a cause it
157
+ reports is real, and the absence of one means only that no cause was provable,
158
+ never that the instance is healthy. Surfaces flagging on it must not word an
159
+ unflagged state as an all-clear. `findActivityNode` and `findStageNode`, the
160
+ tolerant definition tree lookups it uses, are exported alongside it.
161
+
162
+ The tool's instance read is held per engine for the session and replayed on the
163
+ next mount, which changes what the Workflows tab paints while it loads. A first
164
+ visit now waits for that read before the definitions grid appears, where it used
165
+ to lay out cards from the catalog and rearrange them once the instances landed —
166
+ the grid's bands and counts come from the instances, so the cards had no stable
167
+ shape until both reads were in. The grid waits on the reader's identity for the
168
+ same reason: which band a card sits in depends on who is looking, and a
169
+ project-scoped account resolves through the project member directory, so banding
170
+ before that answer arrived would have moved every card once it did. An account
171
+ the directory cannot place, or a directory that fails to load, is an answer
172
+ rather than a wait — the grid paints unbanded and every card reports no assigned
173
+ work. A later visit inside the same session paints the
174
+ held read at once and refreshes behind it.
175
+
176
+ The form strip's assigned-task count tells those two apart as well, so an account
177
+ the member directory cannot place no longer leaves a spinner turning in that slot
178
+ for the rest of the session. It shows no count, which is what it already did for
179
+ an account with nothing assigned. The hold is not local to the grid: the
180
+ task rows under "All documents" and "For me", and the active-instance count on a
181
+ workflow's Definition page, read the same stream, so they too open on held
182
+ numbers before the fresh read lands. A failed read is never held, and a provider remount mints a new engine and
183
+ so starts cold.
184
+
185
+ **No upgrade action required.** The card additions are automatic, and
186
+ `documentStuckCause` is a new export that changes no existing verdict —
187
+ `diagnoseInstance` classifies exactly as before, now sharing one
188
+ implementation of the document-derivable causes.
189
+
190
+ **Docs impact:** Update the Workflows tool guide where it describes the
191
+ definitions grid, to cover the new card signals, the assigned-work ordering, the
192
+ document-versus-instance split between the grid and a workflow's own page, and
193
+ that the tab opens on a held read after the first visit.
194
+ `docs/reference.md` already carries `documentStuckCause` beside
195
+ `diagnoseInstance` with its bound stated; the hosted engine reference needs the
196
+ same entry, so integrators do not read a missing cause as a health check.
197
+
198
+ - 3fd04f1: **BREAKING:** `ActionSemantic` was a closed union of action decision values; it now also includes signal values and the open `custom.${string}` form. TypeScript consumers that exhaustively map `ActionSemantic` must handle signal and custom values or narrow to the decision subset before upgrading, otherwise their build will fail. Workflow authors can attach advisory signals and validated custom semantics to workflow roots, stages, activities, and actions, and evaluation results expose them at each matching level.
199
+
200
+ Existing stored definitions and runtime behavior remain compatible. The Studio plugin continues to render decision faces when decision values are composed with signal or custom semantics, while non-decision values receive the default action face.
201
+
202
+ **Docs impact:** Update the definition and evaluation references plus the concepts guide to document level-wide advisory semantics, the signal vocabulary, custom grammar, and action-only decision values.
203
+
3
204
  ## 0.28.0
4
205
 
5
206
  ### Minor Changes
package/DATAMODEL.md CHANGED
@@ -72,10 +72,12 @@ never matches the new type).
72
72
  the shape, so restamping there would lie.
73
73
  - **`minReaderModel` — the document's reader floor.** The oldest engine model
74
74
  that can safely _interpret this particular document_. Writers derive it
75
- from compatibility-bearing features present in the canonical stored tree.
76
- `DATA_MODEL_MIN_READER` is the maximum floor the current writer can emit,
77
- not a value stamped on every document. Additive features contribute floor
78
- 0; only an incompatible feature fences older readers out of that document.
75
+ from compatibility-bearing features present in the canonical stored tree,
76
+ while retaining the unconditional `DATA_MODEL_MIN_READER` baseline of 4.
77
+ `DATA_MODEL_MAX_READER` is the maximum floor the current writer can emit.
78
+ A deployment acknowledges the highest reader model verified
79
+ for its shared runtimes, and the writer compares it with the submitted
80
+ definitions' required floor.
79
81
  - **Missing stamp = model 0** (floor 0) — the document was last written
80
82
  before the stamp existed. A `modelVersion` with no `minReaderModel` is
81
83
  malformed foreign data and gates conservatively: the stamp is the floor.
@@ -138,13 +140,12 @@ never matches the new type).
138
140
  requirement may still require it. (The failure mode this fences off is
139
141
  documented across the industry: a rewriter that drops the compatibility
140
142
  gate it doesn't understand silently reopens the door the floor closed.)
141
- - **Raising the floor is an availability decision, sequenced readers-first.**
142
- Before any writer that raises `DATA_MODEL_MIN_READER` deploys, every
143
- reader in the fleet — customer-deployed Functions foremost — must already
144
- run an engine at or above the new floor (the BACKWARD deployment order:
145
- upgrade consumers before producers). And like every engine-side check the
146
- floor is advisory — it gates our own clients; the Content Lake stays the
147
- sole enforcement point.
143
+ - **Persisting a floor-bearing feature is an availability decision.** Before a
144
+ definition using that feature deploys, every reader sharing its workflow
145
+ resource — customer-deployed Functions foremost — must run an engine at or
146
+ above the feature's floor. Increasing the writer's maximum alone requires no
147
+ coordinated rollout. Like every engine-side check, the floor is advisory —
148
+ it gates our own clients; the Content Lake stays the sole enforcement point.
148
149
  - **The gate ships from the engine.** `assertReadableModel`, the parse
149
150
  functions, and the error classes are exported from
150
151
  `@sanity/workflow-engine`, so every consumer surface composes the same
@@ -224,8 +225,9 @@ floor only when its stable marker is present in the canonical stored tree. An
224
225
  of its declared type when no reliable marker exists. Readers trust the
225
226
  persisted floor; they do not recompute features they may be too old to know.
226
227
  Every reader-floor increase is represented as a manifest feature and classified
227
- this way; raising the package maximum never by itself raises every document's
228
- floor.
228
+ this way. Raising `DATA_MODEL_MAX_READER` expands what the writer may emit; a
229
+ detectable feature raises only documents carrying its stable marker above the
230
+ retained model-4 baseline.
229
231
 
230
232
  1. **Any change to a persisted field tree is a model change** and must be
231
233
  declared: update the model-surface snapshot, add an entry to the model
@@ -233,16 +235,16 @@ floor.
233
235
  moves **and whether the reader floor moves with it**. Nothing lands
234
236
  "incidentally".
235
237
  2. **Additive optional fields are allowed** without machinery: bump
236
- `DATA_MODEL_VERSION`, leave `DATA_MODEL_MIN_READER` untouched, and
238
+ `DATA_MODEL_VERSION`, leave the reader constants untouched, and
237
239
  declare the tolerant read (what an absent value means on docs written
238
240
  before the field existed) plus why the field survives an old writer's
239
241
  round-trip.
240
242
  3. **Renames, removals, and semantic changes are not allowed yet.** They
241
243
  require versioned upgrade machinery (per-model normalization steps applied
242
244
  on read) that does not exist; until it does, the model may only grow
243
- additively. When such a change eventually lands, it raises
244
- `DATA_MODEL_MIN_READER` a declared fence against older readers, shipped
245
- only after the fleet story is written.
245
+ additively. When such a change eventually lands, it raises the affected
246
+ documents' reader floor through `DATA_MODEL_MIN_READER` when no reliable
247
+ marker exists — and ships only after the fleet story is written.
246
248
  4. **`DATA_MODEL_VERSION` moves when compatibility reasoning changes** — when
247
249
  a reader would need to know which shape it is looking at. A purely
248
250
  internal refactor that provably writes an identical tree does not bump.
@@ -251,7 +253,7 @@ floor.
251
253
  change the MEANING of existing fields — old readers parse it fine and
252
254
  silently misinterpret. That judgment (would an old reader misread, not
253
255
  just fail to parse?) is exactly what decides whether
254
- `DATA_MODEL_MIN_READER` moves, and no snapshot can make it — each model
256
+ a feature floor moves, and no snapshot can make it — each model
255
257
  log entry states it explicitly.
256
258
  6. **Manifest and prose move together.** Every structural model change adds a
257
259
  manifest entry and matching model-log declaration. The ledger snapshots
@@ -619,7 +621,7 @@ older engine's project-scoped actor silently evaluates false (and the
619
621
  mirror-image for legacy values under a new actor). Misinterpretation, not
620
622
  parse failure — exactly the case the floor exists for. `minReaderModel: 4`
621
623
  makes pre-classifier engines refuse the document loudly instead. The writer
622
- maximum (`DATA_MODEL_MIN_READER`) rises to 4; deployment acknowledgements
624
+ baseline (`DATA_MODEL_MIN_READER`) rises to 4; deployment acknowledgements
623
625
  follow the readers-first rollout (`docs/reader-model-rollout.md`).
624
626
 
625
627
  Manifest feature: `classified-principal-ids` (instance, reader-floor,
@@ -702,6 +704,156 @@ its base `date` / `datetime`, so no older code path can write it while skipping
702
704
  validation — there is no silent bypass to fence, and the kinds leave the floor
703
705
  at 0.
704
706
 
707
+ ### Model 6 — node semantics and field patch operations
708
+
709
+ #### Semantics at every definition level
710
+
711
+ The optional non-empty `semantics[]` field now appears on the workflow root,
712
+ stages, and activities as well as actions. Its engine-owned vocabulary adds
713
+ `signal.positive`, `signal.caution`, and `signal.critical` at every level;
714
+ `custom.<meaning>` is an open, grammar-validated namespace at every level.
715
+ Actions retain their action-only `decision.accept` and `decision.decline`
716
+ values. The values remain advisory and change no execution, gating, transition,
717
+ status, history, or instance state. An absent field retains the generic behavior
718
+ of older definitions.
719
+
720
+ Manifest feature: `node-semantics` (definition, additive, detectable, floor 0).
721
+
722
+ Would an old reader misread (rule 5)? No. Definition content has no strict
723
+ read-side parse, and an older engine ignores the optional fields and new
724
+ advisory values. Definition documents are immutable after creation, so an older
725
+ writer never rewrites and drops them. The feature itself remains readable by
726
+ model-0 engines; new writers still stamp the unconditional writer minimum of 4.
727
+ This uses the existing `semantics` slot and extends that same word and shape to
728
+ the remaining definition nodes; a sibling property would duplicate the
729
+ advisory-meaning mechanism and violate the definition language's
730
+ self-similarity rules.
731
+
732
+ #### Field patch operations
733
+
734
+ The stored definition grammar additively admits `field.inc`, `field.dec`, and
735
+ `field.setIfMissing` action ops. Instances may embed the same definition tree
736
+ in `definitionSnapshot`, and effect-completion use records the new discriminator
737
+ in `history[]` rows' `opType`. Missing discriminators retain the historical op
738
+ vocabulary, so documents written before model 6 need no normalization. A
739
+ `field.setIfMissing` whose target already has a value writes no `opApplied` row:
740
+ persisting a targeted no-op would make older provenance and feed readers report
741
+ a field write that never happened.
742
+
743
+ Manifest feature: `field-patch-ops` (definition + instance,
744
+ additive, detectable, floor 0).
745
+
746
+ Would an old reader misread these ops (rule 5)? No. An older engine strictly
747
+ parsing or deploying a definition document rejects the unknown discriminator.
748
+ The instance runtime is different: `definitionSnapshot` is decoded without a
749
+ definition-schema parse, so an older engine can reach `applyOp`; its exhaustive
750
+ switch has no matching arm and the commit aborts when the missing summary is
751
+ used to build the audit row. That is a loud runtime failure rather than silent
752
+ execution with different semantics. Definition documents are create-only,
753
+ instance persists preserve the opaque `definitionSnapshot` string, and history
754
+ readers already treat `opType` as an opaque audit label, so the discriminator
755
+ survives an old writer's round trip. Rule 5 fences silent misinterpretation,
756
+ not loud refusal, therefore this detectable feature contributes floor 0 and does
757
+ not raise the writer maximum. The floor does not protect these definitions from
758
+ older runtimes: upgrade every runtime that can process shared workflow data
759
+ before adopting the new ops.
760
+
761
+ Why could the existing definition vocabulary not carry the behavior (definition
762
+ language rule 8)? Incrementing requires a read-modify-write primitive, while
763
+ `ValueExpr` deliberately describes context-free reads and has no arithmetic.
764
+ The existing `field.set` cannot atomically derive its value from the current
765
+ number. Conditional initialization also has no existing field-op equivalent:
766
+ an action-level `when` gates every op and effect in the action, while
767
+ `initialValue` runs only when an entry is first resolved and cannot initialize a
768
+ value cleared later by `field.unset`. These additions are new values in the
769
+ existing `ops[].type` slot and reuse the established field target and
770
+ value-expression shapes. Their abbreviated names deliberately mirror
771
+ `@sanity/client`'s patch vocabulary (`setIfMissing`, `inc`, `dec`) as one trio;
772
+ definition authors who know Sanity patches can predict the workflow spellings.
773
+
774
+ ### Model 7 — the `$attributes` condition variable (reader floor: 4)
775
+
776
+ One additive growth of the condition dialect ships as model 7. It does not
777
+ raise the reader floor; the writer maximum stays 4.
778
+
779
+ A new caller-bound synthetic variable `$attributes` binds the acting token's
780
+ org-level User Attributes (Enterprise) as a flat key → active-value record —
781
+ the same values lake role filters read via `user::attributes()`, spelled for
782
+ the workflow condition dialect. Soft-gate paths resolve it when a
783
+ caller-bound projection runs (`evaluate` / fireAction filter re-check /
784
+ editField) — project → organization → global-host attributes page with
785
+ `limit=100`, no further pages; warn when the envelope reports `hasMore` —
786
+ parallel to grants on those paths, not on every `resolveAccess`. It is never
787
+ persisted on `Actor` / history, and left unbound (`undefined`) when the fetch
788
+ is an expected absence (401/402/403/404 — non-Enterprise org, missing feature,
789
+ forbidden) so conditions that read it fail closed — the same posture as `$can`
790
+ without grants. Unexpected fetch failures (5xx, network) throw and are not
791
+ cached, so the next soft-gate call retries; ticks and drainers never call the
792
+ endpoint. Legal sites match `$can` (fireAction action filters, activity
793
+ requirements, editable predicates, and those commit re-checks); cascade gates
794
+ and other caller-free sites deploy-reject it.
795
+
796
+ Manifest feature: `attributes-condition-var` (definition condition dialect,
797
+ additive, unconditional, floor 0).
798
+
799
+ Would an old reader misread the var (rule 5)? No — an older engine never binds
800
+ `$attributes`, so a GROQ read is null / fail-closed rather than a silent
801
+ misinterpretation of persisted trees. Definitions that mention `$attributes`
802
+ still deploy and evaluate on older engines; gates that depend on it simply
803
+ never pass until the fleet reads model 7. No reader-floor raise.
804
+
805
+ **Author-predicate shadowing (model ≤6 → 7):** `attributes` joins the reserved
806
+ condition-var names, so a **new** predicate named `attributes` is
807
+ deploy-rejected. A definition already stored under model ≤6 that declared
808
+ `predicates.attributes` is a different story: on a model-7 engine that binds
809
+ the caller bag, `$attributes` in that definition flips from “the author
810
+ predicate” to “the synthetic User Attributes bag” with no redeploy. Likely
811
+ zero real instances; no migration. Authors who hit it rename the predicate
812
+ and redeploy.
813
+
814
+ ### Model 8 — role-constrained assignment fields (reader floor: 8)
815
+
816
+ Assignment-valued field shapes may carry a non-empty `roles` list. The facet
817
+ limits newly written user assignees to project members who fulfil at least one
818
+ listed role, including the definition's role aliases. A collective role
819
+ assignee remains ownership rather than capability and is eligible only when
820
+ its literal role is listed. An absent facet preserves the historical
821
+ unconstrained meaning.
822
+
823
+ Freshly resolved values and singular replacements validate the complete
824
+ assignment. For plural and composite replacements, stale retention is
825
+ count-bounded per declared assignment location: a candidate may retain no more
826
+ occurrences of one assignment identity than the previous value held at that
827
+ location. Array indices are not part of the location, so removal and reordering
828
+ do not make surviving assignments new; an added copy exceeds the previous count
829
+ and must qualify. An `updateWhere` merge validates every assignment location
830
+ present in its incoming fragment strictly, so a fragment must omit rather than
831
+ restate a stale assignment. Each top-level engine verb uses one project-directory
832
+ snapshot across retries, cascade hops, and spawned children; a later verb reads
833
+ a fresh snapshot.
834
+
835
+ The property is valid on `assignee` and `assignees` wherever field shapes are
836
+ declared: scope fields, nested object/array shapes, and effect outputs. This
837
+ follows the definition language's existing facet vocabulary: as `types` says
838
+ which documents may occupy a reference slot, `roles` says which people or
839
+ collective roles may occupy an assignment slot. The existing vocabulary could
840
+ not carry this behavior because `editable` governs who may mutate a field,
841
+ while action `roles` governs who may act; neither describes eligible field
842
+ values.
843
+
844
+ Both stamped trees grow the property: definitions persist it on field shapes,
845
+ and instances copy it onto resolved assignment entries so later writes remain
846
+ self-describing. Model-6 and model-7 writers shipped without this behavior. An
847
+ older reader would preserve the unknown property but silently accept an
848
+ assignee the declaration excludes, so the facet is appended at model 8 rather
849
+ than backfilled into either shipped ledger.
850
+
851
+ Manifest feature: `role-constrained-assignment-fields` (definition + instance,
852
+ reader-floor, detectable, floor 8). A submitted definition set carrying the
853
+ facet requires acknowledgement 8; a set without it may retain acknowledgement 4. Documents without the facet retain the model-4 floor; only definitions and
854
+ instances whose canonical field tree carries a non-empty `roles` list stamp
855
+ floor 8.
856
+
705
857
  ## Pending governed changes
706
858
 
707
859
  - **`temp.system.guard` → `system.guard`** — the guard doc type's `temp.`