Repository navigation
Migrating from version 1
This page is for teams that run a VanillaBP 1 application. It lists every change you have to make, with before and after. It also lists the behaviors that differ even where your code does not change.
In short: your business code stays. Dependencies, configuration and a few behaviors change.
-
What stays as it is
Everything you do not have to touch, above all your business code. -
Dependencies
The runtime versions, the renamed artifacts, and why persistence needs no change. -
Configuration
default-adapterbecomes an order, the module descriptor, the adapter sections, and every key one by one. -
Code changes
The three removed overloads,@BpmnProcess.primary(), and the annotation you remove from your workflow services. -
Behaviors that differ
What behaves differently even where your code stays the same, and the one case in operation which version 1 did not explain. -
Business Cockpit
The renamed adapter artifacts, the moved keys, and the workflow module which quietly stops reporting. -
Migrate in six steps
The order in which to do all of the above.
-
Your business code.
@WorkflowService,@WorkflowTask,@TaskId,@TaskParam,@TaskEvent, the multi-instance annotations andProcessServicekeep their meaning. No method changed its signature, except for the three removed overloads (below). - Your workflow aggregates, their persistence and their relation to the workflow: one aggregate per workflow instance, no process variables.
-
Your BPMN models, per BPMS (business process management system). The wiring conventions are unchanged.
Camunda 8 models even stay byte-identical. VanillaBP 2 injects the same
zeebe:taskListeners and subscription correlation keys as version 1 did, so the upgrade deploys no new process version. An adapter may still add something of its own to a model, and the Business Cockpit does. See below. -
Your workflow modules as a concept, their ids and their configuration files per module
(
<module-id>.yaml/.properties). What changes is how you DECLARE a module, see the descriptor.
| Version 1 | Version 2 | |
|---|---|---|
| Java | 17 | 21 |
| Spring Boot | 3.x | 4.1 |
| Quarkus | not yet supported | 3.37 |
Spring Boot 4 split spring-boot-autoconfigure into modules, so some Spring imports in your own code move. One
example is @EntityScan, which moves from org.springframework.boot.autoconfigure.domain to
org.springframework.boot.persistence.autoconfigure. The test slices also need explicit dependencies now. This is
a change in Spring, not in VanillaBP. Follow Spring Boot's own migration guide for it.
The adapter artifacts were renamed (<bpms>-spring-boot-adapter → <bpms>-adapter-spring-boot). Every adapter
now also has a Quarkus artifact:
<!-- before (Spring Boot, Camunda 7) -->
<dependency>
<groupId>org.camunda.community.vanillabp</groupId>
<artifactId>camunda7-spring-boot-adapter</artifactId>
<version>1.x.x</version>
</dependency>
<!-- after -->
<dependency>
<groupId>org.camunda.community.vanillabp</groupId>
<artifactId>camunda7-adapter-spring-boot</artifactId>
<version>2.0.0</version>
</dependency>| BPMS | Spring Boot | Quarkus |
|---|---|---|
| Camunda 7 | org.camunda.community.vanillabp:camunda7-adapter-spring-boot |
org.camunda.community.vanillabp:camunda7-adapter-quarkus |
| Camunda 8 | org.camunda.community.vanillabp:camunda8-adapter-spring-boot |
org.camunda.community.vanillabp:camunda8-adapter-quarkus |
On Spring Boot the adapter brings in the platform integration as a transitive dependency, so one dependency is enough. On Quarkus both VanillaBP and the adapter are Quarkus extensions, so you add both as dependencies (see Quarkus integration).
Some application modules must not depend on a specific BPMS. They use the support modules
io.vanillabp:vanillabp-spring-boot-support / io.vanillabp:vanillabp-quarkus-support. These carry the VanillaBP
APIs such a module needs to compile, and no API of any BPMS. They also bring along the
integration SPI (io.vanillabp:vanillabp-integration-spi) as a transitive dependency. SPI means
service provider interface. Here it is the set of interfaces your application may implement where VanillaBP needs
something only your application knows. Aggregate persistence is the main case.
Version 1 stored workflow aggregates through Spring Data, JPA or MongoDB, and a repository for the aggregate had to exist. Version 2 does both out of the box, so nothing changes here. On Quarkus the built-in support covers more than before. VanillaBP itself persists an aggregate which is managed by a Panache repository, written as a Panache active record (JPA or MongoDB), or managed by a Spring Data repository.
Your own persistence is new in version 2. Its SPI, io.vanillabp.integration.spi.AggregatePersistenceAware, did
not exist in version 1, so there is nothing to migrate. It only matters once you want custom persistence.
Version 1 named the adapter of a module. Version 2 names an ORDER of adapters, because several BPMS may serve one application. This is what makes BPMS migration a matter of configuration. The key exists at the same three levels as before:
# before
vanillabp:
default-adapter: camunda7
workflow-modules:
ride:
default-adapter: camunda7
workflows:
RideProcess:
default-adapter: camunda7
# after
vanillabp:
prioritized-adapters:
- camunda7
workflow-modules:
ride:
prioritized-adapters:
- camunda7
workflows:
RideProcess:
prioritized-adapters:
- camunda7The first entry starts new workflows. The others are asked about workflows started earlier. The most specific value that is not empty wins as a whole. A workflow's list replaces the module's list, it does not merge with it. No other setting works this way, so read again any list you wrote in version 1. The general rule is on the levels a setting is written at.
Version 1 had three ways to name a workflow module: spring.application.name (for applications with a single
module), the name of the module's configuration file, or a static bean method. That method declared the id together
with the module's properties class.
// version 1
@Bean
public static WorkflowModuleProperties ModuleProperties() {
return new WorkflowModuleProperties(
LoanApprovalProperties.class,
LoanApprovalProperties.WORKFLOW_MODULE_ID);
}Version 2 wants the id written explicitly, in one place. Then a module travels with its own artifact, and both
platforms find it the same way. Add a text file META-INF/workflow-module whose content is the module id.
src/main/resources/META-INF/workflow-module # content: loan-approval
Write one file per workflow module. In a multi-module project it goes into the artifact which implements that use case. The details per platform are on Spring Boot and Quarkus. An application without any descriptor is treated as one single global module. So the file is what makes a use case a module of its own.
Then delete the bean method. WorkflowModuleProperties no longer exists, so a project which keeps the method does
not compile. The properties class it named stays as it is, because the binding of a module's own configuration has
not changed.
Keep the ids you had. They name BPMS tenants and resource directories. If you change one, the running workflows of that module are no longer connected to their configuration.
This is unchanged from version 1. A file named after the workflow module (loan-approval.yaml,
loan-approval-prod.properties) holds defaults, and everything the application configures wins over it. Version 1
did this with a PropertySourcesPlaceholderConfigurer, which looked in the environment first. Version 2 puts the
module's property sources below everything else, and Quarkus does the same with ordinals. Both platforms now behave
the same way (the order).
What version 2 did add: these values are real configuration properties now. Version 1 kept them out of the
environment. ${...} and @Value could see them, but Environment#getProperty and @ConfigurationProperties
could not. Now they can, so you can bind a workflow module to a @ConfigurationProperties class of its own.
In version 1 an adapter's settings lived per workflow module
(vanillabp.workflow-modules.<m>.adapters.<adapter>.*). Its connection was configured by the BPMS' own Spring
integration: camunda.bpm.* for Camunda 7, and the Camunda Spring SDK's camunda.client.*/zeebe.client.* for
Camunda 8. In version 2 an adapter id names one BPMS instance. Everything that instance needs lives in its own
section. VanillaBP owns that section, and no BPMS-specific Spring configuration is involved:
# before (Camunda 8, connection via the Camunda Spring SDK)
camunda:
client:
mode: self-managed
zeebe:
grpc-address: http://localhost:26500
vanillabp:
default-adapter: camunda8
workflow-modules:
ride:
adapters:
camunda8:
task-timeout: PT5M
# after
vanillabp:
prioritized-adapters:
- camunda8
adapters:
camunda8: # the id; 'type' may be omitted because the id IS the type
mode: self-managed
rest-address: http://localhost:8080
job-timeout: PT5M # adapter-wide default, overridable per module/workflow/taskAn id which is not an adapter type needs vanillabp.adapters.<id>.type. This is how you tell two instances of
the same BPMS apart:
vanillabp:
prioritized-adapters:
- saas # new workflows start here
- onprem # existing workflows may still live here
adapters:
saas:
type: camunda8
mode: saas
cluster-id: ...
onprem:
type: camunda8
rest-address: http://camunda.internal:8080The exact keys belong to each adapter. See the adapter wikis: Camunda 7, Camunda 8.
| Version 1 | Version 2 | Note |
|---|---|---|
vanillabp.default-adapter |
vanillabp.prioritized-adapters |
a list now, at all three levels |
vanillabp.workflow-modules.<m>.default-adapter |
vanillabp.workflow-modules.<m>.prioritized-adapters |
|
…workflows.<w>.default-adapter |
…workflows.<w>.prioritized-adapters |
|
vanillabp.workflow-modules.<m>.adapters.<id>.resources-location |
unchanged, but optional | derived as classpath*:<module>/processes/<adapter> (see Workflow modules) |
camunda.bpm.* (Camunda 7 engine) |
vanillabp.adapters.<id>.* |
the adapter runs the plain engine itself, so the Camunda Spring Boot starter is gone. The keys are database-schema-update, history-time-to-live, data-source-name and table-prefix. The last one needs a schema you create yourself, because Camunda does not create tables with a prefix |
camunda.client.* / zeebe.client.* (Camunda 8) |
vanillabp.adapters.<id>.* |
mode, rest-address, grpc-address, prefer-rest-over-grpc, tenant-id, cluster-id, region, client-id, client-secret
|
…adapters.camunda8.task-timeout |
job-timeout |
you can set it per adapter, workflow module, workflow and task |
…adapters.camunda7.use-bpmn-async-definitions |
— | you cannot configure this any more. The adapter sets asyncBefore/asyncAfter on service-like tasks when it parses the model, so every task runs in its own transaction (Camunda 7 configuration) |
…adapters.<bpms>.tenant-id |
vanillabp.adapters.<id>.tenant-id |
the tenant now belongs to the BPMS INSTANCE, not to the workflow module. The key only NAMES the tenant. Without it the workflow module id is used, exactly as in version 1 |
…adapters.<bpms>.use-tenants: false |
vanillabp.adapters.<id>.name-clash-avoidance: none |
one concept replaces it, see below |
…adapters.camunda8.task-id-as-hex-string |
— | not a property to move, but data to migrate. The ids your application stored while this was on have to be converted. The adapter tells you where it finds one (the step, why) |
vanillabp.allow-connectors |
vanillabp.adapters.<id>.allow-connectors |
the key moved under the ADAPTER. Connectors are a Camunda 8 concept, and no other BPMS has anything to do with the marker in the model. The three levels are the ones you had: adapter, workflow module and workflow. The default is still false. What changed: a more specific level may now switch it OFF as well as on. Version 1's booleans could only switch it on. A user task built from an element template now stays wired, so it needs a @WorkflowTask method. Version 1 skipped it. And every boot of a workflow module which allows connectors writes a warning. The warning names the elements the module handed over and what they cost, and no key turns it off (Camunda 8 configuration) |
…adapters.camunda8.retry-backoff |
vanillabp.adapters.<id>.retry-backoff |
the same as before: the backoff of a failed job, which you can set per workflow module, workflow and task. One difference, see Behaviors that differ: it has a default now. The adapter reads the retryBackoff task header of your model as before (Camunda 8 configuration) |
vanillabp.resilience.* |
removed | nothing ever read it. Retry settings come back per adapter together with their first user, which is the row above |
Version 1 kept a workflow module apart with a BPMS tenant, and you could switch that off per module
(use-tenants). Version 2 makes it one explicit setting with three values:
vanillabp:
adapters:
camunda7:
name-clash-avoidance: by-adapter # tenant per workflow module (version 1's behavior)
# name-clash-avoidance: use-prefix # no tenant, VanillaBP prefixes the identifiers
# name-clash-avoidance: none # nothing is scoped (the old 'use-tenants: false')If you configure nothing, by-adapter applies. This is version 1's behavior on both Camunda adapters. So an
application upgraded from version 1 keeps its tenants without any configuration.
Your adapter id, and why it is worth a thought now. Version 1 had one adapter, and its id was its type. Version
2 keeps that: a section named camunda7 or camunda8 needs no type. Keep it while you run only one BPMS. Once a
second one joins, both need names of their own. That may be another cluster, another cluster version, or SaaS next
to on-premises. And you cannot rename the old one to a clearer name later. VanillaBP stores the adapter id to know
what belongs to which BPMS. So a rename creates a new adapter, next to workflows which nothing serves any more. What
that costs in detail, and how to change a BPMS instead, is on
naming adapter ids, and never renaming them. If you
choose none instead, the adapter logs a WARN per workflow module which names the alternatives.
accept-unscoped-identifiers: true records that you made this decision.
Two settings which used to fit together now contradict each other, and the boot tells you. A tenant-id fails the
boot if no level uses by-adapter, because the deployment would ignore the configured name. Where by-adapter
does apply, Camunda 8 also asks the cluster before it deploys whether it can use the tenant at all.
Read how name clashes are avoided before you switch to
use-prefix. It changes the identifiers the BPMS sees, so it is a
migration, not a property change. Two notes for upgraders:
-
Camunda 7 users: nothing to do. Version 1 deployed every workflow module into a tenant named after the
module, and
by-adapteris the default. So the identifiers the engine answers to stay the same, and the workflows you started in version 1 are found through their tenants. If you ran withuse-tenants: false, setnone. If you forget, the deployment still succeeds. So the adapter counts the workflows your configuration will not reach, and says so at every start. -
Camunda 8 users: nothing to do either, as long as your cluster has multi-tenancy enabled and knows the
tenants. In version 1
use-tenantswas on, and the tenant defaulted to the workflow module id. This is exactly whatby-adapterdeploys. Two cases need one line. A cluster WITHOUT multi-tenancy rejects a tenant id. And an application which ran withuse-tenants: falsekeeps that scoping withvanillabp.adapters.<id>.name-clash-avoidance: none. Where the cluster refuses, the startup message names both ways out.
An application with one adapter dependency and one workflow module needs no vanillabp.* property at all. The
classpath is the configuration. The adapter section, the module section and resources-location are all derived.
Only what the BPMS itself needs remains, for example a cluster address. Everything you configured explicitly keeps
working.
Two Camunda 8 tables, if you come from 1.6.3 or earlier. After the upgrade nothing reads CAMUNDA8_DEPLOYMENTS
and CAMUNDA8_RESOURCES. They are the one thing worth dropping by hand
(the step).
An adapter whose BPMS is remote cannot take part in your local transaction. So it starts workflows in two phases,
through a transaction outbox. The outbox is a table of calls which VanillaBP writes in your transaction and carries
out after the commit. Coming from version 1, this concerns the Camunda 8 adapter. The outbox works out of the box,
and you can tune it with vanillabp.outbox.*. Read what it guarantees
before you go to production. Watching for blocked entries is a job for operations.
Four tables come with it, in the database where your workflow aggregates live:
- the outbox store,
- the payloads of the calls which carry one,
- the log of processed task deliveries, which keeps a redelivered task from running your handler twice,
- the claim which says which node removes the old rows tonight.
VanillaBP creates all four at startup. So an application which lets it do that has nothing to prepare. Some
applications have a schema which is reviewed and applied by their deployment pipeline. They set
vanillabp.outbox.create-schema: false, one switch for all four tables, and apply the statements themselves. The
statements ship as the artifact io.vanillabp:vanillabp-schema. It holds a Liquibase changelog and generated Flyway
scripts per database, and it pulls in no runtime. With the switch off, VanillaBP checks at startup that the tables
exist. If one is missing, it ends the boot and names that table, instead of failing at the first workflow start.
The details per platform are on Spring Boot
and Quarkus.
If you forget the outbox, you do not find out in production. Say the first adapter in the priority list needs a two-phase commit, and VanillaBP finds no outbox store. Then the application does not boot, and the message names what to add. At the same moment VanillaBP checks the transaction the outbox entry would be written in. Camunda 7 is included, which is new. The outbox itself is new as well, because version 1 had none. On Camunda 8 a listener completed the job after your transaction had committed, so nothing about that call was written down. Version 1 left no table of two-phase calls behind, so there is nothing to drain or carry over.
The overloads which take a message object are gone. They used the simple name of the object's class as the message name. Message content never travels to the BPMS. So an object which only carries a name did not deserve an API of its own:
// before
processService.correlateMessage(ride, rideConfirmation);
processService.correlateMessage(ride, rideConfirmation, correlationId);
processService.startWorkflowByMessage(ride, rideRequested);
// after — name the message explicitly (formerly message.getClass().getSimpleName())
processService.correlateMessage(ride, "RideConfirmation");
processService.correlateMessage(ride, "RideConfirmation", correlationId);
processService.startWorkflowByMessage(ride, "RideRequested");Put the message's data into the aggregate before you correlate. As before, the aggregate is the single source of truth.
The place where a process is declared tells whether it is the primary one. bpmnProcess is primary,
secondaryBpmnProcesses are not.
// before
@WorkflowService(workflowAggregateClass = Ride.class,
bpmnProcess = @BpmnProcess(bpmnProcessId = "RideProcess", primary = true),
secondaryBpmnProcesses = @BpmnProcess(bpmnProcessId = "RideCancellation", primary = false))
// after
@WorkflowService(workflowAggregateClass = Ride.class,
bpmnProcess = @BpmnProcess(bpmnProcessId = "RideProcess"),
secondaryBpmnProcesses = @BpmnProcess(bpmnProcessId = "RideCancellation"))Note that secondaryBpmnProcesses is honored now. The platform integration of version 1 ignored it. Every
declared BPMN process is wired for task processing. Secondary entries must name an explicit bpmnProcessId.
Some version 1 applications spread the processes of one workflow aggregate over several classes, each with its own
bpmnProcess. These have to bring them together. One class declares the process which startWorkflow starts. The
others go into its secondaryBpmnProcesses. Handlers may still live in separate classes, as long as each of them
declares that same bpmnProcess. Version 2 reports the problem while starting (Quarkus: while building). It does
not pick one of the processes by the order in which the classes were found.
A BPMN process also belongs to exactly one workflow aggregate. Two classes of different aggregates may have named the
same bpmnProcess. Version 1 logged a warning in that case and kept the class it found first. Version 2 stops the
start (Quarkus: the build). It does the same if one of the classes lists the process in secondaryBpmnProcesses.
The message names both classes and both aggregates. Then decide what the process is. If it is a workflow of its own,
only classes of its aggregate declare it. A call activity may still call it. In that case the mappings of the call
activity in your BPMS set up the data the called workflow gets. Still, two workflows are usually better started and
synchronised by messages. If the process is a step of another workflow, only the class of that workflow lists it in
secondaryBpmnProcesses. See
Workflow tasks.
Version 1 asked you to put @Transactional(noRollbackFor = TaskException.class) on workflow services. VanillaBP 2
manages the transaction around a @WorkflowTask method itself: it loads the aggregate, calls the method and saves
the aggregate. This includes the old contract. A TaskException commits the changes to the aggregate and
completes the task with a BPMN error.
// before
@Service
@WorkflowService(workflowAggregateClass = Ride.class, ...)
@Transactional(noRollbackFor = TaskException.class)
public class RideService { ... }
// after — no transaction annotation of your own
@Service
@WorkflowService(workflowAggregateClass = Ride.class, ...)
public class RideService { ... }A transaction boundary of your own around a handler may change when a rollback happens. That is why it has to go.
Code which CALLS ProcessService still needs a transaction. startWorkflow, completeTask, cancelTask,
completeUserTask, cancelUserTask and correlateMessage need an active one. The read-only viewer methods do
not.
Keeping the version 1 annotation is valid too. The startup check accepts a rollback rule which excludes
TaskException. So the version 1 line @Transactional(noRollbackFor = TaskException.class) boots and behaves as
before. The boot fails for an annotation which joins VanillaBP's transaction WITHOUT such a rule. With such an
annotation, a TaskException would throw away the aggregate changes instead of committing them. The check also
reports an annotation inherited from a superclass or an interface, and one hidden inside a custom annotation of
yours. If the annotation sits on a bean the handler CALLS, no startup check can see it. Then the task fails at
runtime, with a message which names the workflow, the task and the same two fixes. See
Workflow tasks.
First check whether the class is also your business service. Small applications often keep everything in one
class. The @WorkflowTask methods sit next to the business methods your API calls. If you delete the annotation
there, the business methods lose their transaction too, and they still need one. VanillaBP wraps only
@WorkflowTask methods, and every call into ProcessService needs an active transaction. So a startWorkflow
from an endpoint fails once the annotation is gone. Nothing reports this. The application starts, the workflows
the BPMS drives keep working, and only the paths which start in your API break. You have two options. Split the
class, and move the @WorkflowTask methods into a class of their own, as the
blueprints do. Or keep
@Transactional(noRollbackFor = TaskException.class) on the combined class, which the startup check accepts.
The one wrong option is to delete the annotation and leave the class combined.
A javax.transaction.Transactional carried over from Spring Boot 2 is honored by neither Spring Framework 7 nor
Quarkus 3. It does not break the contract. It simply does nothing. VanillaBP warns about it at startup, so you do
not rely on a transaction boundary which does not exist.
ProcessService got new methods, and they are default methods so this does not break again. A VanillaBP adapter
implements getProcessDefinitions, getBpmnXml, getWorkflowHistory and startWorkflowByMessage(A, String). So
test doubles keep compiling, and only getWorkflowModuleId() stays abstract.
Custom aggregate persistence is not part of the upgrade, because version 1 had no SPI for it
(above). If you write an AggregatePersistenceAware now, note these
points. All its methods are default methods with guiding messages. loadById(Object) is the one to implement,
because VanillaBP loads aggregates itself (for task processing and aggregate sync). Some IDs do not convert from
and to String without loss. For such an ID, either name its type in getAggregateIdType(), or return null
there to own the serialized form. Remote BPMS also need getAggregateIdName(). Your own implementation always wins
over the built-in one.
Nothing to change in your code, but worth knowing before you go live.
-
A workflow whose aggregate shares everything does not start. This is the one item here which stops your
first boot. Version 1 had no sync model, and the embedded Camunda 7 engine read the aggregate live. So an
application which never annotated anything now hands every attribute to the BPMS. You have two options. Say
what the models need, with
@NoSyncWithBPMSon the class and@SyncWithBPMSon the attributes an expression reads. Or allow it per workflow withvanillabp.workflow-modules.<module>.workflows.<process>.allow-full-sync-with-bpms: true. This permission is not inherited, so you write it at every workflow it applies to (see Workflow aggregates). -
A task somebody else serves can say so. Version 1 ended the start for every task without a
@WorkflowTaskmethod, user tasks included. Version 2 does the same. What is new is a way out for a task you leave to a task list or to another worker on purpose:vanillabp.workflow-modules.<module>.workflows.<process>.tasks.<element-id>.implemented-externally: true(see Workflow tasks). -
A process somebody else serves can say so. Version 1 ended the start for every BPMN process which no
@WorkflowServicedeclared, even for a process which only sits in your file. Version 2 does the same. What is new is the way out:vanillabp.workflow-modules.<module>.workflows.<process>.implemented-externally: true. Such a process is deployed with its file, and VanillaBP leaves it alone (see Workflow tasks). -
An application can learn that a workflow ended. Version 1 offered nothing for this, so applications
modelled a service task in front of every end event.
@WorkflowEndedreplaces that (see Starting workflows). The old service task keeps working. So this is something to simplify when you have time, not a migration step. On Camunda 8, wait until the workflows you upgraded have ended before you delete the old service task. A workflow started under version 1 never triggers the new notification (the step). Camunda 7 needs no such wait. -
BPMN signals can be sent. Version 1 had no API for this, so you could use a signal event only between two
elements of the same model.
ProcessService.sendSignal(name)now broadcasts one (see Message correlation). A signal is a broadcast, so it takes no workflow aggregate. -
An application can tell the BPMS that the aggregate changed. Version 1 pushed the aggregate only at its own
sync points. So a conditional event had nothing to react to, and a gateway evaluated whatever the last completed
task had written.
ProcessService.aggregateChanged(aggregate)pushes on demand, andaggregateChanged(aggregate, taskId)pushes into the scope of one task instance (see Workflow aggregates). On Camunda 7 this makes conditional events usable. On Camunda 8 it finds the workflow by searching the cluster. This is one of the reasons why that adapter needs a cluster it can search. -
Workflows the BPMS starts on its own now get a workflow aggregate. In version 1 you could not use a timer,
signal or conditional start event. The workflow had no aggregate, so nothing could be routed to it. In
VanillaBP 2 a
@WorkflowStartedByBpmsmethod of your workflow service builds the aggregate and gives the workflow its id (see Starting workflows). Nothing changes for the workflows you start yourself. -
Remote BPMS start workflows in two phases.
startWorkflowno longer talks to the BPMS inside your transaction. An outbox entry is written in your transaction, and the instance is created after the commit. So a transaction which is rolled back can no longer leave a workflow behind. In exchange the start shows up in the BPMS a moment later, and it is at-least-once. A duplicate start is unlikely, because VanillaBP checks before it sends a call again. But it is not impossible. Camunda 7 now works this way as well. It delivers tasks inside the engine's transaction, but every operation which moves a workflow forward runs after your commit. So such an operation can be repeated when it loses a concurrency conflict. An application on Camunda 7 therefore needs a transaction outbox, which version 1 did not ask for. -
A failed job waits ten seconds before the cluster hands it out again. On Camunda 8, version 1 sent a
backoff only where one was configured. So a handler which failed on something that needs a moment used up its
three retries as fast as the cluster could deliver the job again.
vanillabp.adapters.<id>.retry-backoffnow defaults toPT10S. You can still set it per workflow module, workflow and task, and a value your application configured keeps its meaning. The second way version 1 offered still works too. AretryBackofftask header in the BPMN model, per element, is read as before. It no longer needs theretriesattribute which version 1 also demanded. If a task is both configured AND modelled, the configured value of that one task wins, and one log line says so. Every level above the task loses to the model (Camunda 8 configuration). -
A repeated delivery of a task no longer runs your handler again. Remote BPMS deliver tasks at least once.
Version 1 passed this straight through. So a redelivery called the
@WorkflowTaskmethod a second time, and every handler had to protect itself. VanillaBP 2 records what it processed and answers a repeated delivery from that record (see Workflow tasks). The guards in your handlers stay correct. They still cover what a record cannot: two deliveries running at the same time, and everything a handler does outside its transaction. -
One error contract for
@WorkflowTaskmethods on all BPMS. A normal return completes the task. ATaskExceptionbecomes a BPMN error, and the aggregate changes are committed. Any other exception rolls the transaction back and leaves retrying to the BPMS. See Workflow tasks. -
Operations on existing workflows find their BPMS by asking.
completeTask,cancelTask, the user task operations andcorrelateMessageask the prioritized adapters, and remember the answer. The task operations usually skip that question. When the task was delivered, VanillaBP wrote a record which names the BPMS that handed it out, see The record also says which BPMS holds the task. A task or workflow which no BPMS knows raises aTaskNotFoundException/WorkflowNotFoundExceptionwith a guiding message. If it is already completed, the operation does nothing and logs a warning. While a BPMS cannot be reached, the operation fails. It does not fall back to another BPMS, because quietly using the wrong BPMS would be worse. - Configuration defects fail at startup, not at runtime. The messages name the property keys to add. An application whose adapter section is missing still boots and tells you what to write. A section which really contradicts itself fails the boot.
-
The start names the expressions in your models. Version 1 read none of them. Version 2 warns per BPMN
process about an expression which walks a path (
${order.shipping.express}) or calls something (${count(order.items) > 3}), because a rename in Java breaks it. An expression which only computes (${not bigItem}) gets a notice. An expression which names a single variable is not mentioned. Workflow aggregates has always recommended that kind. Nothing stops the start, and no model has to change. If a process is meant to stay as it is, writevanillabp.workflow-modules.<module>.workflows.<process>.accept-expressions-in-the-model: true. The same key at the workflow module or atvanillabpcovers everything below it (see What the start says about your expressions). -
Process definition ids of the viewer API have a namespace,
<adapter id>#<BPMS specific id>, and are opaque. Pass them back unchanged. Never parse them or build them yourself. See Viewing workflows. -
versiondoes what version 1 only documented. The attribute exists on@WorkflowTasksince version 1, and now also on@WorkflowStartedByBpmsand@WorkflowEnded. Version 1 never evaluated it, so every method served every version. VanillaBP 2 matches it against the version of the deployed process definition which the BPMS reports. A boundary may also name a version tag of the model (see Workflow tasks). This has consequences for an application which already uses the attribute. Methods whose ranges were meant to be disjoint really are disjoint now. So a version which none of them serves fails the delivery, instead of running the first method. There is one exception, a task of the model the boot deploys. If none of its methods covers the version the BPMS gives that model, the boot ends (see Workflow tasks). A BPMS which reports NO version reaches only methods without the attribute. So a task whose methods all name versions fails the delivery there. And two methods wired to one task with OVERLAPPING ranges fail the boot, where version 1 accepted them. Applications without the attribute see no change. -
@SyncWithBPMS/@NoSyncWithBPMSare real now. Version 1 documented them, but did not ship them. Without any annotation EVERY adapter shares everything, Camunda 7 included. Your models keep reading what they read in version 1, but now through process variables instead of a live read of the aggregate. As soon as you annotate one attribute, you are in charge. Read Workflow aggregates before you annotate, because annotating a single attribute also decides what happens to all the others. -
On Camunda 7 the live read of the aggregate is now a fallback, and it will be removed. Version 1 answered
every BPMN expression by reading the aggregate. Version 2 writes the shared values as process variables and lets
the engine resolve them. Workflows which were already running when you upgraded have no such variables. So
where the engine has none, the adapter still reads the aggregate, and logs once what it did. No version is named
for the removal, so ask the VanillaBP team if you need a date. What you can watch is the adapter's own report.
Once an hour it says how many workflows the fallback has served since the application started. That number
falls by itself. A workflow stops needing the fallback as soon as it reaches the next point where the adapter
writes the shared values. The sharing rules do not cover two cases at all, and both were legal in version 1: an
attribute which can only be read as a FIELD, and an
isX()method which returns something other thanboolean. Give those a getter (getX(), orisX()returning boolean) and share it. While your application starts, the Camunda 7 adapter lists the expressions whose path stops before it reaches a value the engine holds. For each one it names the element, the segment where the path stops, and what this engine will do with the resulting null. That list is your migration backlog, but it is not complete. The check reads declared types. So it says nothing where a path runs through aMap, an interface or a collection without an element type. It also judges no method call. So it leaves out${order.getTotal()}and${order.status.name()}, although both stop working. Those two raise an incident which names the expression, so the engine tells you about them. The check exists for the silent cases. The main one is a conditional event. It answers a condition it cannot evaluate with false, and then waits forever. -
On Camunda 8 your running workflows already have their variables, and two things can spoil that. Version 1
handed the whole workflow aggregate to every command it sent. So an instance started under version 1 has a
process variable for everything its JSON serialization could read, including the ID attribute. That ID variable
is what version 2 looks for when it probes the cluster. Two narrow cases break this, and both are in your code,
not in your configuration. First, the aggregate's ID attribute may be renamed for JSON (
@JsonPropertyon the ID). Then version 1 wrote the variable under the JSON name, while version 2 looks for the name your persistence reports. Every probe for an instance from before the upgrade finds nothing. Second, version 1 serialized FIELDS, and version 2 does not. Such a variable keeps the last value version 1 wrote, forever, because version 2 only refreshes what it reads from a getter. The value is stale, not missing. So give such an attribute a getter and share it. -
A message starts only the process of its own
ProcessService. Version 1 handed the message ofstartWorkflowByMessageto the BPMS, so every process with a start event for that name started. Version 2 refuses a message which the model of the service's own process does not start. It throws anIllegalArgumentExceptionwhich names the messages that do. If your code started another process this way, call theProcessServiceof that process instead (see Message correlation). An adapter which cannot name the messages of its model is not checked, and the start says so. -
Camunda 8 cannot cancel a user task managed by Camunda with a BPMN error (
cancelUserTask), and an error with guidance tells you so. Clusters up to 8.9 have no command for it. The listeners of 8.10 make it possible, but the adapter does not use them for this yet, on any release line. So you take such a task away in your model (the step).
This is about Camunda 8 only, and only if you have seen it. The Business Cockpit shows a user task as active, but a
check about that same task answers 404. Under version 1 this had one of two causes. Version 2 has neither.
The first cause is the storage the question went to. Version 1 asked whether a user task still exists with a
UserTaskGet. That reads the cluster's searchable storage. An exporter writes that storage, and the exporter runs
behind the engine. Your application, and with it the cockpit's record, learns about a task earlier. It learns from
the creating task listener, a job which the engine hands out before the task even exists. So for a while the
cockpit has the task and the storage does not. We measured this on 2026-09-28 against camunda/camunda:8.8.39,
camunda/camunda:8.9.21 and camunda/camunda:8.10.0-rc1, counting from the moment the listener job arrived. The
storage answered 404 for 1667 ms on 8.8, 649 ms on 8.9 and 219 ms on 8.10. Under load the gap is as long as your
exporter's backlog. Version 2 asks the engine instead, with an UpdateUserTask which changes nothing. In the same
runs it answered 204 after 12 ms, 21 ms and 13 ms, in the same order.
The second cause is the shape of the task, and there the 404 never goes away. A user task served by a job worker
has no user task record in the cluster at all. Version 1 modelled user tasks this way up to its release 1.6.3.
Such a task only has a job, and the id your application holds for it is that job's key. So every question asked as
if it were a user task misses. In the same runs UserTaskGet, the empty UpdateUserTask and the user task search
answered only "not found" for a full minute. At the same time the job command on the same key was accepted, and
the engine said it still holds the instance. Version 2 does not serve that shape, and names it while it deploys.
This is the first thing to do in Migrate in six steps: change those models, and
finish what is open on them before you upgrade.
Your business code pays nothing here. The outbox does. Version 1 wrote no outbox entry and kept no record of task deliveries. So everything below is work which version 2 does around code you do not touch. It is counted in rows, transactions and threads. No milliseconds appear, because a time measured on one machine is read as a promise on another.
A workflow start costs one outbox entry. It is written in your transaction, which still commits once. The call to the BPMS then runs after that commit, in a transaction of its own and on a thread which is not yours. On Camunda 8 that second transaction is new. On Camunda 7 it is not. Version 1 wrote a job of its own engine inside your transaction, and the job executor carried it out afterwards. So the count is the same. What changed is the table the row goes into and the thread which picks it up.
On Camunda 7 an application pays when it answers a message, and also when it completes or cancels a task. Version 1 called the embedded engine while your transaction was open, so your commit covered that work. Version 2 writes an entry for it, which is one row and one transaction more than before. On Camunda 8 those operations left the committing thread under version 1. Now they are entries, like the start.
Every task delivery costs one record, written in the transaction of your handler. It costs no transaction of its
own. This record answers a repeated delivery, instead of running your @WorkflowTask method a second time.
Your own handler is not in those numbers, but it did move. Under version 1 it ran inside the engine's job
transaction on Camunda 7, and inside the transaction your @Transactional opened on Camunda 8. Now it runs inside
a transaction which VanillaBP opens around it. How long it holds that transaction is still up to you.
The threads are what you need to size. On Camunda 7, version 1 ran its engine calls on the engine's job executor.
Camunda's Spring Boot starter starts it with three threads and grows it to ten. On Camunda 8, version 1 used the
thread which committed, so it had as many threads as your requests. Version 2 dispatches on four threads
(vanillabp.outbox.dispatch-threads). The workflow aggregate decides which of them takes an entry. So the
operations of one workflow keep their order, while different workflows run at the same time. On Camunda 7 those
four threads are a second pool, not a replacement. The engine's job executor is still there and still delivers
your tasks. The dispatch threads carry the calls VanillaBP makes into the engine. Some applications raised
camunda.bpm.job-execution.max-pool-size because the job executor was their bottleneck. They now have two numbers
to raise. Both pools use connections of the database where your workflow aggregates live.
What VanillaBP is sized for has the rest of the picture.
An application which reported to the VanillaBP Business Cockpit
keeps its Java code. @UserTaskDetailsProvider, @WorkflowDetailsProvider, @DetailsEvent, the prefilled
details objects and BusinessCockpitService are unchanged. What changes is the dependency, where the keys live,
and the moment a report is sent.
Version 1 shipped the cockpit's integration as adapters in the cockpit's own repository:
io.vanillabp.businesscockpit:camunda7-spring-boot-adapter and camunda8-spring-boot-adapter, on top of
adapters-commons. They ran on Spring Boot only, and only next to VanillaBP 1's spring-boot-support. Version 2
makes the cockpit an adapter of its own, which you add next to the adapter of your BPMS. The VanillaBP core has its
own word for such a thing, and the glossary puts both side by side. There is one repository per
BPMS and one variant per platform. All of them use the group io.vanillabp.businesscockpit:
| BPMS | Spring Boot artifact | Quarkus extension | Documentation |
|---|---|---|---|
| Camunda 7 | businesscockpit-camunda7-adapter-spring-boot |
businesscockpit-camunda7-adapter-quarkus |
Wiki |
| Camunda 8 | businesscockpit-camunda8-adapter-spring-boot |
businesscockpit-camunda8-adapter-quarkus |
Wiki |
| Process-Engine-API | businesscockpit-process-engine-api-adapter-spring-boot |
businesscockpit-process-engine-api-adapter-quarkus |
Wiki |
The Camunda 8 artifact is released once per Camunda 8 minor version. So its version names the line it was built
against, 0.x.y-8.9. The Process-Engine-API variant has no predecessor in version 1. Its wiki says what that
engine can tell the cockpit today, and what it cannot. On Quarkus the platform-neutral part is a Quarkus extension
of its own. You add it next to the BPMS extension, as the wikis show.
The Business Cockpit reads the same keys as in version 1, in the same places: vanillabp.cockpit.* for the whole
application, vanillabp.workflow-modules.<id>.cockpit.* for a workflow module, and
vanillabp.workflow-modules.<id>.workflows.<process>.cockpit.* with its user-tasks.<task>.cockpit.* for the two
levels below. An application upgrades by changing a dependency. How the cockpit changed inside is nothing its
configuration needs to know about. The keys which are gone, and the reason for each, are on the Configuration
page of the wiki for your BPMS
(Camunda 7,
Camunda 8,
Process-Engine-API).
VanillaBP itself declares nothing below vanillabp.cockpit. The cockpit binds that part of the tree itself. A
BPMS adapter uses the same overlay for its own keys. So a key which belongs neither to VanillaBP nor to the cockpit
still ends a Quarkus startup, instead of being ignored.
A key which is gone does nothing on Spring Boot if you leave it in place. On Quarkus it ends the build with a
message which names it, because Quarkus refuses a key below vanillabp which nothing reads. On both platforms,
watch for a workflow module whose section is incomplete. A module which sets some keys and leaves others out ends
the boot. The message names every gap in the application. A module which sets nothing at all is read as a module
which does not want to report, and the application starts. One INFO line per such module names the key which
would let it report. The cockpit server never hears about that module.
The details provider methods and BusinessCockpitService keep their API. One attribute is refused now. A
@UserTaskDetailsProvider which names a version ends the boot, with a message which names the method. Version 1
documented that attribute but never read it. Also, the events the adapter reacts to do not say which version of a
process they came from. So nothing could choose between two methods anyway. Match by id or taskDefinition
instead.
A report is now an outbox entry. It is written in the transaction which caused it, and sent once that transaction has committed. So your details provider runs at dispatch time, not inside the engine's own work. If it changes an aggregate, the change is saved in that later transaction, not in the one the task ran in. In return, a report survives a cockpit server which is down. And a burst of changes to one task collapses into a single report with the current state.
On Camunda 8 the cockpit's own three task listeners of a user task are byte-identical to the version 1 ones. But the start of a workflow is now reported from an execution listener on every start event, instead of one listener on the process. This lets the report name the business case the workflow is about. So the model your application deploys after the upgrade differs from the one the cluster holds, and the first deployment creates a new process version. Workflows which are already running stay on the version they started on, and keep reporting their user tasks.
On Camunda 7 the engine has to write history, which version 1 never asked of it. A workflow's lifecycle is now read
from the engine's history, not from the eventing of the Camunda Spring Boot starter. Every history level from
activity upwards has what is read, including Camunda's default audit. So an engine whose level you never
lowered needs no change. An engine whose history is switched off ends the boot with a message.
The full list per BPMS, including the settings which are gone, is on the Migrating from version 1 page of the Camunda 7 and the Camunda 8 wiki.
-
Camunda 8 with user tasks: check the models first, and finish what is open. Up to 1.6.3, VanillaBP
modelled a user task with a
formKeyin itszeebe:formDefinition. Version 2 does not serve such a user task. A BPMN process which one of your@WorkflowServiceclasses claims, and which has such an element, does not deploy at all. So your application will not start until you have changed the model. And a task open on such an element can no longer be completed once you have upgraded (the step). - Lift the runtime: Java 21, Spring Boot 4 (or Quarkus 3.37). Fix the Spring imports which moved when Spring Boot was split into modules.
-
Swap the dependencies: the adapter artifact was renamed, and on Quarkus you add both extensions. Then
build. The compiler points at the three removed overloads and at
@BpmnProcess.primary. Persistence needs nothing here. What version 1 could do, JPA and MongoDB through Spring Data, version 2 does out of the box. -
Declare your workflow modules with one
META-INF/workflow-moduledescriptor each (same ids as before). Delete theWorkflowModulePropertiesbean method where one declared the module. Then move the configuration:default-adapter→prioritized-adapters, adapter settings intovanillabp.adapters.<id>.*, and delete what the conventions now derive. Start the application and follow the startup messages. They name every missing key. -
Delete your transaction annotations on workflow services. Check that nothing else wraps a
@WorkflowTaskmethod in a transaction of its own. Where a class is the workflow service AND your business service, either the annotation stays or you split the class. -
Verify against your BPMS: start one workflow of every process, complete an asynchronous task, and correlate
a message. On a remote BPMS also look into the outbox store once (entries should reach DONE), and decide on
your
retention.
Moving between BPMS, not between versions, is a different topic: BPMS migration.
