pg-workflows 0.0.1-claimed → 0.1.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/LICENSE +22 -0
- package/README.md +430 -0
- package/dist/index.cjs +1050 -0
- package/dist/index.d.cts +202 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.js +1004 -0
- package/dist/index.js.map +16 -0
- package/package.json +83 -7
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,430 @@
|
|
|
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
|
+
[](https://www.npmjs.com/package/pg-workflows)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
[](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
|
+
### Pause and Resume
|
|
220
|
+
|
|
221
|
+
Manually pause a workflow and resume it later:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// Pause inside a workflow
|
|
225
|
+
await step.pause('pause-step');
|
|
226
|
+
|
|
227
|
+
// Resume from outside the workflow
|
|
228
|
+
await engine.resumeWorkflow({
|
|
229
|
+
runId: run.id,
|
|
230
|
+
resourceId: 'resource-123',
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Examples
|
|
237
|
+
|
|
238
|
+
### Conditional Steps
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
const conditionalWorkflow = workflow('conditional', async ({ step }) => {
|
|
242
|
+
const data = await step.run('fetch-data', async () => {
|
|
243
|
+
return { isPremium: true };
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (data.isPremium) {
|
|
247
|
+
await step.run('premium-action', async () => {
|
|
248
|
+
// Only runs for premium users
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Batch Processing with Loops
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
const batchWorkflow = workflow('batch-process', async ({ step }) => {
|
|
258
|
+
const items = await step.run('get-items', async () => {
|
|
259
|
+
return [1, 2, 3, 4, 5];
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
for (const item of items) {
|
|
263
|
+
await step.run(`process-${item}`, async () => {
|
|
264
|
+
// Each item is processed durably
|
|
265
|
+
return processItem(item);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Error Handling with Retries
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
const resilientWorkflow = workflow('resilient', async ({ step }) => {
|
|
275
|
+
await step.run('risky-operation', async () => {
|
|
276
|
+
// Retries up to 3 times with exponential backoff
|
|
277
|
+
return await riskyApiCall();
|
|
278
|
+
});
|
|
279
|
+
}, {
|
|
280
|
+
retries: 3,
|
|
281
|
+
timeout: 60000,
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Monitoring Workflow Progress
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const progress = await engine.checkProgress({
|
|
289
|
+
runId: run.id,
|
|
290
|
+
resourceId: 'resource-123',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
console.log({
|
|
294
|
+
status: progress.status,
|
|
295
|
+
completionPercentage: progress.completionPercentage,
|
|
296
|
+
completedSteps: progress.completedSteps,
|
|
297
|
+
totalSteps: progress.totalSteps,
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## API Reference
|
|
304
|
+
|
|
305
|
+
### WorkflowEngine
|
|
306
|
+
|
|
307
|
+
#### Constructor
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
const engine = new WorkflowEngine({
|
|
311
|
+
boss: PgBoss, // Required: pg-boss instance
|
|
312
|
+
workflows: WorkflowDefinition[], // Optional: register workflows on init
|
|
313
|
+
logger: WorkflowLogger, // Optional: custom logger
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### Methods
|
|
318
|
+
|
|
319
|
+
| Method | Description |
|
|
320
|
+
|--------|-------------|
|
|
321
|
+
| `start(asEngine?, options?)` | Start the engine and workers |
|
|
322
|
+
| `stop()` | Stop the engine gracefully |
|
|
323
|
+
| `registerWorkflow(definition)` | Register a workflow definition |
|
|
324
|
+
| `startWorkflow({ workflowId, resourceId?, input, options? })` | Start a new workflow run |
|
|
325
|
+
| `pauseWorkflow({ runId, resourceId? })` | Pause a running workflow |
|
|
326
|
+
| `resumeWorkflow({ runId, resourceId?, options? })` | Resume a paused workflow |
|
|
327
|
+
| `cancelWorkflow({ runId, resourceId? })` | Cancel a workflow |
|
|
328
|
+
| `triggerEvent({ runId, resourceId?, eventName, data?, options? })` | Send an event to a workflow |
|
|
329
|
+
| `getRun({ runId, resourceId? })` | Get workflow run details |
|
|
330
|
+
| `checkProgress({ runId, resourceId? })` | Get workflow progress |
|
|
331
|
+
| `getRuns(filters)` | List workflow runs with pagination |
|
|
332
|
+
|
|
333
|
+
### workflow()
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
workflow<I extends Parameters>(
|
|
337
|
+
id: string,
|
|
338
|
+
handler: (context: WorkflowContext) => Promise<unknown>,
|
|
339
|
+
options?: {
|
|
340
|
+
inputSchema?: I,
|
|
341
|
+
timeout?: number,
|
|
342
|
+
retries?: number,
|
|
343
|
+
}
|
|
344
|
+
): WorkflowDefinition<I>
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### WorkflowContext
|
|
348
|
+
|
|
349
|
+
The context object passed to workflow handlers:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
{
|
|
353
|
+
input: T, // Validated input data
|
|
354
|
+
workflowId: string, // Workflow ID
|
|
355
|
+
runId: string, // Unique run ID
|
|
356
|
+
timeline: Record<string, unknown>, // Step execution history
|
|
357
|
+
logger: WorkflowLogger, // Logger instance
|
|
358
|
+
step: {
|
|
359
|
+
run: <T>(stepId, handler) => Promise<T>,
|
|
360
|
+
waitFor: <T>(stepId, { eventName, timeout?, schema? }) => Promise<T>,
|
|
361
|
+
waitUntil: (stepId, { date }) => Promise<void>,
|
|
362
|
+
pause: (stepId) => Promise<void>,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### WorkflowStatus
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
enum WorkflowStatus {
|
|
371
|
+
PENDING = 'pending',
|
|
372
|
+
RUNNING = 'running',
|
|
373
|
+
PAUSED = 'paused',
|
|
374
|
+
COMPLETED = 'completed',
|
|
375
|
+
FAILED = 'failed',
|
|
376
|
+
CANCELLED = 'cancelled',
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Configuration
|
|
383
|
+
|
|
384
|
+
### Environment Variables
|
|
385
|
+
|
|
386
|
+
| Variable | Description | Default |
|
|
387
|
+
|----------|-------------|---------|
|
|
388
|
+
| `DATABASE_URL` | PostgreSQL connection string | *required* |
|
|
389
|
+
| `WORKFLOW_RUN_WORKERS` | Number of worker processes | `3` |
|
|
390
|
+
| `WORKFLOW_RUN_EXPIRE_IN_SECONDS` | Job expiration time in seconds | `300` |
|
|
391
|
+
|
|
392
|
+
### Database Setup
|
|
393
|
+
|
|
394
|
+
The engine automatically runs migrations on startup to create the required tables:
|
|
395
|
+
|
|
396
|
+
- `workflow_runs` — Stores workflow execution state, step results, and timeline
|
|
397
|
+
- `pgboss.*` — pg-boss job queue tables for reliable task scheduling
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## The PostgreSQL-for-Everything Philosophy
|
|
402
|
+
|
|
403
|
+
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:
|
|
404
|
+
|
|
405
|
+
- **One database to rule them all** — Your application data and workflow state live in the same PostgreSQL instance. No distributed systems headaches.
|
|
406
|
+
- **Battle-tested reliability** — PostgreSQL's ACID transactions guarantee your workflow state is always consistent.
|
|
407
|
+
- **Zero operational overhead** — No Redis cluster to manage. No message broker to monitor. No external service to pay for.
|
|
408
|
+
- **Full queryability** — Inspect, debug, and analyze workflow runs with plain SQL.
|
|
409
|
+
|
|
410
|
+
If you're already running Postgres (and you probably should be), adding durable workflows is as simple as:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
npm install pg-workflows
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Requirements
|
|
419
|
+
|
|
420
|
+
- Node.js >= 18.0.0
|
|
421
|
+
- PostgreSQL >= 10
|
|
422
|
+
- pg-boss >= 10.0.0
|
|
423
|
+
|
|
424
|
+
## Acknowledgments
|
|
425
|
+
|
|
426
|
+
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.
|
|
427
|
+
|
|
428
|
+
## License
|
|
429
|
+
|
|
430
|
+
MIT
|