Series overview
Part 9 of 1090% complete
2026-09-25•4 min read

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:

xml/BpmnXmlWriter.kt
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

flowable/Flowable.kt
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 metadataFlowable XMLWhat 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:

Generated excerpt
<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:

Generated excerpt
<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:

src/main/kotlin/.../Deploy.kt (illustrative — needs a running Flowable)
@SpringBootApplication
class 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:flowable on a Camunda deployment is harmless noise; flowable:delegateExpression parsed 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 missing requisitionId at 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.

KotlinBPMN

Type to search the site.

↑↓ navigate⏎ openPowered by Pagefind