Part 9: Deploying the generated buyer process to Flowable
Everything before this part is standards-clean BPMN 2.0 — it would load into Camunda, Activiti, or any compliant engine. Flowable integration is deliberately a separate concern: the AST knows delegate = "requisitionValidationDelegate" as a string in a metadata map; only the Flowable adapter decides that becomes flowable:delegateExpression="${requisitionValidationDelegate}". Swap the adapter and the same DSL emits Camunda-flavored XML.
The seam
Part 6’s EngineExtensionSerializer is the single point where vendor specifics enter the output:
interface EngineExtensionSerializer { fun namespaces(): Map<String, String> = emptyMap() fun elementAttributes(node: FlowNode): Map<String, String> = emptyMap() fun extensionElements(node: FlowNode): List<ExtensionElement> = emptyList() object None : EngineExtensionSerializer}BpmnXmlWriter(EngineExtensionSerializer.None) produces vendor-neutral XML. BpmnXmlWriter(FlowableExtensionSerializer) produces the same XML plus flowable: declarations and attributes — the seam, not the surface.
The Flowable adapter
object FlowableExtensionSerializer : EngineExtensionSerializer {
override fun namespaces() = mapOf("flowable" to BpmnNs.FLOWABLE)
override fun elementAttributes(node: FlowNode): Map<String, String> { val m = (node as? ActivityNode)?.metadata ?: return emptyMap() return buildMap { when (node) { is ServiceTask -> { m["delegate"]?.let { put("flowable:delegateExpression", "\${$it}") } m["class"]?.let { put("flowable:class", it) } m["expression"]?.let { put("flowable:expression", it) } } is UserTask -> { m["formKey"]?.let { put("flowable:formKey", it) } m["assignee"]?.let { put("flowable:assignee", it) } m["candidateGroup"]?.let { put("flowable:candidateGroups", it) } m["candidateUsers"]?.let { put("flowable:candidateUsers", it) } } else -> Unit } } }}The mapping is a lookup table — which is exactly what engine adapters should be:
| DSL metadata | Flowable XML | What it does at runtime |
|---|---|---|
delegate = "bean" | flowable:delegateExpression="${bean}" | Spring bean implementing JavaDelegate |
delegateClass = "com.Foo" | flowable:class="com.Foo" | Instantiates a delegate class directly |
expression = "${svc.go()}" | flowable:expression="${svc.go()}" | Evaluates an EL expression |
formKey = "form" | flowable:formKey="form" | Links to a Flowable form definition |
candidateGroup = "g" | flowable:candidateGroups="g" | Task visible to group members |
assignee = "u" | flowable:assignee="u" | Direct assignment |
The delegate value becomes "${bean}" — the wrap in ${} is adapter logic, not DSL syntax. The author’s delegate = "requisitionValidationDelegate" stays readable; the engine gets the expression it expects.
Generated output for a service task:
<bpmn:serviceTask id="validateRequisition" name="Validate Requisition and Supplier" flowable:delegateExpression="${requisitionValidationDelegate}"> <bpmn:incoming>flowSubmitToValidate</bpmn:incoming> <bpmn:incoming>flowCorrectToValidate</bpmn:incoming> <bpmn:outgoing>flowValidateToDecision</bpmn:outgoing></bpmn:serviceTask>Process variables: authoring metadata, not BPMN
BPMN 2.0 standardizes no variable declaration. The AST’s variables { } block serializes into a vendor-neutral extension block:
<bpmn:extensionElements> <bpmndsl:processVariables> <bpmndsl:variable name="requisitionId" type="string" required="true"/> <bpmndsl:variable name="estimatedAmount" type="decimal" required="true"/> <bpmndsl:variable name="regulatedPurchase" type="boolean"/> <!-- … --> </bpmndsl:processVariables></bpmn:extensionElements>Flowable ignores this block — it is documentation that travels with the definition. At runtime, variables materialize the first time a delegate or expression sets them. A production adapter could map them to flowable:field injections or an OpenAPI form schema; neither is a BPMN concern.
Design decision: keep variables descriptive (name + type + required). What they become — a JUEL expression’s operand, a form field, a KPI — is engine behavior. A
variables { }block that pretended to be BPMN would mislead every reader.
Message flows ≠ transport
A bpmn:messageFlow from sendRfq to receiveRfq says the buyer tells the supplier something — nothing more. It does not say how: a REST POST, a Kafka event, an email with a PDF, an EDI interchange, a message into another workflow engine. The generated XML is identical either way.
This is correct — the BPMN collaboration describes business relationships, not protocol bindings. In a deployment, the flowable engine never “delivers” the RFQ; sendRfq’s delegate does. What the diagram gives you is the contract surface: every message the buyer must send, every message it must wait for, and their ordering.
A minimal deployment
The buyer process deploys independently of the supplier pool — the collaboration file can be deployed wholesale, or just its executable process. With flowable-spring-boot-starter-process on the classpath:
@SpringBootApplicationclass ProcureApp
fun deployGenerated(collaboration: CollaborationDefinition, repositoryService: RepositoryService) { val xml = BpmnXmlWriter(FlowableExtensionSerializer) .writeToString(collaboration, LayeredLayoutEngine().layout(collaboration)) repositoryService.createDeployment() .name("enterprise-procurement") .addString("procurement.bpmn", xml) .deploy()}Only the buyer pool is executable (isExecutable="true"); Flowable deploys that process and treats the supplier’s process as documentation of an external participant — which is exactly its role. The supplier’s nodes don’t need delegates, task handlers, or Spring beans because nothing executes them.
Each delegate name resolves at runtime to a Spring bean; each candidateGroup maps to Flowable identity groups; each formKey maps to a form definition in Flowable’s form engine. The DSL produces the references; the application provides the referents.
Common pitfalls
- Serializing vendor attrs unconditionally —
xmlns:flowableon a Camunda deployment is harmless noise;flowable:delegateExpressionparsed by Camunda is an error. The adapter boundary prevents it. - Confusing authoring metadata with runtime variables —
variables { }declares intent; the engine’s variables are created by execution. A missingrequisitionIdat runtime is a process bug, not a declaration bug. - Deploying the collaboration to run the supplier — non-executable participants render and validate but produce no runtime; the diagram is the contract, not the code.
- Expecting message flows to deliver — BPMN message flows are a visual/semantic contract; every one needs a delegate, a listener, or an external system to actually send or receive anything.
Verification
FlowableExtensionSerializer runs in the test suite (Part 8): the generated XML contains flowable:delegateExpression="${worker}" and the xmlns:flowable declaration. Full deployment verification needs a running Flowable instance — left as an integration step, not a unit test.
Reference implementation
The adapter is Flowable.kt (FlowableExtensionSerializer) in the bpmn-flowable module; the dependency-free deployment helper and the Spring Boot RepositoryService sketch are Deploy.kt. Both live in pcnixsys/bpmn-dsl-kotlin.
Next
Part 10 packages the whole thing as a maintainable library: module layout, API stability, extension roadmap, and the honest list of what this series deliberately did not build.