@sanity/workflow-engine 0.27.0 → 0.29.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,202 @@
1
1
  # @sanity/workflow-engine
2
2
 
3
+ ## 0.29.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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`.
8
+
9
+ **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.
10
+
11
+ **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()`.
12
+
13
+ - 26f25e0: Definition authors and effect runtimes can now use `field.inc`, `field.dec`, and
14
+ `field.setIfMissing` operations. Arithmetic operations accept an optional
15
+ numeric value expression that defaults to `1`, validate number-field targets,
16
+ reject non-finite results, and participate in the same atomic engine commits as
17
+ existing field operations. A retry is deduplicated only when the caller supplies
18
+ an idempotency key; the arithmetic operation itself is not idempotent.
19
+ `field.setIfMissing` targets nullable fields; list fields are always arrays and
20
+ are rejected. When the target already has a value, `field.setIfMissing` does
21
+ nothing and writes no `opApplied` history row. Like Sanity Content Lake patches,
22
+ `field.inc` and `field.dec` require the target to already hold a number: a
23
+ declared number without an initial value is `null`, so arithmetic rejects without
24
+ committing. Initialize counters with `field.setIfMissing` set to `0` before
25
+ incrementing when the value may be absent.
26
+
27
+ Persisted-model version 6 adds the three operation discriminators. The change is
28
+ additive and the reader floor remains 4, so existing definitions and instances
29
+ require no migration. Before deploying a definition that uses these operations,
30
+ upgrade every Studio, CLI, MCP server, Function, and other runtime that can
31
+ process workflow data in the shared environment. The unchanged reader floor
32
+ does not fence out an older engine, which rejects or aborts when it encounters a
33
+ new operation rather than executing it with different semantics.
34
+
35
+ **Docs impact:** Update the Operations guide and operation reference with all
36
+ three forms, their defaults and validation, effect-completion availability, the
37
+ Sanity-compatible `field.setIfMissing(0)` then `field.inc` initialization
38
+ pattern, the warning that arithmetic never assumes a missing value is zero, and
39
+ the bounded-retry counting pattern that loops below a fixed total-attempt limit
40
+ and routes exhausted work to manual recovery. Note that no-op
41
+ `field.setIfMissing` attempts write no `opApplied` history row, and include the
42
+ readers-first rollout guidance for model 6 operations.
43
+
44
+ - 25bd1fb: Each card in the Studio plugin's Workflows tab now carries what the definition
45
+ is doing, not just its name: how many of its documents have work in flight, when
46
+ one of them last changed stage, how many hold a task past its due date, and one
47
+ alert glyph when something is wrong. The card reports live state only — what a
48
+ workflow runs on and what it says about itself change on a deploy, and that
49
+ workflow's own Definition page already names both. The
50
+ grid bands into "Assigned to you" and "Other workflows", so whether the reader
51
+ has work in a workflow is carried by where its card sits; rows keep the
52
+ catalog's alphabetical order inside each band. A card wears one alert, and it
53
+ means a run of that workflow cannot continue without help. The engine's
54
+ `blocked` readiness hold earns none, because it is an expected transient state
55
+ and flagging it would mark most cards most of the time; a rejected
56
+ document-type binding earns none either, because it belongs to the definition
57
+ rather than to any of its runs and is reported on that workflow's Definition
58
+ page.
59
+
60
+ The age on a card is a STAGE ARRIVAL, not a last-modified stamp. Every engine
61
+ commit moves an instance's change stamp — an assignment, a date, a drained
62
+ effect — and a workflow can absorb a great deal of that without advancing, so
63
+ the arrival is the event a catalog can read as progress. The consequence is
64
+ deliberate: a busy workflow held inside one stage reads as old, which is the
65
+ pathology worth seeing. Hovering that age names the move — which document went
66
+ to which stage, and the exact time — resolving the document's title through the
67
+ preview store only once the tooltip opens.
68
+
69
+ Overdue is the only signal drawn in a tone — caution, where the alert glyph
70
+ takes critical — and the only one that disappears when it reads zero: it reports
71
+ that something is wrong rather than how things stand, so a card with nothing
72
+ late spends no room saying so. A
73
+ document counts as overdue while an unsettled task on it is past its due date;
74
+ a finished task's date, however old, is not a debt. Overdue is measured on one
75
+ shared rule with the todo rows, so a card's count and a row's face can no
76
+ longer disagree about a single date.
77
+
78
+ The grid counts DOCUMENTS where it used to count instances, because its reader
79
+ is an editor and the document is what they open. Several instances on one
80
+ document count once. An instance whose chain references no document counts as
81
+ one unit of its own, so a workflow that runs on no document does not read as
82
+ idle. The workflow's own page still reports instances, in those words.
83
+
84
+ Hovering that count also says how many of those documents have yet to leave the
85
+ stage their workflow starts from — on a line of its own, so the two facts can be
86
+ scanned rather than read through, and folded into the first line when the answer
87
+ is all of them, where a second line would repeat one number. A document counts
88
+ there while any of its runs still waits at the start, judged against that run's
89
+ own pinned snapshot, so a run begun under an earlier version answers to where
90
+ that version started. The count stays in the hint rather than taking a slot on
91
+ the rule: it is a part of a number the card already shows, meaningless without
92
+ that total beside it, and no glyph in the set says "waiting at the start".
93
+
94
+ The page no longer opens on a project-wide total. The band headings count the
95
+ workflows, so a total above them stated the same figure twice, and a total of
96
+ documents invited a comparison against the cards that can never add up — one
97
+ document running two workflows is one document in a project total and one on
98
+ each of their cards.
99
+
100
+ The engine gains `documentStuckCause`, which classifies an instance as stuck
101
+ from its persisted document and pinned snapshot alone — no evaluation, no
102
+ reads, no actor. It answers for the effect- and activity-level causes
103
+ `diagnoseInstance` reports (`failed-effect`, `failed-activity`,
104
+ `hung-effect`) and returns `undefined` for the transition-level ones, which
105
+ need GROQ results. It is sound but incomplete by construction: a cause it
106
+ reports is real, and the absence of one means only that no cause was provable,
107
+ never that the instance is healthy. Surfaces flagging on it must not word an
108
+ unflagged state as an all-clear. `findActivityNode` and `findStageNode`, the
109
+ tolerant definition tree lookups it uses, are exported alongside it.
110
+
111
+ The tool's instance read is held per engine for the session and replayed on the
112
+ next mount, which changes what the Workflows tab paints while it loads. A first
113
+ visit now waits for that read before the definitions grid appears, where it used
114
+ to lay out cards from the catalog and rearrange them once the instances landed —
115
+ the grid's bands and counts come from the instances, so the cards had no stable
116
+ shape until both reads were in. The grid waits on the reader's identity for the
117
+ same reason: which band a card sits in depends on who is looking, and a
118
+ project-scoped account resolves through the project member directory, so banding
119
+ before that answer arrived would have moved every card once it did. An account
120
+ the directory cannot place, or a directory that fails to load, is an answer
121
+ rather than a wait — the grid paints unbanded and every card reports no assigned
122
+ work. A later visit inside the same session paints the
123
+ held read at once and refreshes behind it.
124
+
125
+ The form strip's assigned-task count tells those two apart as well, so an account
126
+ the member directory cannot place no longer leaves a spinner turning in that slot
127
+ for the rest of the session. It shows no count, which is what it already did for
128
+ an account with nothing assigned. The hold is not local to the grid: the
129
+ task rows under "All documents" and "For me", and the active-instance count on a
130
+ workflow's Definition page, read the same stream, so they too open on held
131
+ numbers before the fresh read lands. A failed read is never held, and a provider remount mints a new engine and
132
+ so starts cold.
133
+
134
+ **No upgrade action required.** The card additions are automatic, and
135
+ `documentStuckCause` is a new export that changes no existing verdict —
136
+ `diagnoseInstance` classifies exactly as before, now sharing one
137
+ implementation of the document-derivable causes.
138
+
139
+ **Docs impact:** Update the Workflows tool guide where it describes the
140
+ definitions grid, to cover the new card signals, the assigned-work ordering, the
141
+ document-versus-instance split between the grid and a workflow's own page, and
142
+ that the tab opens on a held read after the first visit.
143
+ `docs/reference.md` already carries `documentStuckCause` beside
144
+ `diagnoseInstance` with its bound stated; the hosted engine reference needs the
145
+ same entry, so integrators do not read a missing cause as a health check.
146
+
147
+ - 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.
148
+
149
+ 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.
150
+
151
+ **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.
152
+
153
+ ## 0.28.0
154
+
155
+ ### Minor Changes
156
+
157
+ - 5050b06: **BREAKING:** Authoring a deployment now types through `WorkflowDeploymentInput` / `WorkflowConfigInput`, which require `expectedMinReaderModel` as the current reader-floor literal. Passing a parsed `WorkflowDeployment` / `WorkflowConfig` (where the floor is optional/unverified) into `defineWorkflowConfig` or `defineWorkflows` is a type error — share the authored deployment object instead of `config.deployments[n]`. The same pin shows up on the blueprint resource: `EditorialWorkflowsResource.deployment` and `parseWorkflowDeployment`'s return type are now `AcknowledgedDeployment` (floor asserted to the current literal), so a hand-constructed provider resource must carry that literal too.
158
+
159
+ At runtime, config parse no longer rejects a stale or missing floor on an untargeted deployment. Only deployment-scoped commands assert, and only the selected deployment: definition deploy, `--check` / `--dry-run`, blueprint provision, `start`, `definition delete`, and `definition diff` (the last is a deployment-scoped read). Instance-id commands (`fire-action`, `abort`, `set-stage`, `reset-activity`) deliberately do not assert — they resolve by instance id, not by a declared deployment's acknowledgement — so an unacknowledged floor does not stop those instance commits.
160
+
161
+ Upgrade TypeScript configs and blueprint manifests that fed a parsed deployment into those helpers: keep a shared authored object (`satisfies WorkflowDeploymentInput`) for both `defineWorkflowConfig` and `defineWorkflows`, and give any hand-built `EditorialWorkflowsResource` the current floor literal on `deployment`. Existing configs that already acknowledge the current floor keep working at runtime; a stale selected deployment still fails with `ReaderModelAcknowledgementError` (headline + short steps; the CLI renders it through the clean styled-error path).
162
+
163
+ **Docs impact:** Update the CLI / blueprint authoring examples and the prerelease reader-floor guidance so they name `WorkflowConfigInput` / `WorkflowDeploymentInput` / `AcknowledgedDeployment`, forbid passing `config.deployments[n]` into `defineWorkflows`, state that instance-id writes are outside the acknowledgement gate, and stop implying whole-config parse-time rejection for commands that never select a deployment.
164
+
165
+ - a044ba5: Report a Studio start whose auto-advance lost a write race as the finished run
166
+ it is. Starting a workflow from the Studio while another runtime (a deployed
167
+ Function, a script) drives the same instance could hand the editor a warning
168
+ toast — "'…' started but didn't finish", with a raw `unexpected revision ID`
169
+ mutation error under it — for a run that had in fact completed: the Studio's own
170
+ auto-advance simply lost the write race to whoever committed the equivalent move
171
+ first. That toast was indistinguishable from a genuinely stuck workflow. The
172
+ Studio now confirms the completed start instead of warning about it.
173
+
174
+ Narrow by design: only a lost revision race whose run is already complete is
175
+ reclassified. A conflict that left the run in flight, and any other reason an
176
+ auto-advance failed, still surface as before.
177
+
178
+ `@sanity/workflow-engine` exports `isRevisionConflict`, the predicate that tells
179
+ a lost optimistic-locking race from a real error, so integrations can classify
180
+ one without matching on message text. It answers on the error alone, so narrow
181
+ to a single rev-guarded write before asking — a bare 409 also covers a create-id
182
+ collision.
183
+
184
+ **No upgrade action required.** Engine behaviour is unchanged and the new export
185
+ is additive.
186
+
187
+ **Docs impact:** `isRevisionConflict` is new public engine surface, added to the
188
+ errors section of `docs/reference.md` alongside the concurrency errors it
189
+ classifies; mirror that entry wherever published reference material lists the
190
+ engine's error helpers.
191
+
192
+ ### Patch Changes
193
+
194
+ - 1e4a5da: When two runtimes both try to create the same stage guard after each observing it absent — for example a deployed Function reacting to instance commits while a script advances the same workflow — the losing create no longer aborts guard deploy. It falls through to the body-reconciling patch the existing-document path already uses, and writes its own resolution of the guard's conditions rather than assuming the two are identical. Concurrent deploys of an already-present guard were already a patch and are unchanged.
195
+
196
+ **No upgrade action required.**
197
+
198
+ **Docs impact: None** — `docs/reference.md` already documents `deployStageGuards` as an idempotent upsert.
199
+
3
200
  ## 0.27.0
4
201
 
5
202
  ### Minor Changes
package/DATAMODEL.md CHANGED
@@ -702,6 +702,113 @@ its base `date` / `datetime`, so no older code path can write it while skipping
702
702
  validation — there is no silent bypass to fence, and the kinds leave the floor
703
703
  at 0.
704
704
 
705
+ ### Model 6 — node semantics and field patch operations (reader floor: 4)
706
+
707
+ #### Semantics at every definition level
708
+
709
+ The optional non-empty `semantics[]` field now appears on the workflow root,
710
+ stages, and activities as well as actions. Its engine-owned vocabulary adds
711
+ `signal.positive`, `signal.caution`, and `signal.critical` at every level;
712
+ `custom.<meaning>` is an open, grammar-validated namespace at every level.
713
+ Actions retain their action-only `decision.accept` and `decision.decline`
714
+ values. The values remain advisory and change no execution, gating, transition,
715
+ status, history, or instance state. An absent field retains the generic behavior
716
+ of older definitions.
717
+
718
+ Manifest feature: `node-semantics` (definition, additive, detectable, floor 0).
719
+
720
+ Would an old reader misread (rule 5)? No. Definition content has no strict
721
+ read-side parse, and an older engine ignores the optional fields and new
722
+ advisory values. Definition documents are immutable after creation, so an older
723
+ writer never rewrites and drops them. The feature itself remains readable by
724
+ model-0 engines; new writers still stamp the unconditional writer minimum of 4.
725
+ This uses the existing `semantics` slot and extends that same word and shape to
726
+ the remaining definition nodes; a sibling property would duplicate the
727
+ advisory-meaning mechanism and violate the definition language's
728
+ self-similarity rules.
729
+
730
+ #### Field patch operations
731
+
732
+ The stored definition grammar additively admits `field.inc`, `field.dec`, and
733
+ `field.setIfMissing` action ops. Instances may embed the same definition tree
734
+ in `definitionSnapshot`, and effect-completion use records the new discriminator
735
+ in `history[]` rows' `opType`. Missing discriminators retain the historical op
736
+ vocabulary, so documents written before model 6 need no normalization. A
737
+ `field.setIfMissing` whose target already has a value writes no `opApplied` row:
738
+ persisting a targeted no-op would make older provenance and feed readers report
739
+ a field write that never happened.
740
+
741
+ Manifest feature: `field-patch-ops` (definition + instance,
742
+ additive, detectable, floor 0).
743
+
744
+ Would an old reader misread these ops (rule 5)? No. An older engine strictly
745
+ parsing or deploying a definition document rejects the unknown discriminator.
746
+ The instance runtime is different: `definitionSnapshot` is decoded without a
747
+ definition-schema parse, so an older engine can reach `applyOp`; its exhaustive
748
+ switch has no matching arm and the commit aborts when the missing summary is
749
+ used to build the audit row. That is a loud runtime failure rather than silent
750
+ execution with different semantics. Definition documents are create-only,
751
+ instance persists preserve the opaque `definitionSnapshot` string, and history
752
+ readers already treat `opType` as an opaque audit label, so the discriminator
753
+ survives an old writer's round trip. Rule 5 fences silent misinterpretation,
754
+ not loud refusal, therefore this detectable feature contributes floor 0 and the
755
+ writer maximum remains 4. The floor does not protect these definitions from
756
+ older runtimes: upgrade every runtime that can process shared workflow data
757
+ before adopting the new ops.
758
+
759
+ Why could the existing definition vocabulary not carry the behavior (definition
760
+ language rule 8)? Incrementing requires a read-modify-write primitive, while
761
+ `ValueExpr` deliberately describes context-free reads and has no arithmetic.
762
+ The existing `field.set` cannot atomically derive its value from the current
763
+ number. Conditional initialization also has no existing field-op equivalent:
764
+ an action-level `when` gates every op and effect in the action, while
765
+ `initialValue` runs only when an entry is first resolved and cannot initialize a
766
+ value cleared later by `field.unset`. These additions are new values in the
767
+ existing `ops[].type` slot and reuse the established field target and
768
+ value-expression shapes. Their abbreviated names deliberately mirror
769
+ `@sanity/client`'s patch vocabulary (`setIfMissing`, `inc`, `dec`) as one trio;
770
+ definition authors who know Sanity patches can predict the workflow spellings.
771
+
772
+ ### Model 7 — the `$attributes` condition variable (reader floor: 4)
773
+
774
+ One additive growth of the condition dialect ships as model 7. It does not
775
+ raise the reader floor; the writer maximum stays 4.
776
+
777
+ A new caller-bound synthetic variable `$attributes` binds the acting token's
778
+ org-level User Attributes (Enterprise) as a flat key → active-value record —
779
+ the same values lake role filters read via `user::attributes()`, spelled for
780
+ the workflow condition dialect. Soft-gate paths resolve it when a
781
+ caller-bound projection runs (`evaluate` / fireAction filter re-check /
782
+ editField) — project → organization → global-host attributes page with
783
+ `limit=100`, no further pages; warn when the envelope reports `hasMore` —
784
+ parallel to grants on those paths, not on every `resolveAccess`. It is never
785
+ persisted on `Actor` / history, and left unbound (`undefined`) when the fetch
786
+ is an expected absence (401/402/403/404 — non-Enterprise org, missing feature,
787
+ forbidden) so conditions that read it fail closed — the same posture as `$can`
788
+ without grants. Unexpected fetch failures (5xx, network) throw and are not
789
+ cached, so the next soft-gate call retries; ticks and drainers never call the
790
+ endpoint. Legal sites match `$can` (fireAction action filters, activity
791
+ requirements, editable predicates, and those commit re-checks); cascade gates
792
+ and other caller-free sites deploy-reject it.
793
+
794
+ Manifest feature: `attributes-condition-var` (definition condition dialect,
795
+ additive, unconditional, floor 0).
796
+
797
+ Would an old reader misread the var (rule 5)? No — an older engine never binds
798
+ `$attributes`, so a GROQ read is null / fail-closed rather than a silent
799
+ misinterpretation of persisted trees. Definitions that mention `$attributes`
800
+ still deploy and evaluate on older engines; gates that depend on it simply
801
+ never pass until the fleet reads model 7. No reader-floor raise.
802
+
803
+ **Author-predicate shadowing (model ≤6 → 7):** `attributes` joins the reserved
804
+ condition-var names, so a **new** predicate named `attributes` is
805
+ deploy-rejected. A definition already stored under model ≤6 that declared
806
+ `predicates.attributes` is a different story: on a model-7 engine that binds
807
+ the caller bag, `$attributes` in that definition flips from “the author
808
+ predicate” to “the synthetic User Attributes bag” with no redeploy. Likely
809
+ zero real instances; no migration. Authors who hit it rename the predicate
810
+ and redeploy.
811
+
705
812
  ## Pending governed changes
706
813
 
707
814
  - **`temp.system.guard` → `system.guard`** — the guard doc type's `temp.`