Migration C7 > C8
This guide provides step-by-step instructions for migrating your Orchescala application from Camunda 7 to Camunda 8. The migration involves changes to BPMNs/ DMNs, Engine-, Worker- and Simulation configuration.
This is a work in progress. I will add more details as I go along.
For now, it describes running C7 and C8 next to each other.
The documentation is based on the Demo Company. Check it out for the whole context.
Overview
Camunda 8 introduces significant architectural changes compared to Camunda 7:
- Architecture: Camunda 8 uses Zeebe as the process engine (cloud-native, distributed)
- Variables: JSON-only variable handling (no Java objects)
- Deployment: Different deployment mechanisms and APIs
Migration Checklist
Each Project
-
Update BPMNs/ DMNs for Camunda 8 compatibility
- Replace Scripts with FEEL or Workers
- Convert JUEL expressions to FEEL
Company Project
-
Configure for Camunda 8:
- Engine settings
- Worker settings
- Simulation settings
What works out of the box
In your projects you don't need to change anything in:
- Domain Models
- API Documentation
- Workers
- Simulations
- Helper
Preparation
Before you migrate a process, make sure you have:
- migrated every logic to workers.
- that all your subprocesses are migrated.
Future Work
- Postman Open API
- DMN Tester
- Deploy to C8 by Simulation
1. BPMNs
Key Differences
Camunda 7 vs Camunda 8 BPMN Changes:
Aspect | Camunda 7 | Camunda 8 |
---|---|---|
Task Types | External Tasks with topics | Job Types |
Variables | Java objects + JSON | JSON only |
Forms | Embedded/Generated Forms | Tasklist Forms |
Expressions | JUEL expressions | FEEL expressions |
Scripts | Inline/External Scripts | FEEL expressions or Workers |
Listeners | Execution/Task Listeners | Workers |
Migration Steps
Use the Migration Guide to update your BPMNs
-
Create a Camunda 8 directory in your project:
mkdir src/main/resources/camunda8
-
Use the Migration Analyzer
- This creates a Camunda 8 compatible BPMN.
- And a report with stuff you need to adjust manually.
-
Adjust the BPMN manually.
- Use the report from the Migration Analyzer.
- Or just go through the BPMN.
- Replace Scripts with FEEL or Workers.
- Convert JUEL expressions to FEEL.
-
Deploy the BPMN to Camunda 8. (manually for now)
-
Run the Simulation.
2. Engine Configuration
This depends on your Camunda 8 setup.
Migration Steps
-
Create C8 Engine Configuration
Create a new engine configuration class for Camunda 8:
package mycompany.orchescala.engine import orchescala.engine.EngineConfig import orchescala.engine.c8.C8SaasClient trait CompanyEngineC8Config extends C8SaasClient: // Provide EngineConfig as a given instance given EngineConfig = EngineConfig( tenantId = None // Set your tenant ID if needed ) // C8 SaaS Configuration protected def zeebeGrpc: String = sys.env.getOrElse("ZEEBE_GRPC_ADDRESS", "https://bru-2.zeebe.camunda.io:443") protected def zeebeRest: String = sys.env.getOrElse("ZEEBE_REST_ADDRESS", "https://bru-2.zeebe.camunda.io/v1") protected def audience: String = sys.env.getOrElse("ZEEBE_AUDIENCE", "zeebe.camunda.io") protected def clientId: String = sys.env.getOrElse("ZEEBE_CLIENT_ID", "your-client-id") protected def clientSecret: String = sys.env.getOrElse("ZEEBE_CLIENT_SECRET", "your-client-secret") protected def oAuthAPI: String = sys.env.getOrElse("ZEEBE_OAUTH_URL", "https://login.cloud.camunda.io/oauth/token")
-
Environment Variables
Set up the following environment variables for Camunda 8:
# Camunda 8 SaaS Configuration export ZEEBE_GRPC_ADDRESS="https://your-cluster.zeebe.camunda.io:443" export ZEEBE_REST_ADDRESS="https://your-cluster.zeebe.camunda.io/v1" export ZEEBE_AUDIENCE="zeebe.camunda.io" export ZEEBE_CLIENT_ID="your-client-id" export ZEEBE_CLIENT_SECRET="your-client-secret" export ZEEBE_OAUTH_URL="https://login.cloud.camunda.io/oauth/token"
-
Update Dependencies
Add Camunda 8 dependencies to your
project/Settings.scala
:lazy val engineDeps = Seq( "io.github.pme123" %% "orchescala-engine-gateway" % orchescalaV )
With the
engine-gateway
we can abstract our engines, and we can use both engines at the same time.
3. Worker Configuration
Workers are independent of the engine. So you can use the same workers for both engines.
Migration Steps
-
Update Worker Base Class
trait CompanyWorker[In <: Product : InOutCodec, Out <: Product : InOutCodec] extends C7Worker[In, Out], C8Worker[In, Out]: protected def c7Context: C7Context = CompanyEngineC7Context(CompanyRestApiC7Client()) protected def c8Context: C8Context = CompanyEngineC8Context(CompanyRestApiC7Client())
-
Update Worker Registry
trait CompanyWorkerApp extends WorkerApp: lazy val workerRegistries: Seq[WorkerRegistry] = Seq( C7WorkerRegistry(CompanyC7Client), C8WorkerRegistry(CompanyC8Client) )
-
Update Context Implementation
Create a C8-compatible context (just extend the
C8Context
):package mycompany.orchescala.worker import orchescala.worker.c8.C8Context import scala.reflect.ClassTag class CompanyEngineContext(restApiClient: CompanyRestApiClient) extends C8Context: override def sendRequest[ServiceIn: InOutEncoder, ServiceOut: {InOutDecoder, ClassTag}]( request: RunnableRequest[ServiceIn] ): SendRequestType[ServiceOut] = restApiClient.sendRequest(request)
4. Simulation Configuration
This is a bit more involved;).
Migration Steps
-
Update Simulation Configuration
case class SimulationConfig( @description("define tenant if you have one") tenantId: Option[String] = None, @description( """there are Requests that wait until the process is ready - like getTask. |the Simulation waits 1 second between the Requests. |so with a timeout of 10 sec it will try 10 times (retryDuration = 1.second)""".stripMargin) maxCount: Int = 10, @description("Cockpit URL - to provide a link to the process instance. you can provide a different URL for each engine type with a Map") cockpitUrl: String | Map[EngineType, String] = ProcessEngine.c7CockpitUrl, @description("the maximum LogLevel you want to print the LogEntries") logLevel: LogLevel = LogLevel.INFO )
Example for the
CompanySimulation
:trait CompanySimulation extends SimulationRunner, C8SaasClient, CompanyEngineC7Config, CompanyEngineC8Config: given EngineConfig = EngineConfig(tenantId = config.tenantId) // Override this to provide the ZIO layers required by this simulation lazy val requiredLayers: Seq[ZLayer[Any, Nothing, Any]] = Seq( SharedC8ClientManager.layer, SharedC7ClientManager.layer ) // Override engineZIO to create the engine within the SharedC8ClientManager environment override def engineZIO: ZIO[Any, Nothing, ProcessEngine] = (for c8Engine: ProcessEngine <- C8ProcessEngine.withClient(this) c7Engine: ProcessEngine <- CompanyC7Simulation.engineZIO given Seq[ProcessEngine] = Seq(c8Engine,c7Engine) yield GProcessEngine()) .provideLayer(SharedC8ClientManager.layer) .provideLayer(SharedC7ClientManager.layer) override lazy val config: SimulationConfig = SimulationConfig( cockpitUrl = Map( EngineType.C7 -> camundaCockpitUrl, EngineType.C8 -> zeebeOperateUrl ) ) end CompanySimulation
What you need to do is:
- add the ZIO layers for both engines.
- create the engines.
- provide the
cockpitUrl
in theSimulationConfig
for both engines.
5. Testing and Validation
Testing Strategy
Just run both C7 and C8 Simulations side by side.
override def engineZIO: ZIO[Any, Nothing, ProcessEngine] =
(for
c8Engine: ProcessEngine <- C8ProcessEngine.withClient(this)
c7Engine: ProcessEngine <- CompanyC7Simulation.engineZIO
given Seq[ProcessEngine] = Seq(c8Engine,c7Engine) // -> change order to change default engine
yield GProcessEngine())
...
You can just change the order in your CompanySimulation
to change the default engine.
Or you provide separate Simulations for both engines (examples from democompany-cards
).
C7:
class OrderCreditcardC7Simulation extends OrderCreditcardSimulation, CompanyC7Simulation
C8:
class OrderCreditcardC8Simulation extends OrderCreditcardSimulation, CompanyC8Simulation
Both (gateway):
class OrderCreditcardGSimulation extends OrderCreditcardSimulation, CompanyGSimulation
Using the same Simulation:
abstract class OrderCreditcardSimulation extends CompanySimulation:
simulate(
scenario(`OrderCreditcard`)(
`Check Order approved UT`
),
...
Throwing an error in an end event is treated in:
- C8: is an unhandled BPMN error, which can interrupt the process and propagate the error.
- C7: is ignored if there is no boundary error event to catch it.
So in this case, you have to have different Scenarios.
Here an example:
abstract class OrderCreditcardSimulation extends CompanySimulation:
// only needed for an end event that throws an error. see documentation
protected def engineType: EngineType = EngineType.C8
simulate(
...,
// because handled differently
if engineType == EngineType.C8 then
incidentScenario( // thrown in the end event
`OrderCreditcard handled error`,
"Expected to throw an error event with the code 'client-not-found', but it was not caught."
)
else
scenario(`OrderCreditcard handled error`)
)
6. Resources
- Camunda 8 Migration Guide
- Demo Company Repository
- Create issues in the Orchescala GitHub repository