@nanobpm/workflow 0.3.0 → 0.4.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/README.md CHANGED
@@ -34,7 +34,9 @@ Both compile to the same engine durability; pick per workflow.
34
34
  ### Declarative (with control flow, human-in-the-loop signals, and typed I/O)
35
35
 
36
36
  **The recommended surface.** Describe the flow as a **tree of nodes**: `w.run` (a
37
- locally-hosted service task), `w.task` (an external-worker service task), and
37
+ locally-hosted service task), `w.task` (an external-worker service task — job
38
+ type `${flowId}:${name}` by default, or pass `w.task(name, { jobType })` to
39
+ target an existing worker pool, e.g. a `senior:pr-review` agent token), and
38
40
  `w.signal` (a durable message catch that resumes via a correlated message — the
39
41
  human-in-the-loop path), composed with control-flow combinators:
40
42
 
@@ -178,7 +180,7 @@ once.
178
180
  | `defineWorkflow(id, orchFn)` | Imperative (replayed) workflow — experimental/internal. |
179
181
  | `defineFlow(id, [contracts,] build)` | Declarative flow: `run`/`task`/`signal` + `switch`/`branch`/`loop`/`break`/`continue`, with an optional typed contracts map. |
180
182
  | `envelope(name, fields)` | A typed data envelope; lifted into the model as a `nano:shape` + `dataEnvelope` wiring. |
181
- | `externalJobTypes(flow)` | The derived job types of a flow's external `task` steps. |
183
+ | `externalJobTypes(flow)` | The job types of a flow's external `task` steps (each overridable per-step via `w.task(name, { jobType })`). |
182
184
  | `WorkflowClient` | `deploy`, `start`, `signal`, `getInstance` over REST v2. |
183
185
  | `Worker` | Generic job runtime; routes job types → handlers, hosts the replay loop. |
184
186
  | `toBpmn(workflow)` | The derived BPMN XML (for inspection / deployment). |
@@ -33,12 +33,19 @@ export interface FlowBuilder<C extends FlowContracts = Record<string, never>> {
33
33
  run<K extends string>(name: K, handler: TypedHandler<VarsOf<C, K>, ResultOf<C, K>>): FlowBuilder<C>;
34
34
  /**
35
35
  * A durable activity served by a worker OUTSIDE this program (a BPMN service
36
- * task with the derived job type `${flowId}:${name}`, but no locally-hosted
37
- * handler). Use `externalJobTypes(flow)` to list the contract those workers
38
- * must poll. Its contract envelopes (if any) type the model, not a local
39
- * handler.
36
+ * task, but no locally-hosted handler). Its job type defaults to the derived
37
+ * `${flowId}:${name}`; pass `{ jobType }` to override it with an explicit
38
+ * worker token (e.g. a `rank:capability` token like `senior:pr-review` that a
39
+ * `c8ctl nano work` matrix subscribes to) so an existing pool of agents can
40
+ * service it without renaming the flow. The step name stays the BPMN element
41
+ * id; only the emitted `zeebe:taskDefinition` type changes. Use
42
+ * `externalJobTypes(flow)` to list the (possibly overridden) types those
43
+ * workers must poll. Its contract envelopes (if any) type the model, not a
44
+ * local handler.
40
45
  */
41
- task<K extends string>(name: K): FlowBuilder<C>;
46
+ task<K extends string>(name: K, opts?: {
47
+ jobType?: string;
48
+ }): FlowBuilder<C>;
42
49
  /**
43
50
  * A durable wait for an external/human event, correlated on a process
44
51
  * variable (a BPMN message intermediate catch event). Resume it with
@@ -89,8 +96,11 @@ export declare function defineFlow(id: string, build: (w: FlowBuilder) => void):
89
96
  /** Depth-first visit of every node in a flow tree (structural combinators
90
97
  * recurse into their bodies). */
91
98
  export declare function walkNodes(nodes: FlowNode[], visit: (n: FlowNode) => void): void;
92
- /** The derived job types of a flow's external `task` steps (anywhere in the
93
- * tree) — the contract workers outside this program must subscribe to. */
99
+ /** The job types of a flow's external `task` steps (anywhere in the tree) — the
100
+ * contract workers outside this program must subscribe to. Each is the derived
101
+ * `<flowId>:<stepName>` unless the step overrode it via `w.task(name,
102
+ * { jobType })`. Deduplicated (preserving first-seen order) since several steps
103
+ * may intentionally share one override token. */
94
104
  export declare function externalJobTypes(flow: DeclarativeFlow): string[];
95
105
  /** Derive an executable BPMN model from a declarative flow. */
96
106
  export declare function declarativeToBpmn(flow: DeclarativeFlow): string;
@@ -34,7 +34,7 @@
34
34
  // Typed data envelopes are LIFTED into the model (nano:shape + dataEnvelope
35
35
  // zeebe:property), so the generated .bpmn is ejectable to model-first with its
36
36
  // typed contracts intact.
37
- import { assertIdent, escapeXml, jobType, messageName } from "./xml.js";
37
+ import { assertIdent, assertJobType, escapeXml, jobType, messageName } from "./xml.js";
38
38
  /** Ids the emitter generates for structural nodes / flows / messages. A step
39
39
  * name that collides with one of these would produce a duplicate BPMN id and an
40
40
  * invalid model, so reject them at authoring time. */
@@ -77,9 +77,12 @@ function makeBuilder(id, out, ctx) {
77
77
  out.push({ kind: "run", name, envelopes: contractEnvelopes(ctx, name) });
78
78
  return b;
79
79
  },
80
- task(name) {
80
+ task(name, opts) {
81
81
  claimName(ctx, id, name);
82
- out.push({ kind: "task", name, envelopes: contractEnvelopes(ctx, name) });
82
+ const override = opts?.jobType;
83
+ if (override !== undefined)
84
+ assertJobType("task jobType", override);
85
+ out.push({ kind: "task", name, envelopes: contractEnvelopes(ctx, name), jobType: override });
83
86
  return b;
84
87
  },
85
88
  signal(name, opts) {
@@ -181,13 +184,22 @@ export function walkNodes(nodes, visit) {
181
184
  }
182
185
  }
183
186
  }
184
- /** The derived job types of a flow's external `task` steps (anywhere in the
185
- * tree) — the contract workers outside this program must subscribe to. */
187
+ /** The job types of a flow's external `task` steps (anywhere in the tree) — the
188
+ * contract workers outside this program must subscribe to. Each is the derived
189
+ * `<flowId>:<stepName>` unless the step overrode it via `w.task(name,
190
+ * { jobType })`. Deduplicated (preserving first-seen order) since several steps
191
+ * may intentionally share one override token. */
186
192
  export function externalJobTypes(flow) {
193
+ const seen = new Set();
187
194
  const types = [];
188
195
  walkNodes(flow.steps, (n) => {
189
- if (n.kind === "task")
190
- types.push(jobType(flow.id, n.name));
196
+ if (n.kind !== "task")
197
+ return;
198
+ const type = n.jobType ?? jobType(flow.id, n.name);
199
+ if (seen.has(type))
200
+ return;
201
+ seen.add(type);
202
+ types.push(type);
191
203
  });
192
204
  return types;
193
205
  }
@@ -224,7 +236,7 @@ class Compiler {
224
236
  this.envelopes.set(env.name, env.fields);
225
237
  }
226
238
  addServiceTask(node) {
227
- const type = jobType(this.flow.id, node.name);
239
+ const type = node.jobType ?? jobType(this.flow.id, node.name);
228
240
  this.recordEnvelope(node.envelopes?.in);
229
241
  this.recordEnvelope(node.envelopes?.out);
230
242
  const props = [];
package/dist/layout.js CHANGED
@@ -43,8 +43,9 @@ export async function layoutBpmn(bpmnXml) {
43
43
  "Install it to generate diagram layout: npm i bpmn-auto-layout", { cause });
44
44
  }
45
45
  const result = await layoutProcess(bpmnXml);
46
- // Version-shape normalization: bpmn-auto-layout >= 1.4 (and `main`) resolves to
47
- // `{ xml, warnings }`; 1.3.x resolves to the laid-out XML string directly.
46
+ // Version-shape normalization: bpmn-auto-layout 2.x (and >= 1.4) resolves to
47
+ // `{ xml, warnings }`; 1.3.x resolved to the laid-out XML string directly. We
48
+ // pin >= 2.0.0-alpha.2 but normalize both so the SDK tolerates the whole range.
48
49
  return typeof result === "string" ? result : result.xml;
49
50
  }
50
51
  /**
package/dist/types.d.ts CHANGED
@@ -46,6 +46,7 @@ export type FlowNode = {
46
46
  kind: "task";
47
47
  name: string;
48
48
  envelopes?: NodeEnvelopes;
49
+ jobType?: string;
49
50
  } | {
50
51
  kind: "signal";
51
52
  name: string;
package/dist/xml.d.ts CHANGED
@@ -7,4 +7,5 @@ export declare const messageName: (workflowId: string, step: string) => string;
7
7
  /** The single orchestrator job type of an imperative workflow. */
8
8
  export declare const orchestrateType: (workflowId: string) => string;
9
9
  export declare function assertIdent(kind: string, value: string): void;
10
+ export declare function assertJobType(kind: string, value: string): void;
10
11
  export declare function assertWorkflowIds(wf: Workflow): void;
package/dist/xml.js CHANGED
@@ -19,6 +19,16 @@ export function assertIdent(kind, value) {
19
19
  throw new Error(`${kind} "${value}" is not a valid BPMN identifier (expected an NCName)`);
20
20
  }
21
21
  }
22
+ /** An explicit job type override (e.g. a `rank:capability` worker token like
23
+ * `senior:pr-review`). Unlike a step name it is NOT a BPMN element id, so it may
24
+ * carry the `:`/`+` token delimiters a `c8ctl nano work` matrix uses; it only
25
+ * has to be a non-empty, whitespace-free token. */
26
+ const JOB_TYPE = /^[A-Za-z0-9_][A-Za-z0-9_.:+-]*$/;
27
+ export function assertJobType(kind, value) {
28
+ if (typeof value !== "string" || !JOB_TYPE.test(value)) {
29
+ throw new Error(`${kind} "${value}" is not a valid job type (expected a non-empty token of letters, digits, and _ . : + -)`);
30
+ }
31
+ }
22
32
  export function assertWorkflowIds(wf) {
23
33
  assertIdent("workflow id", wf.id);
24
34
  if (wf.kind === "declarative") {
@@ -29,10 +39,14 @@ function assertNodeNames(nodes) {
29
39
  for (const node of nodes) {
30
40
  switch (node.kind) {
31
41
  case "run":
32
- case "task":
33
42
  case "signal":
34
43
  assertIdent("step name", node.name);
35
44
  break;
45
+ case "task":
46
+ assertIdent("step name", node.name);
47
+ if (node.jobType !== undefined)
48
+ assertJobType("job type", node.jobType);
49
+ break;
36
50
  case "switch":
37
51
  for (const c of node.cases)
38
52
  assertNodeNames(c.body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/workflow",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Code-first durable orchestration for nanobpmn (ADR 0044/0045). Author a durable workflow as a declarative flow — `defineFlow(id, w => { w.run(...); w.signal(...); w.task(...) })` — and the SDK derives the executable BPMN model, the job types, and the message/correlation wiring, then hosts a generic worker. `w.run` is a local step, `w.signal` is a durable human-in-the-loop wait, `w.task` is a step serviced by an external worker. `defineFlow` is the single code-first surface; the imperative `defineWorkflow` replay function is experimental/internal, not a recommended API. No diagram, no task-type wiring, no correlation plumbing authored by hand — and you can optionally auto-generate a diagram (DI) for the derived model with `declarativeToLayoutedBpmn` via the optional `bpmn-auto-layout` peer dependency. Talks to a running nanobpmn gateway over the REST v2 API.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -37,11 +37,11 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.20.1",
40
- "bpmn-auto-layout": "^1.3.0",
40
+ "bpmn-auto-layout": "^2.0.0-alpha.2",
41
41
  "typescript": "^5.6.3"
42
42
  },
43
43
  "peerDependencies": {
44
- "bpmn-auto-layout": "^1.3.0"
44
+ "bpmn-auto-layout": "^2.0.0-alpha.2"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "bpmn-auto-layout": {
@@ -1,8 +1,8 @@
1
- // Minimal ambient types for `bpmn-auto-layout` (1.3.0 ships none). `layoutProcess`
2
- // adds diagram interchange (DI) to a BPMN XML string. The resolved shape changed
3
- // across versions: 1.3.x resolves to the laid-out XML string; >= 1.4 / `main`
4
- // resolves to `{ xml, warnings }`. We type the union and normalize at the call
5
- // site (see `layout.ts`).
1
+ // Minimal ambient types for `bpmn-auto-layout` (ships none of its own).
2
+ // `layoutProcess` adds diagram interchange (DI) to a BPMN XML string. The
3
+ // resolved shape has settled on `{ xml, warnings }` in 2.x (and >= 1.4); 1.3.x
4
+ // resolved to the laid-out XML string directly. We type the union and normalize
5
+ // at the call site (see `layout.ts`) so the SDK still works across that range.
6
6
  declare module "bpmn-auto-layout" {
7
7
  export function layoutProcess(
8
8
  xml: string,
@@ -46,7 +46,7 @@ import type {
46
46
  SwitchCase,
47
47
  } from "./types.js";
48
48
  import type { Envelope, EnvelopeField } from "./envelope.js";
49
- import { assertIdent, escapeXml, jobType, messageName } from "./xml.js";
49
+ import { assertIdent, assertJobType, escapeXml, jobType, messageName } from "./xml.js";
50
50
 
51
51
  // --- Authoring surface -------------------------------------------------------
52
52
 
@@ -82,12 +82,17 @@ export interface FlowBuilder<C extends FlowContracts = Record<string, never>> {
82
82
  run<K extends string>(name: K, handler: TypedHandler<VarsOf<C, K>, ResultOf<C, K>>): FlowBuilder<C>;
83
83
  /**
84
84
  * A durable activity served by a worker OUTSIDE this program (a BPMN service
85
- * task with the derived job type `${flowId}:${name}`, but no locally-hosted
86
- * handler). Use `externalJobTypes(flow)` to list the contract those workers
87
- * must poll. Its contract envelopes (if any) type the model, not a local
88
- * handler.
85
+ * task, but no locally-hosted handler). Its job type defaults to the derived
86
+ * `${flowId}:${name}`; pass `{ jobType }` to override it with an explicit
87
+ * worker token (e.g. a `rank:capability` token like `senior:pr-review` that a
88
+ * `c8ctl nano work` matrix subscribes to) so an existing pool of agents can
89
+ * service it without renaming the flow. The step name stays the BPMN element
90
+ * id; only the emitted `zeebe:taskDefinition` type changes. Use
91
+ * `externalJobTypes(flow)` to list the (possibly overridden) types those
92
+ * workers must poll. Its contract envelopes (if any) type the model, not a
93
+ * local handler.
89
94
  */
90
- task<K extends string>(name: K): FlowBuilder<C>;
95
+ task<K extends string>(name: K, opts?: { jobType?: string }): FlowBuilder<C>;
91
96
  /**
92
97
  * A durable wait for an external/human event, correlated on a process
93
98
  * variable (a BPMN message intermediate catch event). Resume it with
@@ -173,9 +178,11 @@ function makeBuilder<C extends FlowContracts>(id: string, out: FlowNode[], ctx:
173
178
  out.push({ kind: "run", name, envelopes: contractEnvelopes(ctx, name) });
174
179
  return b as unknown as FlowBuilder<C>;
175
180
  },
176
- task(name: string): FlowBuilder<C> {
181
+ task(name: string, opts?: { jobType?: string }): FlowBuilder<C> {
177
182
  claimName(ctx, id, name);
178
- out.push({ kind: "task", name, envelopes: contractEnvelopes(ctx, name) });
183
+ const override = opts?.jobType;
184
+ if (override !== undefined) assertJobType("task jobType", override);
185
+ out.push({ kind: "task", name, envelopes: contractEnvelopes(ctx, name), jobType: override });
179
186
  return b as unknown as FlowBuilder<C>;
180
187
  },
181
188
  signal(name: string, opts: { correlationKey: string }): FlowBuilder<C> {
@@ -286,12 +293,20 @@ export function walkNodes(nodes: FlowNode[], visit: (n: FlowNode) => void): void
286
293
  }
287
294
  }
288
295
 
289
- /** The derived job types of a flow's external `task` steps (anywhere in the
290
- * tree) — the contract workers outside this program must subscribe to. */
296
+ /** The job types of a flow's external `task` steps (anywhere in the tree) — the
297
+ * contract workers outside this program must subscribe to. Each is the derived
298
+ * `<flowId>:<stepName>` unless the step overrode it via `w.task(name,
299
+ * { jobType })`. Deduplicated (preserving first-seen order) since several steps
300
+ * may intentionally share one override token. */
291
301
  export function externalJobTypes(flow: DeclarativeFlow): string[] {
302
+ const seen = new Set<string>();
292
303
  const types: string[] = [];
293
304
  walkNodes(flow.steps, (n) => {
294
- if (n.kind === "task") types.push(jobType(flow.id, n.name));
305
+ if (n.kind !== "task") return;
306
+ const type = n.jobType ?? jobType(flow.id, n.name);
307
+ if (seen.has(type)) return;
308
+ seen.add(type);
309
+ types.push(type);
295
310
  });
296
311
  return types;
297
312
  }
@@ -352,8 +367,8 @@ class Compiler {
352
367
  this.envelopes.set(env.name, env.fields);
353
368
  }
354
369
 
355
- private addServiceTask(node: { name: string; envelopes?: NodeEnvelopes }): void {
356
- const type = jobType(this.flow.id, node.name);
370
+ private addServiceTask(node: { name: string; envelopes?: NodeEnvelopes; jobType?: string }): void {
371
+ const type = node.jobType ?? jobType(this.flow.id, node.name);
357
372
  this.recordEnvelope(node.envelopes?.in);
358
373
  this.recordEnvelope(node.envelopes?.out);
359
374
  const props: string[] = [];
package/src/layout.ts CHANGED
@@ -48,8 +48,9 @@ export async function layoutBpmn(bpmnXml: string): Promise<string> {
48
48
  );
49
49
  }
50
50
  const result = await layoutProcess(bpmnXml);
51
- // Version-shape normalization: bpmn-auto-layout >= 1.4 (and `main`) resolves to
52
- // `{ xml, warnings }`; 1.3.x resolves to the laid-out XML string directly.
51
+ // Version-shape normalization: bpmn-auto-layout 2.x (and >= 1.4) resolves to
52
+ // `{ xml, warnings }`; 1.3.x resolved to the laid-out XML string directly. We
53
+ // pin >= 2.0.0-alpha.2 but normalize both so the SDK tolerates the whole range.
53
54
  return typeof result === "string" ? result : result.xml;
54
55
  }
55
56
 
package/src/types.ts CHANGED
@@ -51,7 +51,7 @@ export type StepHandler = (job: Job) => Promise<JsonObject | void> | JsonObject
51
51
  * combinators carry nested `FlowNode[]` bodies. */
52
52
  export type FlowNode =
53
53
  | { kind: "run"; name: string; envelopes?: NodeEnvelopes }
54
- | { kind: "task"; name: string; envelopes?: NodeEnvelopes }
54
+ | { kind: "task"; name: string; envelopes?: NodeEnvelopes; jobType?: string }
55
55
  | { kind: "signal"; name: string; correlationKey: string; payload?: Envelope }
56
56
  | { kind: "switch"; subject: string; cases: SwitchCase[]; default?: FlowNode[] }
57
57
  | { kind: "branch"; condition: string; then: FlowNode[]; else?: FlowNode[] }
package/src/xml.ts CHANGED
@@ -27,6 +27,19 @@ export function assertIdent(kind: string, value: string): void {
27
27
  }
28
28
  }
29
29
 
30
+ /** An explicit job type override (e.g. a `rank:capability` worker token like
31
+ * `senior:pr-review`). Unlike a step name it is NOT a BPMN element id, so it may
32
+ * carry the `:`/`+` token delimiters a `c8ctl nano work` matrix uses; it only
33
+ * has to be a non-empty, whitespace-free token. */
34
+ const JOB_TYPE = /^[A-Za-z0-9_][A-Za-z0-9_.:+-]*$/;
35
+ export function assertJobType(kind: string, value: string): void {
36
+ if (typeof value !== "string" || !JOB_TYPE.test(value)) {
37
+ throw new Error(
38
+ `${kind} "${value}" is not a valid job type (expected a non-empty token of letters, digits, and _ . : + -)`,
39
+ );
40
+ }
41
+ }
42
+
30
43
  export function assertWorkflowIds(wf: Workflow): void {
31
44
  assertIdent("workflow id", wf.id);
32
45
  if (wf.kind === "declarative") {
@@ -38,10 +51,13 @@ function assertNodeNames(nodes: FlowNode[]): void {
38
51
  for (const node of nodes) {
39
52
  switch (node.kind) {
40
53
  case "run":
41
- case "task":
42
54
  case "signal":
43
55
  assertIdent("step name", node.name);
44
56
  break;
57
+ case "task":
58
+ assertIdent("step name", node.name);
59
+ if (node.jobType !== undefined) assertJobType("job type", node.jobType);
60
+ break;
45
61
  case "switch":
46
62
  for (const c of node.cases) assertNodeNames(c.body);
47
63
  if (node.default) assertNodeNames(node.default);