pg-workflows 0.0.1-claimed → 0.1.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sokratis Vidros
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,453 @@
1
+ # pg-workflows
2
+
3
+ **The simplest Postgres workflow engine for TypeScript.** Durable execution, event-driven orchestration, and automatic retries - powered entirely by PostgreSQL. No extra infrastructure. No vendor lock-in.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/pg-workflows.svg)](https://www.npmjs.com/package/pg-workflows)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
8
+ [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-%3E%3D10-336791.svg)](https://www.postgresql.org/)
9
+
10
+ ```bash
11
+ npm install pg-workflows
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Why pg-workflows?
17
+
18
+ Most workflow engines ask you to adopt an entirely new platform - a new runtime, a new deployment target, a new bill. **pg-workflows takes a different approach**: if you already have PostgreSQL, you already have everything you need.
19
+
20
+ | | pg-workflows | Temporal | Inngest | DBOS | pgflow |
21
+ |---|:---:|:---:|:---:|:---:|:---:|
22
+ | **Runs on your existing Postgres** | Yes | No | No | Partial | Supabase only |
23
+ | **Zero extra infrastructure** | Yes | No | No | No | No |
24
+ | **Framework-agnostic** | Yes | Yes | No | Yes | No |
25
+ | **Event-driven pause/resume** | Yes | Yes | Yes | No | No |
26
+ | **Open source** | MIT | MIT | ELv2 | MIT | Apache-2.0 |
27
+ | **TypeScript-first** | Yes | Via SDK | Yes | Via SDK | Yes |
28
+
29
+ ### When to use pg-workflows
30
+
31
+ - You already run **PostgreSQL** and want to add durable workflows without new services
32
+ - You need a **lightweight, self-hosted** workflow engine with zero operational overhead
33
+ - You want **event-driven orchestration** (pause, resume, wait for external signals)
34
+ - You're building with **TypeScript/Node.js** and want a native developer experience
35
+
36
+ ### When to consider alternatives
37
+
38
+ If you need enterprise-grade features like distributed tracing, complex DAG scheduling, or plan to scale to millions of concurrent workflows, consider [Temporal](https://temporal.io/), [Inngest](https://www.inngest.com/), [Trigger.dev](https://trigger.dev/), or [DBOS](https://www.dbos.dev/).
39
+
40
+ ---
41
+
42
+ ## Features
43
+
44
+ - **Durable Execution on Postgres** - Workflow state is persisted in PostgreSQL. Workflows survive process crashes, restarts, and deployments.
45
+ - **Step-by-Step Execution** - Break complex processes into discrete, resumable steps. Each step runs exactly once, even across retries.
46
+ - **Event-Driven Orchestration** - Pause workflows and wait for external events with `step.waitFor()`. Resume automatically when signals arrive.
47
+ - **Pause and Resume** - Manually pause long-running workflows and resume them later via API.
48
+ - **Built-in Retries** - Automatic retries with exponential backoff at the workflow level.
49
+ - **Configurable Timeouts** - Set workflow-level and step-level timeouts to prevent runaway executions.
50
+ - **Progress Tracking** - Monitor workflow completion percentage, completed steps, and total steps in real-time.
51
+ - **Input Validation** - Define schemas with Zod for type-safe, validated workflow inputs.
52
+ - **Built on pg-boss** - Leverages the battle-tested [pg-boss](https://github.com/timgit/pg-boss) job queue for reliable task scheduling.
53
+
54
+ ---
55
+
56
+ ## How It Works
57
+
58
+ pg-workflows uses PostgreSQL as both the **job queue** and the **state store**. Under the hood:
59
+
60
+ 1. **Define** workflows as TypeScript functions with discrete steps
61
+ 2. **Start** a workflow run - the engine creates a database record and enqueues the first execution
62
+ 3. **Execute** steps one by one - each step's result is persisted before moving to the next
63
+ 4. **Pause** on `waitFor()` or `pause()` - the workflow sleeps with zero resource consumption
64
+ 5. **Resume** when an external event arrives or `resumeWorkflow()` is called
65
+ 6. **Complete** - the final result is stored and the workflow is marked as done
66
+
67
+ All state lives in PostgreSQL. No Redis. No message broker. No external scheduler. Just Postgres.
68
+
69
+ ---
70
+
71
+ ## Quick Start
72
+
73
+ ### 1. Install
74
+
75
+ ```bash
76
+ npm install pg-workflows pg-boss
77
+ # or
78
+ yarn add pg-workflows pg-boss
79
+ # or
80
+ bun add pg-workflows pg-boss
81
+ ```
82
+
83
+ ### 2. Define a Workflow
84
+
85
+ ```typescript
86
+ import { WorkflowEngine, workflow } from 'pg-workflows';
87
+ import PgBoss from 'pg-boss';
88
+ import { z } from 'zod';
89
+
90
+ // Define a durable workflow
91
+ const sendWelcomeEmail = workflow(
92
+ 'send-welcome-email',
93
+ async ({ step, input }) => {
94
+ // Step 1: Create user record (runs exactly once)
95
+ const user = await step.run('create-user', async () => {
96
+ return { id: '123', email: input.email };
97
+ });
98
+
99
+ // Step 2: Send email (runs exactly once)
100
+ await step.run('send-email', async () => {
101
+ await sendEmail(user.email, 'Welcome!');
102
+ });
103
+
104
+ // Step 3: Wait for user confirmation (pauses the workflow)
105
+ const confirmation = await step.waitFor('wait-confirmation', {
106
+ eventName: 'user-confirmed',
107
+ timeout: 24 * 60 * 60 * 1000, // 24 hours
108
+ });
109
+
110
+ return { success: true, user, confirmation };
111
+ },
112
+ {
113
+ inputSchema: z.object({
114
+ email: z.string().email(),
115
+ }),
116
+ timeout: 48 * 60 * 60 * 1000, // 48 hours
117
+ retries: 3,
118
+ }
119
+ );
120
+ ```
121
+
122
+ ### 3. Start the Engine
123
+
124
+ ```typescript
125
+ const boss = new PgBoss({
126
+ connectionString: process.env.DATABASE_URL,
127
+ });
128
+
129
+ const engine = new WorkflowEngine({
130
+ boss,
131
+ workflows: [sendWelcomeEmail],
132
+ });
133
+
134
+ await engine.start();
135
+ ```
136
+
137
+ ### 4. Run Workflows
138
+
139
+ ```typescript
140
+ // Start a workflow run
141
+ const run = await engine.startWorkflow({
142
+ workflowId: 'send-welcome-email',
143
+ resourceId: 'user-123',
144
+ input: { email: 'user@example.com' },
145
+ });
146
+
147
+ // Send an event to resume the waiting workflow
148
+ await engine.triggerEvent({
149
+ runId: run.id,
150
+ resourceId: 'user-123',
151
+ eventName: 'user-confirmed',
152
+ data: { confirmedAt: new Date() },
153
+ });
154
+
155
+ // Check progress
156
+ const progress = await engine.checkProgress({
157
+ runId: run.id,
158
+ resourceId: 'user-123',
159
+ });
160
+
161
+ console.log(`Progress: ${progress.completionPercentage}%`);
162
+ ```
163
+
164
+ ---
165
+
166
+ ## What Can You Build?
167
+
168
+ - **User Onboarding Flows** - Multi-step signup sequences with email verification, waiting for user actions, and conditional paths.
169
+ - **Payment & Checkout Pipelines** - Durable payment processing that survives failures, with automatic retries and event-driven confirmations.
170
+ - **AI & LLM Pipelines** - Chain LLM calls with built-in retries for flaky APIs. Persist intermediate results across steps.
171
+ - **Background Job Orchestration** - Replace fragile cron jobs with durable, observable workflows that track progress.
172
+ - **Approval Workflows** - Pause execution and wait for human approval events before proceeding.
173
+ - **Data Processing Pipelines** - ETL workflows with step-by-step execution, error handling, and progress monitoring.
174
+
175
+ ---
176
+
177
+ ## Core Concepts
178
+
179
+ ### Workflows
180
+
181
+ A workflow is a durable function that breaks complex operations into discrete, resumable steps. Define workflows using the `workflow()` function:
182
+
183
+ ```typescript
184
+ const myWorkflow = workflow(
185
+ 'workflow-id',
186
+ async ({ step, input }) => {
187
+ // Your workflow logic here
188
+ },
189
+ {
190
+ inputSchema: z.object({ /* ... */ }),
191
+ timeout: 60000, // milliseconds
192
+ retries: 3,
193
+ }
194
+ );
195
+ ```
196
+
197
+ ### Steps
198
+
199
+ Steps are the building blocks of durable workflows. Each step is executed **exactly once**, even if the workflow is retried:
200
+
201
+ ```typescript
202
+ await step.run('step-id', async () => {
203
+ // This will only execute once - the result is persisted in Postgres
204
+ return { result: 'data' };
205
+ });
206
+ ```
207
+
208
+ ### Event-Driven Workflows
209
+
210
+ Wait for external events to pause and resume workflows without consuming resources:
211
+
212
+ ```typescript
213
+ const eventData = await step.waitFor('wait-step', {
214
+ eventName: 'payment-completed',
215
+ timeout: 5 * 60 * 1000, // 5 minutes
216
+ });
217
+ ```
218
+
219
+ ### Resource ID
220
+
221
+ The optional `resourceId` associates a workflow run with an external entity in your application — a user, an order, a subscription, or any domain object the workflow operates on. It serves two purposes:
222
+
223
+ 1. **Association** — Links each workflow run to the business entity it belongs to, so you can query all runs for a given resource.
224
+ 2. **Scoping** — When provided, all read and write operations (get, update, pause, resume, cancel, trigger events) include `resource_id` in their database queries, ensuring you only access workflow runs that belong to that resource. This is useful for enforcing tenant isolation or ownership checks.
225
+
226
+ `resourceId` is optional on every API method. If you don't need to group or scope runs by an external entity, you can omit it entirely and use `runId` alone.
227
+
228
+ ```typescript
229
+ // Start a workflow scoped to a specific user
230
+ const run = await engine.startWorkflow({
231
+ workflowId: 'send-welcome-email',
232
+ resourceId: 'user-123', // ties this run to user-123
233
+ input: { email: 'user@example.com' },
234
+ });
235
+
236
+ // Later, list all workflow runs for that user
237
+ const { items } = await engine.getRuns({
238
+ resourceId: 'user-123',
239
+ });
240
+ ```
241
+
242
+ ### Pause and Resume
243
+
244
+ Manually pause a workflow and resume it later:
245
+
246
+ ```typescript
247
+ // Pause inside a workflow
248
+ await step.pause('pause-step');
249
+
250
+ // Resume from outside the workflow
251
+ await engine.resumeWorkflow({
252
+ runId: run.id,
253
+ resourceId: 'resource-123',
254
+ });
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Examples
260
+
261
+ ### Conditional Steps
262
+
263
+ ```typescript
264
+ const conditionalWorkflow = workflow('conditional', async ({ step }) => {
265
+ const data = await step.run('fetch-data', async () => {
266
+ return { isPremium: true };
267
+ });
268
+
269
+ if (data.isPremium) {
270
+ await step.run('premium-action', async () => {
271
+ // Only runs for premium users
272
+ });
273
+ }
274
+ });
275
+ ```
276
+
277
+ ### Batch Processing with Loops
278
+
279
+ ```typescript
280
+ const batchWorkflow = workflow('batch-process', async ({ step }) => {
281
+ const items = await step.run('get-items', async () => {
282
+ return [1, 2, 3, 4, 5];
283
+ });
284
+
285
+ for (const item of items) {
286
+ await step.run(`process-${item}`, async () => {
287
+ // Each item is processed durably
288
+ return processItem(item);
289
+ });
290
+ }
291
+ });
292
+ ```
293
+
294
+ ### Error Handling with Retries
295
+
296
+ ```typescript
297
+ const resilientWorkflow = workflow('resilient', async ({ step }) => {
298
+ await step.run('risky-operation', async () => {
299
+ // Retries up to 3 times with exponential backoff
300
+ return await riskyApiCall();
301
+ });
302
+ }, {
303
+ retries: 3,
304
+ timeout: 60000,
305
+ });
306
+ ```
307
+
308
+ ### Monitoring Workflow Progress
309
+
310
+ ```typescript
311
+ const progress = await engine.checkProgress({
312
+ runId: run.id,
313
+ resourceId: 'resource-123',
314
+ });
315
+
316
+ console.log({
317
+ status: progress.status,
318
+ completionPercentage: progress.completionPercentage,
319
+ completedSteps: progress.completedSteps,
320
+ totalSteps: progress.totalSteps,
321
+ });
322
+ ```
323
+
324
+ ---
325
+
326
+ ## API Reference
327
+
328
+ ### WorkflowEngine
329
+
330
+ #### Constructor
331
+
332
+ ```typescript
333
+ const engine = new WorkflowEngine({
334
+ boss: PgBoss, // Required: pg-boss instance
335
+ workflows: WorkflowDefinition[], // Optional: register workflows on init
336
+ logger: WorkflowLogger, // Optional: custom logger
337
+ });
338
+ ```
339
+
340
+ #### Methods
341
+
342
+ | Method | Description |
343
+ |--------|-------------|
344
+ | `start(asEngine?, options?)` | Start the engine and workers |
345
+ | `stop()` | Stop the engine gracefully |
346
+ | `registerWorkflow(definition)` | Register a workflow definition |
347
+ | `startWorkflow({ workflowId, resourceId?, input, options? })` | Start a new workflow run. `resourceId` optionally ties the run to an external entity (see [Resource ID](#resource-id)). |
348
+ | `pauseWorkflow({ runId, resourceId? })` | Pause a running workflow |
349
+ | `resumeWorkflow({ runId, resourceId?, options? })` | Resume a paused workflow |
350
+ | `cancelWorkflow({ runId, resourceId? })` | Cancel a workflow |
351
+ | `triggerEvent({ runId, resourceId?, eventName, data?, options? })` | Send an event to a workflow |
352
+ | `getRun({ runId, resourceId? })` | Get workflow run details |
353
+ | `checkProgress({ runId, resourceId? })` | Get workflow progress |
354
+ | `getRuns(filters)` | List workflow runs with pagination |
355
+
356
+ ### workflow()
357
+
358
+ ```typescript
359
+ workflow<I extends Parameters>(
360
+ id: string,
361
+ handler: (context: WorkflowContext) => Promise<unknown>,
362
+ options?: {
363
+ inputSchema?: I,
364
+ timeout?: number,
365
+ retries?: number,
366
+ }
367
+ ): WorkflowDefinition<I>
368
+ ```
369
+
370
+ ### WorkflowContext
371
+
372
+ The context object passed to workflow handlers:
373
+
374
+ ```typescript
375
+ {
376
+ input: T, // Validated input data
377
+ workflowId: string, // Workflow ID
378
+ runId: string, // Unique run ID
379
+ timeline: Record<string, unknown>, // Step execution history
380
+ logger: WorkflowLogger, // Logger instance
381
+ step: {
382
+ run: <T>(stepId, handler) => Promise<T>,
383
+ waitFor: <T>(stepId, { eventName, timeout?, schema? }) => Promise<T>,
384
+ waitUntil: (stepId, { date }) => Promise<void>,
385
+ pause: (stepId) => Promise<void>,
386
+ }
387
+ }
388
+ ```
389
+
390
+ ### WorkflowStatus
391
+
392
+ ```typescript
393
+ enum WorkflowStatus {
394
+ PENDING = 'pending',
395
+ RUNNING = 'running',
396
+ PAUSED = 'paused',
397
+ COMPLETED = 'completed',
398
+ FAILED = 'failed',
399
+ CANCELLED = 'cancelled',
400
+ }
401
+ ```
402
+
403
+ ---
404
+
405
+ ## Configuration
406
+
407
+ ### Environment Variables
408
+
409
+ | Variable | Description | Default |
410
+ |----------|-------------|---------|
411
+ | `DATABASE_URL` | PostgreSQL connection string | *required* |
412
+ | `WORKFLOW_RUN_WORKERS` | Number of worker processes | `3` |
413
+ | `WORKFLOW_RUN_EXPIRE_IN_SECONDS` | Job expiration time in seconds | `300` |
414
+
415
+ ### Database Setup
416
+
417
+ The engine automatically runs migrations on startup to create the required tables:
418
+
419
+ - `workflow_runs` - Stores workflow execution state, step results, and timeline. The optional `resource_id` column (indexed) associates each run with an external entity in your application. See [Resource ID](#resource-id).
420
+ - `pgboss.*` - pg-boss job queue tables for reliable task scheduling
421
+
422
+ ---
423
+
424
+ ## The PostgreSQL-for-Everything Philosophy
425
+
426
+ As championed by [postgresforeverything.com](https://postgresforeverything.com/), PostgreSQL is one of the most reliable, feature-rich, and cost-effective databases ever built. pg-workflows embraces this philosophy:
427
+
428
+ - **One database to rule them all** - Your application data and workflow state live in the same PostgreSQL instance. No distributed systems headaches.
429
+ - **Battle-tested reliability** - PostgreSQL's ACID transactions guarantee your workflow state is always consistent.
430
+ - **Zero operational overhead** - No Redis cluster to manage. No message broker to monitor. No external service to pay for.
431
+ - **Full queryability** - Inspect, debug, and analyze workflow runs with plain SQL.
432
+
433
+ If you're already running Postgres (and you probably should be), adding durable workflows is as simple as:
434
+
435
+ ```bash
436
+ npm install pg-workflows
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Requirements
442
+
443
+ - Node.js >= 18.0.0
444
+ - PostgreSQL >= 10
445
+ - pg-boss >= 10.0.0
446
+
447
+ ## Acknowledgments
448
+
449
+ Special thanks to the teams behind [Temporal](https://temporal.io/), [Inngest](https://www.inngest.com/), [Trigger.dev](https://trigger.dev/), and [DBOS](https://www.dbos.dev/) for pioneering durable execution patterns and inspiring this project.
450
+
451
+ ## License
452
+
453
+ MIT