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 +21 -0
- package/README.md +288 -0
- package/dist/ai/index.d.ts +397 -0
- package/dist/ai/index.js +466 -0
- package/dist/core-ERYGlYsS.js +574 -0
- package/dist/dispatcher-BydjyrgU.d.ts +26 -0
- package/dist/dispatcher-pgboss/index.d.ts +170 -0
- package/dist/dispatcher-pgboss/index.js +301 -0
- package/dist/hooks-CDRFQZbT.d.ts +56 -0
- package/dist/index-D_fOYfmf.d.ts +580 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.js +1426 -0
- package/dist/logger-BCus3AOa.d.ts +10 -0
- package/dist/observability-BsrvbhDr.d.ts +249 -0
- package/dist/store-pg/index.d.ts +195 -0
- package/dist/store-pg/index.js +671 -0
- package/package.json +116 -0
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
|
+
[](https://github.com/octabits-io/octaflow/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/octaflow)
|
|
5
|
+
[](https://octabits-io.github.io/octaflow/)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](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.
|