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/emaarco/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.