@sanity/workflow-engine 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -17,13 +17,8 @@ function _interopNamespaceCompat(e) {
17
17
  }), n.default = e, Object.freeze(n);
18
18
  }
19
19
  var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
20
- function getPath(value, path) {
21
- let current = value;
22
- for (const part of path.split(".")) {
23
- if (current == null || typeof current != "object") return;
24
- current = current[part];
25
- }
26
- return current;
20
+ function findOpenStageEntry(host) {
21
+ return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
27
22
  }
28
23
  const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
29
24
  function parseGdr(uri) {
@@ -127,12 +122,100 @@ function gdrRef(res, documentId, type) {
127
122
  function isGdr(value) {
128
123
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
129
124
  }
125
+ function buildParams(args) {
126
+ const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
127
+ return {
128
+ self: selfGdr(instance),
129
+ state: renderedState(instance.state ?? [], snapshot),
130
+ parent: instance.ancestors.at(-1)?.id ?? null,
131
+ ancestors: instance.ancestors.map((a) => a.id),
132
+ stage: instance.currentStage,
133
+ now,
134
+ /** `$effects` — parent context handoff by entry name, completed-effect
135
+ * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
136
+ effects: effectsContextMap(instance),
137
+ /** `$tasks` — the current stage's task rows, statuses included. */
138
+ tasks: currentTasks2,
139
+ allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
140
+ anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
141
+ ...extra
142
+ };
143
+ }
144
+ function renderedState(entries, snapshot) {
145
+ const out = {};
146
+ for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
147
+ return out;
148
+ }
149
+ function renderedValue(entry, snapshot) {
150
+ return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
151
+ }
152
+ function scopedStateOverlay(instance, snapshot, taskName) {
153
+ const stageEntry = findOpenStageEntry(instance);
154
+ if (stageEntry === void 0) return {};
155
+ const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
156
+ return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
157
+ }
158
+ function assignedFor(instance, taskName, actor) {
159
+ if (actor === void 0) return !1;
160
+ const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
161
+ return entry === void 0 ? !1 : entry.value.some(
162
+ (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
163
+ );
164
+ }
165
+ function effectsContextMap(instance) {
166
+ const out = {};
167
+ for (const entry of instance.effectsContext)
168
+ out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
169
+ return out;
170
+ }
171
+ function parseJsonContextEntry(entry) {
172
+ try {
173
+ return JSON.parse(entry.value);
174
+ } catch (err) {
175
+ throw new Error(
176
+ `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
177
+ { cause: err }
178
+ );
179
+ }
180
+ }
181
+ function paramsForLake(params) {
182
+ return {
183
+ ...params,
184
+ self: typeof params.self == "string" ? bareId(params.self) : params.self,
185
+ parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
186
+ ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
187
+ state: stripStateForLake(params.state)
188
+ };
189
+ }
190
+ function stripStateForLake(value) {
191
+ if (value == null) return value;
192
+ if (typeof value == "string") return bareId(value);
193
+ if (Array.isArray(value)) return value.map(stripStateForLake);
194
+ if (typeof value == "object") {
195
+ const out = {};
196
+ for (const [k, v2] of Object.entries(value))
197
+ out[k] = stripStateForLake(v2);
198
+ return out;
199
+ }
200
+ return value;
201
+ }
202
+ function bareId(id) {
203
+ return isGdrUri(id) ? extractDocumentId(id) : id;
204
+ }
205
+ function getPath(value, path) {
206
+ let current = value;
207
+ for (const part of path.split(".")) {
208
+ if (current == null || typeof current != "object") return;
209
+ current = current[part];
210
+ }
211
+ return current;
212
+ }
130
213
  function resourceOf(p) {
131
214
  return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
132
215
  }
133
- const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/;
216
+ const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
134
217
  function isGuardReadExpr(expr) {
135
- return expr === "$self" || expr === "$now" || STATE_READ.test(expr);
218
+ return expr === "$self" || expr === "$now" || STATE_READ.test(expr) || EFFECTS_READ.test(expr);
136
219
  }
137
220
  function resolveGuardRead(expr, ctx) {
138
221
  if (expr === "$self") return selfGdr(ctx.instance);
@@ -142,8 +225,13 @@ function resolveGuardRead(expr, ctx) {
142
225
  const value = (ctx.instance.state ?? []).find((s) => s.name === stateRead[1])?.value;
143
226
  return stateRead[2] !== void 0 ? getPath(value, stateRead[2]) : value;
144
227
  }
228
+ const effectsRead = EFFECTS_READ.exec(expr);
229
+ if (effectsRead !== null) {
230
+ const outputs = effectsContextMap(ctx.instance)[effectsRead[1]];
231
+ return effectsRead[2] !== void 0 ? getPath(outputs, effectsRead[2]) : outputs;
232
+ }
145
233
  throw new Error(
146
- `Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
234
+ `Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", "$state.<name>[.path]", or "$effects['<name>'][.path]" (guards store resolved values; the lake cannot see $state or $effects)`
147
235
  );
148
236
  }
149
237
  function resolveIdRefTarget(expr, ctx) {
@@ -225,7 +313,7 @@ function createDefinitionValidator() {
225
313
  entry.source.type === "query" && tryParse(entry.source.query, `${label}.query`);
226
314
  }, checkGuardRead: (expr, where) => {
227
315
  isGuardReadExpr(expr) || errors.push(
228
- ` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
316
+ ` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", "$state.<name>[.path]", or "$effects['<name>'][.path]" (guards store resolved values; the lake cannot see $state or $effects)`
229
317
  );
230
318
  } };
231
319
  }
@@ -404,89 +492,6 @@ async function assertInstanceWriteAllowed(args) {
404
492
  guards: denied
405
493
  });
406
494
  }
407
- function findOpenStageEntry(host) {
408
- return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
409
- }
410
- function buildParams(args) {
411
- const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
412
- return {
413
- self: selfGdr(instance),
414
- state: renderedState(instance.state ?? [], snapshot),
415
- parent: instance.ancestors.at(-1)?.id ?? null,
416
- ancestors: instance.ancestors.map((a) => a.id),
417
- stage: instance.currentStage,
418
- now,
419
- /** `$effects` — parent context handoff by entry name, completed-effect
420
- * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
421
- effects: effectsContextMap(instance),
422
- /** `$tasks` — the current stage's task rows, statuses included. */
423
- tasks: currentTasks2,
424
- allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
425
- anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
426
- ...extra
427
- };
428
- }
429
- function renderedState(entries, snapshot) {
430
- const out = {};
431
- for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
432
- return out;
433
- }
434
- function renderedValue(entry, snapshot) {
435
- return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
436
- }
437
- function scopedStateOverlay(instance, snapshot, taskName) {
438
- const stageEntry = findOpenStageEntry(instance);
439
- if (stageEntry === void 0) return {};
440
- const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
441
- return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
442
- }
443
- function assignedFor(instance, taskName, actor) {
444
- if (actor === void 0) return !1;
445
- const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
446
- return entry === void 0 ? !1 : entry.value.some(
447
- (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
448
- );
449
- }
450
- function effectsContextMap(instance) {
451
- const out = {};
452
- for (const entry of instance.effectsContext)
453
- out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
454
- return out;
455
- }
456
- function parseJsonContextEntry(entry) {
457
- try {
458
- return JSON.parse(entry.value);
459
- } catch (err) {
460
- throw new Error(
461
- `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
462
- { cause: err }
463
- );
464
- }
465
- }
466
- function paramsForLake(params) {
467
- return {
468
- ...params,
469
- self: typeof params.self == "string" ? bareId(params.self) : params.self,
470
- parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
471
- ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
472
- state: stripStateForLake(params.state)
473
- };
474
- }
475
- function stripStateForLake(value) {
476
- if (value == null) return value;
477
- if (typeof value == "string") return bareId(value);
478
- if (Array.isArray(value)) return value.map(stripStateForLake);
479
- if (typeof value == "object") {
480
- const out = {};
481
- for (const [k, v2] of Object.entries(value))
482
- out[k] = stripStateForLake(v2);
483
- return out;
484
- }
485
- return value;
486
- }
487
- function bareId(id) {
488
- return isGdrUri(id) ? extractDocumentId(id) : id;
489
- }
490
495
  function abortReason(instance) {
491
496
  return instance.history.find(
492
497
  (h) => h._type === "aborted"
@@ -609,6 +614,79 @@ function remediationsFor(diagnosis) {
609
614
  available: RUNNABLE_VERBS.has(seed.verb)
610
615
  }));
611
616
  }
617
+ class ActionParamsInvalidError extends Error {
618
+ action;
619
+ task;
620
+ issues;
621
+ constructor(args) {
622
+ const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
623
+ `);
624
+ super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
625
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
626
+ }
627
+ }
628
+ class RequiredStateNotProvidedError extends Error {
629
+ definition;
630
+ missing;
631
+ constructor(args) {
632
+ const lines = args.missing.map((m) => ` - ${m.name} (${m.type})`).join(`
633
+ `), where = args.definition !== void 0 ? ` for workflow "${args.definition}"` : "";
634
+ super(
635
+ `Required init state${where} was not provided:
636
+ ${lines}
637
+ Provide each via \`initialState\` when starting standalone, or via the parent's \`subworkflows.with\` when spawned.`
638
+ ), this.name = "RequiredStateNotProvidedError", args.definition !== void 0 && (this.definition = args.definition), this.missing = args.missing;
639
+ }
640
+ }
641
+ class WorkflowStateDivergedError extends Error {
642
+ instanceId;
643
+ /** The guard-deploy failure that triggered the (attempted) rollback. */
644
+ guardError;
645
+ /** The rollback failure, when rollback was attempted and itself failed. */
646
+ rollbackError;
647
+ constructor(args) {
648
+ super(
649
+ `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
650
+ { cause: args.guardError }
651
+ ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
652
+ }
653
+ }
654
+ class PartialGuardDeployError extends Error {
655
+ stageName;
656
+ deployed;
657
+ constructor(args) {
658
+ super(
659
+ `Partial guard deploy on stage "${args.stageName}": ${args.deployed} guard(s) deployed before a later one failed.`,
660
+ { cause: args.cause }
661
+ ), this.name = "PartialGuardDeployError", this.stageName = args.stageName, this.deployed = args.deployed;
662
+ }
663
+ }
664
+ const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
665
+ class ConcurrentFireActionError extends Error {
666
+ instanceId;
667
+ task;
668
+ action;
669
+ attempts;
670
+ constructor(args) {
671
+ super(
672
+ `Action "${args.action}" on task "${args.task}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
673
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
674
+ }
675
+ }
676
+ function isRevisionConflict(error) {
677
+ if (typeof error != "object" || error === null) return !1;
678
+ const { statusCode, message } = error;
679
+ return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
680
+ }
681
+ class CascadeLimitError extends Error {
682
+ instanceId;
683
+ limit;
684
+ constructor(args) {
685
+ super(
686
+ `Cascade did not stabilise after ${args.limit} auto-transitions on ${args.instanceId} \u2014 likely a runaway loop (transition guards that stay mutually satisfied). Check that each transition's filter eventually goes false.`
687
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
688
+ }
689
+ }
612
690
  function buildSnapshot(args) {
613
691
  const docs = [], knownIds = /* @__PURE__ */ new Set();
614
692
  for (const { doc, resource } of args.docs) {
@@ -874,9 +952,16 @@ async function committedInstance(args) {
874
952
  }
875
953
  async function deployStageGuards(args) {
876
954
  const live = await committedInstance(args);
877
- if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
878
- for (const { client, doc } of resolvedStageGuards(args))
955
+ if (live === void 0 || live.currentStage !== args.stageName || live.abortedAt !== void 0) return;
956
+ let deployed = 0;
957
+ for (const { client, doc } of resolvedStageGuards(args)) {
958
+ try {
879
959
  await upsertGuard(client, doc);
960
+ } catch (cause) {
961
+ throw deployed > 0 ? new PartialGuardDeployError({ stageName: args.stageName, deployed, cause }) : cause;
962
+ }
963
+ deployed += 1;
964
+ }
880
965
  }
881
966
  async function retractStageGuards(args) {
882
967
  const live = await committedInstance(args);
@@ -1065,6 +1150,7 @@ function derivePerspectiveFromState(entries) {
1065
1150
  async function resolveDeclaredState(args) {
1066
1151
  const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
1067
1152
  if (entryDefs === void 0 || entryDefs.length === 0) return [];
1153
+ assertRequiredInitProvided(entryDefs, initialState, ctx.definitionName);
1068
1154
  const out = [];
1069
1155
  for (const entry of entryDefs) {
1070
1156
  const scopedCtx = { ...ctx, resolvedState: out };
@@ -1072,6 +1158,17 @@ async function resolveDeclaredState(args) {
1072
1158
  }
1073
1159
  return out;
1074
1160
  }
1161
+ function assertRequiredInitProvided(entryDefs, initialState, definitionName) {
1162
+ const missing = entryDefs.filter((entry) => entry.required === !0 && entry.source.type === "init").filter((entry) => {
1163
+ const match = initialState.find((s) => s.name === entry.name && s.type === entry.type);
1164
+ return match === void 0 || match.value === void 0 || match.value === null;
1165
+ }).map((entry) => ({ name: entry.name, type: entry.type }));
1166
+ if (missing.length !== 0)
1167
+ throw new RequiredStateNotProvidedError({
1168
+ ...definitionName !== void 0 ? { definition: definitionName } : {},
1169
+ missing
1170
+ });
1171
+ }
1075
1172
  const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
1076
1173
  "doc.refs",
1077
1174
  "checklist",
@@ -1296,56 +1393,6 @@ async function resolveTaskStateEntries(args) {
1296
1393
  randomKey
1297
1394
  });
1298
1395
  }
1299
- class ActionParamsInvalidError extends Error {
1300
- action;
1301
- task;
1302
- issues;
1303
- constructor(args) {
1304
- const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
1305
- `);
1306
- super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
1307
- ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
1308
- }
1309
- }
1310
- class WorkflowStateDivergedError extends Error {
1311
- instanceId;
1312
- /** The guard-deploy failure that triggered the (attempted) rollback. */
1313
- guardError;
1314
- /** The rollback failure, when rollback was attempted and itself failed. */
1315
- rollbackError;
1316
- constructor(args) {
1317
- super(
1318
- `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
1319
- { cause: args.guardError }
1320
- ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
1321
- }
1322
- }
1323
- const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1324
- class ConcurrentFireActionError extends Error {
1325
- instanceId;
1326
- task;
1327
- action;
1328
- attempts;
1329
- constructor(args) {
1330
- super(
1331
- `Action "${args.action}" on task "${args.task}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
1332
- ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
1333
- }
1334
- }
1335
- function isRevisionConflict(error) {
1336
- if (typeof error != "object" || error === null) return !1;
1337
- const { statusCode, message } = error;
1338
- return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
1339
- }
1340
- class CascadeLimitError extends Error {
1341
- instanceId;
1342
- limit;
1343
- constructor(args) {
1344
- super(
1345
- `Cascade did not stabilise after ${args.limit} auto-transitions on ${args.instanceId} \u2014 likely a runaway loop (transition guards that stay mutually satisfied). Check that each transition's filter eventually goes false.`
1346
- ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
1347
- }
1348
- }
1349
1396
  async function deployOrRollback(args) {
1350
1397
  try {
1351
1398
  await args.deploy();
@@ -1367,7 +1414,11 @@ async function deployOrRollback(args) {
1367
1414
  reason: "rollback of the committed move failed"
1368
1415
  });
1369
1416
  }
1370
- throw guardError;
1417
+ throw guardError instanceof PartialGuardDeployError ? new WorkflowStateDivergedError({
1418
+ instanceId: args.instanceId,
1419
+ guardError,
1420
+ reason: "a multi-guard deploy partially applied; the rolled-back move left orphaned guard locks"
1421
+ }) : guardError;
1371
1422
  }
1372
1423
  }
1373
1424
  function applyTaskStatusChange(args) {
@@ -1456,7 +1507,8 @@ function materializeInstance(base, mutation) {
1456
1507
  ...base,
1457
1508
  currentStage: mutation.currentStage,
1458
1509
  state: mutation.state,
1459
- stages: mutation.stages
1510
+ stages: mutation.stages,
1511
+ effectsContext: mutation.effectsContext
1460
1512
  };
1461
1513
  }
1462
1514
  async function persist(ctx, mutation) {
@@ -1471,7 +1523,21 @@ async function persist(ctx, mutation) {
1471
1523
  tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1472
1524
  const actorForPriming = void 0;
1473
1525
  for (const body of pendingCreates)
1474
- await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1526
+ try {
1527
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(
1528
+ ctx.client,
1529
+ body._id,
1530
+ actorForPriming,
1531
+ ctx.clientForGdr,
1532
+ ctx.clock
1533
+ ), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1534
+ } catch (cause) {
1535
+ throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
1536
+ instanceId: ctx.instance._id,
1537
+ guardError: cause,
1538
+ reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
1539
+ });
1540
+ }
1475
1541
  const reloaded = await ctx.client.getDocument(ctx.instance._id);
1476
1542
  if (!reloaded)
1477
1543
  throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
@@ -1736,6 +1802,7 @@ async function prepareChildInstance(args) {
1736
1802
  selfId: childDocId,
1737
1803
  tag: childTag,
1738
1804
  workflowResource,
1805
+ definitionName: definition.name,
1739
1806
  ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1740
1807
  },
1741
1808
  randomKey
@@ -1782,6 +1849,28 @@ async function resolveBindings(args) {
1782
1849
  resolved[key] = await runGroq(groq, args.params, args.snapshot);
1783
1850
  return { ...resolved, ...args.staticInput };
1784
1851
  }
1852
+ async function persistThenDeploy(ctx, mutation, deploy) {
1853
+ const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
1854
+ await deployOrRollback({
1855
+ client: ctx.client,
1856
+ instanceId: ctx.instance._id,
1857
+ committedRev: committed._rev,
1858
+ restore: instanceStateFields(ctx.instance),
1859
+ unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
1860
+ reversible: !spawned,
1861
+ deploy
1862
+ });
1863
+ }
1864
+ async function refreshStageGuards(ctx, mutation, stageName) {
1865
+ await deployStageGuards({
1866
+ client: ctx.client,
1867
+ clientForGdr: ctx.clientForGdr,
1868
+ instance: materializeInstance(ctx.instance, mutation),
1869
+ definition: ctx.definition,
1870
+ stageName,
1871
+ now: ctx.now
1872
+ });
1873
+ }
1785
1874
  async function completeEffect(args) {
1786
1875
  const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1787
1876
  ...options?.clock ? { clock: options.clock } : {},
@@ -1827,9 +1916,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1827
1916
  const mutation = startMutation(ctx.instance);
1828
1917
  mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1829
1918
  const ranAt = ctx.now;
1830
- return mutation.effectHistory.push(
1919
+ mutation.effectHistory.push(
1831
1920
  buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1832
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1921
+ );
1922
+ const wroteEffectsContext = status === "done" && outputs !== void 0;
1923
+ return wroteEffectsContext && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1833
1924
  _key: randomKey(),
1834
1925
  _type: "effectCompleted",
1835
1926
  at: ranAt,
@@ -1839,7 +1930,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1839
1930
  ...outputs !== void 0 ? { outputs } : {},
1840
1931
  ...detail !== void 0 ? { detail } : {},
1841
1932
  ...actor !== void 0 ? { actor } : {}
1842
- }), await persist(ctx, mutation), { effectKey, status };
1933
+ }), await persistThenDeploy(
1934
+ ctx,
1935
+ mutation,
1936
+ () => wroteEffectsContext ? refreshStageGuards(ctx, mutation, ctx.instance.currentStage) : Promise.resolve()
1937
+ ), { effectKey, status };
1843
1938
  }
1844
1939
  function buildQueuedEffect(effect, origin, params, actor, now) {
1845
1940
  const key = randomKey(), pending = {
@@ -2299,28 +2394,6 @@ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
2299
2394
  now: ctx.now
2300
2395
  });
2301
2396
  }
2302
- async function persistThenDeploy(ctx, mutation, deploy) {
2303
- const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
2304
- await deployOrRollback({
2305
- client: ctx.client,
2306
- instanceId: ctx.instance._id,
2307
- committedRev: committed._rev,
2308
- restore: instanceStateFields(ctx.instance),
2309
- unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
2310
- reversible: !spawned,
2311
- deploy
2312
- });
2313
- }
2314
- async function refreshStageGuards(ctx, mutation, stageName) {
2315
- await deployStageGuards({
2316
- client: ctx.client,
2317
- clientForGdr: ctx.clientForGdr,
2318
- instance: materializeInstance(ctx.instance, mutation),
2319
- definition: ctx.definition,
2320
- stageName,
2321
- now: ctx.now
2322
- });
2323
- }
2324
2397
  async function commitTransition(ctx, actor) {
2325
2398
  if (isTerminal(ctx))
2326
2399
  return { fired: !1 };
@@ -3277,6 +3350,7 @@ const workflow = {
3277
3350
  selfId: id,
3278
3351
  tag,
3279
3352
  workflowResource,
3353
+ definitionName: definition.name,
3280
3354
  ...perspective !== void 0 ? { perspective } : {}
3281
3355
  },
3282
3356
  randomKey
@@ -4153,6 +4227,7 @@ function displayDescription(typeKey) {
4153
4227
  return DISPLAY[typeKey]?.description;
4154
4228
  }
4155
4229
  exports.WORKFLOW_DEFINITION_TYPE = schema.WORKFLOW_DEFINITION_TYPE;
4230
+ exports.isStartableDefinition = schema.isStartableDefinition;
4156
4231
  exports.AUTHORING_DISPLAY = AUTHORING_DISPLAY;
4157
4232
  exports.ActionDisabledError = ActionDisabledError;
4158
4233
  exports.ActionParamsInvalidError = ActionParamsInvalidError;
@@ -4167,6 +4242,8 @@ exports.GUARD_OWNER = GUARD_OWNER;
4167
4242
  exports.HISTORY_DISPLAY = HISTORY_DISPLAY;
4168
4243
  exports.MutationGuardDeniedError = MutationGuardDeniedError;
4169
4244
  exports.OP_DISPLAY = OP_DISPLAY;
4245
+ exports.PartialGuardDeployError = PartialGuardDeployError;
4246
+ exports.RequiredStateNotProvidedError = RequiredStateNotProvidedError;
4170
4247
  exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
4171
4248
  exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
4172
4249
  exports.WorkflowStateDivergedError = WorkflowStateDivergedError;