Camunda BPMN
Experimental module
The experimental module is fully working and tested, but requires additional usage validation and analysis.
Therefore, the API may receive minor changes before full readiness.
The module connects an embedded Camunda 7 engine for executing BPMN processes inside a Kora application.
It creates and configures ProcessEngine, connects it to a JDBC data source, registers delegates from the application graph, deploys BPMN / FORM / DMN resources from classpath, and adds execution telemetry.
To expose the Camunda 7 REST API and the Cockpit / Admin / Tasklist web applications over HTTP, use the separate Camunda 7 REST module alongside this one.
Dependency¶
Dependency build.gradle:
Module:
Dependency build.gradle.kts:
Module:
The module requires the JDBC module.
By default, the main application DataSource is used, but you can provide a separate DataSource with the @Tag(CamundaBpmn.class) tag when needed.
Configuration¶
Example of the complete configuration described by the CamundaEngineBpmnConfig class:
camunda {
engine {
bpmn {
jobExecutor {
corePoolSize = 5 //(1)!
maxPoolSize = 25 //(2)!
queueSize = 25 //(3)!
maxJobsPerAcquisition = 2 //(4)!
virtualThreadsEnabled = false //(5)!
}
deployment {
tenantId = "Camunda" //(6)!
name = "KoraEngineAutoDeployment" //(7)!
deployChangedOnly = true //(8)!
resources = ["classpath:bpm"] //(9)!
delay = "1m" //(10)!
}
parallelInitialization {
enabled = true //(11)!
validateIncompleteStatements = true //(12)!
}
admin {
id = "admin" //(13)!
password = "admin" //(14)!
firstname = "Ivan" //(15)!
lastname = "Ivanov" //(16)!
email = "admin@mail.ru" //(17)!
}
telemetry {
logging {
enabled = false //(18)!
stacktrace = true //(19)!
}
metrics {
enabled = true //(20)!
slo = [1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000] //(21)!
tags = { //(22)!
"key1" = "value1"
"key2" = "value2"
}
}
engineTelemetryEnabled = false //(23)!
tracing {
enabled = true //(24)!
attributes = { //(25)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
}
}
- Minimum number of permanently alive threads in
JobExecutor(default:5). - Maximum number of threads in
JobExecutor(default:25). JobExecutortask queue size before new tasks are rejected (default:25).- Maximum number of jobs acquired by
JobExecutorin one request (default:Runtime.getRuntime().availableProcessors() * 2). - Use virtual threads as the
JobExecutorbase (default:false). When this option is enabled, pool and queue size settings are not used. tenantidentifier for resource deployment (default not specified, optional).- Resource deployment name (default:
KoraEngineAutoDeployment). - Deploy only changed resources through
Camundaduplicate filtering (default:true). - List of paths for finding
BPMN/FORM/DMNresources (required, default not specified). Only paths with theclasspath:prefix are supported. - Delay before deploying resources to the engine (default not specified, optional).
- Enable parallel engine initialization (default:
true). - Validate incomplete engine statements during parallel initialization (default:
true). Camundaadministrator identifier (required, default not specified). The wholeadminsection is optional.Camundaadministrator password (required, default not specified). The wholeadminsection is optional.Camundaadministrator first name (default not specified, optional). If not specified, uppercaseidis used.Camundaadministrator last name (default not specified, optional). If not specified, uppercaseidis used.Camundaadministrator email address (default not specified, optional). If not specified,<id>@localhostis used.- Enables module logging (default:
false). - Enables error stack trace logging (default:
true). - Enables module metrics (default:
true). - SLO configuration for metrics (default:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO). - Metric tags (default:
{}). - Enables built-in
Camundaengine telemetry collection (default:false). - Enables module tracing (default:
true). - Tracing attributes (default:
{}).
camunda:
engine:
bpmn:
jobExecutor:
corePoolSize: 5 #(1)!
maxPoolSize: 25 #(2)!
queueSize: 25 #(3)!
maxJobsPerAcquisition: 2 #(4)!
virtualThreadsEnabled: false #(5)!
deployment:
tenantId: "Camunda" #(6)!
name: "KoraEngineAutoDeployment" #(7)!
deployChangedOnly: true #(8)!
resources: #(9)!
- "classpath:bpm"
delay: "1m" #(10)!
parallelInitialization:
enabled: true #(11)!
validateIncompleteStatements: true #(12)!
admin:
id: "admin" #(13)!
password: "admin" #(14)!
firstname: "Ivan" #(15)!
lastname: "Ivanov" #(16)!
email: "admin@mail.ru" #(17)!
telemetry:
logging:
enabled: false #(18)!
stacktrace: true #(19)!
metrics:
enabled: true #(20)!
slo: [1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000] #(21)!
tags: #(22)!
key1: value1
key2: value2
engineTelemetryEnabled: false #(23)!
tracing:
enabled: true #(24)!
attributes: #(25)!
key1: value1
key2: value2
- Minimum number of permanently alive threads in
JobExecutor(default:5). - Maximum number of threads in
JobExecutor(default:25). JobExecutortask queue size before new tasks are rejected (default:25).- Maximum number of jobs acquired by
JobExecutorin one request (default:Runtime.getRuntime().availableProcessors() * 2). - Use virtual threads as the
JobExecutorbase (default:false). When this option is enabled, pool and queue size settings are not used. tenantidentifier for resource deployment (default not specified, optional).- Resource deployment name (default:
KoraEngineAutoDeployment). - Deploy only changed resources through
Camundaduplicate filtering (default:true). - List of paths for finding
BPMN/FORM/DMNresources (required, default not specified). Only paths with theclasspath:prefix are supported. - Delay before deploying resources to the engine (default not specified, optional).
- Enable parallel engine initialization (default:
true). - Validate incomplete engine statements during parallel initialization (default:
true). Camundaadministrator identifier (required, default not specified). The wholeadminsection is optional.Camundaadministrator password (required, default not specified). The wholeadminsection is optional.Camundaadministrator first name (default not specified, optional). If not specified, uppercaseidis used.Camundaadministrator last name (default not specified, optional). If not specified, uppercaseidis used.Camundaadministrator email address (default not specified, optional). If not specified,<id>@localhostis used.- Enables module logging (default:
false). - Enables error stack trace logging (default:
true). - Enables module metrics (default:
true). - SLO configuration for metrics (default:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO). - Metric tags (default:
{}). - Enables built-in
Camundaengine telemetry collection (default:false). - Enables module tracing (default:
true). - Tracing attributes (default:
{}).
The deployment section is optional: if it is not specified, the module does not automatically deploy resources.
If the section is specified, resources must contain at least one path.
Resources are searched recursively in classpath; unsupported paths without the classpath: prefix are skipped.
Module metrics are described in the Metrics Reference section.
Deployment¶
When the deployment section is present, the module automatically deploys process resources into the engine after it is created.
Resources are placed on the classpath (usually under src/main/resources) and referenced by the resources list:
- At least one path is required when the
deploymentsection is present. Only paths with theclasspath:prefix are supported.
Given the following layout, the classpath:bpm path is scanned recursively and every supported resource under it is deployed:
Deployment rules to keep in mind:
- Supported resource types are
BPMNprocess models,FORMforms, andDMNdecision tables. - Only paths with the
classpath:prefix are deployed. Any other path is skipped with a warning in the log. - Paths are scanned recursively, so nested directories under the listed path are included.
- With
deployChangedOnly = true(default)Camundaduplicate filtering is enabled, so only resources that changed since the previous deployment are redeployed. - The optional
tenantIdbinds the deployment to a specifictenant, anddelaypostpones the deployment for the configured duration after startup. - The deployment is registered under the
name(defaultKoraEngineAutoDeployment). - If the whole
deploymentsection is omitted, the module does not deploy any resources — you are expected to deploy them yourself throughRepositoryService.
Delegates¶
Camunda can call application components as process delegates.
Regular JavaDelegate instances are registered in the context by the full class name (canonicalName) and by the short class name (simpleName).
Inside execute(...) you read and write process variables through DelegateExecution:
@Component
public final class ScoreCustomerDelegate implements JavaDelegate {
private static final Logger logger = LoggerFactory.getLogger(ScoreCustomerDelegate.class);
@Override
public void execute(DelegateExecution execution) {
int scoring = ThreadLocalRandom.current().nextInt(1, 100);
logger.info("Scored {} with result {}.", execution.getBusinessKey(), scoring);
execution.setVariable("result", scoring);
}
}
@Component
class ScoreCustomerDelegate : JavaDelegate {
private val logger = LoggerFactory.getLogger(ScoreCustomerDelegate::class.java)
override fun execute(execution: DelegateExecution) {
val scoring = ThreadLocalRandom.current().nextInt(1, 100)
logger.info("Scored {} with result {}.", execution.businessKey, scoring)
execution.setVariable("result", scoring)
}
}
Because a JavaDelegate is registered by its short class name, a serviceTask in the BPMN model references it by simpleName through camunda:delegateExpression:
<bpmn:serviceTask id="Activity_0tusr5p" name="Score Customer"
camunda:delegateExpression="${ScoreCustomerDelegate}">
<bpmn:incoming>Flow_score_in</bpmn:incoming>
<bpmn:outgoing>Flow_score_out</bpmn:outgoing>
</bpmn:serviceTask>
Use KoraDelegate for an arbitrary delegate name.
The key() method returns canonicalName by default, but it can be overridden to specify the name used in BPMN expressions:
A delegate declared this way is referenced as ${myKey} in camunda:delegateExpression, so the name used in the process model no longer depends on the class name.
Every delegate is wrapped by KoraDelegateWrapperFactory before it is called: it forks the current Kora Context for the delegate execution and applies module telemetry around execute(...).
You can provide your own KoraDelegateWrapperFactory as a @Component to change this behavior.
Engine Services¶
The module provides standard Camunda services as dependency graph components:
RuntimeServiceRepositoryServiceManagementServiceAuthorizationServiceDecisionServiceExternalTaskServiceFilterServiceFormServiceTaskServiceHistoryServiceIdentityService
These services can be injected into your components in the usual way.
Starting and interacting with processes¶
Inject ProcessEngine (or any of the engine services above) into your components to start and drive process instances.
A process is started by its BPMN process id through RuntimeService, and process definitions can be queried through RepositoryService:
@Component
@HttpController("/camunda")
public final class CamundaController {
private final ProcessEngine processEngine;
public CamundaController(ProcessEngine processEngine) {
this.processEngine = processEngine;
}
@HttpRoute(method = HttpMethod.GET, path = "/start/onboarding")
public String startOnboarding() {
String businessKey = UUID.randomUUID().toString();
ProcessInstance instance = processEngine.getRuntimeService()
.startProcessInstanceByKey("Onboarding", businessKey);
return instance.getId();
}
}
@Component
@HttpController("/camunda")
class CamundaController(private val processEngine: ProcessEngine) {
@HttpRoute(method = HttpMethod.GET, path = "/start/onboarding")
fun startOnboarding(): String {
val businessKey = UUID.randomUUID().toString()
val instance = processEngine.runtimeService
.startProcessInstanceByKey("Onboarding", businessKey)
return instance.id
}
}
A running process can be advanced from outside the engine as well: RuntimeService.correlateMessage(...) delivers a BPMN message event, and TaskService / FormService complete user tasks and submit forms:
@Component
@HttpController("/camunda/process/onboarding")
public final class OnboardingController {
private final FormService formService;
private final TaskService taskService;
private final RuntimeService runtimeService;
public OnboardingController(FormService formService, TaskService taskService, RuntimeService runtimeService) {
this.formService = formService;
this.taskService = taskService;
this.runtimeService = runtimeService;
}
@HttpRoute(path = "/cancel/{businessKey}", method = HttpMethod.GET)
public String customerCancellation(@Path String businessKey) {
runtimeService.correlateMessage("MessageCustomerCancellation", businessKey);
return "Cancelled: " + businessKey;
}
@HttpRoute(path = "/order/{businessKey}", method = HttpMethod.GET)
public String customerOrder(@Path String businessKey) {
Task task = taskService.createTaskQuery().processInstanceBusinessKey(businessKey).active().singleResult();
formService.submitTaskForm(task.getId(), Map.of("approved", true));
return "Approved: " + businessKey;
}
}
@Component
@HttpController("/camunda/process/onboarding")
class OnboardingController(
private val formService: FormService,
private val taskService: TaskService,
private val runtimeService: RuntimeService
) {
@HttpRoute(path = "/cancel/{businessKey}", method = HttpMethod.GET)
fun customerCancellation(@Path businessKey: String): String {
runtimeService.correlateMessage("MessageCustomerCancellation", businessKey)
return "Cancelled: $businessKey"
}
@HttpRoute(path = "/order/{businessKey}", method = HttpMethod.GET)
fun customerOrder(@Path businessKey: String): String {
val task = taskService.createTaskQuery().processInstanceBusinessKey(businessKey).active().singleResult()
formService.submitTaskForm(task.id, mapOf("approved" to true))
return "Approved: $businessKey"
}
}
DataSource and transactions¶
The engine persists its state through a JDBC DataSource, so the JDBC module is required.
By default the module reuses the main application DataSource, exposed to the engine under the @Tag(CamundaBpmn.class) tag.
To give the engine a dedicated data source, provide your own DataSource with that tag:
The CamundaEngineDataSource component abstracts the engine's DataSource together with its CamundaTransactionManager.
The default implementation runs JDBC over the @Tag(CamundaBpmn.class) DataSource; you can override CamundaEngineDataSource as a @Component to fully control how the engine obtains connections and manages transactions.
A delegate that performs its own JDBC work can run it inside the engine transaction through CamundaTransactionManager.
inContinueTx(...) reuses the connection of the current engine transaction (opening a new one only if none is active), while inNewTx(...) always opens a new transaction; currentConnection() returns a handle to commit() / rollback() the current transaction:
@Component
public final class AuditDelegate implements JavaDelegate {
private final CamundaTransactionManager transactionManager;
public AuditDelegate(CamundaTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
@Override
public void execute(DelegateExecution execution) {
transactionManager.inContinueTx(() -> {
// JDBC work sharing the engine transaction
});
}
}
Job executor and readiness¶
The engine runs asynchronous continuations and timers through a JobExecutor.
The implementation is selected by the jobExecutor.virtualThreadsEnabled option: when false (default) a thread-pool executor is used and sized by corePoolSize / maxPoolSize / queueSize / maxJobsPerAcquisition; when true a virtual-thread executor is used and the pool/queue sizes are ignored (see the Configuration callouts).
The module automatically registers a readiness probe that reports the application as UP only once the JobExecutor is active.
Until the job executor is activated the probe fails with Camunda BPMN Engine JobExecutor is not active, which keeps the application out of rotation while the engine is still starting.
Admin user and Cockpit¶
When the admin section is present, the module provisions a Camunda administrator user, ensures the camunda-admin group with full authorizations exists, and adds the user to it (see the Configuration admin callouts).
This account is what you use to log into the Cockpit / Admin / Tasklist web applications served by the Camunda 7 REST module.
If the admin section is omitted, no user is created.
Engine Configuration¶
For additional configuration, register a ProcessEngineConfigurator component.
The prepare(...) method is called before ProcessEngine is created and receives ProcessEngineConfiguration; setup(...) is called after the engine is created:
Plugins¶
You can register arbitrary ProcessEnginePlugin by providing them as components in the Kora dependency container.
The module collects all such components and passes them to the engine configuration when creating ProcessEngine:
@Component
public final class SimpleProcessEnginePlugin implements ProcessEnginePlugin {
@Override
public void preInit(ProcessEngineConfigurationImpl configuration) {
}
@Override
public void postInit(ProcessEngineConfigurationImpl configuration) {
}
@Override
public void postProcessEngineBuild(ProcessEngine engine) {
}
}
Camunda version¶
The detected Camunda version is available as an injectable CamundaVersion component.
Its version() returns the version string reported by the Camunda package, and isEnterprise() returns true when an enterprise (-ee) distribution is on the classpath:
Telemetry¶
The module reports its own logging, metrics, and tracing for delegate executions through the telemetry configuration section.
Metrics are described in the Metrics Reference section, and the Context fork performed by KoraDelegateWrapperFactory keeps this telemetry scoped to each delegate call.
Independently of the module telemetry, telemetry.engineTelemetryEnabled toggles Camunda's own built-in telemetry collection (disabled by default).