Introduction
In Flowable (Platform, Work, and Engage), the service registry engine is available to deploy, manage, and invoke services.
In this context, a service is described by a service definition or service model that defines the technical details of how to invoke the service. For example, it describes that the service is invoked as an HTTP REST call, what the input or output parameters are, how to map the response, etc.
Once such a service model is made, it can then be used in BPMN or CMMN models to invoke the service. The main benefits of using the service registry engine (versus, for example, using and configuring a service task directly) are:
Encapsulating service implementation details: a technical person can create service models that contain the low-level configuration on how a service is invoked. The modeling user can use these services in their models without having ever to know these details.
Reuse of services in different models without having to repeat or know the technical details.
As the models reference the service model and do not include the technical service details itself, the implementation of the service can be changed without ever having to change the models.
If you’re interested in the modeling side of things the following guides go into the details on how to model and use the service models:
Anatomy of a Service Model
A service is described by a service definition or service model, which contains the service implementation details. Such a service model is visually created in Flowable Design (see the Service Registry Modeler Guide for examples).
Service models in Flowable Design can be added to an app and deployed directly to a runtime Flowable installation.
Alternatively, service definition JSON files can be placed on the classpath are read and loaded when the server boots up.
file suffix: .service
classpath location: com/flowable/service/custom/*.service
A service definition contains four distinct parts:
Metadata: The key, description, etc. of the service.
Operations: Each service consists of one or more operations. For example, a customer service could expose an operation to look up a customer, create a customer, or even remove a customer object.
Input and output parameters: Each operation can have zero or more parameters. The input parameters (name, type, default value, etc.) for an operation and the output parameters that the operation produces (if any) determines the kind of data used and produced when invoking the service.
Configuration: The implementation details that are specific to the service type (e.g., which HTTP method to use for the rest call).
The service definition only describes the input and output parameters for the service as they are when invoking the service. The service definition does not define how these parameters are mapped to process or case variables. The actual mapping is done in the BPMN or CMMN task attributes that are used in the process or case model in Flowable Design. In Design, it is possible to map variables to input parameters for the service and output parameter values to variables. This way, the service definition is not bound to any model implementation, and it can be reused in different models, with different parameter mapping configurations.
Note that only the output parameters are returned eventually and they effectively work as a filter for the data that gets returned.
Example: Creating an Expression-backed Service Definition
Let us start with a simple example to explain the concepts behind a service
definition. In this example, someone has written a customer lookup
service and exposed that service as a Spring bean (e.g., a custom configuration
with @Configuration
that exposes this bean was added to the classpath).
The actual way of how this is done is not important for the service registry.
It could be that service is doing a database lookup to fulfill its purpose, or
maybe it is using a message queue to send a message to a remote microservice.
The point is: for the user of the service in a model, this is not important.
What matters is how data can be passed into this service and what type of
data is getting returned.
In Flowable Design, go in an app and press Create
, select Service for the model type and fill in the name and key.
The key is used to reference the service definition in other models. When exporting BPMN or CMMN models, this is the value that is contained in the XML file.
An empty service definition model is now created. The screen now looks something like this:
Assume that the Spring bean is exposed with the name customerInfoService. For example, the bean could be exposed as follows:
@Bean
public CustomerInfoService customerInfoService() {
return new CustomerInfoServiceImpl();
}
And if this class has one method with following signature:
public interface CustomerInfoService {
ObjectNode getInfo(String customerId);
}
Notice how the return type of the service is com.fasterxml.jackson.databind.node.ObjectNode and not String. When using the service registry engine, returning an ObjectNode or an ArrayNode (since 3.11) is mandatory. The reason is to force the author of the service to think about the serialization of data. Having arbitrary object instances as a return value would lead to storing these as serializable variables in process or case instance, which is a bad practice.
Click the Add operation
and add one input parameter, Customer ID and
one output parameter, Customer name as shown in the screenshots below:
Configure name and key of the operation:
Configure the expression of the operation:
Configure the input parameters for the operation:
Configure the output parameters for the operation:
The JSON representation of this model looks as follows:
{
"name": "Customer Info Service",
"key": "customerInfoService",
"type": "expression",
"operations": [
{
"name": "Get user information",
"key": "getUserInfo",
"config": {
"expression": "${customerInfoService.getInfo(customerId)}"
},
"inputParameters": [
{
"required": false,
"defaultValue": null,
"displayName": "Customer id",
"name": "customerId",
"type": "string"
}
],
"outputParameters": [
{
"nullable": false,
"defaultValue": null,
"displayName": "Customer name",
"name": "name",
"type": "string"
}
]
}
]
}
Service Operations and Parameters
As seen in the screenshot in the previous section (and the related JSON representation):
A service has a name and a key, like all the models supported by Flowable.
The type of the service is set to expression.
The service has one operation which has one input parameter and one output parameter.
Each input parameter has a type, a human-friendly displayName, and a technical name. The latter is used when passing data to the expression in the previous section (customerId).
Each input parameter can be marked as required and can get a default value.
Each output parameter also has a type, a human-friendly displayName, and a technical name.
Each output parameter can be marked as nullable (the service is allowed to return no value for this parameter) and a default value.
Example: Creating a Script-backed Service Definition
v3.12.0+For a script-based service model the Service type needs to be changed to Script.
Creating a script-based service definition is similar to the expression-backed example, but instead of an expression, a JSR-223 compatible scripting language, for instance JavaScript or Groovy is used instead. This allows you to write complex service logic without having to write it in Java, which requires the class to be added to the classpath and an instance being exposed as a Spring bean.
To use JavaScript as a scripting language, it is required to have a JavaScript scripting engine on the classpath. For Java 8 and 11 the Nashorn Engine is included by default. In case you are running a new Java version (e.g. 17) and you are using a customization project, you need to add the dependency manually:
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
</dependency>
The script content might be similar to the one of a Script Task, however, in a script task, the script itself is embedded into the process or case model and therefore cannot be reused and is not easy maintainable.
With a script based service definition, the script itself is referenced from where it is used in a process- or case-model. This way, it can be controlled, maintained and even reused in different context as it is clearly defined using the input and output parameters.
We strongly encourage to use a script-based service definition instead of script tasks as the script is taken out of the process- or case-model where it can be maintained, reused and controlled. The input and output parameter mapping for such a script-based service definition gives you the advantage of clearly defining the boundaries in which the script is invoked, what input it needs and what output it produces.
Behind the Scenes: How a Script-based Service Invocation Works
Before the script is invoked, the service engine creates a context into which all input parameter values are automatically exposed as well as various services and the Flowable API though with you can access various utilities as well as register output parameters or create JSON content.
The script is invoked given this context and afterwards, the collected output parameters are further processed as defined in the service definition.
This way, the engine creates kind of a service invocation context where input is provided and output is collected during script execution.
Simple Script-based Service Model Example
In the following example, we want a service to reassign all open, active tasks of a certain case, no mather the origin of the task (e.g. direct case related tasks, tasks of processes running within the case, etc).
We will need the case id and the id of the new assignee to be set on all active tasks of that given case.
So we define two input parameters:
- Case id (with name
caseId
) of typeString
which is of course required - New assignee user id (with name
newAssigneeUserId
) of type String as well and required too
As the output, we would probably not need anything, but to have a return value example, we want to return the total number of newly assigned or reassigned active tasks and the total number of tasks within the provided case.
We define two output parameters:
- Reassigned task count (with name
reassignedTaskCount
) of typeInteger
which is required, and if the value is missing, we want to throw an exception - Total task count (with name
totalTaskCount
) of typeInteger
which is required as well
Having defined the input and output parameters, we need to write the script itself. Choose Groovy
as the language in this example.
As stated before, all input parameters are exposed to the script context automatically using their defined names, and we can directly access them.
For the output parameters we need to use the API to register the output parameter values throughout the script execution.
We do not make use of the single return value of a script as using the API, it is way more explicit and specially for multiple
return values or even nested values and lists, it is way easier than creating a special return value on its own.
Here is the Groovy script we want to use to reassign active tasks of a given case:
import org.flowable.engine.TaskService;
import org.flowable.task.api.Task;
var caseId = flw.getInput('caseId');
var newAssigneeUserId = flw.getInput('newAssigneeUserId');
// load all tasks in the scope of the case, regardless their position in the execution tree
List tasks = taskService.createTaskQuery()
.caseInstanceIdWithChildren(caseId)
.active()
.list();
for (Task activeTask : tasks) {
taskService.setAssignee(activeTask.getId(), newAssigneeUserId);
}
long totalCount = taskService.createTaskQuery()
.caseInstanceIdWithChildren(caseId)
.count();
flw.setOutput('reassignedTaskCount', tasks.size());
flw.setOutput('totalTaskCount', totalCount);
In Design, this service registry definition / operation would look like this:
Configure name and key of the operation:
Configure the input parameters for the operation:
Configure the output parameters for the operation:
Configure the script of the operation:
We can now use this service definition through a service registry task anywhere in a case or process model, mapping the input and optionally the output parameters, and we will reassign all active tasks within the case to a new assignee.
Exposed Services in the Script Context
Maybe you already spotted it in the example script, we used the task service to query for active tasks and the total task count.
As mentioned before, all input parameters are automatically exposed to the script context, this is why we could directly access
the caseId
or newAssigneeUserId
parameter values without getting them as a variables or anything like that.
The same thing is possible for a list of services directly exposed into the script context like the taskService
.
We can directly access those services without having to somehow resolve them first.
Here is a list of available services:
engineConfiguration
orserviceEngineConfiguration
, returns thecom.flowable.serviceregistry.engine.ServiceRegistryEngineConfiguration
runtimeService
, returns theorg.flowable.engine.RuntimeService
with all the process engine runtime service functionalitycmmnRuntimeService
, returns theorg.flowable.cmmn.api.CmmnRuntimeService
with all the case engine runtime service functionalityhistoryService
, returns theorg.flowable.engine.HistoryService
with all the process engine history service functionalitycmmnHistoryService
, returns theorg.flowable.cmmn.api.CmmnHistoryService
with all the case engine history service functionalitymanagementService
, returns theorg.flowable.engine.ManagementService
with all the process engine management service functionalitycmmnManagementService
, returns theorg.flowable.cmmn.api.CmmnManagementService
with all the case engine management service functionalitytaskService
, returns theorg.flowable.engine.TaskService
with all the process engine task service functionalitycmmnTaskService
, returns theorg.flowable.cmmn.api.CmmnTaskService
with all the case engine task service functionality
You can directly use any of those services without having to worry where to get it from. Lookup the appropriate Java APIs to get an overview of available functionality.
Registering Values for Output Parameters
We already saw a simple example on how to return two values from within a script to be used as output parameters.
In this section, we are going to take a closer look on how to register more complex values and even nested ones or lists of values.
To register values for the service engine to use as output parameters, we need to make use of the exposed API accessible through flw
.
Instead of returning a script value, we need that API to register all our values explicitly for the output.
Single values can be registered using the setOutput
method:
- Register a string value named
foo
:flw.setOutput('foo', myFooVariable);
- Register an integer based value named
bar
:flw.setOutput('bar', 100);
See more details about the Flowable scripting API and available utilities here.
Example: Creating a REST-backed Service Definition
To create a service definition for a service that exposes its functionality using REST, the Type of the definition needs to be changed to REST.
Creating a REST service definition is similar to the expression-backed example in the previous section, but additional configuration about the actual REST details needs to be added.
More specifically:
A REST-backed service can have a Base URL configured that gets applied to all operations of the service. It can be an expression or have an expression (e.g., http://some-url/customers/${customerId}, which would mandate that
customerId
gets passed as an input parameter).A REST operation needs a method (GET/POST/PUT/DELETE) that determines which HTTP method to use.
It could be the data needed for this service is nested deeply in the JSON response that is returned. It is possible to set an output path which gets applied to all output parameters and is interpreted as a JSON Pointer expression starting from the root of the JSON response.
Similarly, each output parameter has an optional path property (a JSON Pointer expression) to pinpoint the wanted data.
Here is an example of a simple REST service definition, similar to the example in the previous section:
Configure name and key of the operation:
Configure the REST configuration of the operation:
Configure the input parameters for the operation:
Configure the output parameters for the operation:
POST and PUT methods typically need a body.
By default, the input parameters are used to create a 'flat' JSON structure
(meaning that each input parameter is one field in the JSON body
that gets constructed automatically). The Body location can be used to have a nested JSON structure.
When a more complex body is needed
for executing the operation, a custom body template resource can be used.
This is a Spring resource reference (e.g.,
classpath:/com/flowable/serviceregistry/engine/template/custom-template.ftl
)
to a Freemarker template that is processed when the body gets constructed.
All the input parameters are available in the template.
Such a template could look as follows:
<#ftl output_format="JSON">
{
"nestedField": {
"name": "${name}",
"accountNr": ${accountNumber}
}
<#if tenantId?has_content >,
,"tenantId": ${tenantId}
</#if>
}
Authorization for REST-backed Service Definition
Depending on how the REST-backed service is protected the service needs to be configured differently.
In most cases the Authorization header needs to be set.
Flowable offers an easy way to configure the Authorization header for HTTP Basic and HTTP Bearer authorizations by configuring the authorization
for a model or an operation.
When the authorization is explicitly configured then a header with the name of Authorization will be ignored and replaced by the configuration from this authorization.
Basic Authorization
When Basic Authorization is used the username and password need to be base64 encoded.
When using the authorization
from the model or operation configuration, Flowable will apply this encoding and prefix the header automatically.
e.g.
{
"key": "Customer REST Service",
"type": "REST",
"config": {
"authorization": {
"basic": {
"username": "${environment.getProperty('com.example.demo-service.username')}",
"password": "${environment.getProperty('com.example.demo-service.password')}"
}
}
}
}
The value in the username
and password
can either be an expression or an actual value.
Bearer Authorization
When Bearer Authorization is used then only the Bearer Token needs to be passed to the Header.
When using the authorization
fro the model or operation configuration, Flowable will add the appropriate authorization header.
e.g.
{
"key": "Customer REST Service",
"type": "REST",
"config": {
"authorization": {
"bearer": {
"bearer": "${environment.getProperty('com.example.demo-service.bearer-token')}"
}
}
}
}
Request / Response handlers
Sometimes more advanced processing of the request or response might be needed. For this purpose a REST Service has the Request and Response handlers.
These handlers can either use a JUEL expression or a script that can do the additional processing.
The HTTP request or response objects can be accessed using flwHttpRequest
and flwHttpResponse
.
The service operation can be accessed using flwServiceOperation
.
To obtain the service operation key, use flwServiceOperation.getKey().
Example: Creating an AI Agent Service Definition
From v3.17.0+ Flowable supports AI Agent services. You can read more in the AI Service Definition section.
Deploying a Service Definition
Service definitions that are referenced in BPMN or CMMN models need to be included in the same app. When deploying the app, the service definition is also deployed to the runtime system.
Extension: Intercepting Service Invocations
It is possible to intercept any service invocation done by the service registry engine just before the service gets invoked and just after it returns.
Potential use cases are, for example:
Adding new data to the context that is passed to the service. This data could be a calculated field value, a value coming from another service, etc.
The response data needs to be filtered or checked.
The response needs to have a calculated field or a completely new field that could not be expressed in the service operation mapping.
To add such logic, create and expose a Spring bean that implements the
com.flowable.serviceregistry.api.interceptor.ServiceInvokerInterceptor
interface. It looks as follows:
public interface ServiceInvokerInterceptor {
void beforeServiceInvocation(ServiceDefinitionModel serviceDefinition, ServiceOperation serviceOperation, ServiceInvocationContext context);
void afterServiceInvocation(ServiceDefinitionModel serviceDefinition, ServiceOperation serviceOperation, ServiceInvocationContext context, ServiceInvocationResult result);
}
The serviceDefinition and the serviceOperation that are passed to the method are a Java representation of the service operation that gets invoked. It can be used to only execute the custom interceptor logic when it matches a specific operation, for example.
The ServiceInvocationResult contains the actual values passed to the service (called service data), and way of passing through free-form additional data. The ServiceInvocationResult for the 'after' callback would contain the response result or an exception if an exception happened.
Instances of this interface exposed as Spring beans are automatically picked up and injected into the Service Registry Engine.
Extension: Enhancing Rest Service Invocations
REST services sometimes need specific enhancements to work in certain environments. For example, a company-specific authentication mechanism could exist that sets a token as a header, or a custom SSL configuration needs to be applied, etc.
For this purpose, the low-level
com.flowable.serviceregistry.engine.impl.invoker.rest.RestServiceInvokerEnhancer
interface exists:
public interface RestServiceInvokerEnhancer {
void enhanceHttpRequest(ServiceDefinitionModel serviceDefinitionModel, ServiceOperation serviceOperation,
ServiceInvocationContext serviceInvocationContext, HttpRequest httpRequest);
void enhanceHttpResponse(ServiceDefinitionModel serviceDefinitionModel, ServiceOperation serviceOperation,
ServiceInvocationContext serviceInvocationContext, HttpResponse httpResponse);
void enhanceJsonResponse(ServiceDefinitionModel serviceDefinitionModel, ServiceOperation serviceOperation,
ServiceInvocationContext serviceInvocationContext, JsonNode jsonResponse);
}
The serviceDefinition and the serviceOperation that are passed to the method are a Java representation of the service operation that gets invoked. It can be used to only execute the custom interceptor logic when it matches a specific operation, for example.
The methods respectively allow to change anything to either the request (before it gets sent), the response (right after it returns) and the JSON response before it is passed further to the mapping of the BPMN or CMMN model.
Instances of this interface exposed as Spring beans are automatically picked up and injected into the Service Registry Engine.
Extension: Custom FlowableHttpClient
v3.14.5+REST Services sometimes need more customizations for how to invoke the REST API.
e.g. custom SSL configuration needs to be applied, or some other more low level REST layer mechanism needs to be provided.
For this purpose a custom FlowableHttpClient
can be provided for a particular Service Definition Model.
See Multiple FlowableHttpClient(s) for how you can expose a custom FlowableHttpClient
.
Once the client is exposed the http client can be provided by using httpClient
as a setting property.
e.g.
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.httpClient=myCustomFlowableHttpClient
The value of property should be the name of the custom FlowableHttpClient
bean.
In case you have a more advanced use case and need access to the model, operation and / or request to create the FlowableHttpClient
then you can use an instance of com.flowable.serviceregistry.engine.impl.invoker.rest.RestServiceInvokerHttpClientProvider
:
public interface RestServiceInvokerHttpClientProvider {
FlowableHttpClient getHttpClient(ServiceDefinitionModel serviceDefinitionModel, ServiceOperation serviceOperation,
ServiceInvocationContext serviceInvocationContext, HttpRequest httpRequest);
}
The same property as for the FlowableHttpClient
can be used for this
e.g.
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.httpClient=myCustomFlowableHttpClientProvider
Global Settings for REST Services
It is possible to set configuration properties for REST service definitions that get applied to all such services.
The naming pattern is
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.property
.
When using default
, this is the default fallback when no more
specific value is set.
This is for example, useful when different environments are used for testing, staging, production, etc. Using these settings, for example, the base URL for all REST services is used.
The following settings allow to overriding configurations of service definitions. The order of resolving is service-override, then default override, then settings in the operation, and finally the setting in the service configuration.
Note that these last two are only relevant when creating the service definition manually (not through Flowable Design).
For example:
flowable.service-registry.rest-settings.myService.url overrides the URL of the service definition with key myService.
flowable.service-registry.rest-settings.default.url is used if the previous value is missing.
The URL from the actual operation/service is used when the above two values are missing.
Additional available settings:
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.baseUrl: The base URL applied to all operations.
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.url: The URL for invoking the service.
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.method: The HTTP method used.
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.outputPath: Configures the output path of the response. See above for more information.
Additional technical settings:
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.headers: A comma-separated list of headers in the form of
name:value
that needs to be applied.flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.bodyEncoding: Configures the encoding of the HTTP body.
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.noRedirects: Boolean flag to indicate whether or not to follow HTTP redirects. Default false.
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.ignoreErrors: Boolean flag to indicate any HTTP errors (500, 404, etc) that gets returned instead of 200.
flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.timeout: The total time (in ms) that a request can take. Not set by default.