octaflow 0.14.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 The Octabits Authors
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.
package/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # octaflow
2
+
3
+ [![CI](https://github.com/octabits-io/octaflow/actions/workflows/ci.yml/badge.svg)](https://github.com/octabits-io/octaflow/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/octaflow.svg)](https://www.npmjs.com/package/octaflow)
5
+ [![docs](https://img.shields.io/badge/docs-octabits--io.github.io%2Fflow-16794a.svg)](https://octabits-io.github.io/octaflow/)
6
+ [![license](https://img.shields.io/npm/l/octaflow.svg)](./LICENSE)
7
+ [![types](https://img.shields.io/badge/types-included-blue.svg)](https://www.npmjs.com/package/octaflow)
8
+
9
+ **Durable workflows for TypeScript that run on the Postgres you already have.**
10
+
11
+ Declare a DAG of Zod-typed steps. The engine runs each step as soon as its dependencies
12
+ complete, persists every transition, retries failures, and resumes after a crash. There is
13
+ no workflow server to operate, no control plane, and no vendor β€” it's a library you import,
14
+ not a platform you adopt.
15
+
16
+ ### πŸ“– [Read the docs β†’](https://octabits-io.github.io/octaflow/)
17
+
18
+ [Quick start](https://octabits-io.github.io/octaflow/start/quick-start/) Β·
19
+ [Concepts](https://octabits-io.github.io/octaflow/core/concepts/) Β·
20
+ [Postgres & pg-boss](https://octabits-io.github.io/octaflow/running/postgres-and-pg-boss/) Β·
21
+ [API reference](https://octabits-io.github.io/octaflow/reference/api/)
22
+
23
+ ```ts
24
+ const wf = buildWorkflow({
25
+ type: 'publish-article',
26
+ inputSchema: z.object({ draftId: z.string() }),
27
+ steps: { fetchDraft, summarize, translate, publish },
28
+ });
29
+ ```
30
+
31
+ `summarize` and `translate` both depend on `fetchDraft`, and `publish` waits for both β€” so the
32
+ engine runs the middle pair **concurrently** and fans back in, without you scheduling anything:
33
+
34
+ ```
35
+ β”Œβ”€β”€ summarize ──┐
36
+ fetchDraft ─── β”œβ”€β”€ publish
37
+ └── translate β”€β”€β”˜
38
+ ```
39
+
40
+ That shape isn't inferred from a trace β€” it *is* the workflow. The DAG is a plain value you can
41
+ walk before anything runs:
42
+
43
+ ```ts
44
+ for (const s of wf.definition.steps) {
45
+ console.log(`${s.key.padEnd(12)} type=${s.type.padEnd(10)} deps: ${s.dependencies?.join(', ') || 'β€”'}`);
46
+ }
47
+ ```
48
+
49
+ ```text
50
+ fetchDraft type=fetch deps: β€”
51
+ summarize type=summarize deps: fetchDraft
52
+ translate type=translate deps: fetchDraft
53
+ publish type=publish deps: summarize, translate
54
+ ```
55
+
56
+ This is the core design choice. Flow is **declarative**, where Temporal, Inngest, Trigger.dev
57
+ and DBOS are **imperative**: there you write a function, and the graph exists only as the trace
58
+ of what it did. Both models are durable β€” they trade off differently, and
59
+ [the tradeoff is spelled out below](#how-it-compares).
60
+
61
+ ---
62
+
63
+ ## Watch it run
64
+
65
+ A fuller pipeline than the snippet above β€” dynamic fan-out over two `map` steps at once, a
66
+ concurrency cap so only 2 of 6 images encode at a time, a flaky step retried, a sub-workflow,
67
+ a step suspended on an external event, and a durable sleep. Then a second workflow fails and
68
+ its completed steps roll back in reverse.
69
+
70
+ <p align="center">
71
+ <img src="./docs/demo.svg" alt="Terminal trace of a publishing pipeline: parallel fan-out over locales and images with a concurrency cap, a retried step, a sub-workflow, a suspend-and-resume on an external event, a durable sleep, then a saga rollback in reverse order" width="760">
72
+ </p>
73
+
74
+ Every line is an engine transition emitted through the [`FlowObserver`](https://octabits-io.github.io/octaflow/running/observability/) seam β€”
75
+ the same one you'd point at OpenTelemetry or an events table β€” not a `console.log` in a handler.
76
+
77
+ ```bash
78
+ npx tsx scripts/demo.ts # reproduce it
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Contents
84
+
85
+ - [How it compares](#how-it-compares)
86
+ - [When not to use Flow](#when-not-to-use-flow)
87
+ - [Performance](#performance)
88
+ - [Features](#features)
89
+ - [Installation](#installation)
90
+ - [Documentation](#documentation)
91
+ - [Examples](#examples)
92
+
93
+ ---
94
+
95
+ ## How it compares
96
+
97
+ All of these give you durable execution. They differ in **what you operate** and **how the
98
+ workflow is expressed**.
99
+
100
+ | | **Flow** | **Temporal** | **Inngest** | **Trigger.dev** | **BullMQ** |
101
+ |---|---|---|---|---|---|
102
+ | **Infra you run** | Postgres | a cluster (frontend, history, matching, worker) + a DB | none β€” engine is hosted | Postgres + Redis (+ ClickHouse recommended at volume) | Redis |
103
+ | **Self-host** | it's a library | yes, MIT | no β€” engine and dashboard are Inngest-hosted | yes, Apache-2.0 | it's a library |
104
+ | **Model** | declarative β€” a static DAG value | imperative workflow code | imperative step functions | imperative tasks | job queue; flows are **trees** |
105
+ | **Fan-in / diamond deps** | yes | yes | yes | yes | no β€” a job can't be shared by two branches |
106
+ | **Inspect before running** | yes β€” the DAG is a value | no β€” the graph is the execution trace | no | no | yes β€” the flow tree is data |
107
+ | **Web dashboard** | **none** β€” build one on `toPublicWorkflow()` | yes | yes | yes | via third-party UIs |
108
+ | **Languages** | TypeScript | polyglot SDKs | TS, Python, Go, Kotlin | TS | Node (+ ports) |
109
+ | **Maturity** | **pre-1.0** | mature | mature | mature | mature, widely deployed |
110
+
111
+ The honest summary: **Flow is the smallest thing that is still a real workflow engine.** If
112
+ you already run Postgres, it adds no infrastructure β€” the queue ([pg-boss](https://github.com/timgit/pg-boss))
113
+ is Postgres too. You give up the dashboards, the polyglot SDKs, and the operational maturity
114
+ that the others have earned.
115
+
116
+ ## When not to use Flow
117
+
118
+ Reach for something else if:
119
+
120
+ - **Your control flow is genuinely dynamic.** A declarative DAG is fixed at definition time.
121
+ Flow softens this with [`defineMapStep`](https://octabits-io.github.io/octaflow/core/fan-out-and-map/) (runtime-sized fan-out),
122
+ [sub-workflows](https://octabits-io.github.io/octaflow/core/sub-workflows/), and [`waitForEvent`](https://octabits-io.github.io/octaflow/core/signals/) β€” but if your
123
+ process is "loop until a human approves, branching on whatever they typed," an imperative
124
+ durable function will express it more naturally.
125
+ - **You want a UI out of the box.** Flow ships a wire-safe projection
126
+ ([`toPublicWorkflow`](https://octabits-io.github.io/octaflow/extending/http/)) and lifecycle events, not a
127
+ dashboard. You build it.
128
+ - **You need non-TypeScript workers.** The DAG and its schemas are TypeScript values.
129
+ - **You can't run Postgres**, or you need throughput past what a Postgres-backed queue gives you.
130
+ - **You need a support contract**, or an API frozen by a 1.0 promise. This is pre-1.0 and 0.x
131
+ minors can break.
132
+
133
+ Flow fits best when the work is a **known pipeline** β€” ingest β†’ enrich β†’ summarize β†’ publish β€”
134
+ that must survive crashes, retry sanely, and stay legible to the next person who reads it.
135
+
136
+ ---
137
+
138
+ ## Performance
139
+
140
+ Reproduce with `npx tsx scripts/bench.ts` (Docker required β€” it starts Postgres 17 via
141
+ Testcontainers). Workload: 200 workflows Γ— 6 steps in a `root β†’ 4 parallel β†’ join` diamond.
142
+ **Handlers are no-ops**, so this measures what the *engine* costs per step β€” claiming it,
143
+ reading dependency outputs, persisting the transition, recomputing readiness β€” not your work.
144
+
145
+ **Engine + Postgres store** (in-process dispatcher), per-step latency:
146
+
147
+ | concurrency | steps/sec | p50 | p95 | p99 |
148
+ |---|---|---|---|---|
149
+ | 1 | 1,031 | 1.0 ms | 2.1 ms | 2.9 ms |
150
+ | 4 | 1,932 | 2.1 ms | 3.9 ms | 4.8 ms |
151
+ | 16 | 2,108 | 7.1 ms | 12.7 ms | 15.9 ms |
152
+ | 64 | 2,270 | 26.8 ms | 46.8 ms | 64.0 ms |
153
+
154
+ **End-to-end through pg-boss workers** β€” the full production path, batch 25:
155
+
156
+ | workers | `burstWhenBatchFull` | `concurrency` | steps/sec |
157
+ |---|---|---|---|
158
+ | 1 | off | 1 | 50 |
159
+ | 1 | **on** | 1 | 274 |
160
+ | 1 | **on** | 8 | 646 |
161
+ | 4 | **on** | 8 | 902 |
162
+
163
+ That first row is not a ceiling, it's a *polling artifact* β€” and the fix is configuration, not
164
+ architecture. A worker drains a batch in milliseconds, then waits out the 0.5 s interval, so
165
+ **`burstWhenBatchFull` is the setting that matters**: it keeps fetching while batches come back
166
+ full. `concurrency` (steps run at once from one batch) then compounds on top β€” but on its own,
167
+ without burst, it changes nothing at all, because the wait, not the work, is the bottleneck.
168
+
169
+ Budget connections before raising `concurrency`: each in-flight step holds one, so
170
+ `workers Γ— concurrency` must fit your pool and Postgres `max_connections`.
171
+
172
+ **How to read this.** Measured on an M-series Mac with Postgres in Docker, which has markedly
173
+ slower disk I/O than a Linux host β€” expect better on a real server. These are an order of
174
+ magnitude and a scaling shape, not a score. A Redis-backed job queue will beat these numbers
175
+ outright, because it isn't writing a durable transition per step to a relational database;
176
+ that write is the feature. And in any real workflow, handler time dwarfs the 1–3 ms of engine
177
+ overhead, so the practical question is usually whether ~1 ms per transition is acceptable
178
+ next to what your steps actually do.
179
+
180
+ ---
181
+
182
+ ## Features
183
+
184
+ | | Capability |
185
+ |---|---|
186
+ | 🧩 | **Typed DAG** β€” Zod-validated input/output per step; dependency outputs are typed |
187
+ | ⚑ | **Auto-parallelism** β€” dependency-free steps run concurrently; a step starts when all its deps complete |
188
+ | πŸ” | **Retry & timeout** β€” per-step `maxAttempts`, fixed/exponential backoff, wall-clock timeout |
189
+ | πŸ’€ | **Durable sleep** β€” hold a step in the queue for N ms (survives restarts) |
190
+ | 🚦 | **Concurrency & rate limiting** β€” per-step-type caps and token buckets via a pluggable gate |
191
+ | ⏰ | **Cron / scheduled starts** β€” fire workflows on a schedule (pg-boss) |
192
+ | πŸ”‘ | **Start idempotency** β€” a dedup key collapses double-clicks / overlapping ticks |
193
+ | πŸ—ΊοΈ | **Dynamic fan-out / map** β€” spawn one child step per item of a runtime-sized list |
194
+ | ⏸️ | **Signals / waitForEvent** β€” suspend a step until an external event (`resumeStep`) |
195
+ | πŸͺ† | **Sub-workflows** β€” a step starts a child workflow and awaits its result |
196
+ | ↩️ | **Saga compensation** β€” run rollback handlers in reverse order on failure |
197
+ | πŸ”­ | **Observability** β€” lifecycle events (run history) + per-step spans, both pluggable |
198
+ | πŸ€– | **AI add-on** β€” instrumented models, token/cost capture, quota, daily rollups |
199
+ | 🧱 | **Pluggable everything** β€” `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
200
+
201
+ ---
202
+
203
+ ## Installation
204
+
205
+ ```bash
206
+ pnpm add octaflow zod
207
+ ```
208
+
209
+ `zod` is a **required** peer. The heavy dependencies are **optional peers** β€” install only
210
+ what the layers you import need:
211
+
212
+ ```bash
213
+ # Postgres store / gate / event sink
214
+ pnpm add pg
215
+
216
+ # pg-boss dispatcher, workers, cron scheduler
217
+ pnpm add pg-boss
218
+
219
+ # the AI add-on
220
+ pnpm add ai @ai-sdk/provider
221
+ ```
222
+
223
+ > Pure in-memory usage (great for tests and single-process apps) needs **nothing** beyond
224
+ > `zod` β€” the engine, `defineStep`/`buildWorkflow`, and the in-memory store are all in the core.
225
+
226
+ ---
227
+
228
+ ---
229
+
230
+ ## Documentation
231
+
232
+ Full docs β€” concepts, every feature, production wiring and the API reference β€” live at
233
+ **[octabits-io.github.io/octaflow](https://octabits-io.github.io/octaflow/)**.
234
+
235
+ | | |
236
+ |---|---|
237
+ | [Quick start](https://octabits-io.github.io/octaflow/start/quick-start/) | a runnable workflow in one file, no database |
238
+ | [Concepts](https://octabits-io.github.io/octaflow/core/concepts/) | step, workflow, registry, store, dispatcher, engine, partition |
239
+ | [Defining steps](https://octabits-io.github.io/octaflow/core/defining-steps/) | `defineStep` and its variants |
240
+ | [Retry & timeout](https://octabits-io.github.io/octaflow/core/retry-and-timeout/) | attempt budgets, backoff, and how a failure is classified |
241
+ | [Fan-out & map](https://octabits-io.github.io/octaflow/core/fan-out-and-map/) | one child step per item of a runtime list |
242
+ | [Signals](https://octabits-io.github.io/octaflow/core/signals/) Β· [Sub-workflows](https://octabits-io.github.io/octaflow/core/sub-workflows/) Β· [Saga](https://octabits-io.github.io/octaflow/core/saga-compensation/) | suspend, nest, and roll back |
243
+ | [Postgres & pg-boss](https://octabits-io.github.io/octaflow/running/postgres-and-pg-boss/) | production wiring, workers, DLQ, cron |
244
+ | [Observability](https://octabits-io.github.io/octaflow/running/observability/) | lifecycle events and spans |
245
+ | [The AI add-on](https://octabits-io.github.io/octaflow/extending/ai/) | token/cost capture, quota, usage rollups |
246
+ | [Extending](https://octabits-io.github.io/octaflow/extending/interfaces/) | custom stores, dispatchers and gates |
247
+ | [API reference](https://octabits-io.github.io/octaflow/reference/api/) | every export, by entry point |
248
+
249
+ ---
250
+
251
+ ## Examples
252
+
253
+ Runnable, focused examples live in [`examples/`](./examples) β€” see [`examples/README.md`](./examples/README.md).
254
+
255
+ | # | File | Shows |
256
+ |---|---|---|
257
+ | 01 | `01-in-memory-quickstart.ts` | minimal setup + run loop |
258
+ | 02 | `02-dag-parallel-fan-in.ts` | parallel branches + fan-in (diamond DAG) |
259
+ | 03 | `03-retry-timeout.ts` | per-step retry + timeout |
260
+ | 04 | `04-durable-sleep.ts` | durable delay between steps |
261
+ | 05 | `05-concurrency-rate-limit.ts` | in-memory `StepGate` |
262
+ | 06 | `06-start-idempotency.ts` | dedup key collapses duplicate starts |
263
+ | 07 | `07-dynamic-map.ts` | runtime fan-out / map |
264
+ | 08 | `08-wait-for-event.ts` | suspend + `resumeStep` |
265
+ | 09 | `09-sub-workflows.ts` | child workflow compose + await |
266
+ | 10 | `10-saga-compensation.ts` | reverse-order rollback on failure |
267
+ | 11 | `11-observability.ts` | observer events + tracer spans |
268
+ | 12 | `12-postgres-pgboss-production.ts` | full pg store + gate + event sink + pg-boss + cron |
269
+ | 13 | `13-ai-workflow.ts` | AI add-on (instrumented model + cost) |
270
+
271
+ The in-memory examples (01–11) share a small driver, [`examples/runtime.ts`](./examples/runtime.ts),
272
+ that builds an engine over the in-memory store and an in-process queue you drain.
273
+
274
+ ---
275
+
276
+ ## Contributing
277
+
278
+ Bug reports with a runnable reproduction are the most useful thing you can send; PRs are
279
+ welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the setup, the layer rules the lint
280
+ enforces, and the correctness requirements a custom `WorkflowStore` has to meet.
281
+
282
+ ---
283
+
284
+ ## Status
285
+
286
+ Pre-1.0 β€” developed in [octabits-io/octaflow](https://github.com/octabits-io/octaflow) (extracted from the
287
+ [octabits platform monorepo](https://github.com/octabits-io/platform), where its earlier history lives).
288
+ The API is stable but may still see breaking changes in 0.x minors.