pg-workflows 0.9.0 → 0.11.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
@@ -65,16 +65,12 @@ var __export = (target, all) => {
65
65
  var exports_src = {};
66
66
  __export(exports_src, {
67
67
  workflow: () => workflow,
68
- validateWorkflowId: () => validateWorkflowId,
69
- validateResourceId: () => validateResourceId,
70
- parseDuration: () => parseDuration,
71
68
  createWorkflowRef: () => createWorkflowRef,
72
69
  WorkflowStatus: () => WorkflowStatus,
73
70
  WorkflowRunNotFoundError: () => WorkflowRunNotFoundError,
74
71
  WorkflowEngineError: () => WorkflowEngineError,
75
72
  WorkflowEngine: () => WorkflowEngine,
76
- WorkflowClient: () => WorkflowClient,
77
- StepType: () => StepType
73
+ WorkflowClient: () => WorkflowClient
78
74
  });
79
75
  module.exports = __toCommonJS(exports_src);
80
76
 
@@ -90,10 +86,15 @@ var WORKFLOW_RUN_DLQ_QUEUE_NAME = "workflow_run_dlq";
90
86
  var DEFAULT_PGBOSS_SCHEMA = "pgboss_v12_pgworkflow";
91
87
  var MAX_WORKFLOW_ID_LENGTH = 256;
92
88
  var MAX_RESOURCE_ID_LENGTH = 256;
89
+ var INVOKE_CHILD_WORKFLOW_TIMELINE_SUFFIX = "invoke-child-workflow";
90
+ var WAIT_FOR_TIMELINE_SUFFIX = "wait-for";
91
+ var invokeChildWorkflowTimelineKey = (stepId) => `${stepId}-${INVOKE_CHILD_WORKFLOW_TIMELINE_SUFFIX}`;
92
+ var waitForTimelineKey = (stepId) => `${stepId}-${WAIT_FOR_TIMELINE_SUFFIX}`;
93
+ var isInvokeChildWorkflowTimelineEntry = (entry) => !!entry && typeof entry === "object" && ("invokeChildWorkflow" in entry);
93
94
 
94
95
  // src/db/migration.ts
95
96
  var MIGRATION_LOCK_ID = 738291645;
96
- var CURRENT_SCHEMA_VERSION = 3;
97
+ var CURRENT_SCHEMA_VERSION = 4;
97
98
  async function runMigrations(db) {
98
99
  if (await isSchemaUpToDate(db)) {
99
100
  return;
@@ -151,6 +152,11 @@ async function runMigrations(db) {
151
152
  commands.push("ALTER TABLE workflow_runs ALTER COLUMN resource_id TYPE varchar(256)");
152
153
  commands.push("ALTER TABLE workflow_runs ALTER COLUMN workflow_id TYPE varchar(256)");
153
154
  }
155
+ if (currentVersion < 4) {
156
+ commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS parent_run_id varchar(32)");
157
+ commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS parent_step_id varchar(256)");
158
+ commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS parent_resource_id varchar(256)");
159
+ }
154
160
  if (currentVersion === 0) {
155
161
  commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
156
162
  } else {
@@ -213,7 +219,10 @@ function mapRowToWorkflowRun(row) {
213
219
  retryCount: row.retry_count,
214
220
  maxRetries: row.max_retries,
215
221
  jobId: row.job_id,
216
- idempotencyKey: row.idempotency_key
222
+ idempotencyKey: row.idempotency_key,
223
+ parentRunId: row.parent_run_id,
224
+ parentStepId: row.parent_step_id,
225
+ parentResourceId: row.parent_resource_id
217
226
  };
218
227
  }
219
228
  async function insertWorkflowRun({
@@ -224,7 +233,10 @@ async function insertWorkflowRun({
224
233
  input,
225
234
  maxRetries,
226
235
  timeoutAt,
227
- idempotencyKey
236
+ idempotencyKey,
237
+ parentRunId,
238
+ parentStepId,
239
+ parentResourceId
228
240
  }, db) {
229
241
  const runId = generateKSUID("run");
230
242
  const now = new Date;
@@ -241,9 +253,12 @@ async function insertWorkflowRun({
241
253
  updated_at,
242
254
  timeline,
243
255
  retry_count,
244
- idempotency_key
256
+ idempotency_key,
257
+ parent_run_id,
258
+ parent_step_id,
259
+ parent_resource_id
245
260
  )
246
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
261
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
247
262
  ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
248
263
  RETURNING *`, [
249
264
  runId,
@@ -258,7 +273,10 @@ async function insertWorkflowRun({
258
273
  now,
259
274
  "{}",
260
275
  0,
261
- idempotencyKey ?? null
276
+ idempotencyKey ?? null,
277
+ parentRunId ?? null,
278
+ parentStepId ?? null,
279
+ parentResourceId ?? null
262
280
  ]);
263
281
  if (result.rows[0]) {
264
282
  return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
@@ -521,15 +539,6 @@ var WorkflowStatus;
521
539
  WorkflowStatus2["FAILED"] = "failed";
522
540
  WorkflowStatus2["CANCELLED"] = "cancelled";
523
541
  })(WorkflowStatus ||= {});
524
- var StepType;
525
- ((StepType2) => {
526
- StepType2["PAUSE"] = "pause";
527
- StepType2["RUN"] = "run";
528
- StepType2["WAIT_FOR"] = "waitFor";
529
- StepType2["WAIT_UNTIL"] = "waitUntil";
530
- StepType2["DELAY"] = "delay";
531
- StepType2["POLL"] = "poll";
532
- })(StepType ||= {});
533
542
 
534
543
  // src/client.ts
535
544
  var LOG_PREFIX = "[WorkflowClient]";
@@ -636,7 +645,8 @@ class WorkflowClient {
636
645
  };
637
646
  await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
638
647
  startAfter: new Date,
639
- expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
648
+ expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
649
+ db: _db
640
650
  });
641
651
  }
642
652
  return insertedRun;
@@ -699,6 +709,11 @@ class WorkflowClient {
699
709
  if (current.status !== "paused" /* PAUSED */) {
700
710
  throw new WorkflowEngineError(`Cannot resume workflow run in '${current.status}' status, must be 'paused'`, current.workflowId, runId);
701
711
  }
712
+ const currentStepId = current.currentStepId;
713
+ const currentStepTimelineEntry = current.timeline[invokeChildWorkflowTimelineKey(currentStepId)];
714
+ if (isInvokeChildWorkflowTimelineEntry(currentStepTimelineEntry)) {
715
+ return current;
716
+ }
702
717
  return this.triggerEvent({
703
718
  runId,
704
719
  resourceId,
@@ -717,8 +732,12 @@ class WorkflowClient {
717
732
  if (run.status !== "paused" /* PAUSED */) {
718
733
  return run;
719
734
  }
720
- const stepId = run.currentStepId;
721
- const waitForEntry = run.timeline[`${stepId}-wait-for`];
735
+ const currentStepId = run.currentStepId;
736
+ const currentStepTimelineEntry = run.timeline[invokeChildWorkflowTimelineKey(currentStepId)];
737
+ if (isInvokeChildWorkflowTimelineEntry(currentStepTimelineEntry)) {
738
+ return run;
739
+ }
740
+ const waitForEntry = run.timeline[waitForTimelineKey(currentStepId)];
722
741
  if (!waitForEntry || typeof waitForEntry !== "object" || !("waitFor" in waitForEntry)) {
723
742
  return run;
724
743
  }
@@ -736,7 +755,7 @@ class WorkflowClient {
736
755
  resourceId,
737
756
  data: {
738
757
  timeline: import_es_toolkit.merge(freshRun.timeline, {
739
- [stepId]: {
758
+ [currentStepId]: {
740
759
  output: data ?? {},
741
760
  timestamp: new Date
742
761
  }
@@ -836,7 +855,10 @@ function createWorkflowRef(id, options) {
836
855
  retries: defineOptions?.retries
837
856
  });
838
857
  Object.defineProperty(ref, "id", { value: id, enumerable: true });
839
- Object.defineProperty(ref, "inputSchema", { value: options?.inputSchema, enumerable: true });
858
+ Object.defineProperty(ref, "inputSchema", {
859
+ value: options?.inputSchema,
860
+ enumerable: true
861
+ });
840
862
  return ref;
841
863
  }
842
864
  function createWorkflowFactory(plugins = []) {
@@ -856,31 +878,6 @@ function createWorkflowFactory(plugins = []) {
856
878
  return factory;
857
879
  }
858
880
  var workflow = createWorkflowFactory();
859
- // src/duration.ts
860
- var import_parse_duration = __toESM(require("parse-duration"));
861
- var MS_PER_SECOND = 1000;
862
- var MS_PER_MINUTE = 60 * MS_PER_SECOND;
863
- var MS_PER_HOUR = 60 * MS_PER_MINUTE;
864
- var MS_PER_DAY = 24 * MS_PER_HOUR;
865
- var MS_PER_WEEK = 7 * MS_PER_DAY;
866
- function parseDuration(duration) {
867
- if (typeof duration === "string") {
868
- if (duration.trim() === "") {
869
- throw new WorkflowEngineError("Invalid duration: empty string");
870
- }
871
- const ms2 = import_parse_duration.default(duration);
872
- if (ms2 == null || ms2 <= 0) {
873
- throw new WorkflowEngineError(`Invalid duration: "${duration}"`);
874
- }
875
- return ms2;
876
- }
877
- const { weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0 } = duration;
878
- const ms = weeks * MS_PER_WEEK + days * MS_PER_DAY + hours * MS_PER_HOUR + minutes * MS_PER_MINUTE + seconds * MS_PER_SECOND;
879
- if (ms <= 0) {
880
- throw new WorkflowEngineError("Invalid duration: must be a positive value");
881
- }
882
- return ms;
883
- }
884
881
  // src/engine.ts
885
882
  var import_es_toolkit2 = require("es-toolkit");
886
883
  var import_pg2 = __toESM(require("pg"));
@@ -931,7 +928,7 @@ function parseWorkflowHandler(handler) {
931
928
  const propertyAccess = node.expression;
932
929
  const objectName = propertyAccess.expression.getText(sourceFile);
933
930
  const methodName = propertyAccess.name.text;
934
- if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll")) {
931
+ if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll" || methodName === "invokeChildWorkflow")) {
935
932
  const firstArg = node.arguments[0];
936
933
  if (firstArg) {
937
934
  const { id, isDynamic } = extractStepId(firstArg);
@@ -956,6 +953,32 @@ function parseWorkflowHandler(handler) {
956
953
  return { steps: Array.from(steps.values()) };
957
954
  }
958
955
 
956
+ // src/duration.ts
957
+ var import_parse_duration = __toESM(require("parse-duration"));
958
+ var MS_PER_SECOND = 1000;
959
+ var MS_PER_MINUTE = 60 * MS_PER_SECOND;
960
+ var MS_PER_HOUR = 60 * MS_PER_MINUTE;
961
+ var MS_PER_DAY = 24 * MS_PER_HOUR;
962
+ var MS_PER_WEEK = 7 * MS_PER_DAY;
963
+ function parseDuration(duration) {
964
+ if (typeof duration === "string") {
965
+ if (duration.trim() === "") {
966
+ throw new WorkflowEngineError("Invalid duration: empty string");
967
+ }
968
+ const ms2 = import_parse_duration.default(duration);
969
+ if (ms2 == null || ms2 <= 0) {
970
+ throw new WorkflowEngineError(`Invalid duration: "${duration}"`);
971
+ }
972
+ return ms2;
973
+ }
974
+ const { weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0 } = duration;
975
+ const ms = weeks * MS_PER_WEEK + days * MS_PER_DAY + hours * MS_PER_HOUR + minutes * MS_PER_MINUTE + seconds * MS_PER_SECOND;
976
+ if (ms <= 0) {
977
+ throw new WorkflowEngineError("Invalid duration: must be a positive value");
978
+ }
979
+ return ms;
980
+ }
981
+
959
982
  // src/engine.ts
960
983
  var LOG_PREFIX2 = "[WorkflowEngine]";
961
984
  var StepTypeToIcon = {
@@ -964,7 +987,8 @@ var StepTypeToIcon = {
964
987
  ["pause" /* PAUSE */]: "⏸",
965
988
  ["waitUntil" /* WAIT_UNTIL */]: "⏲",
966
989
  ["delay" /* DELAY */]: "⏱",
967
- ["poll" /* POLL */]: "↻"
990
+ ["poll" /* POLL */]: "↻",
991
+ ["invokeChildWorkflow" /* INVOKE_CHILD_WORKFLOW */]: "↪"
968
992
  };
969
993
  var defaultLogger2 = {
970
994
  log: (_message) => console.warn(_message),
@@ -976,6 +1000,7 @@ var retrySendOptions = (maxRetries) => ({
976
1000
  retryBackoff: true,
977
1001
  retryDelay: 1
978
1002
  });
1003
+ var getInvokeChildWorkflowEventName = (childRunId) => `__invoke_child_workflow_completed:${childRunId}`;
979
1004
  var defaultHeartbeatSeconds = process.env.WORKFLOW_RUN_HEARTBEAT_SECONDS ? Number.parseInt(process.env.WORKFLOW_RUN_HEARTBEAT_SECONDS, 10) : 30;
980
1005
 
981
1006
  class WorkflowEngine {
@@ -1081,31 +1106,57 @@ class WorkflowEngine {
1081
1106
  this.workflows.clear();
1082
1107
  return this;
1083
1108
  }
1084
- async startWorkflow(refOrParams, inputArg, optionsArg) {
1085
- let workflowId;
1086
- let input;
1087
- let resourceId;
1088
- let idempotencyKey;
1089
- let options;
1109
+ resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg) {
1090
1110
  if (typeof refOrParams === "function" && "id" in refOrParams) {
1091
- workflowId = refOrParams.id;
1092
- input = inputArg;
1093
- options = optionsArg;
1094
- resourceId = optionsArg?.resourceId;
1095
- idempotencyKey = optionsArg?.idempotencyKey;
1096
- } else {
1097
- const params = refOrParams;
1098
- workflowId = params.workflowId;
1099
- input = params.input;
1100
- resourceId = params.resourceId;
1101
- idempotencyKey = params.idempotencyKey;
1102
- options = params.options;
1111
+ return {
1112
+ workflowId: refOrParams.id,
1113
+ input: inputArg,
1114
+ options: optionsArg,
1115
+ resourceId: optionsArg?.resourceId,
1116
+ idempotencyKey: optionsArg?.idempotencyKey
1117
+ };
1103
1118
  }
1104
- validateWorkflowId(workflowId);
1105
- validateResourceId(resourceId);
1119
+ const params = refOrParams;
1120
+ return {
1121
+ workflowId: params.workflowId,
1122
+ input: params.input,
1123
+ resourceId: params.resourceId ?? params.options?.resourceId,
1124
+ idempotencyKey: params.idempotencyKey ?? params.options?.idempotencyKey,
1125
+ options: params.options
1126
+ };
1127
+ }
1128
+ async startWorkflow(refOrParams, inputArg, optionsArg) {
1129
+ const { workflowId, input, resourceId, idempotencyKey, options } = this.resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg);
1106
1130
  if (!this._started) {
1107
- await this.start(false, { batchSize: options?.batchSize ?? 1 });
1131
+ await this.start(false);
1108
1132
  }
1133
+ const { run } = await this.createWorkflowRun({
1134
+ workflowId,
1135
+ input,
1136
+ resourceId,
1137
+ idempotencyKey,
1138
+ options
1139
+ });
1140
+ this.logger.log("Started workflow run", {
1141
+ runId: run.id,
1142
+ workflowId
1143
+ });
1144
+ return run;
1145
+ }
1146
+ async createWorkflowRun({
1147
+ workflowId,
1148
+ input,
1149
+ resourceId,
1150
+ idempotencyKey,
1151
+ options,
1152
+ parentRunId,
1153
+ parentStepId,
1154
+ parentResourceId,
1155
+ enqueue = true,
1156
+ db
1157
+ }) {
1158
+ validateWorkflowId(workflowId);
1159
+ validateResourceId(resourceId);
1109
1160
  const workflow2 = this.workflows.get(workflowId);
1110
1161
  if (!workflow2) {
1111
1162
  throw new WorkflowEngineError(`Unknown workflow ${workflowId}`);
@@ -1122,38 +1173,60 @@ class WorkflowEngine {
1122
1173
  }
1123
1174
  }
1124
1175
  const initialStepId = workflow2.steps[0]?.id ?? "__start__";
1125
- const run = await withPostgresTransaction(this.boss.getDb(), async (_db) => {
1126
- const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : workflow2.timeout ? new Date(Date.now() + workflow2.timeout) : null;
1127
- const { run: insertedRun, created } = await insertWorkflowRun({
1128
- resourceId,
1129
- workflowId,
1130
- currentStepId: initialStepId,
1131
- status: "running" /* RUNNING */,
1132
- input,
1133
- maxRetries: options?.retries ?? workflow2.retries ?? 0,
1134
- timeoutAt,
1135
- idempotencyKey
1136
- }, _db);
1137
- if (created) {
1138
- const job = {
1139
- runId: insertedRun.id,
1140
- resourceId,
1141
- workflowId,
1142
- input
1143
- };
1144
- await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
1145
- startAfter: new Date,
1146
- expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2,
1147
- ...retrySendOptions(insertedRun.maxRetries)
1148
- });
1176
+ const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : workflow2.timeout ? new Date(Date.now() + workflow2.timeout) : null;
1177
+ const insertRun = async (targetDb) => await insertWorkflowRun({
1178
+ resourceId,
1179
+ workflowId,
1180
+ currentStepId: initialStepId,
1181
+ status: "running" /* RUNNING */,
1182
+ input,
1183
+ maxRetries: options?.retries ?? workflow2.retries ?? 0,
1184
+ timeoutAt,
1185
+ idempotencyKey,
1186
+ parentRunId,
1187
+ parentStepId,
1188
+ parentResourceId
1189
+ }, targetDb);
1190
+ const insertAndEnqueue = async (targetDb) => {
1191
+ const result = await insertRun(targetDb);
1192
+ if (enqueue && result.created) {
1193
+ await this.enqueueWorkflowRun(result.run, options, targetDb);
1149
1194
  }
1150
- return insertedRun;
1151
- }, this.pool);
1152
- this.logger.log("Started workflow run", {
1195
+ return result;
1196
+ };
1197
+ const { run, created } = db ? await insertAndEnqueue(db) : await withPostgresTransaction(this.boss.getDb(), insertAndEnqueue, this.pool);
1198
+ return { run, created };
1199
+ }
1200
+ async enqueueWorkflowRun(run, options, db) {
1201
+ const job = {
1153
1202
  runId: run.id,
1154
- workflowId
1203
+ resourceId: run.resourceId ?? undefined,
1204
+ workflowId: run.workflowId,
1205
+ input: run.input
1206
+ };
1207
+ await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
1208
+ startAfter: new Date,
1209
+ expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2,
1210
+ ...retrySendOptions(run.maxRetries),
1211
+ ...db ? { db } : {}
1212
+ });
1213
+ }
1214
+ async notifyParentOfChildTerminalRun(childRun) {
1215
+ if (!childRun.parentRunId || !childRun.parentStepId) {
1216
+ return;
1217
+ }
1218
+ const parentRun = await getWorkflowRun({
1219
+ runId: childRun.parentRunId,
1220
+ resourceId: childRun.parentResourceId ?? undefined
1221
+ }, { db: this.db });
1222
+ if (!parentRun || parentRun.status === "completed" /* COMPLETED */ || parentRun.status === "failed" /* FAILED */ || parentRun.status === "cancelled" /* CANCELLED */) {
1223
+ return;
1224
+ }
1225
+ await this.triggerEvent({
1226
+ runId: parentRun.id,
1227
+ resourceId: parentRun.resourceId ?? undefined,
1228
+ eventName: getInvokeChildWorkflowEventName(childRun.id)
1155
1229
  });
1156
- return run;
1157
1230
  }
1158
1231
  async pauseWorkflow({
1159
1232
  runId,
@@ -1185,6 +1258,9 @@ class WorkflowEngine {
1185
1258
  if (current.status !== "paused" /* PAUSED */) {
1186
1259
  throw new WorkflowEngineError(`Cannot resume workflow run in '${current.status}' status, must be 'paused'`, current.workflowId, runId);
1187
1260
  }
1261
+ if (this.getInvokeChildWorkflowStepEntry(current.timeline, current.currentStepId)) {
1262
+ return current;
1263
+ }
1188
1264
  return this.triggerEvent({
1189
1265
  runId,
1190
1266
  resourceId,
@@ -1204,6 +1280,9 @@ class WorkflowEngine {
1204
1280
  return run;
1205
1281
  }
1206
1282
  const stepId = run.currentStepId;
1283
+ if (this.getInvokeChildWorkflowStepEntry(run.timeline, stepId)) {
1284
+ return run;
1285
+ }
1207
1286
  const waitForStep = this.getWaitForStepEntry(run.timeline, stepId);
1208
1287
  if (!waitForStep) {
1209
1288
  return run;
@@ -1252,6 +1331,7 @@ class WorkflowEngine {
1252
1331
  expectedStatuses: ["pending" /* PENDING */, "running" /* RUNNING */, "paused" /* PAUSED */]
1253
1332
  });
1254
1333
  this.logger.log(`cancelled workflow run with id ${runId}`);
1334
+ await this.notifyParentOfChildTerminalRun(run);
1255
1335
  return run;
1256
1336
  }
1257
1337
  async triggerEvent({
@@ -1489,6 +1569,22 @@ class WorkflowEngine {
1489
1569
  }
1490
1570
  const timeoutMs = options?.timeout ? parseDuration(options.timeout) : undefined;
1491
1571
  return this.pollStep({ run, stepId, conditionFn, intervalMs, timeoutMs });
1572
+ },
1573
+ invokeChildWorkflow: async (stepId, refOrParams, inputArg, optionsArg) => {
1574
+ if (!run) {
1575
+ throw new WorkflowEngineError("Missing workflow run", workflowId, runId);
1576
+ }
1577
+ const resolvedChildCall = this.resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg);
1578
+ const childWorkflowInvocation = {
1579
+ run,
1580
+ stepId,
1581
+ workflowId: resolvedChildCall.workflowId,
1582
+ input: resolvedChildCall.input,
1583
+ options: resolvedChildCall.options,
1584
+ resourceId: resolvedChildCall.resourceId,
1585
+ idempotencyKey: resolvedChildCall.idempotencyKey
1586
+ };
1587
+ return this.invokeChildWorkflowStep(childWorkflowInvocation);
1492
1588
  }
1493
1589
  };
1494
1590
  let step = { ...baseStep };
@@ -1501,7 +1597,9 @@ class WorkflowEngine {
1501
1597
  input: run.input,
1502
1598
  workflowId: run.workflowId,
1503
1599
  runId: run.id,
1504
- timeline: run.timeline,
1600
+ get timeline() {
1601
+ return run?.timeline ?? {};
1602
+ },
1505
1603
  logger: this.logger,
1506
1604
  step
1507
1605
  };
@@ -1513,7 +1611,7 @@ class WorkflowEngine {
1513
1611
  const shouldComplete = run.status === "running" /* RUNNING */ && (noParsedSteps || isLastParsedStep || hasPluginSteps && result !== undefined);
1514
1612
  if (shouldComplete) {
1515
1613
  const normalizedResult = result === undefined ? {} : result;
1516
- await this.updateRun({
1614
+ const completedRun = await this.updateRun({
1517
1615
  runId,
1518
1616
  resourceId: scopedResourceId,
1519
1617
  data: {
@@ -1523,6 +1621,7 @@ class WorkflowEngine {
1523
1621
  jobId: job?.id
1524
1622
  }
1525
1623
  });
1624
+ await this.notifyParentOfChildTerminalRun(completedRun);
1526
1625
  this.logger.log("Workflow run completed.", {
1527
1626
  runId,
1528
1627
  workflowId
@@ -1530,7 +1629,7 @@ class WorkflowEngine {
1530
1629
  }
1531
1630
  } catch (error) {
1532
1631
  if (runId) {
1533
- await this.updateRun({
1632
+ const updatedRun = await this.updateRun({
1534
1633
  runId,
1535
1634
  resourceId: scopedResourceId,
1536
1635
  data: {
@@ -1538,6 +1637,9 @@ class WorkflowEngine {
1538
1637
  jobId: job?.id
1539
1638
  }
1540
1639
  });
1640
+ if (updatedRun.status === "completed" /* COMPLETED */ || updatedRun.status === "failed" /* FAILED */ || updatedRun.status === "cancelled" /* CANCELLED */) {
1641
+ await this.notifyParentOfChildTerminalRun(updatedRun);
1642
+ }
1541
1643
  }
1542
1644
  throw error;
1543
1645
  }
@@ -1549,7 +1651,7 @@ class WorkflowEngine {
1549
1651
  const run = await getWorkflowRun({ runId }, { db: this.db });
1550
1652
  if (!run || run.status !== "running" /* RUNNING */)
1551
1653
  return;
1552
- await this.updateRun({
1654
+ const failedRun = await this.updateRun({
1553
1655
  runId,
1554
1656
  resourceId: run.resourceId ?? undefined,
1555
1657
  data: {
@@ -1557,6 +1659,7 @@ class WorkflowEngine {
1557
1659
  error: run.error ?? "Workflow run worker died or job expired before completion"
1558
1660
  }
1559
1661
  });
1662
+ await this.notifyParentOfChildTerminalRun(failedRun);
1560
1663
  this.logger.log("Marked stuck workflow run as failed", {
1561
1664
  runId,
1562
1665
  workflowId: run.workflowId
@@ -1567,9 +1670,195 @@ class WorkflowEngine {
1567
1670
  return stepEntry && typeof stepEntry === "object" && "output" in stepEntry ? stepEntry : null;
1568
1671
  }
1569
1672
  getWaitForStepEntry(timeline, stepId) {
1570
- const entry = timeline[`${stepId}-wait-for`];
1673
+ const entry = timeline[waitForTimelineKey(stepId)];
1571
1674
  return entry && typeof entry === "object" && "waitFor" in entry ? entry : null;
1572
1675
  }
1676
+ getInvokeChildWorkflowStepEntry(timeline, stepId) {
1677
+ const entry = timeline[invokeChildWorkflowTimelineKey(stepId)];
1678
+ return isInvokeChildWorkflowTimelineEntry(entry) ? entry : null;
1679
+ }
1680
+ getCompletedChildOutput(childRun) {
1681
+ return childRun.output === undefined ? {} : childRun.output;
1682
+ }
1683
+ throwForNonCompletedChild(childRun) {
1684
+ throw new WorkflowEngineError(`Child workflow ${childRun.workflowId} ${childRun.status}${childRun.error ? `: ${childRun.error}` : ""}`, childRun.workflowId, childRun.id);
1685
+ }
1686
+ assertInvokeChildWorkflowStepOwnership({
1687
+ childRun,
1688
+ parentRun,
1689
+ stepId,
1690
+ workflowId
1691
+ }) {
1692
+ const expectedParentResourceId = parentRun.resourceId ?? null;
1693
+ const matches = childRun.workflowId === workflowId && childRun.parentRunId === parentRun.id && childRun.parentStepId === stepId && childRun.parentResourceId === expectedParentResourceId;
1694
+ if (!matches) {
1695
+ throw new WorkflowEngineError(`Idempotency key resolved to workflow run ${childRun.id}, which does not belong to invokeChildWorkflow step '${stepId}'`, workflowId, parentRun.id);
1696
+ }
1697
+ }
1698
+ async invokeChildWorkflowStep({
1699
+ run,
1700
+ stepId,
1701
+ workflowId,
1702
+ input,
1703
+ resourceId,
1704
+ idempotencyKey,
1705
+ options
1706
+ }) {
1707
+ let invokeOutput;
1708
+ let hasInvokeOutput = false;
1709
+ const childResourceId = resourceId ?? run.resourceId ?? undefined;
1710
+ const childIdempotencyKey = idempotencyKey;
1711
+ await withPostgresTransaction(this.db, async (db) => {
1712
+ const lockedRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
1713
+ if (lockedRun.status === "cancelled" /* CANCELLED */ || lockedRun.status === "paused" /* PAUSED */ || lockedRun.status === "failed" /* FAILED */) {
1714
+ return;
1715
+ }
1716
+ const lockedCached = this.getCachedStepEntry(lockedRun.timeline, stepId);
1717
+ if (lockedCached?.output !== undefined) {
1718
+ invokeOutput = lockedCached.output;
1719
+ hasInvokeOutput = true;
1720
+ return;
1721
+ }
1722
+ const lockedInvoke = this.getInvokeChildWorkflowStepEntry(lockedRun.timeline, stepId);
1723
+ if (lockedInvoke) {
1724
+ const existingChildResourceId = "childResourceId" in lockedInvoke.invokeChildWorkflow ? lockedInvoke.invokeChildWorkflow.childResourceId ?? undefined : childResourceId;
1725
+ const existingChildRun = await this.getRun({
1726
+ runId: lockedInvoke.invokeChildWorkflow.childRunId,
1727
+ resourceId: existingChildResourceId
1728
+ });
1729
+ if (existingChildRun.status === "completed" /* COMPLETED */) {
1730
+ invokeOutput = this.getCompletedChildOutput(existingChildRun);
1731
+ hasInvokeOutput = true;
1732
+ await this.updateRun({
1733
+ runId: run.id,
1734
+ resourceId: run.resourceId ?? undefined,
1735
+ data: {
1736
+ timeline: import_es_toolkit2.merge(lockedRun.timeline, {
1737
+ [stepId]: {
1738
+ output: invokeOutput,
1739
+ timestamp: new Date
1740
+ }
1741
+ })
1742
+ }
1743
+ }, { db });
1744
+ return;
1745
+ }
1746
+ if (existingChildRun.status === "failed" /* FAILED */ || existingChildRun.status === "cancelled" /* CANCELLED */) {
1747
+ this.throwForNonCompletedChild(existingChildRun);
1748
+ }
1749
+ await this.pauseRunForWait({
1750
+ run: lockedRun,
1751
+ stepId,
1752
+ eventName: getInvokeChildWorkflowEventName(existingChildRun.id),
1753
+ skipOutput: true,
1754
+ db
1755
+ });
1756
+ return;
1757
+ }
1758
+ const result = await this.createWorkflowRun({
1759
+ workflowId,
1760
+ input,
1761
+ resourceId: childResourceId,
1762
+ idempotencyKey: childIdempotencyKey,
1763
+ options,
1764
+ parentRunId: run.id,
1765
+ parentStepId: stepId,
1766
+ parentResourceId: run.resourceId ?? undefined,
1767
+ enqueue: true,
1768
+ db
1769
+ });
1770
+ const childRun = result.run;
1771
+ if (!result.created) {
1772
+ this.assertInvokeChildWorkflowStepOwnership({
1773
+ childRun,
1774
+ parentRun: lockedRun,
1775
+ stepId,
1776
+ workflowId
1777
+ });
1778
+ if (childRun.status === "completed" /* COMPLETED */) {
1779
+ invokeOutput = this.getCompletedChildOutput(childRun);
1780
+ hasInvokeOutput = true;
1781
+ await this.updateRun({
1782
+ runId: run.id,
1783
+ resourceId: run.resourceId ?? undefined,
1784
+ data: {
1785
+ timeline: import_es_toolkit2.merge(lockedRun.timeline, {
1786
+ [invokeChildWorkflowTimelineKey(stepId)]: {
1787
+ invokeChildWorkflow: {
1788
+ childRunId: childRun.id,
1789
+ childWorkflowId: childRun.workflowId,
1790
+ childResourceId: childRun.resourceId
1791
+ },
1792
+ timestamp: new Date
1793
+ },
1794
+ [stepId]: {
1795
+ output: invokeOutput,
1796
+ timestamp: new Date
1797
+ }
1798
+ })
1799
+ }
1800
+ }, { db });
1801
+ return;
1802
+ }
1803
+ if (childRun.status === "failed" /* FAILED */ || childRun.status === "cancelled" /* CANCELLED */) {
1804
+ this.throwForNonCompletedChild(childRun);
1805
+ }
1806
+ }
1807
+ await this.pauseRunForWait({
1808
+ run: lockedRun,
1809
+ stepId,
1810
+ eventName: getInvokeChildWorkflowEventName(childRun.id),
1811
+ skipOutput: true,
1812
+ db,
1813
+ timeline: import_es_toolkit2.merge(lockedRun.timeline, {
1814
+ [invokeChildWorkflowTimelineKey(stepId)]: {
1815
+ invokeChildWorkflow: {
1816
+ childRunId: childRun.id,
1817
+ childWorkflowId: childRun.workflowId,
1818
+ childResourceId: childRun.resourceId
1819
+ },
1820
+ timestamp: new Date
1821
+ }
1822
+ })
1823
+ });
1824
+ }, this.pool);
1825
+ if (hasInvokeOutput) {
1826
+ return invokeOutput;
1827
+ }
1828
+ }
1829
+ async pauseRunForWait({
1830
+ run,
1831
+ stepId,
1832
+ eventName,
1833
+ timeoutEvent,
1834
+ skipOutput,
1835
+ db,
1836
+ timeline
1837
+ }) {
1838
+ const baseTimeline = timeline ?? run.timeline;
1839
+ const waitFor = {};
1840
+ if (eventName)
1841
+ waitFor.eventName = eventName;
1842
+ if (timeoutEvent)
1843
+ waitFor.timeoutEvent = timeoutEvent;
1844
+ if (skipOutput)
1845
+ waitFor.skipOutput = true;
1846
+ await this.updateRun({
1847
+ runId: run.id,
1848
+ resourceId: run.resourceId ?? undefined,
1849
+ data: {
1850
+ status: "paused" /* PAUSED */,
1851
+ currentStepId: stepId,
1852
+ pausedAt: new Date,
1853
+ timeline: import_es_toolkit2.merge(baseTimeline, {
1854
+ [waitForTimelineKey(stepId)]: {
1855
+ waitFor,
1856
+ timestamp: new Date
1857
+ }
1858
+ })
1859
+ }
1860
+ }, { db });
1861
+ }
1573
1862
  async runStep({
1574
1863
  stepId,
1575
1864
  run,
@@ -1607,7 +1896,7 @@ class WorkflowEngine {
1607
1896
  if (output === undefined) {
1608
1897
  output = {};
1609
1898
  }
1610
- run = await this.updateRun({
1899
+ const updated = await this.updateRun({
1611
1900
  runId: run.id,
1612
1901
  resourceId: run.resourceId ?? undefined,
1613
1902
  data: {
@@ -1619,6 +1908,7 @@ class WorkflowEngine {
1619
1908
  })
1620
1909
  }
1621
1910
  }, { db });
1911
+ Object.assign(run, updated);
1622
1912
  return output;
1623
1913
  } catch (error) {
1624
1914
  this.logger.error(`Step ${stepId} failed:`, error, {
@@ -1658,21 +1948,7 @@ ${error.stack}` : String(error)
1658
1948
  const timeoutEvent = timeoutDate ? `__timeout_${stepId}` : undefined;
1659
1949
  await withPostgresTransaction(this.db, async (db) => {
1660
1950
  const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
1661
- return this.updateRun({
1662
- runId: run.id,
1663
- resourceId: run.resourceId ?? undefined,
1664
- data: {
1665
- status: "paused" /* PAUSED */,
1666
- currentStepId: stepId,
1667
- pausedAt: new Date,
1668
- timeline: import_es_toolkit2.merge(freshRun.timeline, {
1669
- [`${stepId}-wait-for`]: {
1670
- waitFor: { eventName, timeoutEvent },
1671
- timestamp: new Date
1672
- }
1673
- })
1674
- }
1675
- }, { db });
1951
+ return this.pauseRunForWait({ run: freshRun, stepId, eventName, timeoutEvent, db });
1676
1952
  }, this.pool);
1677
1953
  if (timeoutDate && timeoutEvent) {
1678
1954
  try {
@@ -1786,7 +2062,7 @@ ${error.stack}` : String(error)
1786
2062
  pausedAt: new Date,
1787
2063
  timeline: import_es_toolkit2.merge(freshRun.timeline, {
1788
2064
  [`${stepId}-poll`]: { startedAt: startedAt.toISOString() },
1789
- [`${stepId}-wait-for`]: {
2065
+ [waitForTimelineKey(stepId)]: {
1790
2066
  waitFor: { timeoutEvent: pollEvent, skipOutput: true },
1791
2067
  timestamp: new Date
1792
2068
  }
@@ -1861,5 +2137,5 @@ ${error.stack}` : String(error)
1861
2137
  }
1862
2138
  }
1863
2139
 
1864
- //# debugId=CBDC1CAB86CECE9C64756E2164756E21
2140
+ //# debugId=B0870008EFFA04F064756E2164756E21
1865
2141
  //# sourceMappingURL=index.js.map