The Flowable CMMN API
The Process CMMN Engine API and services
The engine API is the most common way of interacting with Flowable. The main starting point is the CmmnEngine, which can be created in several ways as described in the configuration section. From the CmmnEngine, you can obtain the various services that contain the case/CMMN methods. CmmnEngine and the services objects are thread safe, so you can keep a reference to one of those for a whole server.
CmmnEngine cmmnEngine = CmmnEngineConfiguration.createStandaloneCmmnEngineConfiguration();
CmmnRuntimeService runtimeService = cmmnEngine.getCmmnRuntimeService();
CmmnRepositoryService repositoryService = cmmnEngine.getCmmnRepositoryService();
CmmnTaskService taskService = cmmnEngine.getCmmnTaskService();
CmmnManagementService managementService = cmmnEngine.getCmmnManagementService();
CmmnHistoryService historyService = cmmnEngine.getCmmnHistoryService();
CmmnEngineConfiguration.createStandaloneCmmnEngineConfiguration() will initialize and build a CMMN engine and afterwards always return the CMMN engine.
The CmmnEngineConfiguration class will scan for all flowable.cmmn.cfg.xml and flowable-cmmn-context.xml files. For all flowable.cmmn.cfg.xml files, the CMMN engine will be built in the typical Flowable way: CmmnEngineConfiguration.createCmmnEngineConfigurationFromInputStream(inputStream).buildCmmnEngine(). For all flowable-cmmn-context.xml files, the CMMN engine will be built in the Spring way: first the Spring application context is created and then the CMMN engine is obtained from that application context.
All services are stateless. This means that you can easily run Flowable on multiple nodes in a cluster, each going to the same database, without having to worry about which machine actually executed previous calls. Any call to any service is idempotent regardless of where it is executed.
The CmmnRepositoryService is probably the first service needed when working with the Flowable CMMN engine. This service offers operations for managing and manipulating deployments and case definitions. Without going into much detail here, a case definition is a Java counterpart of the CMMN 1.1 case. It is a representation of the structure and behavior of each of the steps of a case. A deployment is the unit of packaging within the Flowable CMMN engine. A deployment can contain multiple CMMN 1.1 XML files and any other resource. The choice of what is included in one deployment is up to the developer. It can range from a single process CMMN 1.1 XML file to a whole package of cases and relevant resources (for example, the deployment 'hr-cases' could contain everything related to HR cases). The CmmnRepositoryService can deploy such packages. Deploying a deployment means it is uploaded to the engine, where all cases are inspected and parsed before being stored in the database. From that point on, the deployment is known to the system and any process included in the deployment can now be started.
Furthermore, this service allows you to:
Query on deployments and case definitions known to the engine.
Retrieve various resources, such as files contained within the deployment or case diagrams that were auto-generated by the engine.
Retrieve a POJO version of the case definition, which can be used to introspect the case using Java rather than XML.
While the CmmnRepositoryService is mostly about static information (data that doesn’t change, or at least not a lot), the CmmnRuntimeService is quite the opposite. It deals with starting new case instances of case definitions. As said above, a case definition defines the structure and behavior of the different steps in a case. A case instance is one execution of such a case definition. For each case definition there typically are many instances running at the same time. The CmmnRuntimeService also is the service that is used to retrieve and store case variables. This is data that is specific to the given case instance and can be used by various constructs in the case (for example, a plan transition condition often uses process variables to determine which path is chosen to continue the case). The CmmnRuntimeservice also allows you to query on case instances and plan items. Plan items are a representation of the enabled plan items of CMMN 1.1. Lastly, the CmmnRuntimeService is used whenever a case instance is waiting for an external trigger and the case needs to be continued. A case instance can have various wait states and this service contains various operations to 'signal' to the instance that the external trigger is received and the case instance can be continued.
Tasks that need to be performed by human users of the system are core to a CMMN engine such as Flowable. Everything around tasks is grouped in the CmmnTaskService, such as:
Querying tasks assigned to users or groups
Creating new standalone tasks. These are tasks that are not related to a process instance.
Manipulating to which user a task is assigned or which users are in some way involved with the task.
Claiming and completing a task. Claiming means that someone decided to be the assignee for the task, meaning that this user will complete the task. Completing means 'doing the work of the tasks'. Typically this is filling in a form of sorts.
The CmmnHistoryService exposes all historical data gathered by the Flowable CMMN engine. When executing cases, a lot of data can be kept by the engine (this is configurable), such as case instance start times, who did which tasks, how long it took to complete the tasks, which path was followed in each case instance, and so on. This service exposes mainly query capabilities to access this data.
The CmmnManagementService gives access to low-level information about the database tables, allows to query for the different types of jobs and to execute them.
For more detailed information on the service operations and the engine API, see the javadocs.
Exception strategy
The base exception in Flowable is the org.flowable.engine.common.api.FlowableException, an unchecked exception. This exception can be thrown at all times by the API, but 'expected' exceptions that happen in specific methods are documented in the javadocs. For example, an extract from CmmnTaskService:
/**
* Called when the task is successfully executed.
* @param taskId the id of the task to complete, cannot be null.
* @throws FlowableObjectNotFoundException when no task exists with the given id.
*/
void complete(String taskId);
In the example above, when an id is passed for which no task exists, an exception will be thrown. Also, since the javadoc explicitly states that taskId cannot be null, an FlowableIllegalArgumentException will be thrown when null is passed.
Even though we want to avoid a big exception hierarchy, the following subclasses are thrown in specific cases. All other errors that occur during process-execution or API-invocation that don’t fit into the possible exceptions below are thrown as regular FlowableExceptions.
FlowableWrongDbException: Thrown when the Flowable engine discovers a mismatch between the database schema version and the engine version.
FlowableOptimisticLockingException: Thrown when an optimistic locking occurs in the data store caused by concurrent access of the same data entry.
FlowableClassLoadingException: Thrown when a class requested to load was not found or when an error occurred while loading it (e.g., JavaDelegates, TaskListeners, …).
FlowableObjectNotFoundException: Thrown when an object that is requested or actioned does not exist.
FlowableIllegalArgumentException: An exception indicating that an illegal argument has been supplied in a Flowable API-call, an illegal value was configured in the engine’s configuration or an illegal value has been supplied or an illegal value is used in a process definition.
FlowableTaskAlreadyClaimedException: Thrown when a task is already claimed, when the taskService.claim(...) is called.
Query API
There are two ways of querying data from the engine: the query API and native queries. The Query API allows you to program completely typesafe queries with a fluent API. You can add various conditions to your queries (all of which are applied together as a logical AND) and precisely one ordering. The following code shows an example:
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee("kermit")
.orderByDueDate().asc()
.list();
Variables
Every case instance needs and uses data to execute the steps it’s made up of. In Flowable, this data is called variables, which are stored in the database. Variables can be used in expressions (for example, in the condition of a sentry), in Java service tasks when calling external services (for example to provide the input or store the result of the service call), and so on.
A case instance can have variables (called case variables), but also plan item instances and human tasks can have variables. A case instance can have any number of variables. Each variable is stored in a row in the ACT_RU_VARIABLE database table.
The createCaseInstanceBuilder method has optional methods to provide the variables when the case instance is created and started through the CmmnRuntimeService:
CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().variable("var1", "test").start();
Variables can be added during case execution. For example, (CmmnRuntimeService):
void setVariables(String caseInstanceId, Map<String, ? extends Object> variables);
Variables can also be retrieved, as shown below. Note that similar methods exist on the CmmnTaskService.
Map<String, Object> getVariables(String caseInstanceId);
Object getVariable(String caseInstanceId, String variableName);
Variables are often used in Java service tasks, expressions, scripts, and so on.
Transient variables
Transient variables are variables that behave like regular variables but are not persisted. Typically, transient variables are used for advanced use cases. When in doubt, use a regular case variable.
The following applies for transient variables:
There is no history stored at all for transient variables.
Like regular variables, transient variables are put on the highest parent when set. This means that when setting a variable on a plan item, the transient variable is actually stored on the case instance execution. Like regular variables, a local variant of the method exists if the variable is set on the specific plan item or task.
A transient variable can only be accessed before the next 'wait state' in the case definition. After that, they are gone. Here, the wait state means the point in the case instance where it is persisted to the data store.
Transient variables can only be set by the setTransientVariable(name, value), but transient variables are also returned when calling getVariable(name) (a getTransientVariable(name) also exists, that only checks the transient variables). The reason for this is to make the writing of expressions easy and existing logic using variables works for both types.
A transient variable shadows a persistent variable with the same name. This means that when both a persistent and transient variable is set on a case instance and getVariable("someVariable") is called, the transient variable value will be returned.
You can set and get transient variables in most places where regular variables are exposed:
On DelegatePlanItemInstance in PlanItemJavaDelegate implementations
When starting a case instance through the runtime service
When completing a task
The methods follow the naming convention of the regular case variables:
CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().transientVariable("var1", "test").start();
Expressions
Flowable uses UEL for expression-resolving. UEL stands for Unified Expression Language and is part of the EE6 specification (see the EE6 specification for detailed information).
Expressions can be used in, for example, Java Service tasks, sentry conditions and plan item listeners. Although there are two types of expressions, value-expression and method-expression, Flowable abstracts this so they can both be used where an expression is expected.
- Value expression: resolves to a value. By default, all case variables are available to use. Also, all spring-beans (if using Spring) are available to use in expressions. In non-Spring environments the beans available to expressions can be set through the setBeans method of the CmmnEngineConfiguration. Some examples:
${myVariable}
${myBean.myProperty}
- Method expression: invokes a method with or without parameters. When invoking a method without parameters, be sure to add empty parentheses after the method-name (as this distinguishes the expression from a value expression). The passed parameters can be literal values or expressions that are resolved themselves. Examples:
${printer.print()}
${myBean.addNewOrder('orderName')}
${myBean.doSomething(myVar, planItemInstance)}
These expressions support resolving primitives (including comparing them), beans, lists, arrays, and maps.
On top of all case instance variables, there are default objects available that can be used in expressions:
caseInstance: Holds additional information about the ongoing case instance. The caseInstance keyword is available in all expressions.
planItemInstance: The DelegatePlanItemInstance holds additional information about the current plan item instance. The planItemInstance keyword is available in all plan item related expressions (e.g. sentry conditions, plan item lifecycle listeners, service task expression, etc.).
planItemInstances: Exposes information about all current plan item instance. See the examples below on how to use it.
variableContainer: The VariableContainer for which the expression is being resolved for. A variable container is an abstraction on top of case instances, plan item instances, process instances and executions. The +variableContainer keyword allows to write expressions that are not bound to a specific implementation.
authenticatedUserId: The id of the user that is currently authenticated. If no user is authenticated, the variable is not available.
For example:
${caseInstance.id}
${caseInstance.getVariable('myVariable') == 'test'}
${caseInstance.setVariable('myVariable', 'test')}
${planItemInstance.getPlanItem().getPlanItemDefinition().getName()}
${planItemInstance.getVariable('myVariable') == 123}
${planItemInstance.setVariable('myVariable', 123)}
${variableContainer.getVariable('myVariable')}
${variableContainer.setVariable('myVariable', 'true')}
The keyword planItemInstances deserves some more explanation. Using this keyword it is possible to retrieve all the current plan item instances, but it also acts like a sort of API to retrieve more information about the current plan item instance.
For example, the following expression
${planItemInstances.active().count()}
Will return a count of all plan item instances that are currently in the 'active' state. If we’re only interested in a certain plan item, it can be filtered down:
${planItemInstances.definitionId('a').active().count()}
As the example shows, the planItemInstances keyword allows to chain together various filter methods. Such chaining will use AND semantics. The following methods are supported.
Methods that filter on plan item instance state:
active()
available()
enabled()
disabled()
completed()
terminated()
The following state filters are also supported, but do note these reflect non-spec compliant internal states:
unavailable()
waitingForRepetition()
asyncActive()
To get all plan item instances that are in a terminal (i.e. terminated or completed) or non-terminal state:
onlyTerminal()
onlyNonTerminal()
To filter based on the identifier that is set in the CMMN model:
definitionId('id1')
definitionIds('id1', 'id2')
To filter based on the name:
name('name1')
names('name1', 'name2', 'name3')
For some use cases, the plan item instances that should be filtered should only be part of the current stage. The 'current stage' is either the parent stage of a plan item or the case instance when there’s no parent stage.
- currentStage()
Lastly, there is a set of methods that cannot be chained, as they return a result.
count(): returns a count of plan item instances, after applying all filters.
getDefinitionIds(): returns a list of strings with all the ids as defined in the CMMN model of the matching plan item instances.
getDefinitionNames(): returns a list of names with all the names as defined in the CMMN model of the matching plan item instances.
getList(): returns the 'raw' list of org.flowable.cmmn.api.runtime.PlanItemInstance instances.
Let’s look at some examples where the above methods are used in expressions:
To count all active plan item instances in a case instance:
${planItemInstances.active().count()}
To do a count of all active plan item instances with an id 'a' or 'b':
${planItemInstances.active().definitionIds('a', 'b').count()}
To get all the ids of the plan item instances which are in a terminal state in the current stage:
${planItemInstances.currentStage().onlyTerminal().getDefinitionIds()()}
To store the result of the above expression in a transient variable
${caseInstance.setTransientVariable('myVar', planItemInstances.currentStage().onlyTerminal().getDefinitionIds()}}
Expression functions
[Experimental] Expression functions have been added in version 6.4.0.
To make working with case variables easier, a set of out-of-the-box functions is available, under the variables namespace.
variables:get(varName): Retrieves the value of a variable. The main difference with writing the variable name directly in the expression is that using this function won’t throw an exception when the variable doesn’t exist. For example ${myVariable == "hello"} would throw an exception if myVariable doesn’t exist, but ${var:get(myVariable) == 'hello'} will just work.
variables:getOrDefault(varName, defaultValue): similar to get, but with the option of providing a default value which is returned when the variable isn’t set or the value is null.
variables:exists(varName): Returns true if the variable has a non-null value.
variables:isEmpty(varName) (alias :empty) : Checks if the variable value is not empty. Depending on the variable type, the behavior is the following:
For String variables, the variable is deemed empty if it’s the empty string.
For java.util.Collection variables, true is returned if the collection has no elements.
For ArrayNode variables, true is returned if there are no elements.
In case the variable is null, true is always returned.
variables:isNotEmpty(varName) (alias :notEmpty) : the reverse operation of isEmpty.
variables:equals(varName, value) (alias :eq) : checks if a variable is equal to a given value. This is a shorthand function for an expression that would otherwise be written as ${execution.getVariable("varName") != null && execution.getVariable("varName") == value}.
- If the variable value is null, false is returned (unless compared to null).
variables:notEquals(varName, value) (alias :ne) : the reverse comparison of equals.
variables:contains(varName, value1, value2, …): checks if all values provided are contained within a variable. Depending on the variable type, the behavior is the following:
For String variables, the passed values are used as substrings that need to be part of the variable.
For java.util.Collection variables, all the passed values need to be an element of the collection (regular contains semantics).
For ArrayNode variables: supports checking if the arraynode contains a JsonNode for the types that are supported as variable type.
When the variable value is null, false is returned in all cases. When the variable value is not null, and the instance type is not one of the types above, false will be returned.
variables:containsAny(varName, value1, value2, …) : similar to the contains function, but true will be returned if any (and not all) the passed values is contained in the variable.
variables:base64(varName) : converts a Binary or String variable to a Base64 String
Comparator functions:
variables:lowerThan(varName, value) (alias :lessThan or :lt) : shorthand for ${execution.getVariable("varName") != null && execution.getVariable("varName") < value}.
variables:lowerThanOrEquals(varName, value) (alias :lessThanOrEquals or :lte) : similar, but now for < =.
variables:greaterThan(varName, value) (alias :gt) : similar, but now for >.
variables:greaterThanOrEquals(varName, value) (alias :gte) : similar, but now for > =.
The variables namespace is aliased to vars or var. So variables:get(varName) is equivalent to writing vars:get(varName) or var:get(varName). Note that it’s not needed to put quotes around the variable name: var:get(varName) is equivalent to var:get(\'varName') or var:get("varName").
Also note that in none of the functions above the planItemInstance or caseInstance needs to be passed into the function (as would be needed when not using a function). The engine will inject the appropriate variable scope when invoking the function. This also means that these functions can be used in exactly the same way when writing expressions in BPMN process definitions.
The use of these variable functions is especially useful in CMMN, for example when it comes to writing the condition of an if-part of sentry. Take the following CMMN case definition:
Assume the sentry has an if-part besides the completion event. Right after a case instance is started, this if-part condition will be evaluated (as the stage becomes available). If the condition is of the form ${someVariable == someValue}, this means the variable needs to be available when starting the case instance. In many cases, this is not possible or the variable comes later (e.g., from a form), which leads to a low-level PropertyNotFoundException. Taking the potential nullability into account, the correct expression would have to be:
${planItemInstance.getVariable('someVariable') != null && planItemInstance.getVariable('someVariable') == someValue}
Which is quite long. Using the functions above however, this can be simplified to
${var:eq(someVariable, someValue)}
or
${var:get(someVariable) == someValue}
The function implementations take into account the nullability of the variable (and not throw an exception in case the variable is null) and will handle the equality correctly.
Additionally, it’s possible to register custom functions that can be used in expressions. See the org.flowable.common.engine.api.delegate.FlowableFunctionDelegate interface for more information.
History Cleaning
By default history data is stored forever, this can cause the history tables to grow very large and impact the performance of the HistoryService. History Cleaning has been introduced with 6.5.0 and allows the deletion of HistoricProcessInstances and their associated data. Once process data no longer needs to be retained it can be deleted to reduce the history database's size.
Automatic History Cleaning Configuration
Automatic cleanup of HistoricCaseInstances is disabled by default but can be enabled and configured programmatically. Once enabled the default is to run a cleanup job at 1 AM to delete all HistoricCaseInstances and associated data that have ended 365 days prior or older. The deletion of the historical processes is done using the Flowable Batch mechanism, by scheduling jobs that are going to delete the processes in batches and store the information about what was done in a batch table.
CmmnEngine cmmnEngine = CmmnEngineConfiguration.
.createProcessEngineConfigurationFromResourceDefault()
.setEnableHistoryCleaning(true)
.setHistoryCleaningTimeCycleConfig("0 0 1 * * ?")
.setCleanInstancesEndedAfter(Duration.ofDays(365))
.buildCmmnEngine();
Spring properties set in an application.properties or externalized configuration are also available:
flowable.enable-history-cleaning=true
flowable.history-cleaning-after=365d
flowable.history-cleaning-cycle=0 0 1 * * ?
Manually Deleting History
Manually cleaning history can be accomplished by executing methods on the CmmnHistoryService query builders.
Delete all HistoricCaseInstances and their related data that are older than one year.
int numberOfCasesInBatch = 10;
Calendar cal = new GregorianCalendar();
cal.set(Calendar.YEAR, cal.get(Calendar.YEAR) - 1);
cmmnHistoryService.createHistoricCaseInstanceQuery()
.finishedBefore(cal.getTime())
.deleteSequentiallyUsingBatch(numberOfCasesInBatch, "Custom Delete Batch");
Unit testing
Cases are an integral part of software projects and they should be tested in the same way normal application logic is tested: with unit tests. Since Flowable is an embeddable Java engine, writing unit tests for business cases is as simple as writing regular unit tests.
Flowable supports JUnit versions 4 and 5 styles of unit testing.
In the JUnit 5 style one needs to use the org.flowable.cmmn.engine.test.FlowableCmmnTest annotation or register the org.flowable.cmmn.engine.test.FlowableCmmnExtension manually. The FlowableCmmnTest annotation is just a meta annotation and the does the registration of the FlowableCmmnExtension (i.e. it does @ExtendWith(FlowableCmmnExtension.class)). This will make the CmmnEngine and the services available as parameters into the test and lifecycle methods (@BeforeAll, @BeforeEach, @AfterEach, @AfterAll). Before each test the cmmnEngine will be initialized by default with the flowable.cmmn.cfg.xml resource on the classpath. In order to specify a different configuration file the org.flowable.cmmn.engine.test.CmmnConfigurationResource annotation needs to be used (see the second example). Cmmn engines are cached statically over multiple unit tests when the configuration resource is the same.
By using FlowableCmmnExtension, you can annotate test methods with org.flowable.cmmn.engine.test.CmmnDeployment. When a test method is annotated with @CmmnDeployment, before each test the cmmn files defined in CmmnDeployment#resources will be deployed. In case there are no resources defined, a resource file of the form testClassName.testMethod.cmmn in the same package as the test class, will be deployed. At the end of the test, the deployment will be deleted, including all related case instances, tasks, and so on. See the CmmnDeployment class for more information.
Taking all that into account, a JUnit 5 test looks as follows:
Junit 5 test with the default resource.
@FlowableCmmnTest
class MyTest {
private CmmnEngine cmmnEngine;
private CmmnRuntimeService cmmnRuntimeService;
private CmmnTaskService cmmnTaskService;
@BeforeEach
void setUp(CmmnEngine cmmnEngine) {
this.cmmnEngine = cmmnEngine;
this.cmmnRuntimeService = cmmnEngine.getCmmnRuntimeService();
this.cmmnTaskService = cmmnEngine.getTaskRuntimeService();
}
@Test
@CmmnDeployment
void testSingleHumanTask() {
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.caseDefinitionKey("myCase")
.start();
assertNotNull(caseInstance);
Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult();
assertEquals("Task 1", task.getName());
assertEquals("JohnDoe", task.getAssignee());
cmmnTaskService.complete(task.getId());
assertEquals(0, cmmnRuntimeService.createCaseInstanceQuery().count());
}
}
With JUnit 5 you can also inject the id of the deployment (with +org.flowable.cmmn.engine.test.CmmnDeploymentId+_) into your test and lifecycle methods.
Junit 5 test with custom resource.
@FlowableCmmnTest
@CmmnConfigurationResource("flowable.custom.cmmn.cfg.xml")
class MyTest {
private CmmnEngine cmmnEngine;
private CmmnRuntimeService cmmnRuntimeService;
private CmmnTaskService cmmnTaskService;
@BeforeEach
void setUp(CmmnEngine cmmnEngine) {
this.cmmnEngine = cmmnEngine;
this.cmmnRuntimeService = cmmnEngine.getCmmnRuntimeService();
this.cmmnTaskService = cmmnEngine.getTaskRuntimeService();
}
@Test
@CmmnDeployment
void testSingleHumanTask() {
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.caseDefinitionKey("myCase")
.start();
assertNotNull(caseInstance);
Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult();
assertEquals("Task 1", task.getName());
assertEquals("JohnDoe", task.getAssignee());
cmmnTaskService.complete(task.getId());
assertEquals(0, cmmnRuntimeService.createCaseInstanceQuery().count());
}
}
In the JUnit 4 style, the org.flowable.cmmn.engine.test.FlowableCmmnTestCase is available as parent class. It uses a configuration file flowable.cmmn.cfg.xml by default or uses a standard CmmnEngine using an H2 in-memory database if such file is missing. Behind the scenes, a CmmnTestRunner is used to initialise the CMMN engine. Note in the example below how the @CmmnDeployment annotation is used to automatically deploy the case definition (it will look for a .cmmn file in the same folder as the test class and expects the file to be named <Test class name>.<test method name>.cmmn.
public class MyTest extends FlowableCmmnTestCase {
@Test
@CmmnDeployment
public void testSingleHumanTask() {
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.caseDefinitionKey("myCase")
.start();
assertNotNull(caseInstance);
Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult();
assertEquals("Task 1", task.getName());
assertEquals("JohnDoe", task.getAssignee());
cmmnTaskService.complete(task.getId());
assertEquals(0, cmmnRuntimeService.createCaseInstanceQuery().count());
}
}
Alternatively, the FlowableCmmnRule is available and allows to set a custom configuration:
JUnit 4 test with a Rule.
@Rule
public FlowableCmmnRule cmmnRule = new FlowableCmmnRule("org/flowable/custom.cfg.xml")
@Test
@CmmnDeployment
public void testSomething() {
// ...
assertThat((String) cmmnRule.getCmmnRuntimeService().getVariable(caseInstance.getId(), "test"), containsString("John"));
// ...
}