import type { DeclarativeFlow, FlowContracts, FlowNode, JsonObject, NodeEnvelopes, StepHandler, TimerStart } from "./types.js"; import type { Envelope } from "./envelope.js"; import type { HumanIoMapping } from "./io-mapping.js"; import "./nodes/index.js"; /** The TS payload type of a contract's input envelope (untyped fallback). */ export type InPayload = Ct extends { in: Envelope; } ? Ct["in"]["type"] & JsonObject : JsonObject; /** The TS payload type of a contract's output envelope (untyped fallback). */ export type OutPayload = Ct extends { out: Envelope; } ? Ct["out"]["type"] & JsonObject : JsonObject; /** The input payload type of step `K` under contracts `C`. */ export type VarsOf = K extends keyof C ? InPayload : JsonObject; /** The output payload type of step `K` under contracts `C`. */ export type ResultOf = K extends keyof C ? OutPayload : JsonObject; /** A typed handler for a `run` step: its job variables and result are resolved * from the flow contracts by the step name. */ export type TypedHandler = (job: { jobKey: string; processInstanceKey: string; elementId: string; type: string; variables: V; }) => Promise | R | void; /** A block: the callback that populates a nested body (a case, arm, or loop). */ export type Block = (b: FlowBuilder) => void; /** The typed builder for a flow body. * * `C` is the flow's contracts map (step name → `{ in, out }` envelopes). The * default is `object`, NOT `Record`: with a `never`-valued record * `keyof C` is `string`, so `VarsOf`/`ResultOf` route EVERY step name into * `InPayload`/`OutPayload`, which distribute over `never` to * `never` — typing an untyped flow's `job.variables` as `never` and forcing its * handler return to `void` (so no untyped flow that returns data compiles). With * `object`, `keyof C` is `never`, so every step falls back to the intended * `JsonObject`. The public `defineFlow` overload keeps * the stricter contracts constraint; this is only the untyped-builder default. */ export interface FlowBuilder { /** * A durable activity served by a worker THIS program hosts (a BPMN service * task; the handler runs in the in-process `Worker`). If `name` is a key in * the flow's contracts, the handler's job variables and return value are typed * from that contract's `in`/`out` envelopes; otherwise they are `JsonObject`. */ run(name: K, handler: TypedHandler, ResultOf>, opts?: { io?: HumanIoMapping; }): FlowBuilder; /** * A durable activity served by a worker OUTSIDE this program (a BPMN service * task, but no locally-hosted handler). Its job type defaults to the derived * `${flowId}:${name}`; pass `{ jobType }` to override it with an explicit * worker token (e.g. a `rank:capability` token like `senior:pr-review` that a * `c8ctl nano work` matrix subscribes to) so an existing pool of agents can * service it without renaming the flow. The step name stays the BPMN element * id; only the emitted `zeebe:taskDefinition` type changes. Use * `externalJobTypes(flow)` to list the (possibly overridden) types those * workers must poll. Its contract envelopes (if any) type the model, not a * local handler. */ task(name: K, opts?: { jobType?: string; io?: HumanIoMapping; }): FlowBuilder; /** * A durable wait for an external/human event, correlated on a process * variable (a BPMN message intermediate catch event). Resume it with * `WorkflowClient.signal(flow, name, correlationKeyValue, vars)`. The message * payload envelope, if any, comes from the contract's `in`. */ signal(name: K, opts: { correlationKey: string; }): FlowBuilder; /** * A durable wait for a point in time (a BPMN timer intermediate catch event). * Pass exactly one of `{ after }` — an ISO-8601 delay (`PT1M30S`, `P1DT6H`) or * FEEL `=`-expression measured from when the token arrives — or `{ at }` — an * absolute ISO-8601 instant (or FEEL expression). The engine holds the token * durably until the timer fires, then continues. Use it for in-flow delays and * scheduled continuations (e.g. "wait 24h, then re-poll"). */ timer(name: K, opts: { after: string; } | { at: string; }): FlowBuilder; /** * Make this flow's start event a durable TIMER start rather than an explicit * `client.start(...)`. Must be the first statement, at the top level. Pass * exactly one of `{ cycle }` — a recurring ISO-8601 interval (`R/PT1H`, * `R5/PT30M`) the engine re-fires each period — `{ after }` — a one-shot delay * from deployment — or `{ at }` — a one-shot absolute instant. A `cycle` start * is the model-native, durable, single-fire-per-cluster replacement for an * app-side cron: the schedule lives in the deployable model, and the engine * (not each app replica) owns firing it exactly once. */ startOn(spec: { cycle: string; } | { after: string; } | { at: string; }): FlowBuilder; /** * A multi-way exclusive choice (a BPMN exclusive gateway). `subject` is a FEEL * expression (usually a variable name); each case routes when `subject` equals * the case value. An optional `default` case is the unconditional fallback. */ switch(subject: string, cases: Record> & { default?: Block; }): FlowBuilder; /** * A two-way exclusive choice on a FEEL boolean `condition` (a BPMN exclusive * gateway). The `then` branch is guarded by the condition; the `else` branch * (the gateway default) runs otherwise. Omitting `else` skips to whatever * follows the branch when the condition is false. */ branch(condition: string, arms: { then: Block; else?: Block; }): FlowBuilder; /** * A durable loop (a back-edge to the loop head). The body runs, then control * returns to the top of the loop unless a branch calls `break()`. Nodes after * the loop run once `break()` is reached. */ loop(body: Block): FlowBuilder; /** * A static parallel fork/join (a pair of BPMN parallel gateways). Every block * runs concurrently on its own branch; control continues past `parallel` only * once ALL branches have reached the joining gateway (AND-join). Pass at least * two branch blocks. Unlike `switch`/`branch` (exclusive choice), no branch is * conditional — all of them run. */ parallel(branches: Block[]): FlowBuilder; /** * A data-driven fan-out over a runtime collection (a BPMN parallel * multi-instance activity). `collection` is a FEEL expression (no leading `=`) * evaluating to a list; one child instance of `body` runs per item, with the * item bound to the `itemVar` variable in the child's scope. A single-activity * body attaches the multi-instance characteristics to that activity; a * multi-step body is wrapped in an embedded multi-instance sub-process. Options: * - `sequential` — run children one at a time (a sequential MI) instead of all * at once (the default: a parallel MI). * - `outputCollection` / `outputElement` — collect each child's `outputElement` * (a FEEL expression, no leading `=`) into the `outputCollection` list * variable, in item order. * - `completionCondition` — a FEEL boolean (no leading `=`); when it holds * after a child completes, the remaining children are cancelled and the body * completes early. */ forEach(collection: string, itemVar: string, body: Block, opts?: { sequential?: boolean; outputCollection?: string; outputElement?: string; completionCondition?: string; }): FlowBuilder; /** Exit the enclosing loop (routes to whatever follows it). Only valid inside * a `loop`. */ break(): FlowBuilder; /** Jump straight back to the top of the enclosing loop, skipping the rest of * the body. Only valid inside a `loop`. */ continue(): FlowBuilder; } /** A builder method as stored on the dynamically-assembled builder. Its precise, * generic user-facing signature comes from the `FlowBuilder` interface (a * built-in method declared centrally, a slice method declaration-merged from * its module); this loose shape is only what the registry stores/installs. */ export type BuilderMethod = (...args: never[]) => unknown; /** The per-body authoring context a kind module's `build` factory closes over to * implement its builder method. Everything a method needs to append its node — * claim its step name, resolve contract envelopes, recurse into nested bodies, * and return the builder for chaining — without reaching into `declarative.ts` * internals or the shared `FlowNode` union. */ export interface BuildApi { /** The flow id (for error messages). */ readonly id: string; /** Whether this is the root (top-level) builder — e.g. `startOn` is root-only. */ readonly isRoot: boolean; /** The node list this builder appends to (`api.out.push(node)`). */ readonly out: FlowNode[]; /** The flow's typed I/O contracts, keyed by step name. */ readonly contracts: FlowContracts; /** The flow's handler registry — a `run`-like kind registers its handler here. */ readonly handlers: Record; /** The current loop-nesting depth (0 outside any loop) — `break`/`continue` * guard on it. */ readonly loopDepth: number; /** The builder itself, for chaining (`return api.self()`). */ self(): FlowBuilder; /** Populate a nested body block, returning its node list. `inLoop` deepens the * loop nesting (a `loop` body); use it for bodies that can `break`/`continue`. */ child(fn: (b: FlowBuilder) => void, inLoop: boolean): FlowNode[]; /** Populate a nested body that runs in its OWN token scope (an embedded * sub-process): `break`/`continue` cannot cross the boundary (loop depth 0). */ childScoped(fn: (b: FlowBuilder) => void): FlowNode[]; /** Claim a step name (rejects duplicates / reserved ids), as every leaf does. */ claim(name: string): void; /** Resolve the declared envelopes for a step name from the contracts. */ contractEnvelopes(name: string): { in?: Envelope; out?: Envelope; } | undefined; /** Lift a durable timer start onto the flow (used only by `startOn`). */ setStartTimer(t: TimerStart): void; } /** A dynamically-assembled builder satisfies `FlowBuilder` once every core * built-in method is installed. The check is a runtime type guard (not an `as` * cast): each own-property must be a function, AND every built-in method in * {@link BUILTIN_BUILDER_METHODS} must be present — so a missing or * tree-shaken node-kind registration fails fast here at builder assembly (a * clear "missing registered methods" error) instead of surfacing later as an * opaque `w.run is not a function` at authoring time. */ export declare function isFlowBuilder(x: object): x is FlowBuilder; /** Diagnostic for a failed builder assembly. Names the missing built-in * method(s) when a registration was tree-shaken/omitted so the failure is * actionable; otherwise, when every built-in is present but some own-property * isn't a function, names that offending property instead. Exported (like * {@link isFlowBuilder}) so the assembly invariant is unit-testable. */ export declare function assemblyFailureMessage(x: object): string; /** * Define a declarative flow. Pass a typed `contracts` map (keyed by step name) * to type each step's I/O and lift its data envelopes into the model; or omit it * for an untyped flow. `build(w)` declares a tree of nodes. */ export declare function defineFlow(id: string, contracts: C, build: (w: FlowBuilder) => void): DeclarativeFlow; export declare function defineFlow(id: string, build: (w: FlowBuilder) => void): DeclarativeFlow; /** Depth-first visit of every node in a flow tree. Recursion into a kind's * nested bodies is DISPATCHED through the registry (each kind's `walk` handler), * so a slice's structural combinator recurses without editing a central switch. * Fails fast (via `requireNodeKind`) on an unregistered kind — matching the * emitter — so a tree-shaken/missing registration surfaces as a clear error * rather than silently skipping recursion into that node's nested bodies. A * registered leaf kind with no `walk` handler is fine (nothing to recurse). */ export declare function walkNodes(nodes: FlowNode[], visit: (n: FlowNode) => void): void; /** The job types of a flow's external `task` steps (anywhere in the tree) — the * contract workers outside this program must subscribe to. Each is the derived * `:` unless the step overrode it via `w.task(name, * { jobType })`. Deduplicated (preserving first-seen order) since several steps * may intentionally share one override token. */ export declare function externalJobTypes(flow: DeclarativeFlow): string[]; /** A renderable BPMN element the emitter has placed. A kind's `emit` handler * adds one (or more) via `api.addNode(...)` (or a typed helper like * `api.addServiceTask`). */ export interface RenderNode { id: string; render(incoming: string[], outgoing: string[]): string; /** For exclusive gateways: the flow id of the unconditional default edge. */ defaultFlow?: string; /** Id of the embedded sub-process this node lives in, or undefined at the * top level (the root process). */ scope?: string; } /** A BPMN sequence flow being wired. A kind's `emit` handler creates danglers * with `api.newEdge(from)` and resolves them with `api.connect(edges, toId)`. */ export interface Edge { id: string; from: string; to?: string; condition?: string; name?: string; /** The scope (sub-process id, or undefined = root process) this flow is * nested in — so it renders inside the right container. */ scope?: string; } /** The enclosing-loop context threaded through `emit`: `break`/`continue` route * to `headId`, and `break` danglers collect in `breaks`. `null` outside a loop. */ export interface LoopCtx { headId: string; breaks: Edge[]; } /** The emitter primitives a kind's `emit` handler uses to place its BPMN nodes * and wire its sequence flows — WITHOUT reaching into `Compiler` internals or a * central emit switch. Built-in kinds and slice-added kinds emit through this * same surface. Node-render string helpers (`incomingOutgoing`, `escapeXml`, …) * are exported alongside for building custom `RenderNode.render` closures. */ export interface EmitApi { /** The flow id (for error messages). */ readonly flowId: string; /** Create a new (dangling) sequence flow from `from`; resolve its target with * `connect`. */ newEdge(from: string, opts?: { condition?: string; name?: string; }): Edge; /** Point every one of `incoming` at `toId`. */ connect(incoming: Edge[], toId: string): void; /** Record a referenced data envelope so it is lifted to a `nano:shape`. */ recordEnvelope(env?: Envelope): void; /** Emit a sequence of nodes, threading danglers; used to emit nested bodies. */ emitList(list: FlowNode[], incoming: Edge[], loop: LoopCtx | null): Edge[]; /** Place a fully custom BPMN element (a slice's own render closure). */ addNode(node: RenderNode): void; /** Place a `` with the standard taskDefinition/envelope * extension elements. `opts.mi` adds multi-instance characteristics; `opts.extraExt` * injects additional `` content (e.g. a * `zeebe:linkedResources` prompt binding) after the taskDefinition/properties, * so a variant task only supplies its delta rather than re-rendering the shell. */ addServiceTask(node: { name: string; envelopes?: NodeEnvelopes; jobType?: string; }, opts?: { mi?: string; extraExt?: string; }): void; /** Place a `` with a message event definition. */ addCatchEvent(node: { name: string; }): void; /** Place a `` with a timer event definition. */ addTimerCatchEvent(node: { name: string; after?: string; at?: string; }): void; /** Place a ``, returning it so `defaultFlow` can be set. */ addGateway(id: string, name?: string): RenderNode; /** Place a ``. */ addParallelGateway(id: string): RenderNode; /** Place an embedded `` whose body renders in its own scope. */ addSubProcess(id: string, mi: string): RenderNode; /** Place a plain none `` (for an embedded sub-process). */ addPlainStart(id: string): void; /** Place a plain `` (for an embedded sub-process). */ addPlainEnd(id: string): void; /** A fresh monotonic counter value for generated gateway/loop/sub ids. */ nextGw(): number; /** The current scope (enclosing sub-process id, or undefined at the top level). */ currentScope(): string | undefined; /** Open a sub-process scope (subsequent nodes/edges nest inside it). */ pushScope(id: string): void; /** Close the current sub-process scope. */ popScope(): void; } /** Derive an executable BPMN model from a declarative flow. */ export declare function declarativeToBpmn(flow: DeclarativeFlow): string; /** Wrap a raw FEEL expression as a Zeebe condition body (leading `=`). Exported * for slice-added combinators that emit conditional sequence flows. */ export declare const feel: (expr: string) => string; /** A FEEL equality test `subject = "value"`, with the value as a FEEL string. * Exported for slice-added combinators that emit an XOR gateway. */ export declare const feelEquals: (subject: string, value: string) => string; /** Render the ``/`` refs of a flow node. Exported * for slice `RenderNode.render` closures. */ export declare function incomingOutgoing(inc: string[], outg: string[]): string; /** Render only the `` refs (for end-like events). Exported. */ export declare const incomingOnly: (inc: string[]) => string; /** Render only the `` refs (for start-like events). Exported. */ export declare const outgoingOnly: (outg: string[]) => string;