Skip to content

SRD-089.I — BPMN import: the definitional Collaboration

Field Value
Status Accepted
Date 2026-08-18
Owner Ruslan Gabitov
Implements ADR-024 v.5 §2.15 (the document-level capability and the Collaboration obligation), §2.9 (the disposition table's "definitional only" row)
Upstream SAD-001 v.1.1 §14 N4 (the Collaboration metamodel is out of execution conformance; inter-process messaging is Message events), ADR-014 v.2 §1 (a MessageFlow connects pools; inside a process the send/receive is tasks and message events), ADR-016 v.1 (correlation, which the two-pool e2e rides)
Related SRD-089.A (the one-assembly parse this stage multiplies), SRD-089.H (the sibling closure stage)
Tracking #284 — Part of; the import half of #335

BPMN's §2.3.2 conformance clause obliges an importer to support Process diagrams "including its definitional Collaboration", and ADR-024 v.5 §2.15 already decided the shape: a document yields a set of processes through ImportDocument; <collaboration> is consumed, not represented — read for which participants exist and which process each refers to; <messageFlow> is reported as dropped, because it is the drawing of an exchange the engine performs through message events and correlation. This stage implements that decision. It is the last import gap that is neither capability-blocked nor standing.

§1 Background (verified)

A second <process> is refused today, and the refusal names this work. parseProcessElem returns unsupported when an assembly already exists, its comment saying "the document-level capability arrives with the collaboration slice" (dispatch.go:312-321). The whole parse threads ONE *assembly: parse()parseDefinitionsbuild(p, asm) (importer.go:328-341), and handleDefinitionsChild keeps the first non-nil assembly (importer.go:414-434). Zero processes is already an error (importer.go:405-410).

The seam is ready; the importer under it is not. convert.Result carries "the definitions the document carried, in document order" (document.go:38-39) — a slice — and today's ImportDocument always returns exactly one (importer.go:146-158). ADR-024 v.5 §2.15's operative sentences (:546-557): Import stays "the process of the document" and "becomes precise about the ambiguous cases: a document with exactly one executable process returns it; a document with none, or with several, is an error naming what it found and pointing at the document-level call" — with the co-requirement "Nothing existing breaks". And: "<collaboration> is read for its definitional content only — which participants exist and which process each refers to — and <messageFlow> is reported as dropped".

isExecutable is never read on import. The attribute exists only in the exporter (exporter.go:141); the importer ignores it. §2.15's Import rule needs it (§4.2).

Per-document vs per-process state already splits, mostly right. The parser owns the document: the catalogs, items, stores, rootDefs, the one id ledger (ids), exprLanguage, dropped (importer.go:260-). The assembly owns the process: proc, byID, declared, specs, flows, refs, datas, dataElems, spec (importer.go:206-257), with the catalog shared by pointer because a <message> may follow the <process>. Two pieces of document work sit inside the per-process build: buildItems (with its reportUnusedImports) and p.reportDataStores() — run once per call, they would double-report on a two-process document (§4.3). The document-wide ledger means cross-process id uniqueness is already enforced for free (SRD-089.F §4.11).

The engine runs multiple processes and messages between them, today. Each process registers and versions on its own; a message travels by name plus correlation key through the MessageBroker (msgflow/send.go:29-97, waiters/message.go:26-36), and a message start event with no incoming flow auto-instantiates the receiving process (RegisterProcess doc; instantiating_starts.go:16-30). The two-process worked example is examples/inter-instance-correlation — no Collaboration object anywhere. That is SAD-001 §14 N4's model: "inter-process messaging covered by Message events".

The Collaboration family has no extract coverage and no § pins. The vendored extract carries no element pages for Collaboration, Participant or MessageFlow — only the conformance prose (§2.3.2 quote, conformance.md:6; the out-of-scope table row, :165) and the moddle source for the attribute shapes: Participant.processRef (attr, ref → Process), MessageFlow.sourceRef/targetRef (attr, ref → InteractionNode) and messageRef (attr, ref → Message) (bpmn-moddle.json:1841-1945). The sections table deliberately pins no § for the family (dispatch.go:226-238), and the pin test locks the absence — restoring them is #334's business, not this stage's.

§2 Requirements

FR-1 — a document yields its process set. Every <process> parses into its own assembly over the shared document state; ImportDocument returns them all, in document order. The one-per-document work — the items, the unused-import report, the store obligations — runs once (§4.3).

FR-2 — Import becomes precise, breaking nothing. A document with exactly one process returns it, whatever its isExecutable says (the compatibility half). A document with several returns the one marked isExecutable="true" when exactly one is; none or several executable is an error naming the counts and pointing at ImportDocument (§4.2).

FR-3 — <collaboration> is consumed definitionally. Its <participant> children parse; a present processRef must resolve to a declared process (the converter's reference refusals — a dangling or wrong-kind ref is a broken file), while an absent one is a black-box pool — the moddle source makes the attribute optional, and a pool whose process is someone else's system is definitional by nature: consumed silently, nothing to validate. Ids join the ledger. Nothing is built: the participants' one definitional fact — which process each names, when it names one — is validated and consumed, per §2.15.

FR-4 — <messageFlow> is reported, never mapped. One Dropped entry per flow, under its own id, the reason saying what the engine does instead (message events and correlation). A messageRef naming a declared <message> is consumed silently; a dangling one refuses — the report must not launder a broken reference (§4.4).

FR-5 — cross-process wiring stays impossible, loudly. A sequence flow or association referencing across process boundaries fails exactly as it does today (per-assembly resolution); the ledger already refuses the shared-id confusions.

NFR-1 — no pkg/model change (there is nothing to model — §2.15's whole point). NFR-2 — diff-coverage ≥95% per committed milestone. NFR-3 — no fabricated content: no synthetic collaboration, no invented participant. NFR-4 — the model's and the seam's own messages keep their voice. NFR-5 — every § the code emits is one the extract supports (this stage emits none for the family — none exist, #334).

§3 Models

// participantSpec is one <participant> as read: its identity and the
// process it names.
type participantSpec struct {
    id, name   string
    processRef string
}

// collabSpec is one <collaboration> as read — its participants and the
// message flows to report. Nothing here is built.
type collabSpec struct {
    id, name     string
    participants []participantSpec
    flows        []messageFlowSpec
}

// messageFlowSpec is one <messageFlow>: reported under its own id, its
// messageRef consumed against the catalog.
type messageFlowSpec struct {
    id, name           string
    sourceRef, targetRef string
    messageRef         string
}

The parser gains asms []*assembly (document order) and collabs []collabSpec; procSpec gains executable bool.

§4 Analysis & decisions

§4.1 One parse, N assemblies

parseProcessElem stops refusing: each <process> gets its own assembly, appended to the parser in document order. The defsParser signature keeps its shape (the returned assembly is appended by the caller); everything inside a process — nodes, flows, data, lanes — keeps its per-assembly world untouched, which is what the .A design already isolated. The parser-level state the processes share (the catalogs, the items, the ledger, owner/container) shares exactly as the catalog already does: by living on the parser.

build runs once per assembly. The document-level work hoisted out of it (§4.3) runs once around the loop. The ledger keeps refusing duplicate ids across processes — the §4.11 property, now doing document-scale work.

§4.2 Import's selection rule, grounded in both §2.15 sentences

§2.15 says both "exactly one executable process returns it" and "nothing existing breaks". Read together, with the fact that most real single-process files never set isExecutable:

Processes Marked executable Import returns
1 any the process — the compatibility half; a one-process document's meaning cannot depend on a flag nobody sets
N > 1 exactly 1 that process — "the process of the document" is well-defined
N > 1 0 or several the §2.15 error: the counts, and "use ImportDocument" by name

ImportDocument is indifferent to the flag: the set is the set. The flag parses into procSpec and is not otherwise consumed — gobpm registers and runs whatever it is handed (SAD-001 §2.3.1 is the library's target), so a non-executable process still imports; the flag only disambiguates Import.

§4.3 The document-level work moves out of the per-process build

buildItems (and its unused-import report) and reportDataStores run inside build today — correct for one process, double-reporting for two. They hoist to the document loop: items built once and handed to each assembly (they are definitions-level declarations, shared by nature — each data element still takes its own copy through copyItem, so per-process isolation is untouched); the store obligations reported once. data.CreateDefaultStates hoists with them.

§4.4 The messageFlow report tells the truth twice

Reporting a <messageFlow> satisfies §2.15's letter; the decisions are in the details:

  • The reason names the mechanism, not just the drop: the engine performs the exchange through message events and correlation keys, so the reader learns the flow is represented differently, not lost — ADR-024 §2.14's "recognized and deliberately not mapped" contract.
  • A dangling messageRef refuses. The report is for content the converter understands; a reference to a message that does not exist is a broken file, and reporting it as "dropped" would launder the break. sourceRef/targetRef are consumed without resolution: they name interaction nodes across pools — participants included — and validating them would mean modeling the half of Collaboration §2.15 declines.
  • Ids join the ledger — collaboration, participant, messageFlow — closing the family's last unclaimed declarations.

§4.5 What stays refused

<participant> outside a collaboration, <messageFlow> outside one, and the rest of the family (participantAssociation, conversation kinds) keep today's refusals, with no § — the extract pins none (#334), and the pin test's deliberate-absence rows stay exactly as they are.

§4a Worked example

<bpmn:definitions xmlns:bpmn="…">
  <bpmn:message id="m1" name="order.placed"/>
  <bpmn:collaboration id="c1">
    <bpmn:participant id="pa1" name="Sales" processRef="P1"/>
    <bpmn:participant id="pa2" name="Fulfilment" processRef="P2"/>
    <bpmn:messageFlow id="mf1" sourceRef="send1" targetRef="start2"
                      messageRef="m1"/>
  </bpmn:collaboration>
  <bpmn:process id="P1" name="Sales" isExecutable="true"></bpmn:process>
  <bpmn:process id="P2" name="Fulfilment"></bpmn:process>
</bpmn:definitions>

yields: ImportDocumentResult.Processes = [P1, P2], in that order; res.Dropped carries exactly one entry — mf1/messageFlow, its reason naming message events and correlation; Import returns P1 (the one marked executable). A processRef="ghost" on a participant refuses; a second process marked executable flips Import to the §2.15 error while ImportDocument is unchanged.

§5 API deltas

None. ImportDocument's signature already returns the set; only its behavior widens to fill it.

§6 Test scenarios

# Scenario Asserts
T-1 two plain processes ImportDocument returns both, document order; both register and run
T-2 each process's elements build in its own world a node id in P1, data in P2 — no bleed; a duplicate id across them still refuses (§4.11)
T-3 one-process document Import unchanged, flag or no flag
T-4 two processes, one executable Import returns it
T-5 two processes, none / both executable the §2.15 error: counts + "ImportDocument" named
T-6 the document-level reports fire once one store obligation, one unused-import report, for a two-process document (§4.3)
T-7 collaboration with two participants consumed; ids in the ledger; nothing in Dropped for them
T-8 participant processRef dangling / wrong kind the converter's reference refusals
T-9 <messageFlow> one Dropped entry under its id, the mechanism named (§4.4)
T-10 messageFlow with a dangling messageRef refused, not laundered into the report
T-11 family ids join the ledger duplicates refuse
T-12 <participant> outside a collaboration still refused, no § (§4.5)
T-13 strangers/foreign children inside <collaboration> settled/skipped by the standing rules
T-14 e2e: two pools, a message start with a correlation key the producer process's send instantiates the consumer — one document, two processes, the engine's own mechanism

§7 Milestones

# Scope
M1 N assemblies: the parse loop, per-process build, hoisted document work, ImportDocument returns the set (FR-1, §4.3)
M2 Import's selection rule + isExecutable (FR-2, §4.2)
M3 The collaboration family: participants, message flows, reports, refusals (FR-3, FR-4, §4.4, §4.5)
M4 The two-pool e2e, the sweeps, §10

§8 Cross-doc

Outgoing: ADR-024 v.5, SAD-001 v.1.1, ADR-014 v.2, ADR-016 v.1, siblings SRD-089.A/.H — up or sideways. No ADR bump: this stage implements §2.15 as written.

§9 Definition of Done

  • FR-1…FR-5 wired and covered by §6.
  • make ci PASS on the committed branch, judged by .ci/last-run.json.
  • Diff-coverage ≥95% per committed milestone (NFR-2).
  • No pkg/model change (NFR-1).
  • §10 filled at landing.

§10 Implementation summary

Four milestones landed as two commits — the pairs proved inseparable in practice: the selection rule (M2) is ten lines once N assemblies exist, and the e2e (M4) is the natural closing test of the family (M3).

M Commit What landed
M1+M2 e715d238 N assemblies in document order, per-process build, the document-level work hoisted to run once (§4.3); ImportDocument returns the set; Import's §4.2 selection rule with the counts-and-pointer error
M3+M4 2e578e2d the collaboration family parsed, validated and consumed; message flows reported under the §4.4 mechanism text; black-box pools legal (FR-3 amended); the T-14 two-pool e2e

FR-3 amended mid-stage, with approval: the draft demanded every processRef resolve; the standard's black-box pool — a participant with no processRef, someone else's system — is legal and definitional by nature. A present ref must still resolve; an absent one is consumed silently.

NFR-1 held. No pkg/model change: every construct in this stage is either consumed (collaboration, participant), reported (messageFlow), or already buildable (the processes themselves).

Empirical finds worth the next reader's minute:

  • The exchange needs no collaboration. T-14 runs the two-pool message hand-off — throw on one side, auto-instantiated message start on the other — on a document whose collaboration is entirely consumed. The engine's mechanism (message name + correlation key) carries it; the drawing adds nothing, which is FR-4's premise proved at runtime, not just asserted.
  • The consumer is observed through discovery. An auto-instantiated instance has no handle to wait on — no StartLatest return, no gofunc channel for a pure-imported process. Thresher.Instances (SRD-084) with Stage: StageSettled is the observation mechanism, polled until both pools settle.
  • A message name may not carry . — the datum derived from orderPlaced's draft name order.placed refused at build with the model's reserved-character error, clearly attributed. Dots are path separators in scope data names; a BPMN document using dotted message names will see that refusal verbatim.

Open questions

None.