Workflow Linter for BPMN workflows
MIT License
Workflow Linter Server (WLS) currently serves three functions:
./gradlew clean build
docker build . -t workflow-linter
docker run -d -p 8080:8080 -v myRules:/rules --name workflow-linter workflow-linter
make sure you setup your volume correctly so you can add rules.
Workflow linting is currently targeting for BPMN workflows.
Linting Rules can be configured using two methods:
Execution of linting rules is performed through a REST Endpoint
Option 1:
Create a Multi-Part Form with a file upload property. Property name must be file
.
POST localhost:8080/workflow/bpmn/linter
If the linting is successful a status-code of 200
will be returned.
Option 2:
POST localhost:8080/workflow/bpmn/linter
XML Body: xml of the BPMN.
Content-Type: application/xml
If the linting is successful a status-code of 200
will be returned.
The linter response will return a object with a results
property.
Each property in the results
object is a elementId in the XML (BaseElement.class).
This property contains a array of linting validation failures.
For each linting validation failure, the following properties are provided:
type
: the type of failure: WARNING
or ERROR
elementId
: the unique element ID. This is the same ID that is in the parent.elementType
: the model API full class path. Used to uniquely identify the type of element.message
: the linting validation message returned by the linting failure.code
: the linting validation code returned by the linting failure.When configuring linting rules, you can set the message
and code
values per linting rule.
{
"results": {
"Task_1pjpqz0": [
{
"type": "WARNING",
"elementId": "Task_1pjpqz0",
"elementType": "io.zeebe.model.bpmn.instance.MultiInstanceLoopCharacteristics",
"message": "my linter warning",
"code": 0
}
]
}
}
The workflow linter provides a linting/validation engine for BPMN workflows that are parsed by the BPMN Model API.
The linter acts as a warning and error system allowing you to validate workflows during the modeling process, and you can implement the linter at deployment time to ensure only valid models are deployed into their respective environments.
ServiceTask
, ReceiveTask
.... (more to come)Target
property defines "targeting rules" that applied to the rule. Targeting rules define when the rule should be applied for the specific Element Type.user-task
. This means the rule will only apply when a Service Task defines a type of user-task
workflow-linter:
rules:
global-rules:
enable: false
description: Global Restrictions for Service Task Types
elementTypes:
- ServiceTask
serviceTaskRule:
allowedTypes:
- some-type
- user-task
user-task-rule:
description: Specific rule for User Task Configuration of Service Tasks
elementTypes:
- ServiceTask
target:
serviceTasks:
types:
- user-task
headerRule:
requiredKeys:
- title
- candidateGroups
- formKey
allowedNonDefinedKeys: false
allowedDuplicateKeys: false
optionalKeys:
- priority
- assignee
- candidateUsers
- dueDate
- description
If the target
property configuration is not provided, then the rule will be applied globally to all Element Types defined in the rule.
Global rules can be a good way to implement some restrictions on your modeling teams to ensure that internal types and correlation keys are not used.
Global rules can be valuable naming conventions as well: "task types cannot start with a underscore _
."
Working model is to have rule factories for each major Element Type (Service Task, Receive Task, Catch message, etc).
Element Type Rules provide specific rule implementations that focus on the Element Type's configuration possibilities:
The Linter is not just about execution implementation rules, you can also define formatting rules for the BPMN.
Formatting rules enable you to prevent common errors in formatting of BPMN.
Kotlin Script .kts
linting rules provide a scripting based approach to writing linting rules.
Using the scripting approach provides rule writers the most flexibility and power, but requires more advanced skills compared to writing YAML based linting rules.
By default the linter will look for a ./rules
folder and walk the folder (and any children folders) looking for .kts files.
Kts files must return a ModelElementValidator or a list of ModelElementValidators.
You can change the folder location with:
linter:
kts:
folder: "./rules123"
If you want to configure individual rule files, you can do so with:
Configure the paths of your .kts files in the configuration:
linter:
kts:
rules:
- ./src/main/resources/rules/rule1.kts
- ./rules/TimerRules.kts
Configuring individual files will ignore any files in the rules folder.
Each script will be compiled at runtime and converted into Element Validators.
The following is a element validator example that checks that there are no timers that have a duration between 0 and 60 seconds.
package rules
import com.github.stephenott.workflowlinter.linter.elementValidator
import com.github.stephenott.workflowlinter.linter.getDuration
import io.zeebe.model.bpmn.instance.TimeDuration
elementValidator<TimeDuration> { e, v ->
if (e.getDuration().seconds in 0..60){
v.addError(60, "Timers Durations must be greater than 60 seconds.")
}
}
Workflow Sanitizer is the capability to remove aspects of a BPMN that are internal configuration that should not be shared when allowing users to download BPMN xml (such as when rendering a BPMN in the bpmn.js / bpmn.io modeler).
Every element in a BPMN can be replaced with a sanitized version: a sanitized version is a new blank instance of the element that replaces the original element. Only configurations that are explicitly desired in a sanitized BPMN are transferred over into the new instance.
The Sanitizer provides flexible usage options depending on your sanitizing needs:
The core of Sanitizer provides a Workflow Linter that allows you to configure which types that inherit from ModelElementInstance
will be targeted for cleaning.
The default Sanitizer Linter configuration targets all elements and applies a error code of 5000
("it's Audi 5000"...).
The default Sanitizer that actions each of the found elements, will take a lean approach:
Process
elements are not modified. But their children would be modified as their are independent instances of ModelElementInstance
What is explicitly not kept?
POST localhost
XML body and file upload options
body example goes here
Response
XML response goes here
see openapi docs folder for generated api docs. @TODO
This is a robust example to demonstrate the linting response capabilities and level of linting rules that can be created.
{
"results": {
"Task_1pjpqz0": [
{
"type": "WARNING",
"elementId": "Task_1pjpqz0",
"elementType": "io.zeebe.model.bpmn.instance.MultiInstanceLoopCharacteristics",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "Task_1pjpqz0",
"elementType": "io.zeebe.model.bpmn.instance.ServiceTask",
"message": "my linter warning",
"code": 0
}
],
"TextAnnotation_1t2spfv": [
{
"type": "WARNING",
"elementId": "TextAnnotation_1t2spfv",
"elementType": "io.zeebe.model.bpmn.instance.TextAnnotation",
"message": "my linter warning",
"code": 0
}
],
"Process_16i55l0": [
{
"type": "WARNING",
"elementId": "Process_16i55l0",
"elementType": "io.zeebe.model.bpmn.instance.Lane",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "Process_16i55l0",
"elementType": "io.zeebe.model.bpmn.instance.Lane",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "Process_16i55l0",
"elementType": "io.zeebe.model.bpmn.instance.LaneSet",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "Process_16i55l0",
"elementType": "io.zeebe.model.bpmn.instance.Process",
"message": "my linter warning",
"code": 0
}
],
"IntermediateThrowEvent_1jlnvjm": [
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_1jlnvjm",
"elementType": "io.zeebe.model.bpmn.instance.BoundaryEvent",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_1jlnvjm",
"elementType": "io.zeebe.model.bpmn.instance.MessageEventDefinition",
"message": "my linter warning",
"code": 0
}
],
"SequenceFlow_0bjdega": [
{
"type": "WARNING",
"elementId": "SequenceFlow_0bjdega",
"elementType": "io.zeebe.model.bpmn.instance.SequenceFlow",
"message": "my linter warning",
"code": 0
}
],
"EndEvent_1qai5bq": [
{
"type": "WARNING",
"elementId": "EndEvent_1qai5bq",
"elementType": "io.zeebe.model.bpmn.instance.EndEvent",
"message": "my linter warning",
"code": 0
}
],
"IntermediateThrowEvent_0pftlc9": [
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_0pftlc9",
"elementType": "io.zeebe.model.bpmn.instance.TimerEventDefinition",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_0pftlc9",
"elementType": "io.zeebe.model.bpmn.instance.BoundaryEvent",
"message": "my linter warning",
"code": 0
}
],
"IntermediateThrowEvent_0ue7kjs": [
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_0ue7kjs",
"elementType": "io.zeebe.model.bpmn.instance.BoundaryEvent",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_0ue7kjs",
"elementType": "io.zeebe.model.bpmn.instance.MessageEventDefinition",
"message": "my linter warning",
"code": 0
}
],
"Task_05r7ocx": [
{
"type": "WARNING",
"elementId": "Task_05r7ocx",
"elementType": "io.zeebe.model.bpmn.instance.MultiInstanceLoopCharacteristics",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "Task_05r7ocx",
"elementType": "io.zeebe.model.bpmn.instance.Task",
"message": "my linter warning",
"code": 0
}
],
"Collaboration_1xisnek": [
{
"type": "WARNING",
"elementId": "Collaboration_1xisnek",
"elementType": "io.zeebe.model.bpmn.instance.Participant",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "Collaboration_1xisnek",
"elementType": "io.zeebe.model.bpmn.instance.Collaboration",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "Collaboration_1xisnek",
"elementType": "io.zeebe.model.bpmn.instance.Participant",
"message": "my linter warning",
"code": 0
}
],
"Task_10ugd34": [
{
"type": "WARNING",
"elementId": "Task_10ugd34",
"elementType": "io.zeebe.model.bpmn.instance.CallActivity",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "Task_10ugd34",
"elementType": "io.zeebe.model.bpmn.instance.MultiInstanceLoopCharacteristics",
"message": "my linter warning",
"code": 0
}
],
"EndEvent_1m14ym9": [
{
"type": "WARNING",
"elementId": "EndEvent_1m14ym9",
"elementType": "io.zeebe.model.bpmn.instance.EndEvent",
"message": "my linter warning",
"code": 0
}
],
"Process_1xanqu0": [
{
"type": "WARNING",
"elementId": "Process_1xanqu0",
"elementType": "io.zeebe.model.bpmn.instance.Process",
"message": "my linter warning",
"code": 0
}
],
"IntermediateThrowEvent_0rqamn8": [
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_0rqamn8",
"elementType": "io.zeebe.model.bpmn.instance.ErrorEventDefinition",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_0rqamn8",
"elementType": "io.zeebe.model.bpmn.instance.BoundaryEvent",
"message": "my linter warning",
"code": 0
}
],
"IntermediateThrowEvent_1c9t58w": [
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_1c9t58w",
"elementType": "io.zeebe.model.bpmn.instance.TimerEventDefinition",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "IntermediateThrowEvent_1c9t58w",
"elementType": "io.zeebe.model.bpmn.instance.BoundaryEvent",
"message": "my linter warning",
"code": 0
}
],
"SequenceFlow_1fetaup": [
{
"type": "WARNING",
"elementId": "SequenceFlow_1fetaup",
"elementType": "io.zeebe.model.bpmn.instance.SequenceFlow",
"message": "my linter warning",
"code": 0
}
],
"SequenceFlow_1hnbx9h": [
{
"type": "WARNING",
"elementId": "SequenceFlow_1hnbx9h",
"elementType": "io.zeebe.model.bpmn.instance.SequenceFlow",
"message": "my linter warning",
"code": 0
}
],
"ServiceTask_1luzsfd": [
{
"type": "WARNING",
"elementId": "ServiceTask_1luzsfd",
"elementType": "io.zeebe.model.bpmn.instance.ServiceTask",
"message": "my linter warning",
"code": 0
}
],
"TextAnnotation_1qi6dhp": [
{
"type": "WARNING",
"elementId": "TextAnnotation_1qi6dhp",
"elementType": "io.zeebe.model.bpmn.instance.TextAnnotation",
"message": "my linter warning",
"code": 0
}
],
"ExclusiveGateway_0zw2nt1": [
{
"type": "WARNING",
"elementId": "ExclusiveGateway_0zw2nt1",
"elementType": "io.zeebe.model.bpmn.instance.ExclusiveGateway",
"message": "my linter warning",
"code": 0
}
],
"SubProcess_0th6hv3": [
{
"type": "WARNING",
"elementId": "SubProcess_0th6hv3",
"elementType": "io.zeebe.model.bpmn.instance.SubProcess",
"message": "my linter warning",
"code": 0
}
],
"SequenceFlow_135m3sa": [
{
"type": "WARNING",
"elementId": "SequenceFlow_135m3sa",
"elementType": "io.zeebe.model.bpmn.instance.SequenceFlow",
"message": "my linter warning",
"code": 0
}
],
"Association_1oysd0g": [
{
"type": "WARNING",
"elementId": "Association_1oysd0g",
"elementType": "io.zeebe.model.bpmn.instance.Association",
"message": "my linter warning",
"code": 0
}
],
"StartEvent_1": [
{
"type": "WARNING",
"elementId": "StartEvent_1",
"elementType": "io.zeebe.model.bpmn.instance.StartEvent",
"message": "my linter warning",
"code": 0
}
],
"Task_1cl9hr3": [
{
"type": "WARNING",
"elementId": "Task_1cl9hr3",
"elementType": "io.zeebe.model.bpmn.instance.ServiceTask",
"message": "my linter warning",
"code": 0
}
],
"Task_00znhpl": [
{
"type": "WARNING",
"elementId": "Task_00znhpl",
"elementType": "io.zeebe.model.bpmn.instance.MultiInstanceLoopCharacteristics",
"message": "my linter warning",
"code": 0
},
{
"type": "WARNING",
"elementId": "Task_00znhpl",
"elementType": "io.zeebe.model.bpmn.instance.ReceiveTask",
"message": "my linter warning",
"code": 0
}
],
"SequenceFlow_1uts8z1": [
{
"type": "WARNING",
"elementId": "SequenceFlow_1uts8z1",
"elementType": "io.zeebe.model.bpmn.instance.SequenceFlow",
"message": "my linter warning",
"code": 0
}
],
"Association_0nb28zy": [
{
"type": "WARNING",
"elementId": "Association_0nb28zy",
"elementType": "io.zeebe.model.bpmn.instance.Association",
"message": "my linter warning",
"code": 0
}
],
"SequenceFlow_15yu6zp": [
{
"type": "WARNING",
"elementId": "SequenceFlow_15yu6zp",
"elementType": "io.zeebe.model.bpmn.instance.SequenceFlow",
"message": "my linter warning",
"code": 0
}
],
"SequenceFlow_1edsqdq": [
{
"type": "WARNING",
"elementId": "SequenceFlow_1edsqdq",
"elementType": "io.zeebe.model.bpmn.instance.SequenceFlow",
"message": "my linter warning",
"code": 0
}
],
"Message_1wmcbh6": [
{
"type": "WARNING",
"elementId": "Message_1wmcbh6",
"elementType": "io.zeebe.model.bpmn.instance.Message",
"message": "my linter warning",
"code": 0
}
],
"StartEvent_1rxkj6g": [
{
"type": "WARNING",
"elementId": "StartEvent_1rxkj6g",
"elementType": "io.zeebe.model.bpmn.instance.StartEvent",
"message": "my linter warning",
"code": 0
}
],
"MessageFlow_0sslgzl": [
{
"type": "WARNING",
"elementId": "MessageFlow_0sslgzl",
"elementType": "io.zeebe.model.bpmn.instance.MessageFlow",
"message": "my linter warning",
"code": 0
}
],
"SequenceFlow_1oknyaf": [
{
"type": "WARNING",
"elementId": "SequenceFlow_1oknyaf",
"elementType": "io.zeebe.model.bpmn.instance.SequenceFlow",
"message": "my linter warning",
"code": 0
}
]
}
}