Flowable: Open-Source BPM, DMN, CMMN, and Forms
Building process automation with Flowable Community Edition. BPMN engine implementation, decision tables, case management, and Spring Boot integration.
Flowable: Open-Source BPM, DMN, CMMN, and Forms
Flowable is a powerful open-source business process engine that supports BPMN 2.0, DMN 1.1, and CMMN 1.1. Originally forked from Activiti, Flowable has evolved into a mature platform for process automation. Here's how to implement Flowable in enterprise applications.
Flowable Architecture Overview
Core Engines
Flowable Engine Architecture:
┌─────────────────────────────────────────────────────────────┐
│ Flowable Platform │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Process │ │ Decision │ │ Case │ │
│ │ Engine │ │ Engine │ │ Engine │ │
│ │ (BPMN) │ │ (DMN) │ │ (CMMN) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────┐ │
│ │ Common Services │ │
│ │ - Identity - Jobs - History - Events │ │
│ └───────────────────────┬───────────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────┐ │
│ │ Database Layer │ │
│ │ (PostgreSQL, MySQL, Oracle, H2) │ │
│ └───────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Spring Boot Integration
<!-- pom.xml dependencies -->
<dependencies>
<!-- Flowable Spring Boot Starter -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>7.0.0</version>
</dependency>
<!-- Process Engine -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-process</artifactId>
<version>7.0.0</version>
</dependency>
<!-- DMN Engine -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-dmn</artifactId>
<version>7.0.0</version>
</dependency>
<!-- CMMN Engine -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-cmmn</artifactId>
<version>7.0.0</version>
</dependency>
<!-- REST API -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-rest</artifactId>
<version>7.0.0</version>
</dependency>
</dependencies>Application Configuration
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/flowable
username: flowable
password: flowable
driver-class-name: org.postgresql.Driver
flowable:
# Process Engine Configuration
process:
definition-cache-limit: 512
enable-safe-xml: true
# Async Executor
async-executor-activate: true
async-executor:
core-pool-size: 8
max-pool-size: 32
queue-size: 100
# Database
database-schema-update: true
# History Level
history-level: audit
# IDM (Identity Management)
idm:
enabled: true
password-encoder: spring
# REST API
rest:
app:
authentication-mode: verify-privilegeBPMN Process Implementation
Process Definition
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn">
<process id="expense-approval" name="Expense Approval" isExecutable="true">
<startEvent id="start" name="Expense Submitted">
<extensionElements>
<flowable:formProperty id="employeeId" name="Employee ID"
type="string" required="true"/>
<flowable:formProperty id="amount" name="Amount"
type="double" required="true"/>
<flowable:formProperty id="description" name="Description"
type="string"/>
</extensionElements>
</startEvent>
<sequenceFlow id="flow1" sourceRef="start" targetRef="checkAmount"/>
<!-- Exclusive Gateway: Check Amount -->
<exclusiveGateway id="checkAmount" name="Amount Check"/>
<sequenceFlow id="flow-auto" sourceRef="checkAmount" targetRef="autoApprove">
<conditionExpression xsi:type="tFormalExpression">
${amount <= 100}
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow-manager" sourceRef="checkAmount" targetRef="managerApproval">
<conditionExpression xsi:type="tFormalExpression">
${amount > 100 && amount <= 1000}
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow-director" sourceRef="checkAmount" targetRef="directorApproval">
<conditionExpression xsi:type="tFormalExpression">
${amount > 1000}
</conditionExpression>
</sequenceFlow>
<!-- Auto Approve Service Task -->
<serviceTask id="autoApprove" name="Auto Approve"
flowable:delegateExpression="${autoApproveDelegate}"/>
<sequenceFlow id="flow2" sourceRef="autoApprove" targetRef="processPayment"/>
<!-- Manager Approval User Task -->
<userTask id="managerApproval" name="Manager Approval"
flowable:candidateGroups="managers">
<extensionElements>
<flowable:formProperty id="approved" name="Approved"
type="boolean" required="true"/>
<flowable:formProperty id="comments" name="Comments"
type="string"/>
</extensionElements>
</userTask>
<sequenceFlow id="flow3" sourceRef="managerApproval" targetRef="checkApproval"/>
<!-- Director Approval User Task -->
<userTask id="directorApproval" name="Director Approval"
flowable:candidateGroups="directors">
<extensionElements>
<flowable:formProperty id="approved" name="Approved"
type="boolean" required="true"/>
<flowable:formProperty id="comments" name="Comments"
type="string"/>
</extensionElements>
</userTask>
<sequenceFlow id="flow4" sourceRef="directorApproval" targetRef="checkApproval"/>
<!-- Check Approval Gateway -->
<exclusiveGateway id="checkApproval" name="Approved?"/>
<sequenceFlow id="flow-approved" sourceRef="checkApproval" targetRef="processPayment">
<conditionExpression>${approved == true}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow-rejected" sourceRef="checkApproval" targetRef="notifyRejection">
<conditionExpression>${approved == false}</conditionExpression>
</sequenceFlow>
<!-- Process Payment Service Task -->
<serviceTask id="processPayment" name="Process Payment"
flowable:delegateExpression="${paymentDelegate}"/>
<sequenceFlow id="flow5" sourceRef="processPayment" targetRef="endApproved"/>
<!-- Notify Rejection Service Task -->
<serviceTask id="notifyRejection" name="Notify Rejection"
flowable:expression="${notificationService.sendRejection(execution)}"/>
<sequenceFlow id="flow6" sourceRef="notifyRejection" targetRef="endRejected"/>
<!-- End Events -->
<endEvent id="endApproved" name="Expense Approved"/>
<endEvent id="endRejected" name="Expense Rejected"/>
</process>
</definitions>Java Delegate Implementation
@Component("paymentDelegate")
public class PaymentDelegate implements JavaDelegate {
private final PaymentService paymentService;
private final AuditService auditService;
public PaymentDelegate(PaymentService paymentService, AuditService auditService) {
this.paymentService = paymentService;
this.auditService = auditService;
}
@Override
public void execute(DelegateExecution execution) {
String employeeId = (String) execution.getVariable("employeeId");
Double amount = (Double) execution.getVariable("amount");
String description = (String) execution.getVariable("description");
// Process payment
PaymentResult result = paymentService.processExpensePayment(
employeeId,
amount,
description
);
// Set result variables
execution.setVariable("paymentId", result.getPaymentId());
execution.setVariable("paymentDate", result.getProcessedDate());
// Audit log
auditService.log(
"EXPENSE_PAYMENT",
employeeId,
Map.of(
"amount", amount,
"paymentId", result.getPaymentId()
)
);
}
}Process Service Layer
@Service
public class ExpenseProcessService {
private final RuntimeService runtimeService;
private final TaskService taskService;
private final HistoryService historyService;
public ExpenseProcessService(
RuntimeService runtimeService,
TaskService taskService,
HistoryService historyService) {
this.runtimeService = runtimeService;
this.taskService = taskService;
this.historyService = historyService;
}
// Start new expense approval process
public String submitExpense(ExpenseRequest request) {
Map<String, Object> variables = new HashMap<>();
variables.put("employeeId", request.getEmployeeId());
variables.put("amount", request.getAmount());
variables.put("description", request.getDescription());
variables.put("submittedDate", LocalDateTime.now());
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
"expense-approval",
request.getEmployeeId() + "-" + System.currentTimeMillis(),
variables
);
return instance.getId();
}
// Get pending tasks for user
public List<TaskInfo> getPendingTasks(String userId) {
List<Task> tasks = taskService.createTaskQuery()
.taskCandidateOrAssigned(userId)
.orderByTaskCreateTime()
.desc()
.list();
return tasks.stream()
.map(this::mapToTaskInfo)
.collect(Collectors.toList());
}
// Complete approval task
public void completeApproval(String taskId, boolean approved, String comments) {
Map<String, Object> variables = new HashMap<>();
variables.put("approved", approved);
variables.put("comments", comments);
variables.put("completedDate", LocalDateTime.now());
taskService.complete(taskId, variables);
}
// Get process history
public ProcessHistory getProcessHistory(String processInstanceId) {
List<HistoricActivityInstance> activities = historyService
.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricActivityInstanceStartTime()
.asc()
.list();
HistoricProcessInstance process = historyService
.createHistoricProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
return new ProcessHistory(process, activities);
}
private TaskInfo mapToTaskInfo(Task task) {
Map<String, Object> variables = taskService.getVariables(task.getId());
return new TaskInfo(
task.getId(),
task.getName(),
task.getCreateTime(),
variables
);
}
}DMN Decision Tables
Decision Definition
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd"
namespace="http://flowable.org/dmn">
<decision id="expense-routing" name="Expense Routing Decision">
<decisionTable id="expense-routing-table" hitPolicy="FIRST">
<input id="input-amount" label="Amount">
<inputExpression typeRef="number">
<text>amount</text>
</inputExpression>
</input>
<input id="input-category" label="Category">
<inputExpression typeRef="string">
<text>category</text>
</inputExpression>
</input>
<output id="output-approver" label="Approver Group"
name="approverGroup" typeRef="string"/>
<output id="output-sla" label="SLA Hours"
name="slaHours" typeRef="number"/>
<!-- Rule 1: Travel expenses over 500 -->
<rule>
<inputEntry><text>> 500</text></inputEntry>
<inputEntry><text>"travel"</text></inputEntry>
<outputEntry><text>"travel-directors"</text></outputEntry>
<outputEntry><text>48</text></outputEntry>
</rule>
<!-- Rule 2: Equipment over 1000 -->
<rule>
<inputEntry><text>> 1000</text></inputEntry>
<inputEntry><text>"equipment"</text></inputEntry>
<outputEntry><text>"it-directors"</text></outputEntry>
<outputEntry><text>72</text></outputEntry>
</rule>
<!-- Rule 3: Any expense over 5000 -->
<rule>
<inputEntry><text>> 5000</text></inputEntry>
<inputEntry><text>-</text></inputEntry>
<outputEntry><text>"finance-directors"</text></outputEntry>
<outputEntry><text>96</text></outputEntry>
</rule>
<!-- Rule 4: Default - manager approval -->
<rule>
<inputEntry><text>-</text></inputEntry>
<inputEntry><text>-</text></inputEntry>
<outputEntry><text>"managers"</text></outputEntry>
<outputEntry><text>24</text></outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>Evaluating Decisions
@Service
public class DecisionService {
private final DmnRuleService dmnRuleService;
public DecisionService(DmnRuleService dmnRuleService) {
this.dmnRuleService = dmnRuleService;
}
public RoutingDecision evaluateExpenseRouting(double amount, String category) {
Map<String, Object> variables = new HashMap<>();
variables.put("amount", amount);
variables.put("category", category);
RuleEngineExecutionResult result = dmnRuleService
.createExecuteDecisionBuilder()
.decisionKey("expense-routing")
.variables(variables)
.executeWithSingleResult();
return new RoutingDecision(
(String) result.getResultVariables().get("approverGroup"),
(Integer) result.getResultVariables().get("slaHours")
);
}
}CMMN Case Management
Case Definition
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL"
targetNamespace="http://flowable.org/cmmn">
<case id="support-case" name="Customer Support Case">
<casePlanModel id="support-plan" name="Support Case Plan">
<!-- Stage: Triage -->
<stage id="triage-stage" name="Triage">
<planItem id="planItem-classify" definitionRef="classify-task"/>
<planItem id="planItem-prioritize" definitionRef="prioritize-task"/>
</stage>
<!-- Stage: Investigation -->
<stage id="investigation-stage" name="Investigation">
<entryCriterion id="entry-investigation" sentryRef="sentry-triage-complete"/>
<planItem id="planItem-research" definitionRef="research-task"/>
<planItem id="planItem-reproduce" definitionRef="reproduce-task">
<itemControl>
<manualActivationRule/>
</itemControl>
</planItem>
<planItem id="planItem-escalate" definitionRef="escalate-task">
<itemControl>
<manualActivationRule/>
</itemControl>
</planItem>
</stage>
<!-- Stage: Resolution -->
<stage id="resolution-stage" name="Resolution">
<entryCriterion id="entry-resolution" sentryRef="sentry-investigation-complete"/>
<planItem id="planItem-implement" definitionRef="implement-task"/>
<planItem id="planItem-verify" definitionRef="verify-task"/>
<planItem id="planItem-document" definitionRef="document-task"/>
</stage>
<!-- Human Tasks -->
<humanTask id="classify-task" name="Classify Issue"/>
<humanTask id="prioritize-task" name="Set Priority"/>
<humanTask id="research-task" name="Research Solution"/>
<humanTask id="reproduce-task" name="Reproduce Issue"/>
<humanTask id="escalate-task" name="Escalate to Engineering"/>
<humanTask id="implement-task" name="Implement Fix"/>
<humanTask id="verify-task" name="Verify Resolution"/>
<humanTask id="document-task" name="Update Documentation"/>
<!-- Sentries -->
<sentry id="sentry-triage-complete">
<planItemOnPart sourceRef="planItem-classify">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<planItemOnPart sourceRef="planItem-prioritize">
<standardEvent>complete</standardEvent>
</planItemOnPart>
</sentry>
<sentry id="sentry-investigation-complete">
<planItemOnPart sourceRef="planItem-research">
<standardEvent>complete</standardEvent>
</planItemOnPart>
</sentry>
<!-- Milestone -->
<milestone id="case-resolved" name="Case Resolved">
<entryCriterion sentryRef="sentry-resolution-complete"/>
</milestone>
<sentry id="sentry-resolution-complete">
<planItemOnPart sourceRef="planItem-verify">
<standardEvent>complete</standardEvent>
</planItemOnPart>
</sentry>
</casePlanModel>
</case>
</definitions>Case Service Implementation
@Service
public class SupportCaseService {
private final CmmnRuntimeService cmmnRuntimeService;
private final CmmnTaskService cmmnTaskService;
private final CmmnHistoryService cmmnHistoryService;
// Start new support case
public String createSupportCase(SupportCaseRequest request) {
Map<String, Object> variables = new HashMap<>();
variables.put("customerId", request.getCustomerId());
variables.put("subject", request.getSubject());
variables.put("description", request.getDescription());
variables.put("createdDate", LocalDateTime.now());
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.caseDefinitionKey("support-case")
.businessKey(request.getCaseNumber())
.variables(variables)
.start();
return caseInstance.getId();
}
// Get available plan items (tasks that can be activated)
public List<PlanItemInfo> getAvailablePlanItems(String caseId) {
List<PlanItemInstance> planItems = cmmnRuntimeService
.createPlanItemInstanceQuery()
.caseInstanceId(caseId)
.planItemInstanceStateEnabled()
.list();
return planItems.stream()
.map(this::mapToPlanItemInfo)
.collect(Collectors.toList());
}
// Manually activate discretionary task
public void activateTask(String caseId, String planItemId) {
cmmnRuntimeService.startPlanItemInstance(planItemId);
}
// Complete task
public void completeTask(String taskId, Map<String, Object> variables) {
cmmnTaskService.complete(taskId, variables);
}
}Flowable Forms
Form Definition (JSON)
{
"key": "expense-form",
"name": "Expense Request Form",
"fields": [
{
"id": "employeeId",
"name": "Employee ID",
"type": "text",
"required": true,
"placeholder": "Enter employee ID"
},
{
"id": "amount",
"name": "Amount",
"type": "decimal",
"required": true,
"params": {
"minValue": 0.01
}
},
{
"id": "category",
"name": "Category",
"type": "dropdown",
"required": true,
"options": [
{ "id": "travel", "name": "Travel" },
{ "id": "equipment", "name": "Equipment" },
{ "id": "supplies", "name": "Office Supplies" },
{ "id": "training", "name": "Training" },
{ "id": "other", "name": "Other" }
]
},
{
"id": "description",
"name": "Description",
"type": "multi-line-text",
"required": true
},
{
"id": "receipt",
"name": "Receipt",
"type": "upload",
"params": {
"multiple": false,
"accept": ".pdf,.jpg,.png"
}
}
]
}REST API Endpoints
Process Management
@RestController
@RequestMapping("/api/processes")
public class ProcessController {
private final RuntimeService runtimeService;
private final TaskService taskService;
@PostMapping("/start/{processKey}")
public ProcessInstanceResponse startProcess(
@PathVariable String processKey,
@RequestBody Map<String, Object> variables) {
ProcessInstance instance = runtimeService
.startProcessInstanceByKey(processKey, variables);
return new ProcessInstanceResponse(
instance.getId(),
instance.getProcessDefinitionId(),
instance.getBusinessKey()
);
}
@GetMapping("/tasks")
public List<TaskResponse> getTasks(
@RequestParam(required = false) String assignee,
@RequestParam(required = false) String candidateGroup) {
TaskQuery query = taskService.createTaskQuery();
if (assignee != null) {
query.taskAssignee(assignee);
}
if (candidateGroup != null) {
query.taskCandidateGroup(candidateGroup);
}
return query.list().stream()
.map(TaskResponse::fromTask)
.collect(Collectors.toList());
}
@PostMapping("/tasks/{taskId}/complete")
public void completeTask(
@PathVariable String taskId,
@RequestBody Map<String, Object> variables) {
taskService.complete(taskId, variables);
}
}Key Takeaways
-
Spring Boot integration: Flowable's starters make embedding straightforward—auto-configuration handles most setup
-
Three engines, one platform: Process (BPMN), Decision (DMN), and Case (CMMN) engines work together seamlessly
-
Delegate expressions: Use Spring beans as delegates for clean separation of process logic and business logic
-
History service: Flowable's audit history provides comprehensive process analytics out of the box
-
REST API included: The REST starter exposes full API—extend with custom endpoints as needed
-
Open source flexibility: No vendor lock-in, full source access, and active community
Flowable provides enterprise-grade process automation without licensing costs. The combination of BPMN, DMN, and CMMN support makes it suitable for complex workflow requirements. \