SokratisVidros/pg-workflows(新しいタブで開きます)
主要言語: TypeScript
Postgres workflows. Durable execution built on pg-boss. Like Temporal, Inngest, or Trigger, powered by Postgres.
- star 数
- 64
- watcher 数
- 1
- fork 数
- 10
- issue 数
- 10
- 最終更新
- 2026/10/27
主要言語: TypeScript
Postgres workflows. Durable execution built on pg-boss. Like Temporal, Inngest, or Trigger, powered by Postgres.
Durable workflows for TypeScript, backed by PostgreSQL. Each step's result is saved, a retried run skips the steps that already finished, and a run can pause for an event, a timer, or a polled condition. There's no Redis, broker, or scheduler to run.
Paste this into Claude Code, Cursor, Codex, or any agent that can fetch a URL:
Add pg-workflows to this project. Fetch https://pgworkflows.dev/skill.md and follow it step by step: pick the right layout for this codebase (monolith, web app plus worker, or microservices), install and verify the engine, add the @pg-workflows/ui dashboard for our stack, then ask me whether to add OpenTelemetry tracing.
Or install the skill: npx skills add SokratisVidros/pg-workflows --skill pg-workflows-install. See Install with an agent.
1. Start Postgres (skip if you already have one):
docker run -d --name pg-workflows-db -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:17
2. Create a project:
mkdir workflows-demo && cd workflows-demo npm init -y npm install pg-workflows pg zod npm install -D tsx
3. Save this as index.ts:
import { WorkflowEngine, WorkflowStatus, workflow } from 'pg-workflows' import { z } from 'zod' const greetUser = workflow( 'greet-user', async ({ step, input }) => { const user = await step.run('load-user', async () => { return { name: input.name, signedUpAt: new Date().toISOString() } }) const message = await step.run('build-message', async () => { return `Welcome, ${user.name}!` }) return { message } }, { inputSchema: z.object({ name: z.string() }) }, ) async function main() { const engine = new WorkflowEngine({ connectionString: process.env.DATABASE_URL ?? 'postgres://postgres:postgres@localhost:5432/postgres', workflows: [greetUser], }) await engine.start() const run = await engine.startWorkflow({ workflowId: 'greet-user', input: { name: 'Ada' }, }) let result = await engine.getRun({ runId: run.id }) while (result.status === WorkflowStatus.PENDING || result.status === WorkflowStatus.RUNNING) { await new Promise((resolve) => setTimeout(resolve, 200)) result = await engine.getRun({ runId: run.id }) } console.log(result.status, result.output) await engine.stop() } main()
4. Run it:
npx tsx index.ts
After the engine's startup logs, you should see:
completed { message: 'Welcome, Ada!' }
engine.start() creates its tables on first run. Each step.run result is saved on the run's row in workflow_runs. When a run is retried (set retries on the workflow), completed steps return their saved result instead of running again.
step.waitFor pauses the run until your code calls triggerEvent. A paused run holds no worker and no connection.
import { WorkflowEngine, WorkflowStatus, workflow } from 'pg-workflows' import { z } from 'zod' const approveExpense = workflow( 'approve-expense', async ({ step, input }) => { await step.run('notify-manager', async () => { return { notified: `manager of ${input.employee}` } }) const review = await step.waitFor('wait-for-review', { eventName: 'expense-reviewed', schema: z.object({ approved: z.boolean() }), }) return { amount: input.amount, approved: review.approved } }, { inputSchema: z.object({ employee: z.string(), amount: z.number() }) }, ) const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) async function main() { const engine = new WorkflowEngine({ connectionString: process.env.DATABASE_URL ?? 'postgres://postgres:postgres@localhost:5432/postgres', workflows: [approveExpense], }) await engine.start() const run = await engine.startWorkflow({ workflowId: 'approve-expense', input: { employee: 'ada', amount: 120 }, }) // In a real app, this is a separate request: an approval button, a webhook, and so on. while ((await engine.getRun({ runId: run.id })).status !== WorkflowStatus.PAUSED) await sleep(200) await engine.triggerEvent({ runId: run.id, eventName: 'expense-reviewed', data: { approved: true }, }) let result = await engine.getRun({ runId: run.id }) while (result.status !== WorkflowStatus.COMPLETED && result.status !== WorkflowStatus.FAILED) { await sleep(200) result = await engine.getRun({ runId: run.id }) } console.log(result.status, result.output) // completed { amount: 120, approved: true } await engine.stop() } main()
| Feature | API |
|---|---|
| Durable steps | step.run(id, fn) |
| Wait for external events | step.waitFor(id, { eventName, timeout?, schema? }) + engine.triggerEvent() |
| Timers | step.delay(id, '3 days'), step.waitUntil(id, date) |
| Polling | step.poll(id, fn, { interval, timeout }) |
| Manual pause and resume | step.pause(id), engine.resumeWorkflow() |
| Child workflows | step.invokeChildWorkflow(id, ref, input) |
| Recurring schedules | workflow(id, fn, { schedule: '0 9 * * 1-5' }) |
| Retries | workflow(id, fn, { retries: 3 }) |
| Priorities | workflow(id, fn, { priority: 'high' }) |
| One run at a time | workflow(id, fn, { singleton: true }) |
| Deduplicated starts | startWorkflow({ idempotencyKey }) |
| Tenant scoping | resourceId on every run and every API call |
| Typed input | Any Standard Schema library (Zod, Valibot, ArkType) |
| API/worker split | WorkflowClient from pg-workflows/client |
Full docs are at pgworkflows.dev.
| Guide | Covers |
|---|---|
| Core concepts | Steps, events, timers, polling, child workflows, schedules, retries, priorities, singletons, idempotency, input validation |
| Architectures | Single service, or microservices with web and worker services |
| Examples | Conditional steps, fan-out loops, reminders, polling, retries, progress |
| AI and agent workflows | Durable LLM pipelines, human review, retrieval |
| API reference | WorkflowEngine, WorkflowClient, WorkflowRef, workflow(), types |
| Configuration | Environment variables, database objects, requirements |
| Package | Purpose |
|---|---|
pg-workflows |
The engine and client |
@pg-workflows/otel |
OpenTelemetry spans for workflow runs and steps |
@pg-workflows/ui |
React dashboard, components, and hooks. Try it with npx @pg-workflows/ui |
Runnable scripts live in examples/.
pg >= 8 (peer dependency). pg-boss ships with the engine and needs no setup.Temporal, Inngest, Trigger.dev, and DBOS pioneered the durable execution patterns this project builds on.
MIT