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 which 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 instances.
-
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.
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 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 FlowableExceptionss.
-
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.
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();
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 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 an 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();
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, and plan item transitions. 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. Some examples:
${myVar} ${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, execution)}
Note that these expressions support resolving primitives (including comparing them), beans, lists, arrays and maps.
On top of all process variables, there are a few default objects available that can be used in expressions:
-
caseInstance: The DelegateCaseInstance holds additional information about the ongoing case instance.
-
planItemInstance: The DelegatePlanItemInstance holds additional information about the current plan item.
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 processes is as simple as writing regular unit tests.
Flowable supports JUnit versions 3/4/5 styles of unit testing.
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());
}
}
Alernatively, the FlowableCmmnRule is available and allows to set a custom configuration:
@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"));
// ...
}