pg-workflows 0.13.2 → 0.15.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
@@ -69,6 +69,7 @@ __export(exports_src, {
69
69
  createWorkflowRef: () => createWorkflowRef,
70
70
  WorkflowStatus: () => WorkflowStatus,
71
71
  WorkflowRunNotFoundError: () => WorkflowRunNotFoundError,
72
+ WorkflowRunInProgressError: () => WorkflowRunInProgressError,
72
73
  WorkflowEngineError: () => WorkflowEngineError,
73
74
  WorkflowEngine: () => WorkflowEngine,
74
75
  WorkflowClient: () => WorkflowClient
@@ -97,7 +98,7 @@ var isInvokeChildWorkflowTimelineEntry = (entry) => !!entry && typeof entry ===
97
98
 
98
99
  // src/db/migration.ts
99
100
  var MIGRATION_LOCK_ID = 738291645;
100
- var CURRENT_SCHEMA_VERSION = 5;
101
+ var CURRENT_SCHEMA_VERSION = 7;
101
102
  async function runMigrations(db) {
102
103
  if (await isSchemaUpToDate(db)) {
103
104
  return;
@@ -173,6 +174,17 @@ async function runMigrations(db) {
173
174
  if (currentVersion < 5) {
174
175
  commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS scheduled_at timestamp with time zone");
175
176
  }
177
+ if (currentVersion < 6) {
178
+ commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS priority integer DEFAULT 0 NOT NULL");
179
+ }
180
+ if (currentVersion < 7) {
181
+ commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS singleton boolean DEFAULT false NOT NULL");
182
+ commands.push(`
183
+ CREATE UNIQUE INDEX IF NOT EXISTS workflow_runs_singleton_active_idx
184
+ ON workflow_runs (workflow_id)
185
+ WHERE singleton AND status IN ('pending', 'running')
186
+ `);
187
+ }
176
188
  if (currentVersion === 0) {
177
189
  commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
178
190
  } else {
@@ -212,9 +224,65 @@ async function getCurrentVersion(db) {
212
224
 
213
225
  // src/db/queries.ts
214
226
  var import_ksuid = __toESM(require("ksuid"));
227
+
228
+ // src/error.ts
229
+ function validateWorkflowId(workflowId) {
230
+ if (workflowId.length > MAX_WORKFLOW_ID_LENGTH) {
231
+ throw new WorkflowEngineError(`workflowId exceeds maximum length of ${MAX_WORKFLOW_ID_LENGTH} characters (got ${workflowId.length})`, workflowId);
232
+ }
233
+ }
234
+ function validateResourceId(resourceId) {
235
+ if (resourceId != null && resourceId.length > MAX_RESOURCE_ID_LENGTH) {
236
+ throw new WorkflowEngineError(`resourceId exceeds maximum length of ${MAX_RESOURCE_ID_LENGTH} characters (got ${resourceId.length})`);
237
+ }
238
+ }
239
+
240
+ class WorkflowEngineError extends Error {
241
+ workflowId;
242
+ runId;
243
+ cause;
244
+ issues;
245
+ constructor(message, workflowId, runId, cause = undefined, issues) {
246
+ super(message);
247
+ this.workflowId = workflowId;
248
+ this.runId = runId;
249
+ this.cause = cause;
250
+ this.issues = issues;
251
+ this.name = "WorkflowEngineError";
252
+ if (Error.captureStackTrace) {
253
+ Error.captureStackTrace(this, WorkflowEngineError);
254
+ }
255
+ }
256
+ }
257
+
258
+ class WorkflowRunNotFoundError extends WorkflowEngineError {
259
+ constructor(runId, workflowId) {
260
+ super("Workflow run not found", workflowId, runId);
261
+ this.name = "WorkflowRunNotFoundError";
262
+ }
263
+ }
264
+
265
+ class WorkflowRunInProgressError extends WorkflowEngineError {
266
+ constructor(workflowId, runId) {
267
+ super(`Workflow "${workflowId}" already has a running run; wait for it to pause, complete, fail, or be cancelled`, workflowId, runId);
268
+ this.name = "WorkflowRunInProgressError";
269
+ }
270
+ }
271
+
272
+ // src/db/queries.ts
215
273
  function generateKSUID(prefix) {
216
274
  return `${prefix ? `${prefix}_` : ""}${import_ksuid.default.randomSync().string}`;
217
275
  }
276
+ function isUniqueViolation(error) {
277
+ if (!error || typeof error !== "object") {
278
+ return false;
279
+ }
280
+ if ("code" in error && error.code === "23505") {
281
+ return true;
282
+ }
283
+ const cause = "cause" in error ? error.cause : undefined;
284
+ return Boolean(cause && typeof cause === "object" && "code" in cause && cause.code === "23505");
285
+ }
218
286
  function mapRowToWorkflowRun(row) {
219
287
  return {
220
288
  id: row.id,
@@ -234,12 +302,14 @@ function mapRowToWorkflowRun(row) {
234
302
  timeoutAt: row.timeout_at ? new Date(row.timeout_at) : null,
235
303
  retryCount: row.retry_count,
236
304
  maxRetries: row.max_retries,
305
+ priority: row.priority,
237
306
  jobId: row.job_id,
238
307
  idempotencyKey: row.idempotency_key,
239
308
  parentRunId: row.parent_run_id,
240
309
  parentStepId: row.parent_step_id,
241
310
  parentResourceId: row.parent_resource_id,
242
- scheduledAt: row.scheduled_at ? new Date(row.scheduled_at) : null
311
+ scheduledAt: row.scheduled_at ? new Date(row.scheduled_at) : null,
312
+ singleton: Boolean(row.singleton)
243
313
  };
244
314
  }
245
315
  async function insertWorkflowRun({
@@ -249,16 +319,26 @@ async function insertWorkflowRun({
249
319
  status,
250
320
  input,
251
321
  maxRetries,
322
+ priority,
252
323
  timeoutAt,
253
324
  idempotencyKey,
254
325
  parentRunId,
255
326
  parentStepId,
256
327
  parentResourceId,
257
- scheduledAt
328
+ scheduledAt,
329
+ singleton = false
258
330
  }, db) {
259
331
  const runId = generateKSUID("run");
260
332
  const now = new Date;
261
- const result = await db.executeSql(`INSERT INTO workflow_runs (
333
+ if (singleton) {
334
+ const active = await getActiveSingletonRun(workflowId, db);
335
+ if (active) {
336
+ throw new WorkflowRunInProgressError(workflowId, active.id);
337
+ }
338
+ }
339
+ let result;
340
+ try {
341
+ result = await db.executeSql(`INSERT INTO workflow_runs (
262
342
  id,
263
343
  resource_id,
264
344
  workflow_id,
@@ -266,6 +346,7 @@ async function insertWorkflowRun({
266
346
  status,
267
347
  input,
268
348
  max_retries,
349
+ priority,
269
350
  timeout_at,
270
351
  created_at,
271
352
  updated_at,
@@ -275,29 +356,39 @@ async function insertWorkflowRun({
275
356
  parent_run_id,
276
357
  parent_step_id,
277
358
  parent_resource_id,
278
- scheduled_at
359
+ scheduled_at,
360
+ singleton
279
361
  )
280
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
362
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
281
363
  ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
282
364
  RETURNING *`, [
283
- runId,
284
- resourceId ?? null,
285
- workflowId,
286
- currentStepId,
287
- status,
288
- JSON.stringify(input),
289
- maxRetries,
290
- timeoutAt,
291
- now,
292
- now,
293
- "{}",
294
- 0,
295
- idempotencyKey ?? null,
296
- parentRunId ?? null,
297
- parentStepId ?? null,
298
- parentResourceId ?? null,
299
- scheduledAt ?? null
300
- ]);
365
+ runId,
366
+ resourceId ?? null,
367
+ workflowId,
368
+ currentStepId,
369
+ status,
370
+ JSON.stringify(input),
371
+ maxRetries,
372
+ priority,
373
+ timeoutAt,
374
+ now,
375
+ now,
376
+ "{}",
377
+ 0,
378
+ idempotencyKey ?? null,
379
+ parentRunId ?? null,
380
+ parentStepId ?? null,
381
+ parentResourceId ?? null,
382
+ scheduledAt ?? null,
383
+ singleton
384
+ ]);
385
+ } catch (error) {
386
+ if (singleton && isUniqueViolation(error)) {
387
+ const active = await getActiveSingletonRun(workflowId, db);
388
+ throw new WorkflowRunInProgressError(workflowId, active?.id);
389
+ }
390
+ throw error;
391
+ }
301
392
  if (result.rows[0]) {
302
393
  return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
303
394
  }
@@ -309,6 +400,20 @@ async function insertWorkflowRun({
309
400
  }
310
401
  return { run: mapRowToWorkflowRun(existing.rows[0]), created: false };
311
402
  }
403
+ async function getActiveSingletonRun(workflowId, db) {
404
+ const result = await db.executeSql(`SELECT * FROM workflow_runs
405
+ WHERE workflow_id = $1
406
+ AND singleton
407
+ AND status IN ('pending', 'running')
408
+ LIMIT 1`, [workflowId]);
409
+ return result.rows[0] ? mapRowToWorkflowRun(result.rows[0]) : null;
410
+ }
411
+ async function assertSingletonSlotAvailable({ workflowId, exceptRunId }, db) {
412
+ const active = await getActiveSingletonRun(workflowId, db);
413
+ if (active && active.id !== exceptRunId) {
414
+ throw new WorkflowRunInProgressError(workflowId, active.id);
415
+ }
416
+ }
312
417
  async function getWorkflowRun({
313
418
  runId,
314
419
  resourceId
@@ -423,7 +528,15 @@ async function updateWorkflowRun({
423
528
  ${whereClause}
424
529
  RETURNING *
425
530
  `;
426
- const result = await db.executeSql(query, values);
531
+ let result;
532
+ try {
533
+ result = await db.executeSql(query, values);
534
+ } catch (error) {
535
+ if (data.status === "running" && isUniqueViolation(error)) {
536
+ throw new WorkflowRunInProgressError("unknown", runId);
537
+ }
538
+ throw error;
539
+ }
427
540
  const run = result.rows[0];
428
541
  if (!run) {
429
542
  return null;
@@ -529,41 +642,28 @@ async function withPostgresTransaction(db, callback, pool) {
529
642
  }
530
643
  }
531
644
 
532
- // src/error.ts
533
- function validateWorkflowId(workflowId) {
534
- if (workflowId.length > MAX_WORKFLOW_ID_LENGTH) {
535
- throw new WorkflowEngineError(`workflowId exceeds maximum length of ${MAX_WORKFLOW_ID_LENGTH} characters (got ${workflowId.length})`, workflowId);
645
+ // src/priority.ts
646
+ var PRIORITY_LEVELS = { high: 100, normal: 0, low: -100 };
647
+ function priorityToInt(value) {
648
+ if (typeof value === "number") {
649
+ if (!Number.isInteger(value)) {
650
+ throw new WorkflowEngineError(`Invalid priority ${value}: numeric priority must be an integer.`);
651
+ }
652
+ return value;
536
653
  }
537
- }
538
- function validateResourceId(resourceId) {
539
- if (resourceId != null && resourceId.length > MAX_RESOURCE_ID_LENGTH) {
540
- throw new WorkflowEngineError(`resourceId exceeds maximum length of ${MAX_RESOURCE_ID_LENGTH} characters (got ${resourceId.length})`);
654
+ const mapped = PRIORITY_LEVELS[value];
655
+ if (mapped === undefined) {
656
+ throw new WorkflowEngineError(`Invalid priority "${value}": expected one of ${Object.keys(PRIORITY_LEVELS).map((k) => `"${k}"`).join(", ")} or an integer.`);
541
657
  }
658
+ return mapped;
542
659
  }
543
-
544
- class WorkflowEngineError extends Error {
545
- workflowId;
546
- runId;
547
- cause;
548
- issues;
549
- constructor(message, workflowId, runId, cause = undefined, issues) {
550
- super(message);
551
- this.workflowId = workflowId;
552
- this.runId = runId;
553
- this.cause = cause;
554
- this.issues = issues;
555
- this.name = "WorkflowEngineError";
556
- if (Error.captureStackTrace) {
557
- Error.captureStackTrace(this, WorkflowEngineError);
660
+ function resolvePriority(...candidates) {
661
+ for (const candidate of candidates) {
662
+ if (candidate !== undefined) {
663
+ return priorityToInt(candidate);
558
664
  }
559
665
  }
560
- }
561
-
562
- class WorkflowRunNotFoundError extends WorkflowEngineError {
563
- constructor(runId, workflowId) {
564
- super("Workflow run not found", workflowId, runId);
565
- this.name = "WorkflowRunNotFoundError";
566
- }
666
+ return PRIORITY_LEVELS.normal;
567
667
  }
568
668
 
569
669
  // src/types.ts
@@ -616,11 +716,10 @@ class WorkflowClient {
616
716
  const db = {
617
717
  executeSql: (text, values) => this.pool.query(text, values)
618
718
  };
619
- if (boss) {
620
- this.boss = boss;
621
- } else {
622
- this.boss = new import_pg_boss.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
623
- }
719
+ this.boss = boss ?? new import_pg_boss.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
720
+ this.boss.on("error", (error) => {
721
+ this.logger.error("pg-boss error", error);
722
+ });
624
723
  this.db = db;
625
724
  }
626
725
  async start() {
@@ -649,6 +748,7 @@ class WorkflowClient {
649
748
  let resourceId;
650
749
  let idempotencyKey;
651
750
  let options;
751
+ let singleton = false;
652
752
  if (typeof refOrParams === "function" && "id" in refOrParams) {
653
753
  const ref = refOrParams;
654
754
  workflowId = ref.id;
@@ -656,6 +756,7 @@ class WorkflowClient {
656
756
  options = optionsArg;
657
757
  resourceId = optionsArg?.resourceId;
658
758
  idempotencyKey = optionsArg?.idempotencyKey;
759
+ singleton = optionsArg?.singleton ?? ref.singleton === true;
659
760
  if (ref.inputSchema) {
660
761
  const result = await ref.inputSchema["~standard"].validate(input);
661
762
  if (result.issues) {
@@ -669,6 +770,7 @@ class WorkflowClient {
669
770
  resourceId = params.resourceId;
670
771
  idempotencyKey = params.idempotencyKey;
671
772
  options = params.options;
773
+ singleton = params.options?.singleton === true;
672
774
  }
673
775
  validateWorkflowId(workflowId);
674
776
  validateResourceId(resourceId);
@@ -681,8 +783,10 @@ class WorkflowClient {
681
783
  status: "running" /* RUNNING */,
682
784
  input,
683
785
  maxRetries: options?.retries ?? 0,
786
+ priority: resolvePriority(options?.priority),
684
787
  timeoutAt,
685
- idempotencyKey
788
+ idempotencyKey,
789
+ singleton
686
790
  }, _db);
687
791
  if (created) {
688
792
  const job = {
@@ -694,6 +798,7 @@ class WorkflowClient {
694
798
  await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
695
799
  startAfter: new Date,
696
800
  expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
801
+ priority: insertedRun.priority,
697
802
  db: _db
698
803
  });
699
804
  }
@@ -711,6 +816,9 @@ class WorkflowClient {
711
816
  }) {
712
817
  await this.ensureStarted();
713
818
  const run = await this.getRun({ runId, resourceId });
819
+ if (run.singleton) {
820
+ await assertSingletonSlotAvailable({ workflowId: run.workflowId, exceptRunId: run.id }, this.db);
821
+ }
714
822
  const job = {
715
823
  runId: run.id,
716
824
  resourceId: resourceId ?? run.resourceId ?? undefined,
@@ -722,7 +830,8 @@ class WorkflowClient {
722
830
  }
723
831
  };
724
832
  await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
725
- expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
833
+ expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
834
+ priority: run.priority
726
835
  });
727
836
  this.logger.log(`${LOG_PREFIX} Event ${eventName} sent for workflow run ${runId}`);
728
837
  return run;
@@ -901,6 +1010,8 @@ function createWorkflowRef(id, options) {
901
1010
  inputSchema: options?.inputSchema,
902
1011
  timeout: defineOptions?.timeout,
903
1012
  retries: defineOptions?.retries,
1013
+ priority: defineOptions?.priority,
1014
+ singleton: defineOptions?.singleton ?? options?.singleton,
904
1015
  schedule: defineOptions?.schedule,
905
1016
  timezone: defineOptions?.timezone
906
1017
  });
@@ -909,15 +1020,29 @@ function createWorkflowRef(id, options) {
909
1020
  value: options?.inputSchema,
910
1021
  enumerable: true
911
1022
  });
1023
+ Object.defineProperty(ref, "singleton", {
1024
+ value: options?.singleton,
1025
+ enumerable: true
1026
+ });
912
1027
  return ref;
913
1028
  }
914
1029
  function createWorkflowFactory(plugins = []) {
915
- const factory = (id, handler, { inputSchema, timeout, retries, schedule, timezone } = {}) => ({
1030
+ const factory = (id, handler, {
1031
+ inputSchema,
1032
+ timeout,
1033
+ retries,
1034
+ priority,
1035
+ singleton,
1036
+ schedule,
1037
+ timezone
1038
+ } = {}) => ({
916
1039
  id,
917
1040
  handler,
918
1041
  inputSchema,
919
1042
  timeout,
920
1043
  retries,
1044
+ priority,
1045
+ singleton,
921
1046
  schedule,
922
1047
  timezone,
923
1048
  plugins: plugins.length > 0 ? plugins : undefined
@@ -1130,11 +1255,10 @@ class WorkflowEngine {
1130
1255
  const db = {
1131
1256
  executeSql: (text, values) => this.pool.query(text, values)
1132
1257
  };
1133
- if (boss) {
1134
- this.boss = boss;
1135
- } else {
1136
- this.boss = new import_pg_boss2.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
1137
- }
1258
+ this.boss = boss ?? new import_pg_boss2.PgBoss({ db, schema: DEFAULT_PGBOSS_SCHEMA });
1259
+ this.boss.on("error", (error) => {
1260
+ this.logger.error("pg-boss error", error);
1261
+ });
1138
1262
  this.db = this.boss.getDb();
1139
1263
  }
1140
1264
  async start(asEngine = true, {
@@ -1188,6 +1312,10 @@ class WorkflowEngine {
1188
1312
  try {
1189
1313
  await this.createWorkflowRun({ workflowId, input: {}, scheduledAt });
1190
1314
  } catch (error) {
1315
+ if (error instanceof WorkflowRunInProgressError) {
1316
+ this.logger.log(`Skipping schedule fire for "${workflowId}": a run is already in progress`, { workflowId, runId: error.runId });
1317
+ return;
1318
+ }
1191
1319
  this.logger.error(`Schedule fire failed to start a run for workflow "${workflowId}"`, error instanceof Error ? error : new Error(String(error)), { workflowId });
1192
1320
  throw error;
1193
1321
  }
@@ -1300,6 +1428,7 @@ class WorkflowEngine {
1300
1428
  parentRunId,
1301
1429
  parentStepId,
1302
1430
  parentResourceId,
1431
+ parentPriority,
1303
1432
  scheduledAt,
1304
1433
  enqueue = true,
1305
1434
  db
@@ -1330,12 +1459,14 @@ class WorkflowEngine {
1330
1459
  status: "running" /* RUNNING */,
1331
1460
  input,
1332
1461
  maxRetries: options?.retries ?? workflow2.retries ?? 0,
1462
+ priority: resolvePriority(options?.priority, workflow2.priority, parentPriority),
1333
1463
  timeoutAt,
1334
1464
  idempotencyKey,
1335
1465
  parentRunId,
1336
1466
  parentStepId,
1337
1467
  parentResourceId,
1338
- scheduledAt
1468
+ scheduledAt,
1469
+ singleton: workflow2.singleton === true || options?.singleton === true
1339
1470
  }, targetDb);
1340
1471
  const insertAndEnqueue = async (targetDb) => {
1341
1472
  const result = await insertRun(targetDb);
@@ -1357,6 +1488,7 @@ class WorkflowEngine {
1357
1488
  await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
1358
1489
  startAfter: new Date,
1359
1490
  expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2,
1491
+ priority: run.priority,
1360
1492
  ...retrySendOptions(run.maxRetries),
1361
1493
  ...db ? { db } : {}
1362
1494
  });
@@ -1493,6 +1625,9 @@ class WorkflowEngine {
1493
1625
  }) {
1494
1626
  await this.checkIfHasStarted();
1495
1627
  const run = await this.getRun({ runId, resourceId });
1628
+ if (run.singleton) {
1629
+ await assertSingletonSlotAvailable({ workflowId: run.workflowId, exceptRunId: run.id }, this.db);
1630
+ }
1496
1631
  const jobResourceId = resourceId ?? run.resourceId ?? undefined;
1497
1632
  const job = {
1498
1633
  runId: run.id,
@@ -1506,6 +1641,7 @@ class WorkflowEngine {
1506
1641
  };
1507
1642
  await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
1508
1643
  expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds2,
1644
+ priority: run.priority,
1509
1645
  ...retrySendOptions(run.maxRetries)
1510
1646
  });
1511
1647
  this.logger.log(`event ${eventName} sent for workflow run with id ${runId}`);
@@ -1915,6 +2051,7 @@ class WorkflowEngine {
1915
2051
  parentRunId: run.id,
1916
2052
  parentStepId: stepId,
1917
2053
  parentResourceId: run.resourceId ?? undefined,
2054
+ parentPriority: run.priority,
1918
2055
  enqueue: true,
1919
2056
  db
1920
2057
  });
@@ -2113,6 +2250,7 @@ ${error.stack}` : String(error)
2113
2250
  await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
2114
2251
  startAfter: timeoutDate.getTime() <= Date.now() ? new Date : timeoutDate,
2115
2252
  expireInSeconds: defaultExpireInSeconds2,
2253
+ priority: run.priority,
2116
2254
  ...retrySendOptions(run.maxRetries)
2117
2255
  });
2118
2256
  } catch (error) {
@@ -2231,6 +2369,7 @@ ${error.stack}` : String(error)
2231
2369
  }, {
2232
2370
  startAfter: new Date(Date.now() + intervalMs),
2233
2371
  expireInSeconds: defaultExpireInSeconds2,
2372
+ priority: run.priority,
2234
2373
  ...retrySendOptions(run.maxRetries)
2235
2374
  });
2236
2375
  } catch (error) {
@@ -2458,5 +2597,5 @@ function otelPlugin(options = {}) {
2458
2597
  };
2459
2598
  }
2460
2599
 
2461
- //# debugId=EB0357F84B3B4EF764756E2164756E21
2600
+ //# debugId=0BBC9686D69E329D64756E2164756E21
2462
2601
  //# sourceMappingURL=index.js.map
package/dist/index.d.cts CHANGED
@@ -18,6 +18,15 @@ type WorkflowRun = {
18
18
  timeoutAt: Date | null;
19
19
  retryCount: number;
20
20
  maxRetries: number;
21
+ /** Resolved scheduling priority (pg-boss integer; higher runs first). */
22
+ priority: number;
23
+ /**
24
+ * When true, this run participates in the per-workflow-ID uniqueness
25
+ * constraint: no other pending or running singleton run of the same
26
+ * workflow ID may exist at the same time. Paused, completed, failed, and
27
+ * cancelled runs release the slot.
28
+ */
29
+ singleton: boolean;
21
30
  jobId: string | null;
22
31
  idempotencyKey: string | null;
23
32
  parentRunId: string | null;
@@ -35,6 +44,18 @@ type DurationObject = {
35
44
  seconds?: number;
36
45
  };
37
46
  type Duration = string | DurationObject;
47
+ /**
48
+ * Named priority levels mapped to pg-boss integer priorities. Higher numbers
49
+ * are fetched first (pg-boss orders by `priority DESC`), and `normal = 0`
50
+ * matches pg-boss's own default. The ±100 spacing leaves room for numeric
51
+ * tuning between tiers via the escape hatch.
52
+ */
53
+ declare const PRIORITY_LEVELS: {
54
+ readonly high: 100;
55
+ readonly normal: 0;
56
+ readonly low: -100;
57
+ };
58
+ type WorkflowPriority = keyof typeof PRIORITY_LEVELS | number;
38
59
  type Schedule = string | Exclude<Duration, string>;
39
60
  declare enum WorkflowStatus {
40
61
  PENDING = "pending",
@@ -61,11 +82,25 @@ type StartWorkflowOptions = {
61
82
  retries?: number;
62
83
  expireInSeconds?: number;
63
84
  idempotencyKey?: string;
85
+ priority?: WorkflowPriority;
86
+ /**
87
+ * Client-side counterpart of `workflow(..., { singleton: true })`.
88
+ * The engine reads this from the registered definition; pass it here when
89
+ * starting from `WorkflowClient` so the uniqueness constraint applies.
90
+ */
91
+ singleton?: boolean;
64
92
  };
65
93
  type WorkflowOptions<I extends InputParameters> = {
66
94
  timeout?: number;
67
95
  retries?: number;
68
96
  inputSchema?: I;
97
+ priority?: WorkflowPriority;
98
+ /**
99
+ * When true, at most one pending or running run of this workflow ID may exist.
100
+ * A second `startWorkflow` throws `WorkflowRunInProgressError` until the
101
+ * current run pauses, completes, fails, or is cancelled.
102
+ */
103
+ singleton?: boolean;
69
104
  /**
70
105
  * Recurring schedule. Accepts a cron expression (`'0 9 * * 1-5'`),
71
106
  * a duration string (`'5m'`, `'1 hour'`), or a `DurationObject`.
@@ -173,6 +208,8 @@ type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
173
208
  inputSchema?: TInput;
174
209
  timeout?: number;
175
210
  retries?: number;
211
+ priority?: WorkflowPriority;
212
+ singleton?: boolean;
176
213
  schedule?: Schedule;
177
214
  timezone?: string;
178
215
  plugins?: WorkflowPlugin[];
@@ -199,6 +236,7 @@ interface WorkflowFactory<TStepExt = object> {
199
236
  TOutput = unknown
200
237
  >(id: string, options?: {
201
238
  inputSchema?: TInput;
239
+ singleton?: boolean;
202
240
  }): WorkflowRef<TInput, TOutput>;
203
241
  }
204
242
  /**
@@ -215,6 +253,7 @@ interface WorkflowRef<
215
253
  (handler: (context: WorkflowContext<TInput, StepBaseContext>) => Promise<unknown>, options?: Omit<WorkflowOptions<TInput>, "inputSchema">): WorkflowDefinition<TInput>;
216
254
  readonly id: string;
217
255
  readonly inputSchema?: TInput;
256
+ readonly singleton?: boolean;
218
257
  }
219
258
  type WorkflowRunProgress = WorkflowRun & {
220
259
  completionPercentage: number;
@@ -321,6 +360,7 @@ declare function createWorkflowRef<
321
360
  TInput extends InputParameters = InputParameters
322
361
  >(id: string, options?: {
323
362
  inputSchema?: TInput;
363
+ singleton?: boolean;
324
364
  }): WorkflowRef<TInput, TOutput>;
325
365
  declare const workflow: WorkflowFactory;
326
366
  import pg2 from "pg";
@@ -494,6 +534,9 @@ declare class WorkflowEngineError extends Error {
494
534
  declare class WorkflowRunNotFoundError extends WorkflowEngineError {
495
535
  constructor(runId?: string, workflowId?: string);
496
536
  }
537
+ declare class WorkflowRunInProgressError extends WorkflowEngineError {
538
+ constructor(workflowId: string, runId?: string);
539
+ }
497
540
  import { AttributeValue, Tracer } from "@opentelemetry/api";
498
541
  type OtelPluginOptions = {
499
542
  /** Tracer to use. Defaults to `trace.getTracer('pg-workflows')`. */
@@ -504,4 +547,4 @@ type OtelPluginOptions = {
504
547
  attributes?: (context: WorkflowContext) => Record<string, AttributeValue>;
505
548
  };
506
549
  declare function otelPlugin(options?: OtelPluginOptions): WorkflowPlugin<StepBaseContext, object>;
507
- export { workflow, otelPlugin, createWorkflowRef, WorkflowStatus, WorkflowRunProgress, WorkflowRunNotFoundError, WorkflowRun, WorkflowRef, WorkflowPlugin, WorkflowOptions, WorkflowLogger, WorkflowEngineOptions, WorkflowEngineError, WorkflowEngine, WorkflowDefinition, WorkflowContext, WorkflowClientOptions, WorkflowClient, StepBaseContext, StartWorkflowOptions, ScheduleContext, Schedule, OtelPluginOptions, InputParameters, InferInputParameters, Duration };
550
+ export { workflow, otelPlugin, createWorkflowRef, WorkflowStatus, WorkflowRunProgress, WorkflowRunNotFoundError, WorkflowRunInProgressError, WorkflowRun, WorkflowRef, WorkflowPriority, WorkflowPlugin, WorkflowOptions, WorkflowLogger, WorkflowEngineOptions, WorkflowEngineError, WorkflowEngine, WorkflowDefinition, WorkflowContext, WorkflowClientOptions, WorkflowClient, StepBaseContext, StartWorkflowOptions, ScheduleContext, Schedule, OtelPluginOptions, InputParameters, InferInputParameters, Duration };