@sanity/workflow-engine 0.3.0 → 0.4.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.
@@ -1,193 +1,168 @@
1
1
  import * as v from "valibot";
2
- const TASK_STATUSES = ["pending", "active", "done", "skipped", "failed"], STATE_SCOPES = ["workflow", "stage", "task"], DOCUMENT_VALUE_PERMISSIONS = ["create", "read", "update"], MUTATION_GUARD_ACTIONS = [
2
+ const TASK_STATUSES = ["pending", "active", "done", "skipped", "failed"], TERMINAL_TASK_STATUSES = ["done", "skipped", "failed"];
3
+ function isTerminalTaskStatus(status) {
4
+ return TERMINAL_TASK_STATUSES.includes(status);
5
+ }
6
+ const STATE_SCOPES = ["workflow", "stage", "task"], DOCUMENT_VALUE_PERMISSIONS = ["create", "read", "update"], MUTATION_GUARD_ACTIONS = [
3
7
  "create",
4
8
  "update",
5
9
  "delete",
6
10
  "publish",
7
11
  "unpublish"
8
- ], NonEmpty = v.pipe(v.string(), v.minLength(1, "must be a non-empty string")), PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1));
12
+ ], NonEmpty = v.pipe(v.string(), v.minLength(1, "must be a non-empty string")), PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
13
+ function groqIdentifier(referencedAs) {
14
+ return v.pipe(
15
+ v.string(),
16
+ v.regex(
17
+ GROQ_IDENTIFIER,
18
+ `must be a GROQ-safe identifier (letters, digits, underscore; not starting with a digit) because it is referenced as ${referencedAs} in GROQ conditions`
19
+ )
20
+ );
21
+ }
9
22
  function picklist(options) {
10
23
  return v.picklist(
11
24
  options,
12
25
  `Invalid option: expected one of ${options.map((o) => `"${o}"`).join("|")}`
13
26
  );
14
27
  }
15
- const GdrSchema = v.strictObject({
16
- id: v.pipe(
17
- v.string(),
18
- v.regex(
19
- /^(dataset|canvas|media-library|dashboard):/,
20
- "must be a GDR URI of form '<scheme>:<...id-parts>' where scheme is one of: dataset, canvas, media-library, dashboard"
21
- )
22
- ),
23
- type: NonEmpty
24
- }), SourceSchema = v.lazy(
28
+ const SourceSchema = v.lazy(
25
29
  () => v.union([
30
+ // State-entry origins: who fills the entry, and when.
31
+ v.strictObject({ type: v.literal("init") }),
32
+ v.strictObject({ type: v.literal("write") }),
26
33
  v.strictObject({
27
- source: v.literal("literal"),
28
- value: v.union([v.string(), v.number(), v.boolean(), v.null()])
29
- }),
30
- v.strictObject({ source: v.literal("param"), paramId: NonEmpty }),
31
- v.strictObject({ source: v.literal("actor") }),
32
- v.strictObject({ source: v.literal("now") }),
33
- v.strictObject({ source: v.literal("self") }),
34
- v.strictObject({ source: v.literal("stageId") }),
35
- v.strictObject({
36
- source: v.literal("stateRead"),
37
- scope: picklist(STATE_SCOPES),
38
- slotId: NonEmpty,
39
- path: v.optional(v.string())
34
+ type: v.literal("query"),
35
+ query: NonEmpty
40
36
  }),
41
- // effectsContext lookup reads from `instance.effectsContext[id === ID].value`.
42
- // Used by chained effects where one handler's output feeds another's binding.
37
+ // Value sources: resolved to concrete JSON when an op or seed applies.
38
+ v.strictObject({ type: v.literal("literal"), value: v.unknown() }),
39
+ v.strictObject({ type: v.literal("param"), param: NonEmpty }),
40
+ v.strictObject({ type: v.literal("actor") }),
41
+ v.strictObject({ type: v.literal("now") }),
42
+ v.strictObject({ type: v.literal("self") }),
43
+ v.strictObject({ type: v.literal("stage") }),
43
44
  v.strictObject({
44
- source: v.literal("effectOutput"),
45
- contextId: NonEmpty,
45
+ type: v.literal("stateRead"),
46
+ scope: v.optional(v.union([v.literal("workflow"), v.literal("stage")])),
47
+ state: NonEmpty,
46
48
  path: v.optional(v.string())
47
49
  }),
48
- v.strictObject({ source: v.literal("row"), path: v.optional(v.string()) }),
49
50
  v.strictObject({
50
- source: v.literal("parentState"),
51
- slotId: NonEmpty,
52
- path: v.optional(v.string())
53
- }),
54
- // Compound object source — resolves each field's Source recursively
55
- // and packages the result into a JSON object. Lets ops append rich
56
- // rows to `checklist` / `notes` / `assignees` slots, e.g.
57
- // `{ source: "object", fields: { signer: { source: "actor" },
58
- // at: { source: "now" } } }`.
59
- v.strictObject({
60
- source: v.literal("object"),
51
+ type: v.literal("object"),
61
52
  fields: v.record(NonEmpty, SourceSchema)
62
53
  })
63
54
  ])
64
- ), ScopeKeySchema = v.strictObject({
55
+ ), StoredStateRefSchema = v.strictObject({
65
56
  scope: picklist(STATE_SCOPES),
66
- slotId: NonEmpty
57
+ state: NonEmpty
58
+ }), AuthoringStateRefSchema = v.strictObject({
59
+ scope: v.optional(picklist(STATE_SCOPES)),
60
+ state: NonEmpty
67
61
  }), OpPredicateSchema = v.lazy(
68
62
  () => v.union([
69
63
  v.strictObject({
64
+ type: v.literal("field"),
70
65
  field: NonEmpty,
71
66
  equals: SourceSchema
72
67
  }),
73
- v.strictObject({ all: v.array(OpPredicateSchema) }),
74
- v.strictObject({ any: v.array(OpPredicateSchema) })
68
+ v.strictObject({ type: v.literal("all"), of: v.array(OpPredicateSchema) }),
69
+ v.strictObject({ type: v.literal("any"), of: v.array(OpPredicateSchema) })
75
70
  ])
76
- ), OpSchema = v.variant("type", [
77
- v.strictObject({
78
- type: v.literal("workflow.op.state.set"),
79
- target: ScopeKeySchema,
80
- value: SourceSchema
81
- }),
82
- v.strictObject({
83
- type: v.literal("workflow.op.state.append"),
84
- target: ScopeKeySchema,
85
- item: SourceSchema
86
- }),
87
- v.strictObject({
88
- type: v.literal("workflow.op.state.updateWhere"),
89
- target: ScopeKeySchema,
90
- where: OpPredicateSchema,
91
- merge: v.record(NonEmpty, SourceSchema)
92
- }),
93
- v.strictObject({
94
- type: v.literal("workflow.op.state.removeWhere"),
95
- target: ScopeKeySchema,
96
- where: OpPredicateSchema
97
- }),
71
+ );
72
+ function opSchemas(targetSchema) {
73
+ return [
74
+ v.strictObject({
75
+ type: v.literal("state.set"),
76
+ target: targetSchema,
77
+ value: SourceSchema
78
+ }),
79
+ v.strictObject({
80
+ type: v.literal("state.unset"),
81
+ target: targetSchema
82
+ }),
83
+ v.strictObject({
84
+ type: v.literal("state.append"),
85
+ target: targetSchema,
86
+ value: SourceSchema
87
+ }),
88
+ v.strictObject({
89
+ type: v.literal("state.updateWhere"),
90
+ target: targetSchema,
91
+ where: OpPredicateSchema,
92
+ value: SourceSchema
93
+ }),
94
+ v.strictObject({
95
+ type: v.literal("state.removeWhere"),
96
+ target: targetSchema,
97
+ where: OpPredicateSchema
98
+ })
99
+ ];
100
+ }
101
+ const StoredOpSchema = v.variant("type", [
102
+ ...opSchemas(StoredStateRefSchema),
98
103
  v.strictObject({
99
- type: v.literal("workflow.op.status.set"),
100
- taskId: NonEmpty,
104
+ type: v.literal("status.set"),
105
+ task: NonEmpty,
101
106
  status: picklist(TASK_STATUSES)
102
107
  })
103
- ]), StateSlotSourceSchema = v.union([
104
- v.strictObject({ kind: v.literal("init") }),
108
+ ]), StoredTransitionOpSchema = v.variant("type", [...opSchemas(StoredStateRefSchema)]), AuditOpSchema = v.strictObject({
109
+ type: v.literal("audit"),
110
+ target: AuthoringStateRefSchema,
111
+ value: SourceSchema,
112
+ stampFields: v.optional(
113
+ v.strictObject({
114
+ actor: v.optional(NonEmpty),
115
+ at: v.optional(NonEmpty)
116
+ })
117
+ )
118
+ }), AuthoringOpSchema = v.variant("type", [
119
+ ...opSchemas(AuthoringStateRefSchema),
105
120
  v.strictObject({
106
- kind: v.literal("query"),
107
- query: NonEmpty,
108
- resource: v.optional(v.union([v.literal("workflow"), v.literal("subject"), GdrSchema]))
121
+ type: v.literal("status.set"),
122
+ task: v.optional(NonEmpty),
123
+ status: picklist(TASK_STATUSES)
109
124
  }),
110
- v.strictObject({ kind: v.literal("write") }),
111
- /**
112
- * Pre-populate the slot at resolution time with a constant value.
113
- * Useful for stage- or task-scope slots that should start non-empty
114
- * (e.g. a default headline, a starting tally).
115
- */
116
- v.strictObject({ kind: v.literal("literal"), value: v.unknown() }),
117
- /**
118
- * Pre-populate by reading another already-resolved slot. `scope:
119
- * "workflow"` reads from the workflow-scope `state[]` (resolved at
120
- * startInstance). `scope: "stage"` reads from an earlier slot in
121
- * this same stage entry's `state[]` (sequential resolution).
122
- * Optional `path` selects a nested field (e.g. `path: "id"` on a
123
- * doc.ref slot).
124
- */
125
- v.strictObject({
126
- kind: v.literal("stateRead"),
127
- scope: v.union([v.literal("workflow"), v.literal("stage")]),
128
- slotId: NonEmpty,
129
- path: v.optional(v.string())
130
- })
131
- ]), StateSlotKindSchema = picklist([
132
- "workflow.state.doc.ref",
133
- "workflow.state.doc.refs",
134
- // Release reference. A workflow declares this slot to say "I target a
135
- // Content Release"; the runtime fills it with a specific release at
136
- // start time, and the engine auto-derives the instance's read
137
- // perspective from it.
138
- "workflow.state.release.ref",
139
- "workflow.state.query",
140
- "workflow.state.value.string",
141
- "workflow.state.value.url",
142
- "workflow.state.value.number",
143
- "workflow.state.value.boolean",
144
- "workflow.state.value.dateTime",
145
- "workflow.state.value.actor",
146
- "workflow.state.checklist",
147
- "workflow.state.notes",
148
- "workflow.state.assignees"
149
- ]), StateSlotSchema = v.strictObject({
150
- type: StateSlotKindSchema,
151
- id: NonEmpty,
152
- title: v.optional(v.string()),
153
- description: v.optional(v.string()),
154
- source: StateSlotSourceSchema
155
- }), FilterSchema = v.union([
156
- // Inline GROQ — only reserved params allowed at runtime.
157
- NonEmpty,
158
- // Reference to a named predicate, optionally with named args.
159
- v.strictObject({
160
- ref: NonEmpty,
161
- args: v.optional(v.record(v.string(), v.unknown()))
162
- })
163
- ]), PredicateParamSchema = v.strictObject({
164
- id: NonEmpty,
125
+ AuditOpSchema
126
+ ]), StateKindSchema = picklist([
127
+ "doc.ref",
128
+ "doc.refs",
129
+ // Release reference. A workflow declares this entry to say "I target a
130
+ // Content Release"; the runtime fills it at start time and auto-derives
131
+ // the instance's read perspective from it.
132
+ "release.ref",
133
+ "query",
134
+ "value.string",
135
+ "value.url",
136
+ "value.number",
137
+ "value.boolean",
138
+ "value.dateTime",
139
+ "value.actor",
140
+ "checklist",
141
+ "notes",
142
+ // The WHO-FOR entry: the inbox reverse-query reads it by kind, and the
143
+ // rendered `$assigned` gate matches the caller against it.
144
+ "assignees"
145
+ ]), StateEntryName = groqIdentifier("`$state.<name>`"), StateEntrySchema = v.strictObject({
146
+ type: StateKindSchema,
147
+ name: StateEntryName,
165
148
  title: v.optional(v.string()),
166
149
  description: v.optional(v.string()),
167
- type: picklist(["string", "number", "boolean", "string[]", "reference"]),
168
- enum: v.optional(v.array(v.string()))
169
- }), PredicateSchema = v.strictObject({
170
- type: v.optional(v.literal("workflow.predicate")),
171
- id: NonEmpty,
150
+ source: SourceSchema
151
+ }), ClaimStateSchema = v.strictObject({
152
+ type: v.literal("claim"),
153
+ name: StateEntryName,
172
154
  title: v.optional(v.string()),
173
- description: v.optional(v.string()),
174
- groq: NonEmpty,
175
- params: v.optional(v.array(PredicateParamSchema))
176
- }), EffectSchema = v.strictObject({
177
- type: v.optional(v.literal("workflow.effect")),
178
- id: NonEmpty,
155
+ description: v.optional(v.string())
156
+ }), AuthoringStateEntrySchema = v.union([StateEntrySchema, ClaimStateSchema]), ConditionSchema = NonEmpty, EffectSchema = v.strictObject({
157
+ name: NonEmpty,
179
158
  title: v.optional(v.string()),
180
159
  description: v.optional(v.string()),
181
- // Bindings resolve typed Source values at commit time, producing
182
- // concrete JSON in the queued effect's `params`. NO GROQ.
183
- bindings: v.optional(v.record(v.string(), SourceSchema)),
160
+ /** GROQ reads over the rendered scope, resolved to concrete JSON at queue time. */
161
+ bindings: v.optional(v.record(v.string(), ConditionSchema)),
162
+ /** Static config, passed through to the handler verbatim. */
184
163
  input: v.optional(v.record(v.string(), v.unknown()))
185
- }), TerminalTaskStatus = picklist(["done", "skipped", "failed"]), PermissionSchema = picklist(DOCUMENT_VALUE_PERMISSIONS), ActionParamSchema = v.strictObject({
186
- type: v.optional(v.literal("workflow.action.param")),
187
- id: NonEmpty,
188
- title: v.optional(v.string()),
189
- description: v.optional(v.string()),
190
- paramType: picklist([
164
+ }), ActionParamSchema = v.strictObject({
165
+ type: picklist([
191
166
  "string",
192
167
  "number",
193
168
  "boolean",
@@ -198,130 +173,113 @@ const GdrSchema = v.strictObject({
198
173
  "doc.refs",
199
174
  "json"
200
175
  ]),
201
- required: v.optional(v.boolean())
202
- }), ActionSchema = v.strictObject({
203
- type: v.optional(v.literal("workflow.action")),
204
- id: NonEmpty,
176
+ name: NonEmpty,
205
177
  title: v.optional(v.string()),
206
178
  description: v.optional(v.string()),
207
- filter: v.optional(FilterSchema),
208
- /** Caller-supplied params, validated before ops/effects run. */
209
- params: v.optional(v.array(ActionParamSchema)),
210
- /**
211
- * Status the task flips to when this action fires. Omit to leave
212
- * the task status unchanged — used by intermediate `claim` /
213
- * `assign` / partial-update actions where the task should stay
214
- * active so a later action can fire on it. Declared values must
215
- * be terminal (`done` / `skipped` / `failed`); the engine never
216
- * mid-flip back to `active` outside of the `workflow.op.status.set`
217
- * op explicit path.
218
- */
219
- setStatus: v.optional(TerminalTaskStatus),
220
- /**
221
- * Inline state-mutation primitives executed during the action commit.
222
- * Each op runs against the in-memory mutation (deterministic, no
223
- * external I/O). Resolved Sources turn into concrete JSON before the
224
- * op applies. Logged on `history[]` as `workflow.history.opApplied`
225
- * and surfaced on the `ActionResult.ranOps`.
226
- */
227
- ops: v.optional(v.array(OpSchema)),
228
- effects: v.optional(v.array(EffectSchema)),
229
- /**
230
- * Role gating — OR-match against `Actor.roles[]`. If omitted, no role
231
- * gate. `"*"` in Actor.roles always matches (bench / all-access).
232
- */
179
+ required: v.optional(v.boolean())
180
+ });
181
+ function actionFields(op) {
182
+ return {
183
+ name: NonEmpty,
184
+ title: v.optional(v.string()),
185
+ description: v.optional(v.string()),
186
+ /**
187
+ * The one gate mechanism: a condition over the rendered scope. Hard
188
+ * enforcement lives outside the engine entirely (the lake ACL on the
189
+ * documents, and guards) — every engine-evaluated gate is authoring/UX.
190
+ */
191
+ filter: v.optional(ConditionSchema),
192
+ params: v.optional(v.array(ActionParamSchema)),
193
+ ops: v.optional(v.array(op)),
194
+ effects: v.optional(v.array(EffectSchema))
195
+ };
196
+ }
197
+ const StoredActionSchema = v.strictObject(actionFields(StoredOpSchema)), TerminalTaskStatus = picklist(TERMINAL_TASK_STATUSES), RawAuthoringActionSchema = v.strictObject({
198
+ ...actionFields(AuthoringOpSchema),
233
199
  roles: v.optional(v.array(NonEmpty)),
234
- /**
235
- * When true, the actor must appear in the task's `assignees[]` (as a
236
- * user-kind assignee matching by id) for the action to be allowed.
237
- */
238
- requireAssignment: v.optional(v.boolean()),
239
- /**
240
- * Which permission on the workflow.instance document the actor must
241
- * hold (per supplied `grants`). Defaults to "update". Only checked
242
- * when grants are supplied at evaluate/fireAction time.
243
- */
244
- requiredPermission: v.optional(PermissionSchema)
245
- }), CompletionPolicySchema = v.union([
246
- v.strictObject({ kind: v.literal("all") }),
247
- v.strictObject({ kind: v.literal("any") }),
248
- v.strictObject({
249
- kind: v.literal("count"),
250
- n: PositiveInt
251
- })
252
- ]), LogicalDefinitionRefSchema = v.strictObject({
253
- workflowId: NonEmpty,
254
- version: v.optional(v.union([PositiveInt, v.literal("latest")]))
255
- }), SpawnForEachSchema = v.strictObject({
256
- groq: NonEmpty,
257
- as: v.strictObject({
258
- initialState: v.optional(
259
- v.array(
260
- v.strictObject({
261
- type: StateSlotKindSchema,
262
- id: NonEmpty,
263
- value: SourceSchema
264
- })
265
- )
266
- ),
267
- effectsContext: v.optional(v.record(v.string(), SourceSchema))
268
- })
269
- }), SpawnSchema = v.strictObject({
270
- type: v.optional(v.literal("workflow.spawn")),
271
- id: v.optional(v.string()),
200
+ status: v.optional(TerminalTaskStatus)
201
+ }), ClaimActionSchema = v.strictObject({
202
+ type: v.literal("claim"),
203
+ name: NonEmpty,
272
204
  title: v.optional(v.string()),
273
205
  description: v.optional(v.string()),
274
- definitionRef: LogicalDefinitionRefSchema,
275
- forEach: SpawnForEachSchema,
276
- completionPolicy: v.optional(CompletionPolicySchema)
277
- }), AssigneeSchema = v.variant("kind", [
278
- v.strictObject({ kind: v.literal("user"), id: NonEmpty }),
279
- v.strictObject({ kind: v.literal("role"), role: NonEmpty })
280
- ]), TaskSchema = v.strictObject({
281
- type: v.optional(v.literal("workflow.task")),
282
- id: NonEmpty,
283
- title: v.optional(v.string()),
284
- description: v.optional(v.string()),
285
- assignees: v.optional(v.array(AssigneeSchema)),
286
- filter: v.optional(FilterSchema),
287
- invokeOn: v.optional(picklist(["stageEnter", "manual"])),
288
- /**
289
- * Auto-completion predicate — same shape as `transition.filter`. When
290
- * truthy at activation or on any subsequent cascade, the engine
291
- * flips the task to `done` with a `{ kind: "system", id: "engine.completeWhen" }`
292
- * actor. A full filter, not just a wall-clock deadline.
293
- */
294
- completeWhen: v.optional(FilterSchema),
206
+ state: v.union([NonEmpty, AuthoringStateRefSchema]),
207
+ roles: v.optional(v.array(NonEmpty)),
208
+ filter: v.optional(ConditionSchema),
209
+ params: v.optional(v.array(ActionParamSchema)),
210
+ effects: v.optional(v.array(EffectSchema))
211
+ }), AuthoringActionSchema = v.union([RawAuthoringActionSchema, ClaimActionSchema]), DefinitionRefSchema = v.strictObject({
212
+ name: NonEmpty,
213
+ version: v.optional(v.union([PositiveInt, v.literal("latest")]))
214
+ }), SubworkflowsSchema = v.strictObject({
215
+ /** GROQ producing one row per subworkflow; each row binds as `$row`. */
216
+ forEach: NonEmpty,
217
+ definition: DefinitionRefSchema,
218
+ /** Initial state for each subworkflow — entry name → GROQ over `$row` + the parent scope. */
219
+ with: v.optional(v.record(NonEmpty, ConditionSchema)),
295
220
  /**
296
- * Auto-failure predicate symmetric to `completeWhen`. Same shape,
297
- * same evaluation cadence (activation + every cascade), but flips
298
- * the task to `failed` (not `done`) with a `{ kind: "system",
299
- * id: "engine.failWhen" }` actor. When both `completeWhen` and
300
- * `failWhen` resolve truthy on the same evaluation, `failWhen`
301
- * wins — failure is the more notable signal and should not be
302
- * silently swallowed.
221
+ * Extra values evaluated in the parent's rendered scope at spawn time and
222
+ * delivered into each subworkflow's `$effects` bag the parent→child
223
+ * handoff, read exactly like an effect output.
303
224
  */
304
- failWhen: v.optional(FilterSchema),
305
- effects: v.optional(v.array(EffectSchema)),
306
- actions: v.optional(v.array(ActionSchema)),
307
- spawns: v.optional(SpawnSchema),
308
- contentRefs: v.optional(v.array(v.unknown())),
309
- /** Task-scoped state slots. Resolved at task activation time. */
310
- state: v.optional(v.array(StateSlotSchema))
311
- }), TransitionSchema = v.strictObject({
312
- type: v.optional(v.literal("workflow.transition")),
313
- id: NonEmpty,
314
- title: v.optional(v.string()),
315
- description: v.optional(v.string()),
316
- to: NonEmpty,
317
- on: v.optional(v.string()),
318
- filter: v.optional(FilterSchema),
319
- effects: v.optional(v.array(EffectSchema))
320
- }), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardMatchSchema = v.strictObject({
225
+ context: v.optional(v.record(NonEmpty, ConditionSchema))
226
+ });
227
+ function taskFields(state, action, op) {
228
+ return {
229
+ name: NonEmpty,
230
+ title: v.optional(v.string()),
231
+ description: v.optional(v.string()),
232
+ activation: v.optional(picklist(["auto", "manual"])),
233
+ filter: v.optional(ConditionSchema),
234
+ /**
235
+ * Auto-completion condition — evaluated at activation and on every
236
+ * cascade; truthy flips the task to `done` with a system actor. On a
237
+ * spawning task it typically reads `$subworkflows`.
238
+ */
239
+ completeWhen: v.optional(ConditionSchema),
240
+ /**
241
+ * Auto-failure condition symmetric to `completeWhen`, flips to
242
+ * `failed`. When both are truthy on the same evaluation, `failWhen`
243
+ * wins — failure is the more notable signal.
244
+ */
245
+ failWhen: v.optional(ConditionSchema),
246
+ ops: v.optional(v.array(op)),
247
+ effects: v.optional(v.array(EffectSchema)),
248
+ actions: v.optional(v.array(action)),
249
+ subworkflows: v.optional(SubworkflowsSchema),
250
+ /** Task-scoped state entries. Resolved at task activation time. */
251
+ state: v.optional(v.array(state))
252
+ };
253
+ }
254
+ const StoredTaskSchema = v.strictObject(
255
+ taskFields(StateEntrySchema, StoredActionSchema, StoredOpSchema)
256
+ ), AuthoringTaskSchema = v.strictObject(
257
+ taskFields(AuthoringStateEntrySchema, AuthoringActionSchema, AuthoringOpSchema)
258
+ );
259
+ function transitionFields(op, filter) {
260
+ return {
261
+ name: NonEmpty,
262
+ title: v.optional(v.string()),
263
+ description: v.optional(v.string()),
264
+ to: NonEmpty,
265
+ filter,
266
+ /** The `state.*` subset — state-write-on-move, the stage's EXIT/ARRIVAL payload. */
267
+ ops: v.optional(v.array(op)),
268
+ effects: v.optional(v.array(EffectSchema))
269
+ };
270
+ }
271
+ const StoredTransitionSchema = v.strictObject(
272
+ transitionFields(StoredTransitionOpSchema, ConditionSchema)
273
+ ), AuthoringTransitionOpSchema = v.variant("type", [
274
+ ...opSchemas(AuthoringStateRefSchema),
275
+ AuditOpSchema
276
+ ]), AuthoringTransitionSchema = v.strictObject(
277
+ transitionFields(AuthoringTransitionOpSchema, v.optional(ConditionSchema))
278
+ ), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardMatchSchema = v.strictObject({
321
279
  /** Subject `_type`(s); empty matches any type. */
322
280
  types: v.optional(v.array(NonEmpty)),
323
- /** Target docs as `Source`s resolved at deploy to bare ids + the resource. */
324
- idRefs: v.optional(v.array(SourceSchema)),
281
+ /** Target docs as `$state` reads (or `"$self"`), resolved at deploy to bare ids + the resource. */
282
+ idRefs: v.optional(v.array(NonEmpty)),
325
283
  /** Glob id patterns (bare, resource-local). */
326
284
  idPatterns: v.optional(v.array(NonEmpty)),
327
285
  actions: v.pipe(
@@ -329,58 +287,71 @@ const GdrSchema = v.strictObject({
329
287
  v.minLength(1, "a guard must match at least one action")
330
288
  )
331
289
  }), GuardSchema = v.strictObject({
332
- name: v.optional(v.string()),
290
+ name: NonEmpty,
291
+ title: v.optional(v.string()),
333
292
  description: v.optional(v.string()),
334
293
  match: GuardMatchSchema,
335
294
  /**
336
- * Lake GROQ predicate. Reads `document.before`/`document.after`, `mutation`,
337
- * `guard`, and `identity()`. Bare ids/fields only — no GDRs. Omitted or
338
- * empty means UNCONDITIONAL DENY.
295
+ * Lake GROQ predicate a distinct eval context reading
296
+ * `document.before`/`document.after`, `mutation`, `guard`, and
297
+ * `identity()`. Bare ids/fields only. Omitted or empty means
298
+ * UNCONDITIONAL DENY.
339
299
  */
340
300
  predicate: v.optional(v.string()),
341
301
  /**
342
- * Projected workflow state the predicate reads as `guard.metadata.*`. Values
343
- * are `Source`s resolved at deploy and re-synced by the engine. Copied data,
344
- * never a cross-resource pointer.
345
- */
346
- metadata: v.optional(v.record(NonEmpty, SourceSchema))
347
- }), StageKindSchema = picklist(["initial", "intermediate", "terminal", "waiting"]), StageSchema = v.strictObject({
348
- type: v.optional(v.literal("workflow.stage")),
349
- id: NonEmpty,
350
- title: v.optional(v.string()),
351
- description: v.optional(v.string()),
352
- kind: v.optional(StageKindSchema),
353
- tasks: v.optional(v.array(TaskSchema)),
354
- transitions: v.optional(v.array(TransitionSchema)),
355
- completion: v.optional(FilterSchema),
356
- onEnter: v.optional(v.array(EffectSchema)),
357
- onExit: v.optional(v.array(EffectSchema)),
358
- /**
359
- * Lake mutation guards active while this stage holds. Each compiles to a
360
- * `temp.system.guard` doc deployed on stage entry and retracted on exit.
361
- * Plural because one stage's intent may span resources (one guard each).
302
+ * Projected workflow state the predicate reads as `guard.metadata.*`
303
+ * the only bridge from the lake eval context (which cannot see `$state`)
304
+ * to workflow state. Values are NOT GROQ: each is a deploy-time read in
305
+ * the guard mini-language — `"$self"`, `"$now"`, or
306
+ * `"$state.<name>[.path]"` — resolved into a bare value at deploy and
307
+ * re-synced by the post-state-op guard refresh.
362
308
  */
363
- guards: v.optional(v.array(GuardSchema)),
364
- contentRefs: v.optional(v.array(v.unknown())),
365
- /** Stage-scoped state slots. Resolved at stage entry and persisted on
366
- * the StageEntry. */
367
- state: v.optional(v.array(StateSlotSchema))
368
- }), WorkflowDefinitionSchema = v.strictObject({
369
- workflowId: NonEmpty,
370
- version: PositiveInt,
371
- title: NonEmpty,
372
- description: v.optional(v.string()),
373
- initialStageId: NonEmpty,
374
- /**
375
- * Workflow-level state slots. Persist for the instance lifetime.
376
- * Every stage's filters/ops can read them via
377
- * `*[_id == $self][0].state[key == "<key>"][0].value`. Init-sourced
378
- * slots are supplied at `startInstance` via `initialState`.
379
- */
380
- state: v.optional(v.array(StateSlotSchema)),
381
- stages: v.pipe(v.array(StageSchema), v.minLength(1, "must declare at least one stage")),
382
- predicates: v.optional(v.array(PredicateSchema))
383
- }), WORKFLOW_DEFINITION_TYPE = "workflow.definition";
309
+ metadata: v.optional(v.record(NonEmpty, NonEmpty))
310
+ });
311
+ function stageFields(state, task, transition) {
312
+ return {
313
+ name: NonEmpty,
314
+ title: v.optional(v.string()),
315
+ description: v.optional(v.string()),
316
+ tasks: v.optional(v.array(task)),
317
+ transitions: v.optional(v.array(transition)),
318
+ /**
319
+ * Lake mutation guards active while this stage holds. Each compiles to a
320
+ * `temp.system.guard` doc deployed on stage entry and retracted on exit.
321
+ */
322
+ guards: v.optional(v.array(GuardSchema)),
323
+ /** Stage-scoped state entries. Resolved at stage entry. */
324
+ state: v.optional(v.array(state))
325
+ };
326
+ }
327
+ const StoredStageSchema = v.strictObject(
328
+ stageFields(StateEntrySchema, StoredTaskSchema, StoredTransitionSchema)
329
+ ), AuthoringStageSchema = v.strictObject(
330
+ stageFields(AuthoringStateEntrySchema, AuthoringTaskSchema, AuthoringTransitionSchema)
331
+ );
332
+ function workflowFields(state, stage) {
333
+ return {
334
+ name: NonEmpty,
335
+ version: PositiveInt,
336
+ title: NonEmpty,
337
+ description: v.optional(v.string()),
338
+ /** Reference field: named for the target, holds the stage's `name`. */
339
+ initialStage: NonEmpty,
340
+ /** Workflow-scope state entries. Persist for the instance lifetime. */
341
+ state: v.optional(v.array(state)),
342
+ stages: v.pipe(v.array(stage), v.minLength(1, "must declare at least one stage")),
343
+ /**
344
+ * Nullary named conditions — each `name: groq` entry is pre-evaluated
345
+ * and bound as the boolean `$name` var, composable with native GROQ.
346
+ * Redefining a built-in var is a deploy error (never silently shadow).
347
+ */
348
+ predicates: v.optional(v.record(groqIdentifier("`$<name>`"), ConditionSchema))
349
+ };
350
+ }
351
+ v.strictObject(workflowFields(StateEntrySchema, StoredStageSchema));
352
+ const AuthoringWorkflowSchema = v.strictObject(
353
+ workflowFields(AuthoringStateEntrySchema, AuthoringStageSchema)
354
+ ), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
384
355
  function formatValidationError(label, issues) {
385
356
  const lines = issues.map((issue) => ` - ${issue.path.length === 0 ? "(root)" : formatPath(issue.path)}: ${issue.message}`);
386
357
  return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):
@@ -400,17 +371,20 @@ function formatPath(path) {
400
371
  return out;
401
372
  }
402
373
  export {
403
- ActionSchema,
404
- AssigneeSchema,
374
+ AuthoringActionSchema,
375
+ AuthoringOpSchema,
376
+ AuthoringStageSchema,
377
+ AuthoringStateEntrySchema,
378
+ AuthoringTaskSchema,
379
+ AuthoringTransitionSchema,
380
+ AuthoringWorkflowSchema,
381
+ DOCUMENT_VALUE_PERMISSIONS,
405
382
  EffectSchema,
406
- FilterSchema,
407
- PredicateSchema,
408
- StageSchema,
409
- TaskSchema,
410
- TransitionSchema,
383
+ GROQ_IDENTIFIER,
384
+ GuardSchema,
411
385
  WORKFLOW_DEFINITION_TYPE,
412
- WorkflowDefinitionSchema,
413
386
  formatValidationError,
387
+ isTerminalTaskStatus,
414
388
  issuesFromValibot
415
389
  };
416
390
  //# sourceMappingURL=schema.js.map