SRD-089.C — BPMN import: the flow nodes a graph is built from¶
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-08-11 |
| Owner | Ruslan Gabitov |
| Implements | ADR-024 v.4 §2.9 (the element set), §2.11 (scripts, wired here), §2.12 (business rules, wired here), §2.13 (the global-task refusal) |
| Upstream | ADR-005 v.5 (gateway semantics), ADR-031 v.1, ADR-027 v.1 |
| Related | SRD-089.A (the dispatch tables this extends), SRD-089.B (the script and decision-reference policies this wires) |
| Tracking | #284 — Part of |
This is the stage where the fence actually moves. .A rebuilt the spine and .B taught it the languages a definition carries; this one adds elements. It takes the flow nodes whose constructors need nothing the converter cannot yet supply, and it refuses — precisely and by name — the two that need something it cannot.
§1 Background (verified)¶
The importer maps nine elements. What this stage can add is decided not by appetite but by what each model constructor requires, and three of those requirements bite:
Script and business-rule tasks need only what .B already built.
activities.NewScriptTask(name, format, body string, …)
(pkg/model/activities/script_task.go:39) needs a format and a body — the
format policy landed in .B, and the body is <bpmn:script>, a child element
(elements/activities.md:212). activities.NewBusinessRuleTask(name,
decisionRef string, …) (brule_task.go:35) needs an opaque reference, which
.B's dialect table already reads.
Two gateways need nothing at all. gateways.NewInclusiveGateway(opts …)
(inclusive.go:41) and NewEventBasedGateway(opts …) (event_based.go:122)
take only options, like the two already imported.
The complex gateway needs something the XML does not carry.
NewComplexGateway fails unless an activation rule is set: "complex gateway
requires an activation rule (WithActivationThreshold or WithActivation)"
(complex.go:113-119), and those take an integer threshold or
Triples — per-gate token counts. BPMN carries activation as
activationCondition, an Expression (elements/gateways.md:158), which
the semantics describe as "an explicit Boolean expression over
per-incoming-flow token counts" (semantics/gateways.md:119).
Send and receive tasks need a message. NewSendTask(name, msg
*bpmncommon.Message, …) (send_task.go:29) and NewReceiveTask(name, msg
*bpmncommon.Message, …) (receive_task.go:58) both take a message
positionally. A message comes from a definitions-level <bpmn:message>
catalog, which the importer refuses today and which belongs with the message
events that share its machinery.
Intermediate events need a definition. NewIntermediateCatchEvent(name,
def flow.EventDefinition, …) rejects a nil definition
(intermediate_catch.go:38-45), as does the throw. There is no such thing as
a "plain" intermediate event to import ahead of the definitions.
§2 Requirements¶
Functional¶
- FR-1 —
<scriptTask>imports when itsscriptFormatnames Lua and it carries a<bpmn:script>body; .B's policy supplies every refusal. - FR-2 —
<businessRuleTask>imports carrying its decision reference, read from the recognized dialect or fromimplementation. A rule task with no resolvable reference is refused: a rule task with no decision fails at its first execution with far less context than the importer has now. - FR-3 —
<inclusiveGateway>and<eventBasedGateway>import, with thegatewayDirectionand (inclusive only)defaultthe existing gateway path already handles. - FR-4 —
<complexGateway>is refused, naming the mismatch (§4.1). - FR-5 — The
GlobalTaskfamily is refused with a distinct not supported yet error, separate from the unsupported-element error, per ADR-024 §2.13 — and so is a<callActivity>whosecalledElementnames one.
Landed as one half, and now superseded whole. The family's own five tags refuse as specified. The
calledElement-names-one clause was never coded:buildCallActivitychecks for a QName prefix and nothing else, so the refusal held only transitively — a file declaring the<globalTask>tripped on the declaration first, while acalledElementnaming an undeclared global task imported as an ordinary key. The gap is moot rather than open: SRD-096 removes the family's refusal entirely — a global task becomes a callable process, so acalledElementnaming one is a key the registry serves. - FR-6 —<sendTask>,<receiveTask>and the intermediate events keep their current refusal, unchanged, until .D supplies the message catalog and the event definitions their constructors require.
Non-functional¶
- NFR-1 — Every added element round-trips through export at least as far as the existing subset does, or the export gap is stated. (Export still covers the MVP set: ADR-024 §5 accepts import ⊃ export until slice 3, and this stage widens that gap knowingly.)
- NFR-2 —
make cigreen,cover-checkatCOVER_MIN. - NFR-3 — Refusals stay distinguishable. Three kinds now travel: not in the subset, not supported yet (a deferral), and not expressible (§4.1). A host that branches on them must be able to tell them apart.
§3 Models¶
// The new rows are rows — nodeBuilders gains four entries, and the process
// table derives from it as .A established, so nothing else changes.
var nodeBuilders = map[string]nodeBuilder{
// … the eight already there …
tagScriptTask: buildScriptTask,
tagBusinessRuleTask: buildBusinessRuleTask,
tagInclusiveGateway: buildGateway,
tagEventBasedGtw: buildGateway, // spelled short in the landed table
}
// nodeBody gains the script body — the first child that decides WHAT is
// built rather than decorating it, which is the shape .A's body-before-build
// reorder was made for.
type nodeBody struct {
docs []docSpec
script string // <bpmn:script>, for a scriptTask
}
// notYet is the third disposition ADR-024 §2.13 asks for. It was left out
// of .A deliberately — a disposition with no rows is untested code that
// looks like coverage — and arrives here with its first users.
const notYet dispositionKind = iota + 2
§4 Analysis & decisions¶
§4.1 The complex gateway is refused, and the reason is structural¶
| Option | Verdict |
|---|---|
Refuse <complexGateway>, naming the mismatch |
Chosen. |
Derive a threshold from the activationCondition |
Rejected. The condition is an arbitrary Boolean expression; recovering "how many incoming flows" from it is not mechanical, and a wrong threshold changes when the gateway fires — silently, at run time. |
| Import it with a default rule (say, all incoming) | Rejected, and worse: it produces a gateway that works, just not the one the file describes. |
This is the uncomfortable case in the whole slice: an element the engine executes that its XML form cannot reach. The model encodes activation as per-gate counts, which is a structured view of the same domain the standard describes with an expression — and structure cannot be recovered from an arbitrary expression by a converter. The refusal says exactly that and points at the programmatic constructor, so nothing about it reads like a gap someone forgot to fill.
§4.2 Refusals are now three kinds, and stay so¶
.A's dispositions were skip and refuse. This stage needs a third: the global-task refusal means "waiting on a subsystem", the complex-gateway refusal means "the two forms do not correspond", and the ordinary one means "not in the subset yet". They lead a reader to three different actions — wait, rewrite, or check the roadmap — so flattening them into one message would waste the only information the error carries.
§4.3 What a script task's body is¶
<bpmn:script> is a child element, so it arrives through the nodeBody
collected before construction — the mechanism .A introduced for documentation
and predicted would be needed "from the next stage on, [when] the children
decide WHAT to build". This is that stage: a script task with no body cannot
be constructed at all, since NewScriptTask requires a non-empty one.
§5 API deltas¶
None. Every element added here uses an existing constructor; the converter's
own surface is unchanged, and pkg/model is untouched.
§6 Test scenarios¶
| # | Scenario | Asserts |
|---|---|---|
| T-1 | A Lua <scriptTask> with a <bpmn:script> body |
imports; format and body reach the model (FR-1) |
| T-2 | scriptFormat of gofunc, javascript, and absent |
each refused with .B's reason (FR-1) |
| T-3 | A <scriptTask> with a format but no <script> |
refused, naming the missing body |
| T-4 | <businessRuleTask camunda:decisionRef="d1"> |
imports; DecisionRef() == "d1" (FR-2) |
| T-5 | <businessRuleTask> with no reference anywhere |
refused (FR-2) |
| T-6 | <inclusiveGateway> with default and gatewayDirection |
imports; default resolves through .A's deferred references (FR-3) |
| T-7 | <eventBasedGateway> |
imports (FR-3) |
| T-8 | <complexGateway>, with and without an activationCondition |
refused both times, naming the mismatch and the constructor (FR-4) |
| T-9 | Each global*Task, and a callActivity naming one |
refused as not supported yet, distinguishable from the ordinary refusal (FR-5, NFR-3) |
| T-10 | <sendTask>, <receiveTask>, <intermediateCatchEvent> |
still refused, unchanged (FR-6) |
| T-11 | A process using every newly added element, run on a thresher | registers and completes — the elements are wired, not merely constructed. Delivered late, by TestFlowNodesRunOnAThresher (script task, business rule task, inclusive gateway split/join) and TestEventBasedGatewayRunsOnAThresher (the gateway completes by waiting on its timer branch) — the stage landed without it, and a /check-srd audit before the status flip is what found the DoD item open. |
| T-12 | The .A/.B fixture corpus | unchanged verdicts |
§7 Milestones¶
| M | Content |
|---|---|
| M1 | The two gateways (FR-3) |
| M2 | <scriptTask> + the script body in nodeBody (FR-1, §4.3) |
| M3 | <businessRuleTask> + the decision reference (FR-2) |
| M4 | The refusals: complex gateway, the global-task family, notYet (FR-4, FR-5, §4.2) |
§8 Cross-doc¶
- Implements ADR-024 v.4 §2.9, §2.11–§2.13. The ADR is Draft, accepted when the element set completes at .E.
- SRD-089.A and SRD-089.B are landed stages of this same branch and are not edited here.
§9 Definition of Done¶
- FR-1…FR-6 wired, each with its §6 test.
make cigreen, judged from.ci/last-run.json.- T-11 demonstrates the added elements running, not merely importing.
/check-srdPASS;/pr-reviewruns once at the branch's end, after .E.
§10 Implementation summary¶
Filled retroactively at the .F landing, from the branch history.
Four milestones, in the order §7 planned them.
| M | Commit | What landed |
|---|---|---|
| M1 | 14682808 |
the inclusive and event-based gateways (FR-3) |
| M2 | be53b53c |
<scriptTask>, and the body reader that lets a child decide what is built (FR-1, §4.3) |
| M3 | 8a6fc6de |
<businessRuleTask> and its decision reference (FR-2) |
| M4 | 23c44461 |
the refusals: complex gateway, the global-task family, notYet (FR-4, FR-5, §4.2) |
M2's body reader became the load-bearing piece of the stage: the same children-before-constructor order later carries .D's event definitions and .F's properties, exactly because a child that decides WHAT to build cannot follow the construction it decides.
Open questions¶
None. §4.1 records the one uncomfortable outcome — an executable element the XML form cannot reach — as a decision with its alternatives, not as an open question.