{"_id":"@dmytromykhailiuk/flow-engine","_rev":"2-0f02f8be5f1a595cd8e7fc5ccff405a6","name":"@dmytromykhailiuk/flow-engine","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@dmytromykhailiuk/flow-engine","version":"1.0.0","keywords":["flow","flow-engine","workflow","workflow-engine","orchestration","state-machine","steps","pipeline","json-config","expressions","typescript","type-safe","durable","checkpoint","resume","suspend","cancel","retry","zero-dependencies"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/flow-engine@1.0.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/flow-engine#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/flow-engine/issues"},"dist":{"shasum":"ca89a72764271ef60cd5f30d36f17abfc6f93c38","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/flow-engine/-/flow-engine-1.0.0.tgz","fileCount":9,"integrity":"sha512-Wy0QxswvOZDX9GvOVv3Lc1ASQsTiLfYy09jSm8SoVjbbiDiiwsZSwGhu6Y9pL+ZKZ2zQwV89Qx34uLz7kgk4vQ==","signatures":[{"sig":"MEUCIQCtjLyV0VULEDVFoU78kmB5jKkb2IB6m4/9RbrnmTQwXQIgXYmnETond9Vf3ZakuL4emC41ja80UNC//iIY5WqV6vk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":661102},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"fa199c529e77157cbe2024a6899cf4186b3fbb55","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/flow-engine.git","type":"git"},"_npmVersion":"11.6.2","description":"Typed flow engine with JSON-serializable flows, safe string expressions, pause/cancel/suspend, checkpoints and restore. Works in the browser and on the backend.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","dependencies":{"@dmytromykhailiuk/execution-blocker":"^1.0.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^5.4.11","jsdom":"^25.0.1","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4"},"_npmOperationalInternal":{"tmp":"tmp/flow-engine_1.0.0_1786298154129_0.822649742723738","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@dmytromykhailiuk/flow-engine","version":"1.0.1","description":"Typed flow engine with JSON-serializable flows, safe string expressions, pause/cancel/suspend, checkpoints and restore. Works in the browser and on the backend.","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["flow","flow-engine","workflow","workflow-engine","orchestration","state-machine","steps","pipeline","json-config","expressions","typescript","type-safe","durable","checkpoint","resume","suspend","cancel","retry","zero-dependencies"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","playground":"vite --config vite.playground.config.ts","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"dependencies":{"@dmytromykhailiuk/execution-blocker":"^1.0.0"},"devDependencies":{"@biomejs/biome":"^1.9.4","@types/node":"^22.10.5","jsdom":"^25.0.1","tsup":"^8.3.5","typescript":"^5.7.3","vite":"^5.4.11","vitest":"^2.1.8"},"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/flow-engine.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/flow-engine/issues"},"homepage":"https://dmytromykhailiuk.github.io/flow-engine/","gitHead":"07f6392c45a1cb52f1924d88e0b3a3945b684006","_id":"@dmytromykhailiuk/flow-engine@1.0.1","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-Bxu59TTyni6Eo4umrchMikS6r44HLTXnZWcJkRvB2iXNVbjSw9przGVMtx/atNKmOBzyiF2llaZR8Ds6sibw0Q==","shasum":"474ff3bf1f1bb263c1680d566bfd76a1bbd6913d","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/flow-engine/-/flow-engine-1.0.1.tgz","fileCount":9,"unpackedSize":661095,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHmhLtoRqNw3iZecIADSQO+cB5bReXa1/w312nVrpd5dAiBnu30qPhHDU/X6RqIeYQCHvssxBtZWYnInBbsLiBS9VA=="}]},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"directories":{},"maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/flow-engine_1.0.1_1786638478941_0.5310726735835574"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-09T17:55:53.923Z","modified":"2026-08-13T16:27:59.401Z","1.0.0":"2026-08-09T17:55:54.316Z","1.0.1":"2026-08-13T16:27:59.117Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/flow-engine/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/flow-engine/","keywords":["flow","flow-engine","workflow","workflow-engine","orchestration","state-machine","steps","pipeline","json-config","expressions","typescript","type-safe","durable","checkpoint","resume","suspend","cancel","retry","zero-dependencies"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/flow-engine.git"},"description":"Typed flow engine with JSON-serializable flows, safe string expressions, pause/cancel/suspend, checkpoints and restore. Works in the browser and on the backend.","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# @dmytromykhailiuk/flow-engine\n\nTyped flow engine with JSON-serializable flows, safe string expressions, pause/cancel/suspend, checkpoints and restore. Works the same in the browser and on the backend.\n\n> **Full documentation: [Docs](https://dmytromykhailiuk.github.io/flow-engine/)** — every node type, every option, with examples.\n\n> **The one rule:** steps are code, flows are data. Anything that talks to the world is registered as a typed step handler in the builder; everything that decides *what happens when* — branching, loops, retries, conditions — lives in a JSON config that can be stored, sent from a backend, versioned and validated.\n\nYou have multi-step processes: a checkout, an onboarding, a document pipeline, a sync job. The steps themselves are easy — the hard part is the choreography around them: branching on intermediate results, retrying the flaky ones, cancelling when the user leaves, surviving a page refresh or a worker crash, and not littering the codebase with one-off state machines. This package is that choreography, factored out once.\n\nEach run carries an explicit context — `{ input, steps, vars }` — where every step's output is addressable by name. Conditions and data wiring are written as string expressions (`\"steps.loadUser.user.age >= 18\"`) evaluated by a built-in safe interpreter: no `eval`, no `new Function`, no reaching globals or prototypes, so configs from a backend are data, not code. After every node the engine emits a JSON checkpoint; `restore()` continues an unfinished run after a refresh — or on a different machine.\n\n## Install\n\n```bash\nnpm i @dmytromykhailiuk/flow-engine\n```\n\n## Quick start\n\n```ts\nimport { createFlowRunner } from \"@dmytromykhailiuk/flow-engine\";\n\nconst runner = createFlowRunner((b) =>\n  b\n    .registerStep(\"loadUser\", async (input: { id: string }, _cfg: void, ctx) => ({\n      user: await api.getUser(input.id, { signal: ctx.signal }),\n    }))\n    .registerStep(\"greet\", (input: { name: string }, cfg: { greeting: string }) => ({\n      message: `${cfg.greeting}, ${input.name}!`,\n    }))\n    .registerCondition(\"isPremium\", (ctx) => (ctx.steps.loadUser as any)?.user.plan === \"premium\")\n    .registerFlow(\"welcome\", {\n      nodes: [\n        { step: \"loadUser\", input: { id: \"input.userId\" } },\n        {\n          if: [\n            {\n              when: \"$isPremium\",\n              flow: [{ step: \"greet\", input: { name: \"steps.loadUser.user.name\" }, config: { greeting: \"Welcome back\" } }],\n            },\n          ],\n          else: [{ step: \"greet\", input: { name: \"steps.loadUser.user.name\" }, config: { greeting: \"Hello\" } }],\n        },\n      ],\n      output: \"steps.greet.message\",\n    }),\n);\n\nconst handle = runner.run(\"welcome\", { userId: \"42\" });\n\nhandle.pause();            // parks at the next step boundary\nhandle.resume();\nhandle.cancel(\"navigated away\"); // aborts ctx.signal in the running step\n\nconst result = await handle.getResult(); // never rejects\nif (result.status === \"completed\") console.log(result.output); // \"Welcome back, Ada!\"\n```\n\n## What's in a flow\n\nTen node types cover the control flow; each is JSON:\n\n| Node | Purpose |\n| --- | --- |\n| `step` | run a registered handler — with `input` mapping, `config`, `timeout`, `retry`, `onError` |\n| `if` | first branch whose `when` holds wins; optional `else` |\n| `loop` | `{ times }`, `{ while, max }` or `{ forEach, as }` over data |\n| `parallel` | branches via `Promise.all`; `settle: \"all\"` to collect every error |\n| `subflow` | call another flow like a function — own context, output stored under `as` |\n| `assign` | compute vars with expressions — the safe replacement for an eval step |\n| `finish` | early exit; with a `tag` it unwinds nested flows like a labeled break |\n| `break` / `continue` | loop control, optionally by loop `label` |\n| `try` | `try`/`catch`/`finally` over nodes; `catch` sees the `error` root |\n\nExpressions may read `input`, `steps`, `vars` — plus `loop` inside loops, `error` inside `catch`/`retry.when` — and call a whitelist of pure methods. `\"$name\"` references a predicate registered in code.\n\n## Never lose a run\n\n```ts\nconst runner = createFlowRunner(setup, {\n  onEvent: (e) => {\n    if (e.type === \"checkpoint\") localStorage.setItem(`flow:${e.runId}`, JSON.stringify(e.snapshot));\n    if (e.type === \"flowEnd\") localStorage.removeItem(`flow:${e.runId}`);\n  },\n});\n\n// after a refresh — continue everything unfinished:\nfor (const key of Object.keys(localStorage)) {\n  if (key.startsWith(\"flow:\")) runner.restore(JSON.parse(localStorage.getItem(key)!));\n}\n```\n\nSnapshots are self-contained JSON (position, context, and the flow config itself). A step interrupted mid-flight re-runs on restore — at-least-once — and `ctx.executionKey` gives you a stable idempotency key to dedupe side effects.\n\nFor long external waits there is `ctx.suspend()`: the run ends in this process with a final snapshot, freeing memory, queue slots and locks; when the reply arrives, `restore()` re-runs the waiting step, which picks the result up by its `executionKey`.\n\n## Sequential when you say so\n\nRuns are parallel by default. Give flows a queue and they serialize FIFO:\n\n```ts\nrunner.run(\"syncCart\", input, { queue: \"cart\" }); // or  { queue: \"cart\" }  in the flow config\n```\n\nQueues ride on [`@dmytromykhailiuk/execution-blocker`](https://www.npmjs.com/package/@dmytromykhailiuk/execution-blocker) — the package's only dependency, itself zero-dep. Pass your own instance via `queueAdapter` to share queues with non-flow code, or a Redis-backed `{ run(id, fn) }` adapter to make them cluster-wide.\n\n## And the rest\n\n- **Guards** — declarative preconditions with `onSuccess`/`onFailure` side flows; a failing guard rejects the run (`status: \"rejected\"`), which is neither success nor error.\n- **Hooks** — named entry points: `runner.runHook(\"onLogin\", input)` runs the first entry whose condition matches.\n- **Background flows** — \"when this condition over external sources becomes true, run that flow\"; sources are anything with `{ getSnapshot, subscribe }` (signals, Redux, `useSyncExternalStore`).\n- **applyConfig** — load or update the whole declarative layer (flows/hooks/guards/background) from JSON at runtime, atomically; active runs keep the version they started with.\n- **Events** — a typed stream (`flowStart`, `stepEnd`, `checkpoint`, `heartbeat`, …) for logging, devtools or distributed-lock lease renewal.\n- **validateFlow** — every expression parsed, every reference checked, with exact paths; `run()` refuses a broken config synchronously.\n\n## TypeScript\n\nThe builder accumulates a registry in its generics: step ids autocomplete inside configs, each step's `config` is type-checked by id, registered flow and predicate names are suggested. What the type system cannot see — the contents of expression strings, JSON from a backend — is covered by runtime validation with precise error paths. `FlowResult` is a discriminated union (`completed | rejected | cancelled | failed | suspended`), so handling every outcome is a `switch`, not guesswork.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}