{"_id":"@allma/flow-builder","_rev":"2-9580b91cec2f91fe59db389ea77c79e7","name":"@allma/flow-builder","dist-tags":{"latest":"0.3.0"},"versions":{"0.2.0":{"name":"@allma/flow-builder","version":"0.2.0","keywords":["allma","flow","builder","dsl","flows-as-code","typescript","zod","serverless","ai","orchestration"],"author":{"name":"@webjema"},"license":"Apache-2.0","_id":"@allma/flow-builder@0.2.0","maintainers":[{"name":"webjema","email":"webjema@gmail.com"}],"homepage":"https://docs.allma.dev","bugs":{"url":"https://github.com/ALLMA-dev/allma-core/issues"},"bin":{"allma-flows":"dist/cli/allma-flows.js"},"dist":{"shasum":"5c36639418c6568f4bac8d4bcfc582fad91044e5","tarball":"https://registry.npmjs.org/@allma/flow-builder/-/flow-builder-0.2.0.tgz","fileCount":82,"integrity":"sha512-uweZmGPv25EZV9kGHa9LJTrKYDf9krtGh6LXGHaQv8lMhqWRDBJ9rD12pL8RVji+lTiwkTsCjLB4X2GuNJOiyg==","signatures":[{"sig":"MEYCIQDGubyxsm6OnENu3gTNrDbKNP+nd1bC+7vmqO4ynOO0HQIhAOvAnSTTetyLaW5tV+6ZRcAjeMC1fa6z+ojRMhKxGWtY","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":244449},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"d396aa80d26a6f53a4bada1b9c018d19550b261c","scripts":{"lint":"eslint .","test":"vitest run","build":"tsc","type-cost":"node scripts/type-cost-guard.mjs","test:watch":"vitest"},"_npmUser":{"name":"webjema","email":"webjema@gmail.com"},"repository":{"url":"git+https://github.com/ALLMA-dev/allma-core.git","type":"git","directory":"packages/flow-builder"},"_npmVersion":"10.8.2","description":"TypeScript builder and CLI for authoring Allma flow definitions as code, with compile-time type safety and a deterministic JSON artifact.","directories":{},"_nodeVersion":"20.20.2","dependencies":{"@allma/core-sdk":"^1.1.1","@allma/core-types":"^1.7.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^3.22.4","eslint":"^9.8.0","vitest":"^1.2.0","typescript":"^5.9.3","@types/node":"^20.0.0","typescript-eslint":"^8.0.0"},"peerDependencies":{"zod":"^3.22.4"},"_npmOperationalInternal":{"tmp":"tmp/flow-builder_0.2.0_1785853455024_0.48314787054398733","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@allma/flow-builder","version":"0.3.0","description":"TypeScript builder and CLI for authoring Allma flow definitions as code, with compile-time type safety and a deterministic JSON artifact.","main":"dist/index.js","types":"dist/index.d.ts","type":"module","bin":{"allma-flows":"dist/cli/allma-flows.js"},"scripts":{"build":"tsc","test":"vitest run","test:watch":"vitest","lint":"eslint .","type-cost":"node scripts/type-cost-guard.mjs"},"author":{"name":"@webjema"},"license":"Apache-2.0","homepage":"https://docs.allma.dev","repository":{"type":"git","url":"git+https://github.com/ALLMA-dev/allma-core.git","directory":"packages/flow-builder"},"keywords":["allma","flow","builder","dsl","flows-as-code","typescript","zod","serverless","ai","orchestration"],"publishConfig":{"access":"public"},"dependencies":{"@allma/core-sdk":"^1.2.0","@allma/core-types":"^1.8.0"},"devDependencies":{"@types/node":"^22.13.0","eslint":"^9.8.0","typescript":"~5.7.3","typescript-eslint":"^8.0.0","vitest":"^1.2.0","zod":"^3.22.4"},"peerDependencies":{"zod":"^3.22.4"},"gitHead":"bba7d73bb962d823f5f564ee24628aa351deda41","_id":"@allma/flow-builder@0.3.0","bugs":{"url":"https://github.com/ALLMA-dev/allma-core/issues"},"_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-QN1zPN5NBp+63WzXiwCrL2JdxXMwQQuwpF9vzij1P8otaNrckA0BMmz8RIlBw1RD+a6D56M4eeoGx9EySsPm/w==","shasum":"4e1ba0a5447fea6633feabb5749f4092f1ba66bd","tarball":"https://registry.npmjs.org/@allma/flow-builder/-/flow-builder-0.3.0.tgz","fileCount":82,"unpackedSize":245512,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBSasIat+DYmTtgEbLC4KLB4fhGceHP/czz6cn8NbHUZAiB0aJVQg/CFfAtS4DfBwgcTAz2YJx9KEkqLr1OO6eBGRQ=="}]},"_npmUser":{"name":"webjema","email":"webjema@gmail.com"},"directories":{},"maintainers":[{"name":"webjema","email":"webjema@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/flow-builder_0.3.0_1785929147061_0.17105560412194554"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-04T14:24:14.823Z","modified":"2026-08-05T11:25:47.395Z","0.2.0":"2026-08-04T14:24:15.177Z","0.3.0":"2026-08-05T11:25:47.196Z"},"bugs":{"url":"https://github.com/ALLMA-dev/allma-core/issues"},"author":{"name":"@webjema"},"license":"Apache-2.0","homepage":"https://docs.allma.dev","keywords":["allma","flow","builder","dsl","flows-as-code","typescript","zod","serverless","ai","orchestration"],"repository":{"type":"git","url":"git+https://github.com/ALLMA-dev/allma-core.git","directory":"packages/flow-builder"},"description":"TypeScript builder and CLI for authoring Allma flow definitions as code, with compile-time type safety and a deterministic JSON artifact.","maintainers":[{"name":"webjema","email":"webjema@gmail.com"}],"readme":"# @allma/flow-builder\n\nAuthor Allma **flow definitions in TypeScript** — with compile-time type safety,\nrefactor-safe references, a strict build-time validation gate, and a\ndeterministic JSON artifact the existing CDK config-importer deploys unchanged.\n\nThis is a **build-time tool**. It introduces no new runtime, orchestrator, or\nDynamoDB schema; it emits the existing `AllmaExportFormat` JSON. Consume it as a\n`devDependency` in an example/consumer app.\n\n```ts\nimport { defineFlow, s3DataLoad, llmInvocation, s3DataSave, deployVar } from '@allma/flow-builder';\n\nconst flow = defineFlow({\n  id: 'basic-extract-and-store',\n  description: 'Load → summarize → store',\n  variables: { outputBucket: deployVar('basic-deployment-output-{{stage}}') },\n});\n\nconst s = flow.steps({                                   // Phase 1: declare\n  load:      s3DataLoad({ sourceS3Uri: 's3://in/doc.txt', outputFormat: 'TEXT' }),\n  summarize: llmInvocation({ llmProvider: 'AWS_BEDROCK', modelId: 'anthropic.claude-3-sonnet-20240229-v1:0' }),\n  store:     s3DataSave({ destinationS3UriTemplate: 's3://{{flow_variables.outputBucket}}/out.json' }),\n});\n\ns.load.next(s.summarize);                                // Phase 2: wire (refs, not strings)\ns.summarize.next(s.store);\nflow.start(s.load);\n\nexport default flow;\n```\n\nSee [`samples/basic-extract-and-store.flow.ts`](./samples/basic-extract-and-store.flow.ts)\nfor the full worked example (with a fallback step).\n\n## Why two phases\n\n`flow.steps({...})` declares every step first and returns a typed record of\n**refs**. Wiring then uses those refs — `.next(ref)`, `.when(condition, ref)`,\n`.onError({ fallback: ref })`, `flow.start(ref)`. Because every ref exists before\nwiring begins, forward and backward edges (cycles, self-loops) are symmetric and\nfully typed — there is **no string-ref escape hatch** to drift out of sync.\n\n## Step factories\n\n- **17 typed-payload factories**, one per non-module step type: `llmInvocation`,\n  `apiCall`, `mcpCall`, `customLambdaInvoke`, `parallelForkManager`,\n  `startSubFlow`, `startFlowExecution`, `noOp`, `endFlow`,\n  `waitForExternalEvent`, `pollExternalApi`, `sqsSend`, `snsPublish`,\n  `emailSend`, `emailStartPoint`, `scheduleStartPoint`, `fileDownload`. Each\n  factory's config is typed from its **own leaf payload schema**.\n- **16 typed module wrappers**, one per registered system module (config typed\n  from `SYSTEM_MODULE_CONFIG_SCHEMAS`): `s3DataLoad`, `dynamoDataLoad`,\n  `ddbQueryToS3Manifest`, `s3ListFiles`, `sqsGetQueueAttributes`,\n  `sqsReceiveMessages`, `s3DataSave`, `dynamoUpdateItem`, `dynamoQueryAndUpdate`,\n  `arrayAggregator`, `composeObjectFromInput`, `dateTimeCalculator`,\n  `flattenArray`, `generateArray`, `joinData`, `generateUuid`.\n- **4 generic escape hatches** for any other (e.g. consumer-defined) module:\n  `dataLoad`, `dataSave`, `dataTransform`, `customLogic` — `{ moduleIdentifier,\n  customConfig }`, with `customConfig` left opaque.\n\nA completeness test fails CI if a new `StepType` lacks a factory, or a newly\nregistered module lacks a typed wrapper.\n\n## The build gate\n\n`build()` is strict and runs, in order:\n\n1. **Deploy-token placement scan.** See below.\n2. **Strict leaf clones** — each step's payload is parsed against a `.strict()`\n   clone of its leaf schema, catching unknown keys that the persisted\n   `.passthrough()` schemas silently allow (a *stricter-than-deploy* gate).\n3. **`customConfig` via the registry** — module steps with a known\n   `moduleIdentifier` have their `customConfig` validated against the centralized\n   schema. This is the earliest point any `customConfig` is validated.\n4. **Shared `FlowAuthoringSchema`** — cross-references (start / transition /\n   default-next / fallback targets exist) and JSONPath well-formedness.\n\nFailures are aggregated and thrown as a single `FlowBuildError`, each issue\nprefixed with the offending step id and field path. `toExport()` wraps `build()`\nin the `AllmaExportFormat` envelope with a deterministic `exportedAt`.\n\n## Deploy variables vs. runtime templates\n\nTwo different `{{...}}` worlds — the builder keeps them straight:\n\n- **Deploy tokens** `{{stage}}`, `{{accountId}}`, `{{region}}` are substituted\n  by the CDK importer, and **only inside `flowVariables`**. Declare them with\n  `deployVar(...)` (which rejects unknown tokens eagerly) and place them in\n  `variables`. The build gate **errors** if one of these three appears anywhere\n  else — the importer would not render it and the runtime context has no such\n  key, so it would silently render empty.\n- **Runtime templates** like `{{flow_variables.x}}`, `{{steps_output.y}}`,\n  `{{config.z}}` are rendered by the execution-time Handlebars engine and are\n  **allowed in any string field**. (The build gate deliberately does *not* flag\n  these — only the three deploy tokens when misplaced.)\n\n## Config-as-code: prompts, step definitions & MCP connections\n\nThe cross-artifact entities a flow references can themselves be authored in code,\neach with the same strict gate and deterministic emit as `defineFlow`:\n\n```ts\nimport { definePrompt, defineStep, defineMcpConnection, llmInvocation } from '@allma/flow-builder';\n\nexport const summaryPrompt = definePrompt({\n  id: 'summary-prompt', name: 'Summary', content: 'Summarize: {{document}}',\n});\n\nexport const summarize = defineStep(\n  { id: 'summarize-doc', name: 'Summarize document' },\n  llmInvocation({ llmProvider: 'AWS_BEDROCK', modelId: 'anthropic.claude-3-sonnet-20240229-v1:0' }),\n);\n\nexport const githubMcp = defineMcpConnection({\n  id: 'github-mcp', name: 'GitHub MCP',\n  serverUrl: 'https://mcp.example.com', authentication: { type: 'NONE' },\n});\n```\n\nEach returns a typed **handle** (`{ id, kind, build(), toExport() }`). The handle\n*is* a typed object reference: pass it straight into a step instead of a bare\nstring id, and a renamed or deleted artifact becomes a **compile error**.\n\n```ts\nllmInvocation({ llmProvider: 'AWS_BEDROCK', modelId, promptTemplateId: summaryPrompt });\nmcpCall({ mcpConnectionId: githubMcp, toolName: 'search' });\nstartSubFlow({ subFlowDefinitionId: childFlow });        // a flow is a FlowRef too\ns.step.fromDefinition(summarize);                         // step-def handle\n```\n\nThe builder normalizes a handle to its `id` in the emitted artifact, so the wire\ncontract is unchanged. `external('id')` still wraps an intentional out-of-dir id.\n\n> These artifacts carry the fixed placeholder timestamp `1970-01-01T00:00:00.000Z`\n> for `createdAt`/`updatedAt` — byte-stable for the drift check, and overwritten by\n> the server on import (the deploy validator requires the field to be present).\n\n## Cross-artifact resolution\n\n`resolveReferences(flows, known)` (and `allma-flows check`) verify that every\n`subFlowDefinitionId` / `flowDefinitionId` / `promptTemplateId` /\n`stepDefinitionId` / `mcpConnectionId` resolves to a known artifact, or is wrapped\nin `external(id)` to document an intentional out-of-dir reference. When prompts,\nstep definitions, MCP connections and flows are built together (one `check`/`build`\nrun), locally-authored artifacts seed the catalog automatically, so a flow that\nreferences a sibling prompt handle resolves with no extra wiring.\n\n## Ergonomics: `jp()` and `class Flow`\n\n`jp('$.steps_output.x')` validates a JSONPath eagerly (a malformed path throws at\nauthor time) and returns it for use in `inputs`/`outputs` and conditions. Its\ncomparison builders emit transition conditions in the runtime evaluator's grammar:\n\n```ts\ns.poll.when(jp.eq('$.poll.status', 'DONE'), s.done);\ns.poll.when(jp.gt('$.poll.attempts', 5), s.giveUp);\n```\n\n`class Flow` is an imperative authoring facade over the same internals as\n`defineFlow`, for teams preferring OO construction (it produces identical artifacts):\n\n```ts\nconst flow = new Flow({ id: 'order-intake' });\nconst load = flow.addStep('load', s3DataLoad({ sourceS3Uri: 's3://in/x' }));\nconst done = flow.addStep('done', endFlow());\nload.next(done);\nflow.start(load);\nexport default flow;\n```\n\n## Typed context for mappings (opt-in)\n\n`flowContext<Ctx>()` returns a `jp`-shaped helper whose path argument is\nconstrained to the dotted key paths of a context type `Ctx` — so a stale or\nmistyped context path is a **compile error**, not a silent runtime miss:\n\n```ts\ninterface Ctx { steps_output: { summarize: { text: string } } }\nconst $ = flowContext<Ctx>();\n\ns.store.outputs({ [$('$.steps_output.summarize.text')]: '$.text' });   // ok\ns.store.outputs({ [$('$.steps_output.summarize.txet')]: '$.text' });   // compile error\n```\n\nThis is **opt-in** by design: the generic lives only on this helper (not threaded\nthrough every step), and the path-enumeration type is bounded to a recursion depth\nof 3 to stay clear of the inferred-type blow-up the package guards against (RFC §9).\nDefault `jp`/`inputs`/`outputs` remain plain `string`. A CI type-cost guard\nmeasures this file; deepen the bound deliberately and re-measure if you need it.\n\n## CLI\n\n```\nallma-flows build  \"flows/**/*.ts\" --out config/flows                  # TS -> deterministic *.<kind>.json\nallma-flows check  \"flows/**/*.ts\" [--out config/flows] [--remote <baseUrl>]\n                                                                       # validate + drift checks\nallma-flows eject  <flowId> --from \"config/flows/*.json\" [--out flows] # JSON -> *.flow.ts (adoption)\nallma-flows deploy \"flows/**/*.ts\" --remote <baseUrl> [--publish] [--overwrite false]\n                                                                       # promote via admin API (no CDK redeploy)\n```\n\nModules must `export default` a builder or a `define*` artifact handle. `build`\nemits one file per artifact, suffixed by kind (`*.flow.json`, `*.prompt.json`,\n`*.step.json`, `*.mcp.json`). Run under a TypeScript loader (e.g. `tsx`) when\npointing at `.ts` sources. Commit the generated JSON and run `allma-flows check`\nin CI: the byte-stable artifact makes the diff meaningful and drift between the\n`.ts` source and committed `.json` fails the build. With `--remote <baseUrl>` (and\nan `ALLMA_ADMIN_TOKEN` bearer token), `check` also fails if a code-owned flow's\n**deployed** copy was taken over in the Visual Editor.\n\n### `deploy` — promote to a running environment\n\n`allma-flows deploy` merges the built artifacts into one `AllmaExportFormat` and\n`POST`s it to the admin **`/v1/allma/import`** route (`ALLMA_ADMIN_TOKEN` bearer),\nso CI can ship a flow/prompt/step/MCP change without a CDK redeploy. It uses the\nsame importer as `cdk deploy`, so the same **version-slot contract** applies: a\nbrand-new flow id is created; an existing flow id only *updates* a version slot\nthat already exists (\"creating new versions for existing flows on import is not\nsupported\"). Bumping an existing flow to a not-yet-existing version is an admin\nversion-management step first; `deploy` surfaces the importer's per-item errors\nrather than masking them. `--publish` then publishes each imported flow version.\nThe network/auth lives behind a thin adapter, so `planDeploy`/`executeDeploy` are\npure and unit-tested with a stubbed adapter.\n\n## Coexistence with the Visual Editor\n\nCode- and editor-authored flows coexist under an explicit, per-flow ownership\nmodel (RFC §6):\n\n- The builder stamps `authoringSource: 'code'` and emits **no step positions**, so\n  the editor's existing Dagre auto-layout arranges code-owned flows on first open.\n- `@allma/admin-shell` opens `authoringSource === 'code'` flows **read-only**:\n  structural edits and Save are disabled (a \"Managed in code\" banner explains why),\n  while viewing and the single-step Sandbox stay available.\n- Ownership transfer is deliberate and one-way: `allma-flows eject` adopts a flow\n  **into** code (JSON → TS), and the editor's \"Unlock for visual editing\" action\n  flips ownership **back** to the editor.\n\n## `customConfig` validation (author-time)\n\nThe builder hard-enforces each module step's `customConfig` against the centralized\nregistry schema at **build time** — the earliest point any `customConfig` is\nvalidated. The shared `FlowDefinitionSchema` (the wire/storage contract) keeps the\nregistry check **advisory** (warn-mode in the importer): a required `customConfig`\nfield may legitimately be supplied at runtime via `inputMappings`, so a hard error\nthere would reject valid flows. Author in code to get the strict, earliest check.\n\n## Known limitations\n\n- `customConfig` validation checks shape/type via the registry but does not\n  reject unknown `customConfig` keys (the runtime strips them); leaf **payload**\n  keys are strict.\n- `defineStep` ignores graph-dependent wiring (transitions, `onError.fallback`) —\n  a stored step definition has no sibling steps; wire those on the step instance.\n- Typed-context (`flowContext`) checks **context-side** paths and is bounded to a\n  recursion depth of 3; `inputs()`/`outputs()` keys (step-input dot-paths) stay\n  untyped to avoid threading a pervasive generic through the step union (RFC §9).\n- `eject` covers flows (round-trips, including typed object references, which\n  serialize back to string ids); prompts/step-defs/MCP connections are authored\n  forward in code, not ejected.\n","readmeFilename":"README.md"}