pg-workflows 0.6.1 → 0.7.1

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/README.md CHANGED
@@ -51,7 +51,8 @@ If you need enterprise-grade features like distributed tracing, complex DAG sche
51
51
  - **Built-in Retries** - Automatic retries with exponential backoff at the workflow level.
52
52
  - **Configurable Timeouts** - Set workflow-level and step-level timeouts to prevent runaway executions.
53
53
  - **Progress Tracking** - Monitor workflow completion percentage, completed steps, and total steps in real-time.
54
- - **Input Validation** - Define schemas with Zod for type-safe, validated workflow inputs.
54
+ - **Input Validation** - Define schemas with any [Standard Schema](https://github.com/standard-schema/standard-schema)-compliant library (Zod, Valibot, ArkType, etc.) for type-safe, validated workflow inputs.
55
+ - **Idempotent starts** - Optional `idempotencyKey` on `startWorkflow()` so duplicate API calls or retries return the same run instead of enqueueing another job.
55
56
  - **Built on pg-boss** - Leverages the battle-tested [pg-boss](https://github.com/timgit/pg-boss) job queue for reliable task scheduling. pg-boss is bundled as a dependency - no separate install or configuration needed.
56
57
 
57
58
  ---
@@ -151,6 +152,8 @@ const run = await engine.startWorkflow({
151
152
  workflowId: 'send-welcome-email',
152
153
  resourceId: 'user-123',
153
154
  input: { email: 'user@example.com' },
155
+ // Optional: same key returns the existing run (deduplicates double-submits / retries)
156
+ idempotencyKey: 'welcome:user-123',
154
157
  });
155
158
 
156
159
  // Send an event to resume the waiting workflow
@@ -357,8 +360,8 @@ const myWorkflow = workflow(
357
360
  // Your workflow logic here
358
361
  },
359
362
  {
360
- inputSchema: z.object({ /* ... */ }),
361
- timeout: 60000, // milliseconds
363
+ inputSchema: mySchema, // any Standard Schema-compliant schema
364
+ timeout: 60000, // milliseconds
362
365
  retries: 3,
363
366
  }
364
367
  );
@@ -426,6 +429,29 @@ const { items } = await engine.getRuns({
426
429
  });
427
430
  ```
428
431
 
432
+ ### Idempotency key
433
+
434
+ Pass an optional `idempotencyKey` to `startWorkflow()` when the same logical start might be requested more than once (user double-clicks, API retries, or at-least-once webhooks). The engine stores the key on the run; a second `startWorkflow` with the **same** key returns the **existing** run and does **not** enqueue a second job.
435
+
436
+ Keys are **globally unique** in the database (up to 256 characters), not scoped per workflow or resource. Prefer stable, namespaced strings so different workflows never collide—for example `send-welcome-email:order-123` instead of a bare order id.
437
+
438
+ ```typescript
439
+ const run = await engine.startWorkflow({
440
+ workflowId: 'send-welcome-email',
441
+ input: { email: 'user@example.com' },
442
+ idempotencyKey: 'send-welcome-email:checkout-session_cs_abc123',
443
+ });
444
+
445
+ // Idempotent: returns the same run and run.id as above
446
+ const again = await engine.startWorkflow({
447
+ workflowId: 'send-welcome-email',
448
+ input: { email: 'other@example.com' }, // ignored for deduplication — existing run wins
449
+ idempotencyKey: 'send-welcome-email:checkout-session_cs_abc123',
450
+ });
451
+ ```
452
+
453
+ The returned `WorkflowRun` includes `idempotencyKey` (or `null` if omitted).
454
+
429
455
  ### Pause and Resume
430
456
 
431
457
  Manually pause a workflow and resume it later:
@@ -476,6 +502,96 @@ await engine.fastForwardWorkflow({
476
502
  | `step.poll()` | Writes `data` as the poll result and triggers resolution |
477
503
  | `step.pause()` | Delegates to `resumeWorkflow()` |
478
504
 
505
+ ### Input Validation
506
+
507
+ pg-workflows supports any [Standard Schema](https://github.com/standard-schema/standard-schema)-compliant validation library for `inputSchema`. This means you can use Zod, Valibot, ArkType, or any library that implements the Standard Schema spec. When a schema is provided, the workflow input is validated before execution and the handler's `input` parameter is fully typed.
508
+
509
+ #### With Zod
510
+
511
+ ```typescript
512
+ import { workflow } from 'pg-workflows';
513
+ import { z } from 'zod';
514
+
515
+ const myWorkflow = workflow(
516
+ 'user-onboarding',
517
+ async ({ step, input }) => {
518
+ // input is typed as { email: string; name: string }
519
+ await step.run('send-welcome', async () => {
520
+ return await sendEmail(input.email, `Welcome, ${input.name}!`);
521
+ });
522
+ },
523
+ {
524
+ inputSchema: z.object({
525
+ email: z.string().email(),
526
+ name: z.string(),
527
+ }),
528
+ }
529
+ );
530
+ ```
531
+
532
+ #### With Valibot
533
+
534
+ ```typescript
535
+ import { workflow } from 'pg-workflows';
536
+ import * as v from 'valibot';
537
+
538
+ const myWorkflow = workflow(
539
+ 'user-onboarding',
540
+ async ({ step, input }) => {
541
+ // input is typed as { email: string; name: string }
542
+ await step.run('send-welcome', async () => {
543
+ return await sendEmail(input.email, `Welcome, ${input.name}!`);
544
+ });
545
+ },
546
+ {
547
+ inputSchema: v.object({
548
+ email: v.pipe(v.string(), v.email()),
549
+ name: v.string(),
550
+ }),
551
+ }
552
+ );
553
+ ```
554
+
555
+ #### Without a Schema
556
+
557
+ When no `inputSchema` is provided, input is not validated and `input` is typed as `unknown`. This is because the engine has no guarantee about the shape of the data — it passes through whatever was provided to `startWorkflow()`. You are responsible for narrowing the type yourself, either with a type assertion or runtime checks:
558
+
559
+ ```typescript
560
+ import { workflow } from 'pg-workflows';
561
+
562
+ const myWorkflow = workflow(
563
+ 'process-order',
564
+ async ({ step, input }) => {
565
+ // Option 1: Type assertion — you trust the caller
566
+ const { orderId, amount } = input as { orderId: string; amount: number };
567
+
568
+ await step.run('charge', async () => {
569
+ return await chargeOrder(orderId, amount);
570
+ });
571
+ }
572
+ );
573
+
574
+ const myDefensiveWorkflow = workflow(
575
+ 'process-order-safe',
576
+ async ({ step, input }) => {
577
+ // Option 2: Runtime checks — you verify before using
578
+ if (typeof input !== 'object' || input === null) {
579
+ throw new Error('Expected input to be an object');
580
+ }
581
+ const { orderId, amount } = input as Record<string, unknown>;
582
+ if (typeof orderId !== 'string' || typeof amount !== 'number') {
583
+ throw new Error('Invalid input shape');
584
+ }
585
+
586
+ await step.run('charge', async () => {
587
+ return await chargeOrder(orderId, amount);
588
+ });
589
+ }
590
+ );
591
+ ```
592
+
593
+ Using an `inputSchema` is recommended — it validates input at the engine boundary before your handler runs, and gives you full type inference with no manual work.
594
+
479
595
  ---
480
596
 
481
597
  ## Examples
@@ -618,7 +734,7 @@ When `boss` is omitted, pg-boss is created automatically with an isolated schema
618
734
  | `start(asEngine?, options?)` | Start the engine and workers |
619
735
  | `stop()` | Stop the engine gracefully |
620
736
  | `registerWorkflow(definition)` | Register a workflow definition |
621
- | `startWorkflow({ workflowId, resourceId?, input, options? })` | Start a new workflow run. `resourceId` optionally ties the run to an external entity (see [Resource ID](#resource-id)). |
737
+ | `startWorkflow({ workflowId, resourceId?, input, idempotencyKey?, options? })` | Start a new workflow run. `resourceId` optionally ties the run to an external entity (see [Resource ID](#resource-id)). `idempotencyKey` optionally deduplicates starts (see [Idempotency key](#idempotency-key)). |
622
738
  | `pauseWorkflow({ runId, resourceId? })` | Pause a running workflow |
623
739
  | `resumeWorkflow({ runId, resourceId?, options? })` | Resume a paused workflow |
624
740
  | `cancelWorkflow({ runId, resourceId? })` | Cancel a workflow |
@@ -699,7 +815,7 @@ enum WorkflowStatus {
699
815
 
700
816
  The engine automatically runs migrations on startup to create the required tables:
701
817
 
702
- - `workflow_runs` - Stores workflow execution state, step results, and timeline in the `public` schema. The optional `resource_id` column (indexed) associates each run with an external entity in your application. See [Resource ID](#resource-id).
818
+ - `workflow_runs` - Stores workflow execution state, step results, and timeline in the `public` schema. The optional `resource_id` column (indexed) associates each run with an external entity in your application. See [Resource ID](#resource-id). The optional `idempotency_key` column has a unique partial index for [idempotent starts](#idempotency-key).
703
819
  - `pgboss_v12_pgworkflow.*` - pg-boss job queue tables for reliable task scheduling (isolated schema to avoid conflicts)
704
820
 
705
821
  ---
@@ -726,7 +842,7 @@ npm install pg-workflows pg
726
842
  - Node.js >= 18.0.0
727
843
  - PostgreSQL >= 10
728
844
  - `pg` >= 8.0.0 (peer dependency)
729
- - `zod` >= 3.0.0 (optional peer dependency, needed only if using `inputSchema`)
845
+ - A [Standard Schema](https://github.com/standard-schema/standard-schema)-compliant validation library (Zod, Valibot, ArkType, etc.) if using `inputSchema`
730
846
 
731
847
  ## Acknowledgments
732
848
 
package/dist/index.cjs CHANGED
@@ -99,11 +99,13 @@ class WorkflowEngineError extends Error {
99
99
  workflowId;
100
100
  runId;
101
101
  cause;
102
- constructor(message, workflowId, runId, cause = undefined) {
102
+ issues;
103
+ constructor(message, workflowId, runId, cause = undefined, issues) {
103
104
  super(message);
104
105
  this.workflowId = workflowId;
105
106
  this.runId = runId;
106
107
  this.cause = cause;
108
+ this.issues = issues;
107
109
  this.name = "WorkflowEngineError";
108
110
  if (Error.captureStackTrace) {
109
111
  Error.captureStackTrace(this, WorkflowEngineError);
@@ -240,17 +242,17 @@ function parseWorkflowHandler(handler) {
240
242
  }
241
243
 
242
244
  // src/db/migration.ts
245
+ var MIGRATION_LOCK_ID = 738291645;
246
+ var CURRENT_SCHEMA_VERSION = 2;
243
247
  async function runMigrations(db) {
244
- const tableExistsResult = await db.executeSql(`
245
- SELECT EXISTS (
246
- SELECT FROM information_schema.tables
247
- WHERE table_schema = 'public'
248
- AND table_name = 'workflow_runs'
249
- );
250
- `, []);
251
- if (!tableExistsResult.rows[0]?.exists) {
252
- await db.executeSql(`
253
- CREATE TABLE workflow_runs (
248
+ if (await isSchemaUpToDate(db)) {
249
+ return;
250
+ }
251
+ const currentVersion = await getCurrentVersion(db);
252
+ const commands = [];
253
+ if (currentVersion < 1) {
254
+ commands.push(`
255
+ CREATE TABLE IF NOT EXISTS workflow_runs (
254
256
  id varchar(32) PRIMARY KEY NOT NULL,
255
257
  created_at timestamp with time zone DEFAULT now() NOT NULL,
256
258
  updated_at timestamp with time zone DEFAULT now() NOT NULL,
@@ -269,25 +271,66 @@ async function runMigrations(db) {
269
271
  retry_count integer DEFAULT 0 NOT NULL,
270
272
  max_retries integer DEFAULT 0 NOT NULL,
271
273
  job_id varchar(256)
272
- );
273
- `, []);
274
- await db.executeSql(`
275
- CREATE INDEX workflow_runs_created_at_idx ON workflow_runs USING btree (created_at);
276
- CREATE INDEX workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC);
277
- CREATE INDEX workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC);
278
- CREATE INDEX workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC);
279
- CREATE INDEX workflow_runs_resource_id_workflow_id_created_at_idx ON workflow_runs USING btree (resource_id, workflow_id, created_at DESC);
280
- `, []);
274
+ )
275
+ `);
276
+ commands.push(`
277
+ CREATE INDEX IF NOT EXISTS workflow_runs_created_at_idx ON workflow_runs USING btree (created_at)
278
+ `);
279
+ commands.push(`
280
+ CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC)
281
+ `);
282
+ commands.push(`
283
+ CREATE INDEX IF NOT EXISTS workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC)
284
+ `);
285
+ commands.push(`
286
+ CREATE INDEX IF NOT EXISTS workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC)
287
+ `);
288
+ commands.push(`
289
+ CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_workflow_id_created_at_idx ON workflow_runs USING btree (resource_id, workflow_id, created_at DESC)
290
+ `);
291
+ }
292
+ if (currentVersion < 2) {
293
+ commands.push("DROP INDEX IF EXISTS workflow_runs_workflow_id_idx");
294
+ commands.push("DROP INDEX IF EXISTS workflow_runs_resource_id_idx");
295
+ commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS idempotency_key varchar(256)");
296
+ commands.push(`
297
+ CREATE UNIQUE INDEX IF NOT EXISTS workflow_runs_idempotency_key_idx ON workflow_runs (idempotency_key) WHERE idempotency_key IS NOT NULL
298
+ `);
299
+ }
300
+ if (currentVersion === 0) {
301
+ commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
281
302
  } else {
282
- await db.executeSql(`
283
- DROP INDEX IF EXISTS workflow_runs_workflow_id_idx;
284
- DROP INDEX IF EXISTS workflow_runs_resource_id_idx;
285
- CREATE INDEX IF NOT EXISTS workflow_runs_created_at_idx ON workflow_runs USING btree (created_at);
286
- CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC);
287
- CREATE INDEX IF NOT EXISTS workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC);
288
- CREATE INDEX IF NOT EXISTS workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC);
289
- CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_workflow_id_created_at_idx ON workflow_runs USING btree (resource_id, workflow_id, created_at DESC);
290
- `, []);
303
+ commands.push(`UPDATE workflow_schema_version SET version = ${CURRENT_SCHEMA_VERSION}`);
304
+ }
305
+ if (commands.length === 0) {
306
+ return;
307
+ }
308
+ const sql = [
309
+ "BEGIN",
310
+ "SET LOCAL lock_timeout = '30s'",
311
+ "SET LOCAL idle_in_transaction_session_timeout = '30s'",
312
+ `SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_ID})`,
313
+ "CREATE TABLE IF NOT EXISTS workflow_schema_version (version integer NOT NULL)",
314
+ ...commands,
315
+ "COMMIT"
316
+ ].join(`;
317
+ `);
318
+ await db.executeSql(sql, []);
319
+ }
320
+ async function isSchemaUpToDate(db) {
321
+ try {
322
+ const result = await db.executeSql("SELECT version FROM workflow_schema_version LIMIT 1", []);
323
+ return (result.rows[0]?.version ?? 0) >= CURRENT_SCHEMA_VERSION;
324
+ } catch {
325
+ return false;
326
+ }
327
+ }
328
+ async function getCurrentVersion(db) {
329
+ try {
330
+ const result = await db.executeSql("SELECT version FROM workflow_schema_version LIMIT 1", []);
331
+ return result.rows[0]?.version ?? 0;
332
+ } catch {
333
+ return 0;
291
334
  }
292
335
  }
293
336
 
@@ -315,7 +358,8 @@ function mapRowToWorkflowRun(row) {
315
358
  timeoutAt: row.timeout_at ? new Date(row.timeout_at) : null,
316
359
  retryCount: row.retry_count,
317
360
  maxRetries: row.max_retries,
318
- jobId: row.job_id
361
+ jobId: row.job_id,
362
+ idempotencyKey: row.idempotency_key
319
363
  };
320
364
  }
321
365
  async function insertWorkflowRun({
@@ -325,7 +369,8 @@ async function insertWorkflowRun({
325
369
  status,
326
370
  input,
327
371
  maxRetries,
328
- timeoutAt
372
+ timeoutAt,
373
+ idempotencyKey
329
374
  }, db) {
330
375
  const runId = generateKSUID("run");
331
376
  const now = new Date;
@@ -341,9 +386,11 @@ async function insertWorkflowRun({
341
386
  created_at,
342
387
  updated_at,
343
388
  timeline,
344
- retry_count
389
+ retry_count,
390
+ idempotency_key
345
391
  )
346
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
392
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
393
+ ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
347
394
  RETURNING *`, [
348
395
  runId,
349
396
  resourceId ?? null,
@@ -356,13 +403,19 @@ async function insertWorkflowRun({
356
403
  now,
357
404
  now,
358
405
  "{}",
359
- 0
406
+ 0,
407
+ idempotencyKey ?? null
408
+ ]);
409
+ if (result.rows[0]) {
410
+ return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
411
+ }
412
+ const existing = await db.executeSql("SELECT * FROM workflow_runs WHERE idempotency_key = $1", [
413
+ idempotencyKey
360
414
  ]);
361
- const insertedRun = result.rows[0];
362
- if (!insertedRun) {
363
- throw new Error("Failed to insert workflow run");
415
+ if (!existing.rows[0]) {
416
+ throw new Error(`Idempotency conflict: existing run not found for key "${idempotencyKey}"`);
364
417
  }
365
- return mapRowToWorkflowRun(insertedRun);
418
+ return { run: mapRowToWorkflowRun(existing.rows[0]), created: false };
366
419
  }
367
420
  async function getWorkflowRun({
368
421
  runId,
@@ -682,6 +735,7 @@ class WorkflowEngine {
682
735
  resourceId,
683
736
  workflowId,
684
737
  input,
738
+ idempotencyKey,
685
739
  options
686
740
  }) {
687
741
  if (!this._started) {
@@ -697,33 +751,36 @@ class WorkflowEngine {
697
751
  throw new WorkflowEngineError(`Workflow ${workflowId} has no steps`, workflowId);
698
752
  }
699
753
  if (workflow2.inputSchema) {
700
- const result = workflow2.inputSchema.safeParse(input);
701
- if (!result.success) {
702
- throw new WorkflowEngineError(result.error.message, workflowId);
754
+ const result = await workflow2.inputSchema["~standard"].validate(input);
755
+ if (result.issues) {
756
+ throw new WorkflowEngineError(JSON.stringify(result.issues), workflowId, undefined, undefined, result.issues);
703
757
  }
704
758
  }
705
759
  const initialStepId = workflow2.steps[0]?.id ?? "__start__";
706
760
  const run = await withPostgresTransaction(this.boss.getDb(), async (_db) => {
707
761
  const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : workflow2.timeout ? new Date(Date.now() + workflow2.timeout) : null;
708
- const insertedRun = await insertWorkflowRun({
762
+ const { run: insertedRun, created } = await insertWorkflowRun({
709
763
  resourceId,
710
764
  workflowId,
711
765
  currentStepId: initialStepId,
712
766
  status: "running" /* RUNNING */,
713
767
  input,
714
768
  maxRetries: options?.retries ?? workflow2.retries ?? 0,
715
- timeoutAt
769
+ timeoutAt,
770
+ idempotencyKey
716
771
  }, _db);
717
- const job = {
718
- runId: insertedRun.id,
719
- resourceId,
720
- workflowId,
721
- input
722
- };
723
- await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
724
- startAfter: new Date,
725
- expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
726
- });
772
+ if (created) {
773
+ const job = {
774
+ runId: insertedRun.id,
775
+ resourceId,
776
+ workflowId,
777
+ input
778
+ };
779
+ await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
780
+ startAfter: new Date,
781
+ expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
782
+ });
783
+ }
727
784
  return insertedRun;
728
785
  }, this.pool);
729
786
  this.logger.log("Started workflow run", {
@@ -1423,5 +1480,5 @@ ${error.stack}` : String(error)
1423
1480
  }
1424
1481
  }
1425
1482
 
1426
- //# debugId=12BE08AB4C2E4D0564756E2164756E21
1483
+ //# debugId=644364C840A9480364756E2164756E21
1427
1484
  //# sourceMappingURL=index.js.map
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import { StandardSchemaV1 } from "@standard-schema/spec";
2
2
  type WorkflowRun = {
3
3
  id: string;
4
4
  createdAt: Date;
@@ -18,6 +18,7 @@ type WorkflowRun = {
18
18
  retryCount: number;
19
19
  maxRetries: number;
20
20
  jobId: string | null;
21
+ idempotencyKey: string | null;
21
22
  };
22
23
  type DurationObject = {
23
24
  weeks?: number;
@@ -44,8 +45,8 @@ declare enum StepType {
44
45
  DELAY = "delay",
45
46
  POLL = "poll"
46
47
  }
47
- type InputParameters = z.ZodTypeAny;
48
- type InferInputParameters<P extends InputParameters> = P extends z.ZodTypeAny ? z.infer<P> : never;
48
+ type InputParameters = StandardSchemaV1;
49
+ type InferInputParameters<P extends InputParameters> = StandardSchemaV1.InferOutput<P>;
49
50
  type WorkflowOptions<I extends InputParameters> = {
50
51
  timeout?: number;
51
52
  retries?: number;
@@ -109,12 +110,10 @@ type WorkflowContext<
109
110
  timeline: Record<string, unknown>;
110
111
  logger: WorkflowLogger;
111
112
  };
112
- type WorkflowDefinition<
113
- TInput extends InputParameters = InputParameters,
114
- TStep extends StepBaseContext = StepBaseContext
115
- > = {
113
+ type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
116
114
  id: string;
117
- handler: (context: WorkflowContext<TInput, TStep>) => Promise<unknown>;
115
+ /** Widest context avoids contravariance when collecting definitions; `workflow()` still types the handler narrowly. */
116
+ handler: (context: WorkflowContext<InputParameters, StepBaseContext>) => Promise<unknown>;
118
117
  inputSchema?: TInput;
119
118
  timeout?: number;
120
119
  retries?: number;
@@ -127,10 +126,7 @@ type StepInternalDefinition = {
127
126
  loop: boolean;
128
127
  isDynamic: boolean;
129
128
  };
130
- type WorkflowInternalDefinition<
131
- TInput extends InputParameters = InputParameters,
132
- TStep extends StepBaseContext = StepBaseContext
133
- > = WorkflowDefinition<TInput, TStep> & {
129
+ type WorkflowInternalDefinition<TInput extends InputParameters = InputParameters> = WorkflowDefinition<TInput> & {
134
130
  steps: StepInternalDefinition[];
135
131
  };
136
132
  /**
@@ -138,7 +134,7 @@ type WorkflowInternalDefinition<
138
134
  * TStepExt is the accumulated step extension from all plugins (step = StepContext & TStepExt).
139
135
  */
140
136
  interface WorkflowFactory<TStepExt = object> {
141
- (id: string, handler: (context: WorkflowContext<InputParameters, StepBaseContext & TStepExt>) => Promise<unknown>, options?: WorkflowOptions<InputParameters>): WorkflowDefinition<InputParameters, StepBaseContext & TStepExt>;
137
+ <I extends InputParameters = InputParameters>(id: string, handler: (context: WorkflowContext<I, StepBaseContext & TStepExt>) => Promise<unknown>, options?: WorkflowOptions<I>): WorkflowDefinition<I>;
142
138
  use<TNewExt>(plugin: WorkflowPlugin<StepBaseContext & TStepExt, TNewExt>): WorkflowFactory<TStepExt & TNewExt>;
143
139
  }
144
140
  type WorkflowRunProgress = WorkflowRun & {
@@ -186,13 +182,14 @@ declare class WorkflowEngine {
186
182
  batchSize?: number;
187
183
  }): Promise<void>;
188
184
  stop(): Promise<void>;
189
- registerWorkflow<TStep extends StepBaseContext>(definition: WorkflowDefinition<InputParameters, TStep>): Promise<WorkflowEngine>;
185
+ registerWorkflow(definition: WorkflowDefinition<InputParameters>): Promise<WorkflowEngine>;
190
186
  unregisterWorkflow(workflowId: string): Promise<WorkflowEngine>;
191
187
  unregisterAllWorkflows(): Promise<WorkflowEngine>;
192
- startWorkflow({ resourceId, workflowId, input, options }: {
188
+ startWorkflow({ resourceId, workflowId, input, idempotencyKey, options }: {
193
189
  resourceId?: string;
194
190
  workflowId: string;
195
191
  input: unknown;
192
+ idempotencyKey?: string;
196
193
  options?: {
197
194
  timeout?: number;
198
195
  retries?: number;
@@ -278,11 +275,13 @@ declare class WorkflowEngine {
278
275
  hasPrev: boolean;
279
276
  }>;
280
277
  }
278
+ import { StandardSchemaV1 as StandardSchemaV12 } from "@standard-schema/spec";
281
279
  declare class WorkflowEngineError extends Error {
282
280
  readonly workflowId?: string | undefined;
283
281
  readonly runId?: string | undefined;
284
282
  readonly cause: Error | undefined;
285
- constructor(message: string, workflowId?: string | undefined, runId?: string | undefined, cause?: Error | undefined);
283
+ readonly issues?: StandardSchemaV12.FailureResult["issues"] | undefined;
284
+ constructor(message: string, workflowId?: string | undefined, runId?: string | undefined, cause?: Error | undefined, issues?: StandardSchemaV12.FailureResult["issues"] | undefined);
286
285
  }
287
286
  declare class WorkflowRunNotFoundError extends WorkflowEngineError {
288
287
  constructor(runId?: string, workflowId?: string);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import { StandardSchemaV1 } from "@standard-schema/spec";
2
2
  type WorkflowRun = {
3
3
  id: string;
4
4
  createdAt: Date;
@@ -18,6 +18,7 @@ type WorkflowRun = {
18
18
  retryCount: number;
19
19
  maxRetries: number;
20
20
  jobId: string | null;
21
+ idempotencyKey: string | null;
21
22
  };
22
23
  type DurationObject = {
23
24
  weeks?: number;
@@ -44,8 +45,8 @@ declare enum StepType {
44
45
  DELAY = "delay",
45
46
  POLL = "poll"
46
47
  }
47
- type InputParameters = z.ZodTypeAny;
48
- type InferInputParameters<P extends InputParameters> = P extends z.ZodTypeAny ? z.infer<P> : never;
48
+ type InputParameters = StandardSchemaV1;
49
+ type InferInputParameters<P extends InputParameters> = StandardSchemaV1.InferOutput<P>;
49
50
  type WorkflowOptions<I extends InputParameters> = {
50
51
  timeout?: number;
51
52
  retries?: number;
@@ -109,12 +110,10 @@ type WorkflowContext<
109
110
  timeline: Record<string, unknown>;
110
111
  logger: WorkflowLogger;
111
112
  };
112
- type WorkflowDefinition<
113
- TInput extends InputParameters = InputParameters,
114
- TStep extends StepBaseContext = StepBaseContext
115
- > = {
113
+ type WorkflowDefinition<TInput extends InputParameters = InputParameters> = {
116
114
  id: string;
117
- handler: (context: WorkflowContext<TInput, TStep>) => Promise<unknown>;
115
+ /** Widest context avoids contravariance when collecting definitions; `workflow()` still types the handler narrowly. */
116
+ handler: (context: WorkflowContext<InputParameters, StepBaseContext>) => Promise<unknown>;
118
117
  inputSchema?: TInput;
119
118
  timeout?: number;
120
119
  retries?: number;
@@ -127,10 +126,7 @@ type StepInternalDefinition = {
127
126
  loop: boolean;
128
127
  isDynamic: boolean;
129
128
  };
130
- type WorkflowInternalDefinition<
131
- TInput extends InputParameters = InputParameters,
132
- TStep extends StepBaseContext = StepBaseContext
133
- > = WorkflowDefinition<TInput, TStep> & {
129
+ type WorkflowInternalDefinition<TInput extends InputParameters = InputParameters> = WorkflowDefinition<TInput> & {
134
130
  steps: StepInternalDefinition[];
135
131
  };
136
132
  /**
@@ -138,7 +134,7 @@ type WorkflowInternalDefinition<
138
134
  * TStepExt is the accumulated step extension from all plugins (step = StepContext & TStepExt).
139
135
  */
140
136
  interface WorkflowFactory<TStepExt = object> {
141
- (id: string, handler: (context: WorkflowContext<InputParameters, StepBaseContext & TStepExt>) => Promise<unknown>, options?: WorkflowOptions<InputParameters>): WorkflowDefinition<InputParameters, StepBaseContext & TStepExt>;
137
+ <I extends InputParameters = InputParameters>(id: string, handler: (context: WorkflowContext<I, StepBaseContext & TStepExt>) => Promise<unknown>, options?: WorkflowOptions<I>): WorkflowDefinition<I>;
142
138
  use<TNewExt>(plugin: WorkflowPlugin<StepBaseContext & TStepExt, TNewExt>): WorkflowFactory<TStepExt & TNewExt>;
143
139
  }
144
140
  type WorkflowRunProgress = WorkflowRun & {
@@ -186,13 +182,14 @@ declare class WorkflowEngine {
186
182
  batchSize?: number;
187
183
  }): Promise<void>;
188
184
  stop(): Promise<void>;
189
- registerWorkflow<TStep extends StepBaseContext>(definition: WorkflowDefinition<InputParameters, TStep>): Promise<WorkflowEngine>;
185
+ registerWorkflow(definition: WorkflowDefinition<InputParameters>): Promise<WorkflowEngine>;
190
186
  unregisterWorkflow(workflowId: string): Promise<WorkflowEngine>;
191
187
  unregisterAllWorkflows(): Promise<WorkflowEngine>;
192
- startWorkflow({ resourceId, workflowId, input, options }: {
188
+ startWorkflow({ resourceId, workflowId, input, idempotencyKey, options }: {
193
189
  resourceId?: string;
194
190
  workflowId: string;
195
191
  input: unknown;
192
+ idempotencyKey?: string;
196
193
  options?: {
197
194
  timeout?: number;
198
195
  retries?: number;
@@ -278,11 +275,13 @@ declare class WorkflowEngine {
278
275
  hasPrev: boolean;
279
276
  }>;
280
277
  }
278
+ import { StandardSchemaV1 as StandardSchemaV12 } from "@standard-schema/spec";
281
279
  declare class WorkflowEngineError extends Error {
282
280
  readonly workflowId?: string | undefined;
283
281
  readonly runId?: string | undefined;
284
282
  readonly cause: Error | undefined;
285
- constructor(message: string, workflowId?: string | undefined, runId?: string | undefined, cause?: Error | undefined);
283
+ readonly issues?: StandardSchemaV12.FailureResult["issues"] | undefined;
284
+ constructor(message: string, workflowId?: string | undefined, runId?: string | undefined, cause?: Error | undefined, issues?: StandardSchemaV12.FailureResult["issues"] | undefined);
286
285
  }
287
286
  declare class WorkflowRunNotFoundError extends WorkflowEngineError {
288
287
  constructor(runId?: string, workflowId?: string);