From 0d3bf9131b79dca5a785675a5cd8c7dbf08d9dae Mon Sep 17 00:00:00 2001 From: delchev Date: Thu, 6 Aug 2026 13:40:38 +0300 Subject: [PATCH 1/2] spec(processes): parallel fork/join step (kind: parallel) A parallel step runs its branch steps concurrently and joins before next - a parallel-gateway fork/join. v1: >=2 single-step task branches joining directly, no branch chains or nested parallels (recorded under Planned). --- docs/reference.md | 1 + docs/spec/processes.md | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/reference.md b/docs/reference.md index e052dc5..b8964e5 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -159,5 +159,6 @@ The following are parsed (or reserved) but not yet materialised by a generator; - Reserved `function` values for upcoming presentations (`Board`, `Gantt`, `Timeline`). - **`manyToMany`** — parsed but never materialised; the supported shape is the [explicit intermediate entity](/spec/relations#many-to-many). - **Cross-model schedule source** — a schedule's `entity` must be local (the generate target may be cross-model). +- **Parallel branch chains and nested parallels** — a `kind: parallel` branch is a single step that joins directly; a branch that chains onward (its own `next`/`then`/boundary timer) or is itself `parallel` (nesting) is not yet materialised. - Event-driven document generation (produce a document on an event), a declarative state machine, and shadow audit-history entities (audit *columns* via `audit: true` ship today). - Arbitrary resolver-path task assignment beyond `assignee: personal`. diff --git a/docs/spec/processes.md b/docs/spec/processes.md index 4873077..2e52279 100644 --- a/docs/spec/processes.md +++ b/docs/spec/processes.md @@ -21,7 +21,7 @@ processes: Generates one process definition per `processes[]` entry (a standard workflow model plus its diagram layout, so a modeller renders it). -Step kinds: `userTask`, `serviceTask`, `decision`, `script`, `wait`, `end`. +Step kinds: `userTask`, `serviceTask`, `decision`, `script`, `wait`, `parallel`, `end`. ### Step routing — the linear chain and `next:` @@ -39,6 +39,19 @@ A `notify` service task stands alone: it cannot carry another action (`setField` A decision condition may walk **one hop** off the trigger entity (`customer.creditLimit > 10000`): a resolver step is generated before the gateway to load the related entity and rewrite the condition. +### parallel — concurrent branches (fork/join) + +A `parallel` step runs several branch steps **at the same time** and rejoins before the next step — two independent reviews of one order, say, instead of one after the other. It declares the `branches` to run concurrently and the `next` step to continue at once every branch is done: + +```yaml + - { name: reviews, kind: parallel, args: { branches: [techReview, commercialReview], next: consolidate } } + - { name: techReview, kind: userTask, args: { assignee: engineer, form: ReviewOrder } } + - { name: commercialReview, kind: userTask, args: { assignee: sales, form: ReviewOrder } } + - { name: consolidate, kind: serviceTask, args: { setRelationField: Status, value: 2, next: done } } +``` + +It is a **parallel-gateway fork/join**: a diverging gateway fans an unconditioned flow to each branch, and a converging gateway waits for **all** branches before continuing. The branch steps are off the linear chain (like decision targets). At least two `branches`; each is a single declared task step (`userTask` / `serviceTask` / `script`) that joins directly; `next` is a declared step or `end`. A branch that chains onward (its own `next`/`then`/boundary timer) and a branch that is itself `parallel` (nesting) are not part of this level of the specification. + ### wait — park the process on a data event A `wait` step parks the process until an entity lifecycle event resumes it — a case waiting for a reply, a flow waiting for a payment, an order waiting for its goods receipt: From 4d95ed30c29ee0233a2a92b18f6d24022458a3a4 Mon Sep 17 00:00:00 2001 From: delchev Date: Sun, 9 Aug 2026 09:32:49 +0300 Subject: [PATCH 2/2] spec(processes): a parallel branch is a chain, and forks may nest A branch continues through its own routing (its `next`, a decision's `then`/`else`, a boundary timer branch) and may itself be a `parallel`. Everything reachable that way belongs to the branch; a step declaring no routing is a terminal and joins, and the new routing literal `join` converges on the innermost enclosing join gateway - which is how a decision inside a branch rejoins from both arms. Records the rules that keep a branch a closed sub-flow: `join` only inside a branch (and never a step name), no `end` inside a branch, no step in two branches, a branch entered through its fork only, and `next` optional on a nested fork. Drops the corresponding "Planned" entry. --- docs/reference.md | 1 - docs/spec/processes.md | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index b8964e5..e052dc5 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -159,6 +159,5 @@ The following are parsed (or reserved) but not yet materialised by a generator; - Reserved `function` values for upcoming presentations (`Board`, `Gantt`, `Timeline`). - **`manyToMany`** — parsed but never materialised; the supported shape is the [explicit intermediate entity](/spec/relations#many-to-many). - **Cross-model schedule source** — a schedule's `entity` must be local (the generate target may be cross-model). -- **Parallel branch chains and nested parallels** — a `kind: parallel` branch is a single step that joins directly; a branch that chains onward (its own `next`/`then`/boundary timer) or is itself `parallel` (nesting) is not yet materialised. - Event-driven document generation (produce a document on an event), a declarative state machine, and shadow audit-history entities (audit *columns* via `audit: true` ship today). - Arbitrary resolver-path task assignment beyond `assignee: personal`. diff --git a/docs/spec/processes.md b/docs/spec/processes.md index 2e52279..2ae402e 100644 --- a/docs/spec/processes.md +++ b/docs/spec/processes.md @@ -50,7 +50,25 @@ A `parallel` step runs several branch steps **at the same time** and rejoins bef - { name: consolidate, kind: serviceTask, args: { setRelationField: Status, value: 2, next: done } } ``` -It is a **parallel-gateway fork/join**: a diverging gateway fans an unconditioned flow to each branch, and a converging gateway waits for **all** branches before continuing. The branch steps are off the linear chain (like decision targets). At least two `branches`; each is a single declared task step (`userTask` / `serviceTask` / `script`) that joins directly; `next` is a declared step or `end`. A branch that chains onward (its own `next`/`then`/boundary timer) and a branch that is itself `parallel` (nesting) are not part of this level of the specification. +It is a **parallel-gateway fork/join**: a diverging gateway fans an unconditioned flow to each branch, and a converging gateway waits for **all** branches before continuing. + +A branch is a **chain**, not a single step. It starts at the declared branch step and continues through that step's own routing — its `next`, a decision's `then` / `else`, a boundary `timeout` / `expire` branch — and it may itself be a nested `parallel`, which contributes its own fork/join pair. Everything reachable that way belongs to the branch and runs concurrently with the sibling branches: + +```yaml + - { name: reviews, kind: parallel, args: { branches: [techReview, commercial], next: consolidate } } + # a two-step chain: the second step declares no routing, so it joins + - { name: techReview, kind: userTask, args: { assignee: engineer, form: ReviewOrder, next: techSignoff } } + - { name: techSignoff, kind: serviceTask, args: { setRelationField: TechStatus, value: 2 } } + # a nested fork: it declares no `next`, so its join flows into the enclosing one + - { name: commercial, kind: parallel, args: { branches: [pricing, legal] } } + - { name: pricing, kind: decision, args: { if: "amount > 1000", then: escalate, else: join } } + - { name: escalate, kind: userTask, args: { assignee: manager, form: ReviewOrder } } + - { name: legal, kind: userTask, args: { assignee: legal, form: ReviewOrder } } +``` + +A branch and everything it reaches are off the linear chain (like decision targets), so their declaration order carries no meaning — and inside a branch there is **no positional fall-through**. A step routes explicitly, or, declaring no routing at all, is a branch **terminal** and flows into the join. The routing literal **`join`** converges on the innermost enclosing join gateway, which is how a decision inside a branch rejoins from both arms. + +Rules: at least two distinct `branches`, each a declared step other than the fork itself. `join` is valid only inside a branch, and no step may be named `join`. A branch must never route to `end` — the join would wait forever for a token that ended; end the process after the fork instead. A step may belong to only one branch: a step two concurrent tokens reach runs twice and still leaves the join waiting. A branch is entered through its fork only, so a branch converges on `join`, never on the fork's own `next` directly. A top-level fork declares `next` (a declared step or `end`); a **nested** fork may omit it, and then joins into its own enclosing join. ### wait — park the process on a data event