Step‑by‑Step: Configure, Deploy, and Run a Camunda BPM Workflow with Spring Boot
This guide walks through setting up Camunda BPM 7.18 with Spring Boot 2.6.14, configuring dependencies and application properties, designing a two‑step approval workflow, deploying it via custom REST endpoints, and managing process start and task approval using Camunda’s services and APIs.
Environment
Spring Boot 2.6.14 combined with camunda-spring-boot-starter version 7.18.0.
Dependency Configuration
<code><camunda.version>7.18.0</camunda.version>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
<version>${camunda.version}</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
<version>${camunda.version}</version>
</dependency></code>Application Configuration (YAML)
<code>camunda.bpm:
webapp:
# Set admin console context path
application-path: /workflow
auto-deployment-enabled: true
admin-user:
# Admin user for console
id: admin
password: admin
firstName: admin
filter:
create: All tasks
database:
# Database type
type: mysql
# Auto‑update schema
schema-update: true
logging:
level:
# Enable SQL logging during development
'[org.camunda.bpm.engine.impl.persistence.entity]': debug
---
spring:
jersey:
application-path: /api-flow
type: servlet
servlet:
load-on-startup: 0</code>Accessing the Console
After the above configuration, the Camunda admin console is reachable at:
http://localhost:8100/workflow/
Designing the Process
The example defines a two‑node approval workflow: manager approval → HR approval. Each user task is assigned via an expression that resolves the approver.
BPMN XML
<code><?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="sample-diagram" targetNamespace="http://pack.org/bpmn" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
<bpmn2:process id="Process_1" isExecutable="true">
<bpmn2:startEvent id="StartEvent_1">
<bpmn2:outgoing>Flow_18pxcpx</bpmn2:outgoing>
</bpmn2:startEvent>
<bpmn2:sequenceFlow id="Flow_18pxcpx" sourceRef="StartEvent_1" targetRef="Activity_0vs8hu4" />
<bpmn2:userTask id="Activity_0vs8hu4" camunda:assignee="${uid}">
<bpmn2:incoming>Flow_18pxcpx</bpmn2:incoming>
<bpmn2:outgoing>Flow_0n014x3</bpmn2:outgoing>
</bpmn2:userTask>
<bpmn2:sequenceFlow id="Flow_0n014x3" sourceRef="Activity_0vs8hu4" targetRef="Activity_0bcruuz" />
<bpmn2:userTask id="Activity_0bcruuz" camunda:assignee="${mid}">
<bpmn2:incoming>Flow_0n014x3</bpmn2:incoming>
<bpmn2:outgoing>Flow_0dsfy6s</bpmn2:outgoing>
</bpmn2:userTask>
<bpmn2:endEvent id="Event_1xosttx">
<bpmn2:incoming>Flow_0dsfy6s</bpmn2:incoming>
</bpmn2:endEvent>
<bpmn2:sequenceFlow id="Flow_0dsfy6s" sourceRef="Activity_0bcruuz" targetRef="Event_1xosttx" />
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
... (graphical elements omitted for brevity) ...
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions></code>Deploying the Process
A custom REST controller uploads the BPMN file and invokes a service that uses Camunda’s RepositoryService to create a deployment.
<code>@RestController
@RequestMapping("/camunda")
public class BpmnController {
@Value("${gx.camunda.upload}")
private String path;
@Resource
private ProcessService processService;
@PostMapping("/bpmn/upload")
public AjaxResult uploadFile(MultipartFile file, String fileName, String name) throws Exception {
try {
InputStream is = file.getInputStream();
File storageFile = new File(path + File.separator + fileName);
FileOutputStream fos = new FileOutputStream(storageFile);
byte[] buf = new byte[10 * 1024];
int len;
while ((len = is.read(buf)) > -1) {
fos.write(buf, 0, len);
}
fos.close();
is.close();
processService.createDeploy(fileName, name, new FileSystemResource(storageFile));
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
}</code> <code>@Resource
private RepositoryService repositoryService;
public void createDeploy(String resourceName, String name, org.springframework.core.io.Resource resource) {
try {
Deployment deployment = repositoryService.createDeployment()
.addInputStream(resourceName, resource.getInputStream())
.name(name)
.deploy();
logger.info("Deployment id: {}", deployment.getId());
logger.info("Deployment name: {}", deployment.getName());
} catch (IOException e) {
throw new RuntimeException(e);
}
}</code>Starting the Process
<code>@RestController
@RequestMapping("/process")
public class ProcessController {
@Resource
private ProcessService processService;
@GetMapping("/start/{processDefinitionId}")
public AjaxResult startProcess(@PathVariable("processDefinitionId") String processDefinitionId) {
Map<String, Object> variables = new HashMap<>();
variables.put("uid", "1");
variables.put("mid", "1000");
processService.startProcessInstanceAssignVariables(processDefinitionId, "AKF", variables);
return AjaxResult.success("Process started successfully");
}
}
</code> <code>@Resource
private RuntimeService runtimeService;
public ProcessInstance startProcessInstanceAssignVariables(String processDefinitionId, String businessKey, Map<String, Object> variables) {
ProcessInstance pi = runtimeService.startProcessInstanceById(processDefinitionId, businessKey, variables);
logger.info("Process definition ID: {}", pi.getProcessDefinitionId());
logger.info("Process instance ID: {}", pi.getId());
logger.info("Business key: {}", pi.getBusinessKey());
return pi;
}
</code>Approving Tasks
<code>@RestController
@RequestMapping("/process")
public class ProcessController {
@Resource
private ProcessService processService;
@GetMapping("/approve/{id}")
public AjaxResult approve(@PathVariable("id") String instanceId) {
if (StringUtils.isEmpty(instanceId)) {
return AjaxResult.error("Unknown approval task");
}
Map<String, Object> variables = new HashMap<>();
variables.put("uid", "1");
processService.executionTask(variables, instanceId, task -> {}, null);
return AjaxResult.success();
}
}
</code> <code>@Resource
private TaskService taskService;
@Resource
private RuntimeService runtimeService;
@Transactional
public void executionTask(Map<String, Object> variables, String instanceId, Consumer<Task> consumer, String type) {
Task task = taskService.createTaskQuery().processInstanceId(instanceId).singleResult();
if (task == null) {
logger.error("Task {} does not exist", instanceId);
throw new RuntimeException("Task " + instanceId + " does not exist");
}
taskService.setVariables(task.getId(), variables);
taskService.complete(task.getId(), variables);
long count = runtimeService.createExecutionQuery().processInstanceId(instanceId).count();
if (count == 0) {
consumer.accept(task);
}
}
</code>Result
After deployment, start, and approval, the full lifecycle – design → deployment → start → approval – is demonstrated, and the current pending approval tasks can be viewed in the Camunda task list.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.