Skip to content

v2.0.0 Changelog ​

Unreleased — Directional Variables via typed wrappers ​

Direction moves into the variable's type, not its path ​

Every variable in Variables.<Element> is now typed with a direction-aware subtype of the sealed interface VariableName: VariableName.Input, VariableName.Output, or VariableName.InOut (when the same variable is read and written by one element). The Inputs / Outputs nested objects that earlier iterations introduced are gone — the extra nesting is no longer needed because the type system carries the direction.

kotlin
// Before (earlier v2 iteration — never released)
ProcessApi.Variables.ActivitySendConfirmationMail.Inputs.SUBSCRIPTION_ID
ProcessApi.Variables.StartEventSubmitRegistrationForm.Outputs.SUBSCRIPTION_ID

// After
ProcessApi.Variables.ActivitySendConfirmationMail.SUBSCRIPTION_ID  // VariableName.Input
ProcessApi.Variables.StartEventSubmitRegistrationForm.SUBSCRIPTION_ID  // VariableName.Output

Consumer APIs can now enforce direction at compile time:

kotlin
fun <T> setOutput(name: VariableName.Output, value: T) { ... }
setOutput(ProcessApi.Variables.StartEvent.SUBSCRIPTION_ID, id)  // compiles
// setOutput(ProcessApi.Variables.Activity.INPUT_ONLY_VAR, id)  // compile error

Bidirectional variables (read and written by the same element) appear once as VariableName.InOut, not twice as separate entries.

Wrapper types override toString() ​

Every wrapper type (MessageName, ElementId, ProcessId, SignalName, and each VariableName subtype) overrides toString() to return the underlying string. This eliminates the .value tax in most consumer code — string templates, logging, println, and any Any?-accepting API now work without an unwrap:

kotlin
println(ProcessApi.Messages.FOO)                    // prints "Message_Foo"
log.info("got message {}", ProcessApi.Messages.FOO) // SLF4J calls toString()

.value / .value() remains required where a strict String parameter is declared (e.g. val s: String = v.value, mapOf<String, String>("k" to v.value)). The preferred fix is to retype the consumer API to accept the wrapper — see the "Leaf wrapper ergonomics" section below.

additionalVariables replaced by directional variants ​

The legacy undirected camunda:property name="additionalVariables" extension property is no longer extracted. Two directional replacements are supported on any BPMN element:

xml
<bpmn:extensionElements>
  <camunda:properties>
    <camunda:property name="additionalInputVariables" value="orderId, customerEmail" />
    <camunda:property name="additionalOutputVariables" value="processingResult" />
  </camunda:properties>
</bpmn:extensionElements>

Migration: search your BPMN files for name="additionalVariables" and split each into the two directional variants. The migrate-bpmn-to-code-v1-to-v2 skill flags BPMN occurrences and Variables.<Element>.X source references for manual review but cannot rewrite BPMN files automatically.


v2.0.1 ​

Bug Fixes ​

  • Escape quotes in generated Kotlin BpmnFlow condition expressions (#318)

What's New ​

New API Sections ​

v2.0.0 adds four new sections to every generated Process API. They are additive — existing code continues to compile without changes.

SectionDescription
FlowsSequence flow definitions with id, optional name, sourceRef, targetRef, optional condition, and isDefault
RelationsPer-element topology: optional name, previousElements, followingElements, parentId, attachedToRef, attachedElements
EscalationsEscalation definitions with name and code (BpmnEscalation)
CompensationsCompensation event IDs

Shared Types in bpmn-to-code-runtime ​

BpmnTimer, BpmnError, BpmnEscalation, BpmnFlow, BpmnRelations, BpmnEngine, and the leaf identifier wrappers ship in a new published artifact, io.github.emaarco:bpmn-to-code-runtime. The generator no longer emits these types into {packagePath}/types/ — generated Process API files import them directly from io.github.emaarco.bpmn.runtime.*.

The Gradle plugin adds the runtime dependency automatically when applied. Maven users add it once:

xml
<dependency>
    <groupId>io.github.emaarco</groupId>
    <artifactId>bpmn-to-code-runtime</artifactId>
    <version>2.0.0</version>
</dependency>

This is the change that makes multi-module setups work: every consuming module (common libraries, service modules) sees the same ProcessId / MessageName / etc. class, so typed wrappers like fun startProcess(id: ProcessId) can live in a shared module and accept identifiers from any service.

Typed Leaf Identifiers ​

Leaf constants across PROCESS_ID, Elements, CallActivities, Messages, Compensations, Signals, and Variables are now wrapped in type-safe classes so the compiler can catch category mix-ups. Both Kotlin and Java consumers see the same Kotlin data class types — a regular JVM class with String value / getValue() — so mixed Kotlin+Java codebases retain full type safety at call sites. (An earlier iteration used @JvmInline value class; name-mangling made those methods unreachable from Java, breaking the multi-module story. See ADR 014.)

WrapperApplies to
ProcessIdPROCESS_ID, CallActivities.*
ElementIdElements.*, Compensations.*
MessageNameMessages.*
SignalNameSignals.*
VariableName.Input / .Output / .InOutVariables.<Element>.* — direction chosen per variable

ServiceTasks.* and PROCESS_ENGINE remain as const val String / public static final String because Kotlin/Java annotation arguments (e.g. @JobWorker(type = ...)) require compile-time constants.

Every wrapper overrides toString() to return its underlying string, so consumers can usually drop .value in string-building contexts (logging, templates, println). .value remains necessary where a strict String parameter is declared — the preferred fix is to retype the receiving API to accept the wrapper.

Variables Nested Per Element ​

Variables are now grouped by the element they belong to, eliminating duplicate entries when the same variable name appears in multiple tasks.

Breaking Changes ​

1. TaskTypes renamed to ServiceTasks ​

kotlin
// v1.1.0
ProcessApi.TaskTypes.SEND_CONFIRMATION_MAIL

// v2.0.0
ProcessApi.ServiceTasks.SEND_CONFIRMATION_MAIL

2. BpmnTimer and BpmnError ship in bpmn-to-code-runtime ​

Previously these types were nested inside the process API object. They are now hand-written types in the new bpmn-to-code-runtime artifact (Gradle auto-adds it; Maven users declare the dependency once), and generated code imports them from io.github.emaarco.bpmn.runtime.*.

kotlin
val timer: NewsletterSubscriptionProcessApi.Timers.BpmnTimer =
    NewsletterSubscriptionProcessApi.Timers.TIMER_EVERY_DAY
kotlin
import io.github.emaarco.bpmn.runtime.BpmnTimer

val timer: BpmnTimer = NewsletterSubscriptionProcessApi.Timers.TIMER_EVERY_DAY

3. Variables are nested per element; direction lives in the wrapper type ​

Variable constants are scoped under a sub-object named after the element they belong to. Direction is carried by the wrapper type, not by the path (see the "Directional Variables via typed wrappers" section above).

kotlin
// v1.1.0
ProcessApi.Variables.SUBSCRIPTION_ID

// v2.0.0
ProcessApi.Variables.ActivitySendConfirmationMail.SUBSCRIPTION_ID       // VariableName.Input
ProcessApi.Variables.StartEventSubmitRegistrationForm.SUBSCRIPTION_ID   // VariableName.Output

Check the generated API file for the exact element sub-object names and the direction subtype of each variable.

4. Leaf constants are typed wrappers, not String ​

Every leaf constant except ServiceTasks.* and PROCESS_ENGINE is now a value-class / record instance. In preference order, three ways to consume them:

1. Retype the consumer API to accept the wrapper (preferred). Removes unwrap boilerplate and keeps type safety. Works wherever you control the signature.

kotlin
fun sendMessage(name: MessageName) { engine.send(name.value) }
sendMessage(ProcessApi.Messages.MESSAGE_FORM_SUBMITTED)

2. Rely on toString() for logging, string templates, println, Any?-accepting APIs. No unwrap needed.

kotlin
log.info("sending {}", ProcessApi.Messages.MESSAGE_FORM_SUBMITTED)
val s = "message = ${ProcessApi.Messages.MESSAGE_FORM_SUBMITTED}"

3. Explicit .value / .value() when a third-party API declares a strict String parameter and you cannot change its signature. Kotlin does not implicitly convert wrapper types to String, so val s: String = wrapper will not compile.

kotlin
val s: String = ProcessApi.Messages.MESSAGE_FORM_SUBMITTED.value
mapOf<String, String>("key" to ProcessApi.Variables.X.FOO.value)

Wrapped constants cannot be used as annotation arguments (Kotlin and Java require compile-time constants there). If a consumer previously used e.g. Messages.X in an annotation, keep a local const val String alongside it for the annotation site. @JobWorker(type = ServiceTasks.X) continues to work unchanged.

Reducing verbosity ​

For files that touch many constants from one section, language-level shortening is available and requires no generator support:

kotlin
import com.x.NewsletterSubscriptionProcessApi.Variables.StartEventRequestReceived.*
// later:
mapOf(SUBSCRIPTION_ID.value to id.value.toString())
kotlin
with(NewsletterSubscriptionProcessApi.Variables.StartEventRequestReceived) {
    mapOf(SUBSCRIPTION_ID.value to id.value.toString())
}
java
import static com.x.NewsletterSubscriptionProcessApi.Variables.StartEventRequestReceived.*;
// later:
Map.of(SUBSCRIPTION_ID.value(), id.value().toString());

Migration ​

Most breaking changes can be applied automatically using the migrate-bpmn-to-code-v1-to-v2 agent skill.

Install the skill and run it in your project:

bash
npx skills add https://github.com/Miragon/bpmn-to-code/tree/main/bpmn-to-code-skills/skills/migrate-bpmn-to-code-v1-to-v2

Then invoke it:

/migrate-bpmn-to-code-v1-to-v2

The skill scans your source files, proposes replacements, and applies them after your approval. Variable path changes (item 3 above) require manual review and are flagged separately.

Prompt ​

If you have the skill installed, invoke it directly:

/migrate-bpmn-to-code-v1-to-v2

Manual steps ​

  1. Search for .TaskTypes. and replace with .ServiceTasks..
  2. Search for qualified BpmnTimer and BpmnError type references — replace with the simple class name and add the import {pkg}.types.* statement.
  3. For each variable reference (ProcessApi.Variables.VAR_NAME), look up the correct per-element path in the regenerated API file.