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.
// 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.OutputConsumer APIs can now enforce direction at compile time:
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 errorBidirectional 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:
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:
<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
BpmnFlowcondition 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.
| Section | Description |
|---|---|
Flows | Sequence flow definitions with id, optional name, sourceRef, targetRef, optional condition, and isDefault |
Relations | Per-element topology: optional name, previousElements, followingElements, parentId, attachedToRef, attachedElements |
Escalations | Escalation definitions with name and code (BpmnEscalation) |
Compensations | Compensation 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:
<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.)
| Wrapper | Applies to |
|---|---|
ProcessId | PROCESS_ID, CallActivities.* |
ElementId | Elements.*, Compensations.* |
MessageName | Messages.* |
SignalName | Signals.* |
VariableName.Input / .Output / .InOut | Variables.<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
// v1.1.0
ProcessApi.TaskTypes.SEND_CONFIRMATION_MAIL
// v2.0.0
ProcessApi.ServiceTasks.SEND_CONFIRMATION_MAIL2. 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.*.
val timer: NewsletterSubscriptionProcessApi.Timers.BpmnTimer =
NewsletterSubscriptionProcessApi.Timers.TIMER_EVERY_DAYimport io.github.emaarco.bpmn.runtime.BpmnTimer
val timer: BpmnTimer = NewsletterSubscriptionProcessApi.Timers.TIMER_EVERY_DAY3. 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).
// v1.1.0
ProcessApi.Variables.SUBSCRIPTION_ID
// v2.0.0
ProcessApi.Variables.ActivitySendConfirmationMail.SUBSCRIPTION_ID // VariableName.Input
ProcessApi.Variables.StartEventSubmitRegistrationForm.SUBSCRIPTION_ID // VariableName.OutputCheck 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.
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.
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.
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:
import com.x.NewsletterSubscriptionProcessApi.Variables.StartEventRequestReceived.*
// later:
mapOf(SUBSCRIPTION_ID.value to id.value.toString())with(NewsletterSubscriptionProcessApi.Variables.StartEventRequestReceived) {
mapOf(SUBSCRIPTION_ID.value to id.value.toString())
}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.
With Claude Code (recommended)
Install the skill and run it in your project:
npx skills add https://github.com/emaarco/bpmn-to-code/tree/main/bpmn-to-code-skills/skills/migrate-bpmn-to-code-v1-to-v2Then invoke it:
/migrate-bpmn-to-code-v1-to-v2The 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-v2Manual steps
- Search for
.TaskTypes.and replace with.ServiceTasks.. - Search for qualified
BpmnTimerandBpmnErrortype references — replace with the simple class name and add theimport {pkg}.types.*statement. - For each variable reference (
ProcessApi.Variables.VAR_NAME), look up the correct per-element path in the regenerated API file.