Given the upcoming Camunda 7 EOL and the advantages of Camunda 8, many developers are now planning their migration journey. Camunda is actively investing in tooling to make this journey as smooth as possible. In this post, we’ll explore an end-to-end example of migrating a Camunda 7 process solution to Camunda 8 using the available migration tools.
If you’ve been dreading migration, fear not—we’ll break it down step by step together. By the end, you’ll see that moving to Camunda 8 is totally doable—and maybe even fun!
Table of contents
Is migrating to Camunda 8 challenging?
Migrating between major versions isn’t just a version bump. Camunda 8’s architecture is quite different from 7, which presents a few challenges for migration. For example, in Camunda 7, the engine ran in the same JVM and you interacted with it directly; in Camunda 8, the engine is a separate cluster and you interact via the REST or gRPC APIs. This means service tasks in Camunda 8 are performed by external workers instead of in-engine Java delegates, so your JavaDelegate classes need to be migrated.
There are other differences, of course, but the good news is that Camunda has provided tooling that automates many of the required changes! There’s a Migration Analyzer & Diagram Converter to tackle model differences, OpenRewrite migration recipes to refactor your code, and the Data Migrator for runtime data. Our plan: use all these tools on a Camunda 7 project and see how they help us conquer each migration obstacle. So, grab your favorite beverage and let’s dive in!
A step-by-step Camunda 7 to 8 migration example
To make this concrete, we’ll use a deliberately simple Camunda 7 project and walk through migrating it to Camunda 8. This example covers the full journey: converting BPMN models, updating code, adjusting configs, migrating tests, and even transferring active process instances. The project may be small, but it hits many common migration points—perfect for learning how the migration tooling works.
Our original Camunda 7 process can be found in our Community Hub. It features one service task implemented by a Java delegate, another using an expression (Spring bean method), a sub-process call, a decision gateway, a user task, and a timer—a nice little microcosm of common Camunda 7 usage. Now, our mission is to carry this entire solution to Camunda 8 world.
Step 1: Converting the BPMN models (Diagram Converter)
The first migration task is updating the BPMN diagrams to be Camunda 8 compatible. Camunda provides a Migration Analyzer & Diagram Converter tool for this. Using the converter’s handy UI, we uploaded our BPMN files and enabled an important option: “Add Data Migration Execution Listener”.

What does it do? In short, it analyzes a Camunda 7 BPMN/DMN model and lists all the changes needed to make it Camunda 8-ready—and then it can automatically convert the model for you. It produces a report of migration tasks categorized by severity so you know what to double-check. In our case, because the process is fairly simple, the converter was able to handle most changes automatically.
The “Add Data Migration Execution Listener” option inserts a special execution listener on the start event of the process, which we’ll need later for migrating running instances (more on this in Step 5).
After conversion, download the modified BPMN files and drop them into your project, replacing the old Camunda 7 versions. Now our process model is officially Camunda 8-friendly! Before we celebrate, though, remember that the model conversion might leave some “TO-DO” flags in the report for things requiring manual attention. It’s always good to review the converter’s output. But in our example, we’re in good shape to move on to the Java code.
Step 2: Auto-Refactoring the code (OpenRewrite recipes)
With the diagrams taken care of, the next big task is migrating the Java code—particularly, swapping out Camunda 7 APIs and patterns for Camunda 8 equivalents. Enter Camunda’s OpenRewrite migration recipes. OpenRewrite is a refactoring tool that can apply automated code changes via predefined recipes.
You can integrate these recipes into your project using the Maven plugin rewrite-maven-plugin. In our pom.xml, we added the Camunda 7-to-8 rewrite dependency and activated the relevant recipes: “AllClientRecipes” (to handle engine API usage like RuntimeService, TaskService, etc.) and “AllDelegateRecipes” (to handle JavaDelegate classes and related patterns).
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>6.0.5</version>
<configuration>
<activeRecipes>
<recipe>org.camunda.migration.rewrite.recipes.AllClientRecipes</recipe>
<recipe>org.camunda.migration.rewrite.recipes.AllDelegateRecipes</recipe>
</activeRecipes>
<skipMavenParsing>false</skipMavenParsing>
</configuration>
<dependencies>
<dependency>
<groupId>org.camunda.community</groupId>
<artifactId>camunda-7-to-8-rewrite-recipes</artifactId>
<version>0.0.1-alpha2</version>
</dependency>
</dependencies>
</plugin>
Then run the refactoring with a simple command: mvn rewrite:run. After the plugin finishes, it’s time to see what the robot overlord did to our code! You should find that the JavaDelegate was transformed into a job worker, and the job type for the worker should match what is found in the migrated BPMN model (in this example, both job types should be “sampleJavaDelegate”). You should also find that the code that starts the process was migrated to the new Camunda 8 Java client.
Step 3: Reviewing and tweaking the code for Camunda 8
The automated refactoring gave us a great head start. However, there are a few items in the code that need to be manually migrated.
In Camunda 7, the Spring Boot starter would automatically deploy any BPMN resources on startup. Camunda 8’s Spring integration is a bit more explicit. To enable automatic deployments, add the @Deployment annotation to the main application class:
@SpringBootApplication
@Deployment(resources = "classpath*:/**/*.bpmn")
public class Application { ... }
Remember that second service task, which calls sampleBean.someMethod(y) and had a resultVariable="theAnswer" in Camunda 7? This one needed a bit of extra work. The diagram converter transformed that task into a job with type “sampleBeanSomeMethod”, and it moved the expression into a task header (essentially a parameter) with key “y” (the variable it uses).

To implement this in Camunda 8, we need to turn the bean method itself into a job worker. In our SampleBean class, we can annotate the method:
@JobWorker(type = "sampleBeanSomeMethod")
public int someMethod(@Variable("y") String text) {
System.out.println("SampleBean.someMethod('" + text + "')");
return 42;
}
You can use @JobWorker with the type “sampleBeanSomeMethod” (matching what the BPMN now expects). You can also use the @Variable("y") annotation on the method parameter, which tells Camunda to inject the process variable “y” into that argument. So effectively, when a job of type “sampleBeanSomeMethod” is picked up, it will call this method, passing in the current value of variable “y”.
Camunda 8 does not know about the resultVariable concept. Simply returning “42” in our worker won’t magically set theAnswer in the process instance. To bridge this gap, we need to introduce a custom Spring configuration (CamundaSdkCustomConfiguration) that tweaks how results are handled. Essentially, it provides a custom resultProcessor bean that checks if the job has a header “resultVariable” and, if so, assigns the worker’s result to that process variable name. In our case, the converted BPMN includes a header “resultVariable: theAnswer” on that task. The custom result processor detects that and stores the method’s return value under “theAnswer”.
The main takeaway for our migration journey is we had to write a small piece of glue code to preserve Camunda 7’s result variable behavior. This is a good example of a manual intervention that the automated tools can’t fully solve, but at least we knew about it from the converter’s report and Camunda’s migration guide.
There is one last code step: implement a tiny “noop” worker in our code:
@JobWorker(name = "noop")
public void noop() { }
Why on earth do we need a no-op worker? This goes back to that execution listener we added for the Data Migrator (which is explained in Step 5 below). The listener was configured such that if a process instance is started without a legacy ID (i.e. a normal new instance, not migrated), it will create a job of type “noop” on the start event. That job just sits there unless a worker picks it up. We don’t want our processes to hang at the start, so we register a dummy worker for type “noop” that immediately completes (does nothing). This way, when we start processes normally in Camunda 8, the “noop” job will be completed and the process can continue past the start event. Essentially, the noop is a safety to bypass the migrator listener when it’s not needed. It’s an odd detail, but an important one—otherwise, when we run our app and start new processes, they’d all stall at the beginning waiting for a “migrator” that isn’t there. With our noop in place, we’re good to go.
Lastly, our Maven dependencies can be cleaned up. You can remove all Camunda 7 libraries and add the Camunda 8 ones. In our case, the final pom needed only:
<dependency>
<groupId>io.camunda</groupId>
<artifactId>spring-boot-starter-camunda-sdk</artifactId>
<version>8.8.0-alpha6-rc3</version>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>camunda-process-test-spring</artifactId>
<version>8.8.0-alpha6-rc3</version>
<scope>test</scope>
</dependency>
(Note: you might be wondering, why version 8.8.0-alpha6-rc3? For this example, we needed the most recent version, at the time of writing, of camunda-process-test-spring. Keep an eye out for the full 8.8 release in October 2025!)
With these changes in place, we recompiled the project. It’s now a Camunda 8 application! Time to give it a spin and see if it runs!
- Start Camunda 8. For this example, we can use Camunda 8 Run.
- Launch the Spring Boot app. The app starts up, deploys the BPMN, and immediately you should see some process instances have started.
So, we’ve successfully migrated the process model and code and done a smoke test to ensure the basic workflow runs on Camunda 8. But our job isn’t finished yet—we need to tackle the tests and then the running data migration.
Step 4: Updating the JUnit tests (or “Honey, I broke the tests!”)
Unsurprisingly, the Camunda 7 test cases didn’t compile or run against the new code. All those nice helper methods like runtimeService(), task(), complete(task()), and even some assertions were part of Camunda 7’s test utilities. In Camunda 8, the testing story is a bit different. Camunda provides the Camunda Process Test (CPT) library with annotations and utilities for writing tests against the Zeebe engine.
Since automated recipes for test migration weren’t available at the time we wrote this blog, we tried a fun approach: let AI assist in refactoring the test. (Yes, we used generative AI to help migrate our Camunda tests!) We prompted it with instructions on how to convert the Camunda 7 test to Camunda 8, referencing Camunda’s official testing patterns. After a few iterations (the AI needed some guidance and corrections—as they do), we arrived at a working Camunda 8 test. (Covering CPT is a bit outside the scope of this already lengthy blog post, so please review the generated code in the example repository as well as the CPT documentation for more details!)
When we ran the updated tests, they all went green—success! The new tests accomplish the same validations as the old ones, but using Camunda 8’s testing toolkit. We did have to spend a bit more effort on these (the AI helper was cool but not perfect), yet overall it was manageable. In a real project, if you have many tests, you’d systematically refactor them, possibly write some custom scripts or simply grind through using the new APIs. The key is that Camunda 8’s testing libraries provide the necessary hooks (controlling time, starting processes, completing tasks, assertions) to do everything you did with Camunda 7 tests—just in a different form.
At this stage, our process logic and tests are fully migrated to Camunda 8. We have a working Camunda 8 application that passes its test suite. If this were a real project, we’d now be confident that new process instances will run fine on the new platform. But what about those 100 instances we left hanging in Camunda 7’s database? On to the final boss: migrating the running instances.
Step 5: Migrating running instances (Using the Data Migrator)
One of the hardest parts of migration can be dealing with in-flight processes. You don’t want to lose or manually terminate processes that started on Camunda 7 but have not yet finished. Ideally, they should continue on Camunda 8 as if nothing happened. That’s exactly what the Camunda 7 to 8 Data Migrator tool does.
The Data Migrator operates by connecting to both your Camunda 7 database and the Camunda 8 cluster, and transferring state. It has a mode for runtime instance migration, which takes each active (running) process instance from Camunda 7 and creates an equivalent instance in Camunda 8, at the same point in the workflow. (There’s also a history migration mode to bring over historical data, but this example focuses on runtime.) Of course, certain things have to line up: the process definitions must exist in Camunda 8 (hence we made sure to deploy the converted model there), and some features like multi-instance or certain patterns might not be supported yet. In our case, our process is simple enough to be fully supported.
Earlier, in Step 1, we took the precaution of adding an execution listener on the start event with a condition: =if legacyId != null then "migrator" else "noop". Let’s explain that: when the Data Migrator creates a process instance in Camunda 8, it will set a special legacyId (the ID of the instance from Camunda 7). Our execution listener expression checks for that. If legacyId is present, it means “this instance came from Camunda 7 migration”—in that case, the listener creates a job of type “migrator” at the start. If legacyId is not present (normal new instance), it creates a “noop” job.
We already saw how we handle “noop”. But what about “migrator”? The Data Migrator tool itself will deploy a worker that listens for “migrator” jobs, and the “migrator” job will bring the new process instance in Camunda 8 to the correct state by updating the process variables and moving the token forward to the correct task. In other words, the process instances migrated over will pause at the start line until the Data Migrator gives the go-ahead, ensuring everything is in sync. Without this, an imported instance might immediately race ahead in Camunda 8 (potentially duplicating work or diverging). So that little listener is critical for a controlled transfer. The good news is the diagram converter adds it for us automatically.
Now, to run the Data Migrator, we provided it a configuration pointing to:
- The Camunda 7 database (in our example, it’s an H2 file; in real life it could be PostgreSQL, etc.). We give the JDBC connection details so the migrator can read the Camunda 7 tables.
- The Camunda 8 endpoints (the gRPC address of the Zeebe broker and the REST address of key Camunda 8 components).
- The job type names to use (in our case, “migrator” and the conditional expression we used for validation-job-type).
With everything configured, start the migrator (in the example, via a script start.bat --runtime). The migrator connected to the Camunda 7 DB, found those 100 running instances, and began migrating them one by one. Because our Camunda 8 process model was deployed and had the matching execution listener, each migrated instance was created and then held at the start (with a “migrator” job). The migrator’s worker then completed those jobs, allowing the instances to proceed in Camunda 8 from where they left off in Camunda 7. Since our instances were all at either the user task or waiting on a timer when we cut over, now they appear in Camunda 8 in those same states. (The migration also preserves the Camunda 7 process instance ID as a process variable in the new Camunda 8 instance!)
It is always a good idea to review the logs for any errors and check Operate to verify the results. Lo and behold:

One important consideration: In a real migration scenario, you wouldn’t normally leave that migrator execution listener in your process model forever. The recommended approach is:
- Deploy a special version of the process with the migrator listener on the start event.
- Run the Data Migrator to transfer the instances, which will start on this special version.
- Once done, deploy a clean version of the process model without the migrator listener for ongoing use, so new instances don’t have the overhead or risk of the listener.
- Switch your production to use Camunda 8, and retire Camunda 7.
Our example glossed over that by keeping everything in one version, but it’s a crucial detail to avoid new instances hanging at a “migrator” step when there’s no migrator running.
Conclusion
We’ve reached the finish line of our migration journey. To recap, we took a Camunda 7 project and migrated it to Camunda 8 by addressing each layer:
- Models: Converted with the Migration Analyzer & Diagram Converter, which handled XML differences and highlighted needed changes.
- Code: Refactored using Camunda’s OpenRewrite recipes for common patterns (engine API usage and JavaDelegates), then manually adjusted a few things (like adding @Deployment, handling the expression bean with a custom result mapping, etc.).
- Tests: Rewritten to use Camunda 8’s testing framework, ensuring we can validate the process just as we did before (thanks to tools like CamundaProcessTestContext for time-travel and task completion).
- Running instances: Migrated from the old system to the new one using the Data Migrator, which let us carry over the state of active workflows seamlessly into Camunda 8.
The tools took care of the heavy lifting, leaving us to focus on the higher-level adjustments and validations. It’s fair to say that without these tools, migrating would be a far more daunting prospect. Camunda 8 still has some sharp edges and differences, but the ecosystem is making it easier every day to bridge the gap from Camunda 7.
For Camunda developers, the big takeaway is: don’t panic—migration is achievable! Yes, you have to invest some time in it, but as we saw, you can incrementally tackle it step by step. Run the analyzer on your models to see what needs changing. Use the OpenRewrite recipes to accelerate code updates. Plan how to handle your running instances based on your business needs (maybe you can drain them with a bit of patience, or use the migrator if you need immediate switch-over). And test everything in a safe environment before flipping the production switch.
Moving to Camunda 8 sets you up for the future—scalability, cloud readiness, agentic orchestration, and all the new goodies Camunda is building. And now you have a blueprint of how to do it without losing your sanity (or your process data)!
Start the discussion at forum.camunda.io