pg-workflows 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -765
- package/dist/client.entry.cjs +822 -0
- package/dist/client.entry.d.cts +227 -0
- package/dist/client.entry.d.ts +227 -0
- package/dist/client.entry.js +13 -0
- package/dist/client.entry.js.map +16 -0
- package/dist/index.cjs +710 -356
- package/dist/index.d.cts +123 -11
- package/dist/index.d.ts +123 -11
- package/dist/index.js +75 -450
- package/dist/index.js.map +12 -10
- package/dist/shared/chunk-8n9chg7z.js +753 -0
- package/dist/shared/chunk-8n9chg7z.js.map +16 -0
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pg-workflows
|
|
2
2
|
|
|
3
|
-
**The simplest Postgres workflow engine for TypeScript.** Durable execution, event-driven orchestration, and automatic retries - powered entirely by PostgreSQL. No
|
|
3
|
+
**The simplest Postgres workflow engine for TypeScript.** Durable execution, event-driven orchestration, and automatic retries - powered entirely by PostgreSQL. No Redis, no message broker, no new infrastructure.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/pg-workflows)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -13,836 +13,154 @@ npm install pg-workflows pg
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## A complete workflow
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 **AI agents or LLM pipelines** that need durable execution, retries, and human-in-the-loop
|
|
35
|
-
- You're building with **TypeScript/Node.js** and want a native developer experience
|
|
18
|
+
```typescript
|
|
19
|
+
import { WorkflowEngine, workflow } from 'pg-workflows'
|
|
20
|
+
import { z } from 'zod'
|
|
36
21
|
|
|
37
|
-
|
|
22
|
+
const onboardUser = workflow(
|
|
23
|
+
'onboard-user',
|
|
24
|
+
async ({ step, input }) => {
|
|
25
|
+
const user = await step.run('create-account', () => db.users.create(input))
|
|
26
|
+
await step.run('send-welcome', () => sendEmail(user.email, 'Welcome!'))
|
|
27
|
+
return { userId: user.id }
|
|
28
|
+
},
|
|
29
|
+
{ inputSchema: z.object({ email: z.string().email() }) },
|
|
30
|
+
)
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
const engine = new WorkflowEngine({
|
|
33
|
+
connectionString: process.env.DATABASE_URL,
|
|
34
|
+
workflows: [onboardUser],
|
|
35
|
+
})
|
|
36
|
+
await engine.start()
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
await engine.startWorkflow({
|
|
39
|
+
workflowId: 'onboard-user',
|
|
40
|
+
input: { email: 'alice@example.com' },
|
|
41
|
+
})
|
|
42
|
+
```
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- **Durable Execution on Postgres** - Workflow state is persisted in PostgreSQL. Workflows survive process crashes, restarts, and deployments.
|
|
46
|
-
- **Step-by-Step Execution** - Break complex processes into discrete, resumable steps. Each step runs exactly once, even across retries.
|
|
47
|
-
- **Event-Driven Orchestration** - Pause workflows and wait for external events with `step.waitFor()`. Resume automatically when signals arrive.
|
|
48
|
-
- **Polling Steps** - Repeatedly check a condition with `step.poll()` at a configurable interval (minimum 30s) until it returns a truthy value or a timeout expires.
|
|
49
|
-
- **Scheduled & Delay Steps** - Wait until a specific date with `step.waitUntil()`, or use `step.delay()` / `step.sleep()` with human-readable durations (`'3 days'`, `{ hours: 2 }`). Past dates run immediately.
|
|
50
|
-
- **Pause and Resume** - Manually pause long-running workflows and resume them later via API.
|
|
51
|
-
- **Built-in Retries** - Automatic retries with exponential backoff at the workflow level.
|
|
52
|
-
- **Configurable Timeouts** - Set workflow-level and step-level timeouts to prevent runaway executions.
|
|
53
|
-
- **Progress Tracking** - Monitor workflow completion percentage, completed steps, and total steps in real-time.
|
|
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.
|
|
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.
|
|
44
|
+
That's it. Each step runs **exactly once**. Crash, redeploy, or retry - the workflow resumes from where it left off. State lives in your existing PostgreSQL database.
|
|
57
45
|
|
|
58
46
|
---
|
|
59
47
|
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
pg-workflows uses PostgreSQL as both the **job queue** and the **state store**. Under the hood:
|
|
48
|
+
## Why pg-workflows
|
|
63
49
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
50
|
+
- **Zero new infrastructure** - if you have Postgres, you're done. No Redis, no Temporal server, no SaaS bill.
|
|
51
|
+
- **Feels like plain TypeScript** - workflows are async functions. No DSL, no YAML, no DAG builder.
|
|
52
|
+
- **Durable by default** - step results are persisted. Process crashes don't lose work or repeat expensive calls.
|
|
53
|
+
- **Pause, wait, resume** - `step.waitFor('event-name')` pauses the workflow until your API fires an event. Zero resources consumed while waiting.
|
|
54
|
+
- **Schedules & polling built in** - `step.delay('3 days')`, `step.waitUntil('2025-01-01')`, `step.poll(...)` - no cron, no external scheduler.
|
|
55
|
+
- **Built for AI agents** - cache expensive LLM calls, retry on 429s, pause for human review. [See AI patterns →](docs/ai-agents.md)
|
|
56
|
+
- **Client/worker separation** - keep your API service light; run handlers in a worker. [See architecture →](docs/architecture.md)
|
|
57
|
+
- **Idempotent starts** - pass an `idempotencyKey` and duplicate calls safely return the same run.
|
|
72
58
|
|
|
73
59
|
---
|
|
74
60
|
|
|
75
|
-
## Quick
|
|
61
|
+
## Quick start
|
|
76
62
|
|
|
77
63
|
### 1. Install
|
|
78
64
|
|
|
79
65
|
```bash
|
|
80
66
|
npm install pg-workflows pg
|
|
81
|
-
# or
|
|
82
|
-
yarn add pg-workflows pg
|
|
83
|
-
# or
|
|
84
|
-
bun add pg-workflows pg
|
|
85
67
|
```
|
|
86
68
|
|
|
87
|
-
> `pg` is a peer dependency -
|
|
69
|
+
> `pg` is a peer dependency. `pg-boss` is bundled - nothing else to configure. The engine runs migrations automatically on start.
|
|
88
70
|
|
|
89
|
-
### 2. Define a
|
|
71
|
+
### 2. Define a workflow
|
|
90
72
|
|
|
91
73
|
```typescript
|
|
92
|
-
import {
|
|
93
|
-
import { z } from 'zod'
|
|
74
|
+
import { workflow } from 'pg-workflows'
|
|
75
|
+
import { z } from 'zod'
|
|
94
76
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'send-welcome-email',
|
|
77
|
+
export const sendWelcome = workflow(
|
|
78
|
+
'send-welcome',
|
|
98
79
|
async ({ step, input }) => {
|
|
99
|
-
// Step 1: Create user record (runs exactly once)
|
|
100
80
|
const user = await step.run('create-user', async () => {
|
|
101
|
-
return { id: '123', email: input.email }
|
|
102
|
-
})
|
|
81
|
+
return { id: '123', email: input.email }
|
|
82
|
+
})
|
|
103
83
|
|
|
104
|
-
// Step 2: Send email (runs exactly once)
|
|
105
84
|
await step.run('send-email', async () => {
|
|
106
|
-
await sendEmail(user.email, 'Welcome!')
|
|
107
|
-
})
|
|
85
|
+
await sendEmail(user.email, 'Welcome!')
|
|
86
|
+
})
|
|
108
87
|
|
|
109
|
-
//
|
|
110
|
-
const confirmation = await step.waitFor('
|
|
88
|
+
// Pause until your API confirms the user. Zero cost while waiting.
|
|
89
|
+
const confirmation = await step.waitFor('await-confirmation', {
|
|
111
90
|
eventName: 'user-confirmed',
|
|
112
91
|
timeout: 24 * 60 * 60 * 1000, // 24 hours
|
|
113
|
-
})
|
|
92
|
+
})
|
|
114
93
|
|
|
115
|
-
return { success: true, user, confirmation }
|
|
94
|
+
return { success: true, user, confirmation }
|
|
116
95
|
},
|
|
117
96
|
{
|
|
118
|
-
inputSchema: z.object({
|
|
119
|
-
email: z.string().email(),
|
|
120
|
-
}),
|
|
121
|
-
timeout: 48 * 60 * 60 * 1000, // 48 hours
|
|
97
|
+
inputSchema: z.object({ email: z.string().email() }),
|
|
122
98
|
retries: 3,
|
|
123
|
-
}
|
|
124
|
-
)
|
|
99
|
+
},
|
|
100
|
+
)
|
|
125
101
|
```
|
|
126
102
|
|
|
127
|
-
### 3. Start the
|
|
103
|
+
### 3. Start the engine and run it
|
|
128
104
|
|
|
129
105
|
```typescript
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
connectionString: process.env.DATABASE_URL,
|
|
133
|
-
workflows: [sendWelcomeEmail],
|
|
134
|
-
});
|
|
106
|
+
import { WorkflowEngine } from 'pg-workflows'
|
|
107
|
+
import { sendWelcome } from './workflows'
|
|
135
108
|
|
|
136
|
-
// Option B: Bring your own pg.Pool
|
|
137
|
-
import pg from 'pg';
|
|
138
|
-
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
|
|
139
109
|
const engine = new WorkflowEngine({
|
|
140
|
-
|
|
141
|
-
workflows: [
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
await engine.start();
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### 4. Run Workflows
|
|
110
|
+
connectionString: process.env.DATABASE_URL,
|
|
111
|
+
workflows: [sendWelcome],
|
|
112
|
+
})
|
|
113
|
+
await engine.start()
|
|
148
114
|
|
|
149
|
-
```typescript
|
|
150
|
-
// Start a workflow run
|
|
151
115
|
const run = await engine.startWorkflow({
|
|
152
|
-
workflowId: 'send-welcome
|
|
153
|
-
resourceId: 'user-123',
|
|
116
|
+
workflowId: 'send-welcome',
|
|
154
117
|
input: { email: 'user@example.com' },
|
|
155
|
-
|
|
156
|
-
idempotencyKey: 'welcome:user-123',
|
|
157
|
-
});
|
|
118
|
+
})
|
|
158
119
|
|
|
159
|
-
//
|
|
120
|
+
// Later - resume the workflow with an event:
|
|
160
121
|
await engine.triggerEvent({
|
|
161
122
|
runId: run.id,
|
|
162
|
-
resourceId: 'user-123',
|
|
163
123
|
eventName: 'user-confirmed',
|
|
164
124
|
data: { confirmedAt: new Date() },
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// Check progress
|
|
168
|
-
const progress = await engine.checkProgress({
|
|
169
|
-
runId: run.id,
|
|
170
|
-
resourceId: 'user-123',
|
|
171
|
-
});
|
|
125
|
+
})
|
|
172
126
|
|
|
173
|
-
|
|
127
|
+
// Track progress anytime:
|
|
128
|
+
const progress = await engine.checkProgress({ runId: run.id })
|
|
129
|
+
console.log(`${progress.completionPercentage}% complete`)
|
|
174
130
|
```
|
|
175
131
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
## What Can You Build?
|
|
179
|
-
|
|
180
|
-
- **AI Agents & LLM Pipelines** - Build durable AI agents that survive crashes, retry on flaky LLM APIs, and pause for human-in-the-loop review. [See examples below.](#ai--agent-workflows)
|
|
181
|
-
- **User Onboarding Flows** - Multi-step signup sequences with email verification, waiting for user actions, and conditional paths.
|
|
182
|
-
- **Payment & Checkout Pipelines** - Durable payment processing that survives failures, with automatic retries and event-driven confirmations.
|
|
183
|
-
- **Background Job Orchestration** - Replace fragile cron jobs with durable, observable workflows that track progress.
|
|
184
|
-
- **Approval Workflows** - Pause execution and wait for human approval events before proceeding.
|
|
185
|
-
- **Data Processing Pipelines** - ETL workflows with step-by-step execution, error handling, and progress monitoring.
|
|
132
|
+
That's the whole loop. No extra services. Everything durable. Everything queryable with plain SQL.
|
|
186
133
|
|
|
187
134
|
---
|
|
188
135
|
|
|
189
|
-
##
|
|
136
|
+
## What can you build?
|
|
190
137
|
|
|
191
|
-
AI agents
|
|
138
|
+
- **AI agents & LLM pipelines** - durable multi-step agents, cached expensive calls, human-in-the-loop
|
|
139
|
+
- **User onboarding** - signup flows with email verification and conditional paths
|
|
140
|
+
- **Payment & checkout** - retry-safe payment processing with event-driven confirmations
|
|
141
|
+
- **Background job orchestration** - replace fragile cron jobs with observable workflows
|
|
142
|
+
- **Approval workflows** - pause indefinitely until a human reviews
|
|
143
|
+
- **Data pipelines** - ETL with step-by-step durability and progress tracking
|
|
192
144
|
|
|
193
|
-
|
|
194
|
-
- **Automatic retries** - LLM APIs return 429s and 500s. Built-in exponential backoff handles transient failures without custom retry logic.
|
|
195
|
-
- **Human-in-the-loop** - Pause an AI pipeline with `step.waitFor()` to wait for human review, approval, or feedback before continuing.
|
|
196
|
-
- **Observable progress** - Track which step your agent is on, how far along it is, and inspect intermediate results with `checkProgress()`.
|
|
197
|
-
- **Long-running agents** - Multi-step agents that run for minutes or hours don't need to hold a connection open. They persist state and resume.
|
|
198
|
-
|
|
199
|
-
### Example: Multi-Step AI Agent
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
const researchAgent = workflow(
|
|
203
|
-
'research-agent',
|
|
204
|
-
async ({ step, input }) => {
|
|
205
|
-
// Step 1: Plan the research (persisted - never re-runs on retry)
|
|
206
|
-
const plan = await step.run('create-plan', async () => {
|
|
207
|
-
return await llm.chat({
|
|
208
|
-
model: 'gpt-4o',
|
|
209
|
-
messages: [{ role: 'user', content: `Create a research plan for: ${input.topic}` }],
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Step 2: Execute each research task durably
|
|
214
|
-
const findings = [];
|
|
215
|
-
for (const task of plan.tasks) {
|
|
216
|
-
const result = await step.run(`research-${task.id}`, async () => {
|
|
217
|
-
return await llm.chat({
|
|
218
|
-
model: 'gpt-4o',
|
|
219
|
-
messages: [{ role: 'user', content: `Research: ${task.description}` }],
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
findings.push(result);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Step 3: Synthesize results
|
|
226
|
-
const report = await step.run('synthesize', async () => {
|
|
227
|
-
return await llm.chat({
|
|
228
|
-
model: 'gpt-4o',
|
|
229
|
-
messages: [{ role: 'user', content: `Synthesize these findings: ${JSON.stringify(findings)}` }],
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
return { plan, findings, report };
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
retries: 3,
|
|
237
|
-
timeout: 30 * 60 * 1000, // 30 minutes
|
|
238
|
-
}
|
|
239
|
-
);
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
If the process crashes after completing 3 of 5 research tasks, the agent **resumes from task 4** - no LLM calls are wasted.
|
|
243
|
-
|
|
244
|
-
### Example: Human-in-the-Loop AI Pipeline
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
const contentPipeline = workflow(
|
|
248
|
-
'ai-content-pipeline',
|
|
249
|
-
async ({ step, input }) => {
|
|
250
|
-
// Step 1: Generate draft with AI
|
|
251
|
-
const draft = await step.run('generate-draft', async () => {
|
|
252
|
-
return await llm.chat({
|
|
253
|
-
model: 'gpt-4o',
|
|
254
|
-
messages: [{ role: 'user', content: `Write a blog post about: ${input.topic}` }],
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// Step 2: Pause for human review - costs nothing while waiting
|
|
259
|
-
const review = await step.waitFor('human-review', {
|
|
260
|
-
eventName: 'content-reviewed',
|
|
261
|
-
timeout: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// Step 3: Revise based on feedback
|
|
265
|
-
if (review.approved) {
|
|
266
|
-
return { status: 'published', content: draft };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const revision = await step.run('revise-draft', async () => {
|
|
270
|
-
return await llm.chat({
|
|
271
|
-
model: 'gpt-4o',
|
|
272
|
-
messages: [
|
|
273
|
-
{ role: 'user', content: `Revise this draft based on feedback:\n\nDraft: ${draft}\n\nFeedback: ${review.feedback}` },
|
|
274
|
-
],
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
return { status: 'revised', content: revision };
|
|
279
|
-
},
|
|
280
|
-
{ retries: 3 }
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
// A reviewer approves or requests changes via your API
|
|
284
|
-
await engine.triggerEvent({
|
|
285
|
-
runId: run.id,
|
|
286
|
-
eventName: 'content-reviewed',
|
|
287
|
-
data: { approved: false, feedback: 'Make the intro more engaging' },
|
|
288
|
-
});
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### Example: RAG Pipeline with Tool Use
|
|
292
|
-
|
|
293
|
-
```typescript
|
|
294
|
-
const ragAgent = workflow(
|
|
295
|
-
'rag-agent',
|
|
296
|
-
async ({ step, input }) => {
|
|
297
|
-
// Step 1: Generate embeddings (cached on retry)
|
|
298
|
-
const embedding = await step.run('embed-query', async () => {
|
|
299
|
-
return await openai.embeddings.create({
|
|
300
|
-
model: 'text-embedding-3-small',
|
|
301
|
-
input: input.query,
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// Step 2: Search vector store
|
|
306
|
-
const documents = await step.run('search-docs', async () => {
|
|
307
|
-
return await vectorStore.search(embedding, { topK: 10 });
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// Step 3: Generate answer with context
|
|
311
|
-
const answer = await step.run('generate-answer', async () => {
|
|
312
|
-
return await llm.chat({
|
|
313
|
-
model: 'gpt-4o',
|
|
314
|
-
messages: [
|
|
315
|
-
{ role: 'system', content: `Answer using these documents:\n${documents.map(d => d.text).join('\n')}` },
|
|
316
|
-
{ role: 'user', content: input.query },
|
|
317
|
-
],
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// Step 4: Validate and fact-check
|
|
322
|
-
const validation = await step.run('fact-check', async () => {
|
|
323
|
-
return await llm.chat({
|
|
324
|
-
model: 'gpt-4o',
|
|
325
|
-
messages: [
|
|
326
|
-
{ role: 'user', content: `Fact-check this answer against the source documents. Answer: ${answer}` },
|
|
327
|
-
],
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
return { answer, validation, sources: documents };
|
|
332
|
-
},
|
|
333
|
-
{ retries: 3, timeout: 5 * 60 * 1000 }
|
|
334
|
-
);
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Why Durable Execution Matters for AI
|
|
338
|
-
|
|
339
|
-
| Problem | Without pg-workflows | With pg-workflows |
|
|
340
|
-
|---------|---------------------|-------------------|
|
|
341
|
-
| Process crashes mid-pipeline | All LLM calls re-run from scratch | Resumes from the last completed step |
|
|
342
|
-
| LLM API returns 429/500 | Manual retry logic everywhere | Automatic retries with exponential backoff |
|
|
343
|
-
| Human review needed | Custom polling/webhook infrastructure | `step.waitFor()` - zero resource consumption while waiting |
|
|
344
|
-
| Debugging failed agents | Lost intermediate state | Full timeline of every step's input/output in PostgreSQL |
|
|
345
|
-
| Cost control | Repeated expensive LLM calls on failure | Each LLM call runs exactly once, result cached |
|
|
346
|
-
| Long-running pipelines | Timeout or lost connections | Runs for hours/days, state persisted in Postgres |
|
|
145
|
+
See [runnable examples](https://github.com/SokratisVidros/pg-workflows/tree/main/examples) and [common patterns →](docs/examples.md)
|
|
347
146
|
|
|
348
147
|
---
|
|
349
148
|
|
|
350
|
-
##
|
|
351
|
-
|
|
352
|
-
### Workflows
|
|
353
|
-
|
|
354
|
-
A workflow is a durable function that breaks complex operations into discrete, resumable steps. Define workflows using the `workflow()` function:
|
|
355
|
-
|
|
356
|
-
```typescript
|
|
357
|
-
const myWorkflow = workflow(
|
|
358
|
-
'workflow-id',
|
|
359
|
-
async ({ step, input }) => {
|
|
360
|
-
// Your workflow logic here
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
inputSchema: mySchema, // any Standard Schema-compliant schema
|
|
364
|
-
timeout: 60000, // milliseconds
|
|
365
|
-
retries: 3,
|
|
366
|
-
}
|
|
367
|
-
);
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
### Steps
|
|
371
|
-
|
|
372
|
-
Steps are the building blocks of durable workflows. Each step is executed **exactly once**, even if the workflow is retried:
|
|
373
|
-
|
|
374
|
-
```typescript
|
|
375
|
-
await step.run('step-id', async () => {
|
|
376
|
-
// This will only execute once - the result is persisted in Postgres
|
|
377
|
-
return { result: 'data' };
|
|
378
|
-
});
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
### Event-Driven Workflows
|
|
382
|
-
|
|
383
|
-
Wait for external events to pause and resume workflows without consuming resources:
|
|
384
|
-
|
|
385
|
-
```typescript
|
|
386
|
-
const eventData = await step.waitFor('wait-step', {
|
|
387
|
-
eventName: 'payment-completed',
|
|
388
|
-
timeout: 5 * 60 * 1000, // 5 minutes
|
|
389
|
-
});
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Scheduled and Delay Steps
|
|
393
|
-
|
|
394
|
-
Wait until a specific time, or delay for a duration (sugar over `waitUntil`). If the date is in the past, the step runs immediately.
|
|
395
|
-
|
|
396
|
-
```typescript
|
|
397
|
-
// Wait until a specific date (Date, ISO string, or { date })
|
|
398
|
-
await step.waitUntil('scheduled-step', new Date('2025-06-01'));
|
|
399
|
-
await step.waitUntil('scheduled-step', '2025-06-01T12:00:00.000Z');
|
|
400
|
-
await step.waitUntil('scheduled-step', { date: new Date('2025-06-01') });
|
|
401
|
-
|
|
402
|
-
// Delay for a duration (string or object). sleep is an alias of delay.
|
|
403
|
-
await step.delay('cool-off', '3 days');
|
|
404
|
-
await step.delay('cool-off', { days: 3 });
|
|
405
|
-
await step.delay('ramp-up', '2 days 12 hours');
|
|
406
|
-
await step.sleep('backoff', '1 hour');
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### Resource ID
|
|
410
|
-
|
|
411
|
-
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:
|
|
412
|
-
|
|
413
|
-
1. **Association** - Links each workflow run to the business entity it belongs to, so you can query all runs for a given resource.
|
|
414
|
-
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.
|
|
415
|
-
|
|
416
|
-
`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.
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
// Start a workflow scoped to a specific user
|
|
420
|
-
const run = await engine.startWorkflow({
|
|
421
|
-
workflowId: 'send-welcome-email',
|
|
422
|
-
resourceId: 'user-123', // ties this run to user-123
|
|
423
|
-
input: { email: 'user@example.com' },
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
// Later, list all workflow runs for that user
|
|
427
|
-
const { items } = await engine.getRuns({
|
|
428
|
-
resourceId: 'user-123',
|
|
429
|
-
});
|
|
430
|
-
```
|
|
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
|
-
|
|
455
|
-
### Pause and Resume
|
|
456
|
-
|
|
457
|
-
Manually pause a workflow and resume it later:
|
|
458
|
-
|
|
459
|
-
```typescript
|
|
460
|
-
// Pause inside a workflow
|
|
461
|
-
await step.pause('pause-step');
|
|
462
|
-
|
|
463
|
-
// Resume from outside the workflow
|
|
464
|
-
await engine.resumeWorkflow({
|
|
465
|
-
runId: run.id,
|
|
466
|
-
resourceId: 'resource-123',
|
|
467
|
-
});
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
### Fast-Forward
|
|
471
|
-
|
|
472
|
-
Skip the current waiting step and immediately resume execution. `fastForwardWorkflow` inspects the paused step and dispatches the right internal action — `triggerEvent` for `waitFor`, timeout triggers for `delay`/`waitUntil`, resume for `pause`, and direct output writes for `poll`. If the workflow is not paused, it's a no-op.
|
|
473
|
-
|
|
474
|
-
This is useful for testing, debugging, or manually advancing workflows past long waits.
|
|
475
|
-
|
|
476
|
-
```typescript
|
|
477
|
-
// Fast-forward a waitFor step, providing mock event data
|
|
478
|
-
await engine.fastForwardWorkflow({
|
|
479
|
-
runId: run.id,
|
|
480
|
-
resourceId: 'user-123',
|
|
481
|
-
data: { approved: true, reviewer: 'admin' },
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
// Fast-forward a delay/waitUntil step (no data needed)
|
|
485
|
-
await engine.fastForwardWorkflow({
|
|
486
|
-
runId: run.id,
|
|
487
|
-
resourceId: 'user-123',
|
|
488
|
-
});
|
|
149
|
+
## Documentation
|
|
489
150
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
| Paused step type | Behavior |
|
|
499
|
-
|------------------|----------|
|
|
500
|
-
| `step.waitFor()` | Triggers the event with `data` (defaults to `{}`) |
|
|
501
|
-
| `step.delay()` / `step.waitUntil()` | Triggers the timeout event to skip the wait |
|
|
502
|
-
| `step.poll()` | Writes `data` as the poll result and triggers resolution |
|
|
503
|
-
| `step.pause()` | Delegates to `resumeWorkflow()` |
|
|
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
|
-
|
|
595
|
-
---
|
|
596
|
-
|
|
597
|
-
## Examples
|
|
598
|
-
|
|
599
|
-
### Conditional Steps
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
const conditionalWorkflow = workflow('conditional', async ({ step }) => {
|
|
603
|
-
const data = await step.run('fetch-data', async () => {
|
|
604
|
-
return { isPremium: true };
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
if (data.isPremium) {
|
|
608
|
-
await step.run('premium-action', async () => {
|
|
609
|
-
// Only runs for premium users
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
### Batch Processing with Loops
|
|
616
|
-
|
|
617
|
-
```typescript
|
|
618
|
-
const batchWorkflow = workflow('batch-process', async ({ step }) => {
|
|
619
|
-
const items = await step.run('get-items', async () => {
|
|
620
|
-
return [1, 2, 3, 4, 5];
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
for (const item of items) {
|
|
624
|
-
await step.run(`process-${item}`, async () => {
|
|
625
|
-
// Each item is processed durably
|
|
626
|
-
return processItem(item);
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
### Scheduled Reminder with Delay
|
|
633
|
-
|
|
634
|
-
```typescript
|
|
635
|
-
const reminderWorkflow = workflow('send-reminder', async ({ step, input }) => {
|
|
636
|
-
await step.run('send-initial', async () => {
|
|
637
|
-
return await sendEmail(input.email, 'Welcome!');
|
|
638
|
-
});
|
|
639
|
-
// Pause for 3 days, then send follow-up (durable - survives restarts)
|
|
640
|
-
await step.delay('cool-off', '3 days');
|
|
641
|
-
await step.run('send-follow-up', async () => {
|
|
642
|
-
return await sendEmail(input.email, 'Here’s a reminder…');
|
|
643
|
-
});
|
|
644
|
-
}, { inputSchema: z.object({ email: z.string().email() }) });
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### Polling Until a Condition Is Met
|
|
648
|
-
|
|
649
|
-
```typescript
|
|
650
|
-
const paymentWorkflow = workflow('await-payment', async ({ step, input }) => {
|
|
651
|
-
const result = await step.poll(
|
|
652
|
-
'wait-for-payment',
|
|
653
|
-
async () => {
|
|
654
|
-
const payment = await getPaymentStatus(input.paymentId);
|
|
655
|
-
return payment.completed ? payment : false;
|
|
656
|
-
},
|
|
657
|
-
{ interval: '1 minute', timeout: '24 hours' },
|
|
658
|
-
);
|
|
659
|
-
|
|
660
|
-
if (result.timedOut) {
|
|
661
|
-
return { status: 'expired' };
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
return { status: 'paid', payment: result.data };
|
|
665
|
-
});
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
`conditionFn` returns `false` to keep polling, or a truthy value to resolve the step. The minimum interval is 30s (default). If `timeout` is omitted the step polls indefinitely.
|
|
669
|
-
|
|
670
|
-
### Error Handling with Retries
|
|
671
|
-
|
|
672
|
-
```typescript
|
|
673
|
-
const resilientWorkflow = workflow('resilient', async ({ step }) => {
|
|
674
|
-
await step.run('risky-operation', async () => {
|
|
675
|
-
// Retries up to 3 times with exponential backoff
|
|
676
|
-
return await riskyApiCall();
|
|
677
|
-
});
|
|
678
|
-
}, {
|
|
679
|
-
retries: 3,
|
|
680
|
-
timeout: 60000,
|
|
681
|
-
});
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
### Monitoring Workflow Progress
|
|
685
|
-
|
|
686
|
-
```typescript
|
|
687
|
-
const progress = await engine.checkProgress({
|
|
688
|
-
runId: run.id,
|
|
689
|
-
resourceId: 'resource-123',
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
console.log({
|
|
693
|
-
status: progress.status,
|
|
694
|
-
completionPercentage: progress.completionPercentage,
|
|
695
|
-
completedSteps: progress.completedSteps,
|
|
696
|
-
totalSteps: progress.totalSteps,
|
|
697
|
-
});
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
---
|
|
701
|
-
|
|
702
|
-
## API Reference
|
|
703
|
-
|
|
704
|
-
### WorkflowEngine
|
|
705
|
-
|
|
706
|
-
#### Constructor
|
|
707
|
-
|
|
708
|
-
```typescript
|
|
709
|
-
// With connection string (engine creates and owns the pool)
|
|
710
|
-
const engine = new WorkflowEngine({
|
|
711
|
-
connectionString: string, // PostgreSQL connection string
|
|
712
|
-
workflows?: WorkflowDefinition[], // Optional: register workflows on init
|
|
713
|
-
logger?: WorkflowLogger, // Optional: custom logger
|
|
714
|
-
boss?: PgBoss, // Optional: bring your own pg-boss instance
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
// With existing pool (you manage the pool lifecycle)
|
|
718
|
-
const engine = new WorkflowEngine({
|
|
719
|
-
pool: pg.Pool, // Your pg.Pool instance
|
|
720
|
-
workflows?: WorkflowDefinition[],
|
|
721
|
-
logger?: WorkflowLogger,
|
|
722
|
-
boss?: PgBoss,
|
|
723
|
-
});
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
Pass either `connectionString` or `pool` (exactly one). When `connectionString` is used, the engine creates the pool internally and closes it on `stop()`.
|
|
727
|
-
|
|
728
|
-
When `boss` is omitted, pg-boss is created automatically with an isolated schema (`pgboss_v12_pgworkflow`) to avoid conflicts with other pg-boss installations.
|
|
729
|
-
|
|
730
|
-
#### Methods
|
|
731
|
-
|
|
732
|
-
| Method | Description |
|
|
733
|
-
|--------|-------------|
|
|
734
|
-
| `start(asEngine?, options?)` | Start the engine and workers |
|
|
735
|
-
| `stop()` | Stop the engine gracefully |
|
|
736
|
-
| `registerWorkflow(definition)` | Register a workflow definition |
|
|
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)). |
|
|
738
|
-
| `pauseWorkflow({ runId, resourceId? })` | Pause a running workflow |
|
|
739
|
-
| `resumeWorkflow({ runId, resourceId?, options? })` | Resume a paused workflow |
|
|
740
|
-
| `cancelWorkflow({ runId, resourceId? })` | Cancel a workflow |
|
|
741
|
-
| `triggerEvent({ runId, resourceId?, eventName, data?, options? })` | Send an event to a workflow |
|
|
742
|
-
| `fastForwardWorkflow({ runId, resourceId?, data? })` | Skip the current waiting step and resume execution |
|
|
743
|
-
| `getRun({ runId, resourceId? })` | Get workflow run details |
|
|
744
|
-
| `checkProgress({ runId, resourceId? })` | Get workflow progress |
|
|
745
|
-
| `getRuns(filters)` | List workflow runs with pagination |
|
|
746
|
-
|
|
747
|
-
### workflow()
|
|
748
|
-
|
|
749
|
-
```typescript
|
|
750
|
-
workflow<I extends Parameters>(
|
|
751
|
-
id: string,
|
|
752
|
-
handler: (context: WorkflowContext) => Promise<unknown>,
|
|
753
|
-
options?: {
|
|
754
|
-
inputSchema?: I,
|
|
755
|
-
timeout?: number,
|
|
756
|
-
retries?: number,
|
|
757
|
-
}
|
|
758
|
-
): WorkflowDefinition<I>
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
### WorkflowContext
|
|
762
|
-
|
|
763
|
-
The context object passed to workflow handlers:
|
|
764
|
-
|
|
765
|
-
```typescript
|
|
766
|
-
{
|
|
767
|
-
input: T, // Validated input data
|
|
768
|
-
workflowId: string, // Workflow ID
|
|
769
|
-
runId: string, // Unique run ID
|
|
770
|
-
timeline: Record<string, unknown>, // Step execution history
|
|
771
|
-
logger: WorkflowLogger, // Logger instance
|
|
772
|
-
step: {
|
|
773
|
-
run: <T>(stepId, handler) => Promise<T>,
|
|
774
|
-
// without timeout: always returns event data T
|
|
775
|
-
waitFor: <T>(stepId, { eventName, schema? }) => Promise<T>,
|
|
776
|
-
// with timeout: returns event data T or undefined if timeout fires first
|
|
777
|
-
waitFor: <T>(stepId, { eventName, timeout, schema? }) => Promise<T | undefined>,
|
|
778
|
-
waitUntil: (stepId, date | dateString | { date }) => Promise<void>,
|
|
779
|
-
delay: (stepId, duration) => Promise<void>,
|
|
780
|
-
sleep: (stepId, duration) => Promise<void>,
|
|
781
|
-
pause: (stepId) => Promise<void>,
|
|
782
|
-
poll: <T>(stepId, conditionFn, { interval?, timeout? }) => Promise<{ timedOut: false; data: T } | { timedOut: true }>,
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
`duration` is a string (e.g. `'3 days'`, `'2h'`) or an object (`{ weeks?, days?, hours?, minutes?, seconds? }`). See the `Duration` type and `parseDuration` from the package.
|
|
788
|
-
|
|
789
|
-
### WorkflowStatus
|
|
790
|
-
|
|
791
|
-
```typescript
|
|
792
|
-
enum WorkflowStatus {
|
|
793
|
-
PENDING = 'pending',
|
|
794
|
-
RUNNING = 'running',
|
|
795
|
-
PAUSED = 'paused',
|
|
796
|
-
COMPLETED = 'completed',
|
|
797
|
-
FAILED = 'failed',
|
|
798
|
-
CANCELLED = 'cancelled',
|
|
799
|
-
}
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
---
|
|
803
|
-
|
|
804
|
-
## Configuration
|
|
805
|
-
|
|
806
|
-
### Environment Variables
|
|
807
|
-
|
|
808
|
-
| Variable | Description | Default |
|
|
809
|
-
|----------|-------------|---------|
|
|
810
|
-
| `DATABASE_URL` | PostgreSQL connection string | *required* |
|
|
811
|
-
| `WORKFLOW_RUN_WORKERS` | Number of worker processes | `3` |
|
|
812
|
-
| `WORKFLOW_RUN_EXPIRE_IN_SECONDS` | Job expiration time in seconds | `300` |
|
|
813
|
-
|
|
814
|
-
### Database Setup
|
|
815
|
-
|
|
816
|
-
The engine automatically runs migrations on startup to create the required tables:
|
|
817
|
-
|
|
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).
|
|
819
|
-
- `pgboss_v12_pgworkflow.*` - pg-boss job queue tables for reliable task scheduling (isolated schema to avoid conflicts)
|
|
820
|
-
|
|
821
|
-
---
|
|
822
|
-
|
|
823
|
-
## The PostgreSQL-for-Everything Philosophy
|
|
824
|
-
|
|
825
|
-
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:
|
|
826
|
-
|
|
827
|
-
- **One database to rule them all** - Your application data and workflow state live in the same PostgreSQL instance. No distributed systems headaches.
|
|
828
|
-
- **Battle-tested reliability** - PostgreSQL's ACID transactions guarantee your workflow state is always consistent.
|
|
829
|
-
- **Zero operational overhead** - No Redis cluster to manage. No message broker to monitor. No external service to pay for.
|
|
830
|
-
- **Full queryability** - Inspect, debug, and analyze workflow runs with plain SQL.
|
|
831
|
-
|
|
832
|
-
If you're already running Postgres (and you probably should be), adding durable workflows is as simple as:
|
|
833
|
-
|
|
834
|
-
```bash
|
|
835
|
-
npm install pg-workflows pg
|
|
836
|
-
```
|
|
151
|
+
- **[Architecture](docs/architecture.md)** - single-service and microservices (client/worker) setups
|
|
152
|
+
- **[Core Concepts](docs/core-concepts.md)** - workflows, steps, events, delays, polling, pause/resume, idempotency, input validation
|
|
153
|
+
- **[AI & Agent Workflows](docs/ai-agents.md)** - durable LLM pipelines, human-in-the-loop, RAG
|
|
154
|
+
- **[Examples](docs/examples.md)** - conditional steps, batch loops, scheduled reminders, retries, monitoring
|
|
155
|
+
- **[API Reference](docs/api-reference.md)** - `WorkflowEngine`, `WorkflowClient`, `WorkflowRef`, types
|
|
156
|
+
- **[Configuration](docs/configuration.md)** - env vars, database setup, requirements
|
|
837
157
|
|
|
838
158
|
---
|
|
839
159
|
|
|
840
160
|
## Requirements
|
|
841
161
|
|
|
842
|
-
- Node.js >= 18
|
|
162
|
+
- Node.js >= 18
|
|
843
163
|
- PostgreSQL >= 10
|
|
844
|
-
- `pg` >= 8.0.0 (peer dependency)
|
|
845
|
-
- A [Standard Schema](https://github.com/standard-schema/standard-schema)-compliant validation library (Zod, Valibot, ArkType, etc.) if using `inputSchema`
|
|
846
164
|
|
|
847
165
|
## Acknowledgments
|
|
848
166
|
|