Getting started
The process automation component is designed to gradually increase complexity when needed. This guide will take you through the most common functionality of the component and set you up to tackle more advanced use-cases later.
Prerequisites
This guide assumes some (basic) prior knowledge. It is advised to read up on these topics if they are not immediately clear yet.
- Component model: We will extend the Component model with Workflows.
- Finite state machines: Processing is modeled using state machines. We will refer to state machine terminology such as state and transition.
Installing the component
If you are already using the workflow component, make sure to migrate it first. Go to migration guide.
The first step is to add the component to your application. We start by adding the process-automation-component
expansion resource. Additionally, you may want to add the process-automation-tracing
expansion resource. This will
add a log statement for every executed transition within your application.
- µRadiant
- Project Files
- Navigate to settings > expansionResources.
- Add a row with name
net.democritus.workflow:process-automation-component
and version1.5.3
. - (Optional) Add a row with name
net.democritus.workflow:process-automation-tracing
and version1.5.3
.
<expansionSettings>
<expansionResources>
<expansionResource name="net.democritus.workflow:process-automation-component" version="1.5.3"/>
<!-- optionally -->
<expansionResource name="net.democritus.workflow:process-automation-tracing" version="1.5.3"/>
</expansionResources>
</expansionSettings>
Finally, we need to add the component itself into the application model. Add processAutomation
to the application components
and don't forget to add processAutomation
as a component dependency to the components that will use it as well.
Creating a workflow
We will mostly be working with the Workflow model, which is an extension of Component. This means it lives next to (and not inside) DataElement and TaskElement. Each workflow defines a single state machine. Most applications are too complex to put into a single state-machine, which is why multiple state machines can run in parallel.
Selecting a target
The state within our state machine needs to be stored somewhere. Since we already model data using DataElements, we will
use these as a target for workflows. A String
field is added to a known DataElement to maintain the active state of a
specific DataElement instance.
- µRadiant
- $ CLI
- Model Files
- Navigate to your Component.
- In the tree-view, open the context menu of the target DataElement (right-click).
- Select "Create workflow environment". This will create a
«DataElement»Flow
workflow element.
Run the following command to transmute the model.
mvn expanders:transmute-model -Dtransmutation=CreateWorkflowEnvironment -DtargetElement={component}::{dataElement}
Running the transmutation will add a status
field of type String
with a corresponding Se finder to the targeted element. Furthermore a workflow will be created.
<workflow name="MyElementFlow">
<packageName>net.demo</packageName>
<targetField component="mycomp" dataElement="MyElement" name="status"/>
<states/>
<transitions/>
</workflow>
Marking the initial state
A workflow defines a set of states
, which lists all possible states in our state-machine. Each state has a name property
to make it easier when reasoning about a workflow. Additionally one state can be marked as the initial state. As a
result of defining the initial state, every instance from the targeted DataElement will start with a status
value equal
to the name of the initial state. This value is set in the pre-create
anchor on the «DataElement»Bean
.
- µRadiant
- Model Files
- In the tree-view, open the context menu of the Workflow (right-click).
- Select "New State". This will open the edit screen of a new state.
- Give the state a name and enable the
initialState
checkbox to mark it as initial state. - Click 'Create'.
<workflow name="MyElementFlow">
<states>
<state name="Initial">
<initialState>true</initialState>
</state>
</states>
</workflow>
A state machine should always have exactly one initial state. Read the documentation on initial states to see how having multiple initial states can be prevented.
Adding transitions
Now that we have the basic elements in place, it is time to add functionality. We define transitions to model functionality within a workflow. A workflow transition however is slightly different from a classical state-machine transition. A workflow transition in fact defines three or more transitions. The main components are a begin state, interim state, failed state and end state. The state machine of a single workflow transition is depicted below.
You may wonder why we define an interim state. The primary reason is to facilitate a separation of state, it becomes immediately obvious when a dataElement is being processed by a workflow, and when it is finished. A second more technical reason is related to locking. Enterprise applications have a great concurrency model, but we do not want to concurrently modify the same object. By eagerly modifying the state to an interim state, we can avoid such modifications.
A simple transition
Now lets actually create a transition. This is similar to creating a state, but we will have to enter the related states.
You may notice the end state to be defined in a list of endStates
, we will discuss this further in Branching tasks.
- µRadiant
- Model Files
Before creating a transition, it is advised to have a begin and end state already present. For interim and failed state the µRadiant provides a generator.
- In the tree-view, open the context menu of the Workflow (right-click).
- Select "New Transition". This will open the edit screen of a new state.
- Give the transition a name and select the
beginState
from the provided dropdown. - (Optional) Explicitly select an interim and/or failed state, these are generated otherwise.
- Click " New workflows::EndState::model" under
endStates
- Select a
state
from the dropdown in the newly created row. LeavetaskOutcome
empty. - Click 'Create'.
<workflow name="MyElementFlow">
<states>
<state name="Initial">
<initialState>true</initialState>
</state>
<state name="Making"/>
<state name="FailedToMake"/>
<state name="Made"/>
</states>
<transitions>
<transition name="Make">
<beginState component="mycomp" workflow="MyElementFlow" name="Initial"/>
<interimState component="mycomp" workflow="MyElementFlow" name="Making"/>
<failedState component="mycomp" workflow="MyElementFlow" name="FailedToMake"/>
<endStates>
<endState>
<state component="mycomp" workflow="MyElementFlow" name="Made"/>
</endState>
</endStates>
</transition>
</transitions>
</workflow>
Executing tasks
Now that we can define the stateful lifecycle of a dataElement, it is time to perform some actual work! We already model
each unit of work as a TaskElement which makes it a good
candidate for integration with workflows. To execute a task within a transition, simply configure the task as
executeTask
field on the transition.
One thing to consider is the that the targetField
on a workflow must be from the same DataElement as the TaskElement
targetElement. The task will contain any business logic needed for the transition. Below is the updated version of our
previous example, where the only two results the task can have are a TaskResult.success
and a TaskResult.error
.
Branching tasks
With tasks as discussed so far it is already possible to model many use-cases for a workflow. By introducing branching the model can become even more expressive. A TaskElement allows you to define a set of TaskOutcomes. Each outcome can then be interpreted by a transition in order to determine the correct end state. Consider the example of Schrödingers cat and a task to open the box. The following is a more concrete example of the diagram shown earlier.
Since the TaskOutcomes are decoupled from the resulting state, it is possible to re-use (branching) Tasks in multiple transitions.
- µRadiant
- Model Files
Adding a TaskOutcome to the TaskElement (repeatable):
- In the tree-view, open the context menu of the Task (right-click).
- Select "New TaskOutcome". This will open the edit screen of a new outcome.
- Give the outcome a name and optionally a description.
- Click 'Create'
Defining endStates on a Transition with TaskOutcomes
- In the tree-view, find and select the Transition (left-click).
- Under
endStates
, add as many rows as you need to fit all outcomes. - In each row, select an outcome from the
taskOutcome
dropdown and an end state from thestate
dropdown.
<taskElement name="ValidateMyElement">
<targetElement component="mycomp" name="MyElement"/>
<outcomes>
<taskOutcome name="Valid"/>
<taskOutcome name="Invalid"/>
</outcomes>
</taskElement>
<workflow name="MyElementFlow">
<states>
<state name="Initial">
<initialState>true</initialState>
</state>
<state name="Validating"/>
<state name="ValidationFailed"/>
<state name="MyElementValid"/>
<state name="MyElementInvalid"/>
</states>
<transitions>
<transition name="Validate">
<executeTask component="mycomp" name="ValidateMyElement"/>
<beginState component="mycomp" workflow="MyElementFlow" name="Initial"/>
<interimState component="mycomp" workflow="MyElementFlow" name="Validating"/>
<failedState component="mycomp" workflow="MyElementFlow" name="ValidationFailed"/>
<endStates>
<endState>
<state component="mycomp" workflow="MyElementFlow" name="MyElementValid"/>
<taskOutcome component="mycomp" task="ValidateMyElement" name="Valid"/>
</endState>
<endState>
<state component="mycomp" workflow="MyElementFlow" name="MyElementInvalid"/>
<taskOutcome component="mycomp" task="ValidateMyElement" name="Invalid"/>
</endState>
</endStates>
</transition>
</transitions>
</workflow>
Modeling behavior
So far we've only discussed what our application should be doing, but we are missing when this should be done. The
workflow model has the concept of Triggers which model the behavior of transitions. Process automation ships with
several useful triggers out of the box: OnCreate
, OnModify
, OnTransition
, ByDataCommand
, Schedule
and FlowEngine
.
Creating triggers is very similar regardless of its type.
- µRadiant
- Model Files
- In the tree-view, open the context menu of the Transition (right-click).
- Select "New <trigger>" where <trigger> is the desired trigger. This will open the edit screen of a new Trigger.
- Configure the trigger. Most triggers only need a name, others will be described more in the coming sections.
- Click 'Create'
<workflow name="MyElementFlow">
<transitions>
<transition name="Make">
<!-- other fields omitted for brevity -->
<triggers>
<trigger type="workflows::OnCreate" name="OnCreate"/>
<trigger type="workflows::FlowEngine" name="FlowEngine"/>
</triggers>
</transition>
</transitions>
</workflow>
OnCreate
It is not uncommon for dataElements to require some pre-processing after they are created. The OnCreate trigger is perfect for such scenario. The result of adding this trigger is that a transition is immediately added to the processing queue when a dataElement is created, causing a near instant transition as a result. This should only be used on transitions with the initial state as their begin state.
OnModify
Very similar to the OnCreate trigger, but is activated when a DataElement is modified. This is useful when additional processing or validation is required after a user has changed the element in some way.
OnTransition
Another event based trigger. OnTransition will activate after each transition within the workflow. This makes it suitable for chaining multiple transitions together. It is recommended to keep Task implementations as small as possible, by using this Trigger the induced overhead of multiple sequential Transitions is minimal.
ByDataCommand
We often see certain transitions originate in user interaction. To facilitate this, the ByDataCommand trigger integrates execution of a transition within a command.
In order for this to work, we first need to define a dataCommand. This command has two important options to customize
behavior. By default, the command will only schedule the transition and return immediately. To make the command wait
while transitioning, the workflow.trigger.blockingMs
option can be either set to a positive integer (the timeout) or
to the value until_complete
(no timeout). The options workflow.trigger.button
can be used to conveniently add a button
to the knockout UI, if present in your application.
- µRadiant
- Model Files
- In the tree-view, open the context menu of the DataElement (right-click).
- Select "New DataCommand". This will open the edit screen of a new DataCommand.
- Name the dataCommand and check 'hasTargetInstance'.
- (Optional) Add
workflow.trigger.button
andworkflow.trigger.blockingMs
options for additional configuration. - Click 'Create'
- The newly created dataCommand can now be selected as the
dataCommand
when creating a ByDataCommand trigger.
<dataElement name="MyElement">
<dataCommands>
<dataCommand name="trigger">
<hasTargetInstance value="true"/>
<options>
<workflow.trigger.button/>
<workflow.trigger.blockingMs>500</workflow.trigger.blockingMs>
</options>
</dataCommand>
</dataCommands>
</dataElement>
The ByDataCommand trigger is an abstraction well suited for integration-testing workflow transitions. Its independence on time or other (uncontrollable) events makes it great for swift testing without working around stateful execution.
Schedule
Some actions may be bound to time. For example sending out messages at a specific time or requesting data from an external service at regular intervals. The Schedule trigger is added to achieve this using a cron-like schedule. The default values in the schedule result in "Every day at 00:00 local time".
field | default | example |
---|---|---|
second | 0 | */30 "every 30 seconds" |
minute | 0 | */5 "every 5 minutes" |
hour | 0 | 1,13 "twice a day at 1 'o clock" |
dayOfWeek | * | Tue,Fri "every tuesday and friday" |
dayOfMonth | * | 1 "first of every month" |
month | * | 1,2 "only in January and February |
year | * | 2025 "only in 2025" |
timezone | JVM timezone | Europe/Amsterdam |
FlowEngine
Primarily added to ease the migration from the 'workflow' component to process automation. Results in a single interval timer per workflow which fetches all elements which are in the beginState of any transition with the FlowEngine trigger.
The main difference is that this will only perform transitions of the current state, and not traverse the whole transition graph. If you need this behavior, configure the follow-up transitions with the OnTransition trigger.
There are various settings to tweak the behavior of FlowEngines, which are documented on the triggers page.
FlowEngines have a limited batch size unless configured otherwise. In case the begin- and endState are equal, you may
need to define a sorting such that each element is picked up eventually. Use the flowEngine.sortField
option to
configure this sorting.
Field
Let flowEngines on this element sort their batches by the field this option applies to. Sorting direction can be
specified through the value and defaults to asc
. Fields annotated with the
audit.modify.timestamp
option will automatically be added to the sortFields.
<options>
<flowEngine.sortField/>
</options>
<options>
<flowEngine.sortField>(asc|desc)</flowEngine.sortField>
</options>
Transition recovery
It is possible a transition cannot complete successfully. As a result a DataElement may be stuck in an interim state. To resolve this issue process automation attempts a recovery procedure at startup. The default behavior is to move to the failed state.
Recovery state
Alternatively, the recovery state can explicitly be configured on the Transition. Simply select a recovery state from the dropdown to configure it as the state to be used when recovering the transition.
- µRadiant
- Model Files
- In the tree-view, find and select the Transition (left-click).
- Select the
recoveryState
from the dropdown or leave it empty to use the default.
<workflow name="MyElementFlow">
<transitions>
<transition name="Make">
<!-- other fields omitted for brevity -->
<recoveryState component="mycomp" workflow="MyElementFlow" state="MakeFailed"/>
</transition>
</transitions>
</workflow>
Transactions
It is possible to use the begin state as the recovery state with one restriction. The executed task must be transactional. In order for recovery to the begin state to be valid, the task must not have performed any side effects. Having a transaction around a task at least ensures there were no database modifications as a result of a failed task.
You still need to make sure no other side effects were performed within the task. For example when making a request to an external api, a compensating request may be needed in order to cancel the initial request.