Skip to main content

Part 2: Make the Approval Process Multi-instance

Outdated documentation page

This is an old version of the documentation for Flowable until version 3.13 and for the Angular-based Flowable Design 3.14/3.15. If you are running the latest version of Flowable please check out the current version of this page.

Chapter 8: Create DMN Based Multi-instance Approval Process

In this second part, we are going to extend the approval process with a multi-instance activity based on the number of approvers we need. That number is calculated using a DMN decision table according to the specified travel data.

Add a New Main Approval Process

We currently have a straightforward, fixed approval process, and we are going to make it a bit more sophisticated. To get started, go back to the case model and select the Travel request approval process task in the Approval travel stage. Select the Details>Process reference attribute (but not the referenced process link). Select "New" as we cannot re-use the existing one later on as a sub-process. So we create a new sub-process where we add the DMN based decision and multi-instance path.

Set the new name to Travel Request Main Approval Process, hit Tab and then Enter to create the new main approval process.

As before, delete the predefined Start event and drag a new Swimlanes>Pool onto the process diagram and name it Main Approval Process. Then name the existing lane to Travel Requestor and add a new lane below (Swimlanes>Lane) named System.

Add a Start events>Start event within the System lane and a new Activities>Decision task, name it Evaluate number of approvals needed. Next, connect Start event with the Decision task by dragging the small sequence flow icon (arrows) from the Start event onto the decision task.

Use the exclusive gateway icon after selecting the Decision task to add a gateway and then the User task icon to add a new User task after the gateway and name it Select approvers.

After the User task, add a gateway again by using that small icon, then in the same fashion an end event.

Go back to the first gateway, use the sequence flow icon again and drag it to the second gateway to add an alternative path, then use the centric handle of that sequence flow to drag it below the user task.

The process model now looks like this:

39 multi instance prc 1

Define a DMN Decision Table for the Number of Approval Evaluation

In the next step, we want to define a DMN based decision table to evaluate if and how many approvals we need for the travel request.

Select the Decision task and click the Details>Decision table reference property, enter Travel Request Approval Decision Table, hit Tab and then Enter to create a new decision table.

A DMN based decision table is defined using some input values and one or even more output values. By default, the First hit policy is selected which means, the engine evaluates all the conditions on the input values defined and stops at the first row where all conditions are met and returns the output value(s) specified on that row.

Click the New Input blue header to define our first input parameter and modify it like this:

40 dmn input1

We can map process variable values to our DMN input columns on which to base our decisions.

For the next column, click the small, blue plus icon to add another input column and define it like this:

41 dmn input2

As you can see, we can use our previously defined transportation options, so it is easier to use those values within our table later on.

Let us add a third column with the following values:

42 dmn input3

Finally, we want to define our outcome of the decision table, which is the number of approvals needed for the travel request according to the input values we have. For this click on the grey New Output header to define our output value:

43A dmn output

Now we have defined our input and output values. The next step is to add some rules for the decision table, playing with the input values:

43B values

It is a good practice for a first-hit policy table to add the last row without any conditions on the input values to ensure the table always produces an outcome.

In our example, we defined some rules (rows) according to the estimated costs and transportation; we did not take the accommodation into account. Of course, you can adapt the rules to your needs.

Make sure you save the decision table and then return to our process.

Now we want to make use of our outcome, so select the first gateway and name it Approvals needed? and place the label on top of the gateway.

Making more space

If you need more space between existing elements, select the Make space tool in the toolbar and drag the elements away from each other to make more space or towards each other to reduce the space between elements. Manipulating the amount of space works both horizontally and vertically (see the selected icon in the image below, it must be activated to make use of the spacing tool):

44 make space

Now select the first outgoing flow leading to the User task, name it yes, set the Details>Condition type to Conditional flow and enter ${approvalsNeeded > 0} as the Details>Condition expression. The engine evaluates that condition and if true (in our case, if the decision tables outcome was more than 0), it follows that path.

Now select the other outgoing flow, name it no and check the Details>Default flow checkbox. It is a good practice to always have one outgoing flow set as the default. As an alternative, we also could make it a conditional flow and set its condition to ${approvalsNeeded == 0}.

We also want to move the Select approvers user task to the Travel Requestor lane. You can drag and drop it from one lane to the other. You can then also rearrange the sequence flows and labels to get the process looking nice:

45 main approver process

Modify the Task Form to Select Multiple Approvers

As there can be more than one approver, we need to modify the approver selection task form. For this first select the Select approvers user task in our process, then the Details>Form reference property and choose "Reference", as we already have such a task form available, we need to modify it.

Select the Select Approver Task Form and then click on the form link to open it.

Double click the first text element and set its content to something similar to this:

46 select approver content

Please select {{approvalsNeeded}} person(s) to review and approve or decline your travel request.

This might be your superior or propject manager, depending on the reason for the travel.

We use the number of approvals needed from the output of our decision table as an expression.

In our old selection task form, we just had one selection element where we selected one person as the approver. Now we might need more than one. The multi-entry subform now comes in handy for this.

So let us add a Container>Subform at the bottom of the form and name it Approvers. Check the Details>Multiple elements attribute, and two new properties show up to enter the number of minimum and maximum elements for the subform. We have that information as an output of our decision table, so use that information by clicking the small "lightning" icon on the right and then enter {{approvalsNeeded}} for both the Validation>Minimum elements and Validation>Maximum elements. As we want the user to enter exactly that amount of elements in the subform. We do not need to add or remove elements, so uncheck the Details>Show remove button and Details>Show add button.

We want to store the selected approvers and their approving result directly on the case itself to be able to show the current state, even if the process is not finished, so set the General>Store subform data in single variable expression to {{root.approvers}} to save the subform data directly on the case, rather than the process (which is the default). The properties of the subform should look now like this:

47 subform props

Next, we need an actual subform, so select the Details>Form reference property, enter Select Travel Request Approver Subform as its name, hit Tab and then Enter to create a new subform. This new subform is used within that list of elements of our root subform component. Each element, depending on the number of approvers we need, renders that exact form.

Go back to the Select Approver Task Form where we added the subform widget and select the Select approver component, then either hit command-X to delete it (or use the scissor icon in the toolbar) as we want it moved to the subform.

Go to the subform by clicking the Details>Form reference property link and paste the element from the clipboard onto the empty form.

The Select Approver Task Form now looks something like this:

48 select approvers task form

and the Select Travel Request Approver Subform like this:

49A approver subform

The final step is to define the input variables needed for our process. Go back to the case model, select the Travel request approval process, and click the Details>In property to open the in variable mapping for the process, and enter this data there:

49B input variables

Now save everything (the best way is with the save-all toolbar icon), and we are ready for another test spin, at least to test the decision table output and the approver selection. Of course, we need to adapt the case again later, however, let us deploy and test in between nonetheless.

Deploy and Run the Travel Request Again

Publish the app again, switch to the runtime, and start a new travel request case. Fill out the details and choose something larger than 1000 to make sure we need two approvers to be selected. Then file the request, and you see a task named Select approvers. Select it, and there should be two selections required for approvers:

50 select approvers task

We want to see what the decision table outcome was as there is a debug-view available at runtime. Select the Travel Request Main Approval Process in the header of the task to navigate to the process. Next, select the "History" tab, and you can see the process diagram with the Select approvers as the current state. Now navigate to the sub-tab "Decisions" and the evaluated decision table is visible with the data we entered in the travel request:

51 decision table evaluation

During modeling, this is very helpful, especially if the table does not produce the expected output. In this view, you can see how the rules are evaluated and which one was a hit or matched.

Chapter 9: Add a Multi-instance Call Activity for Approvals

In this chapter, we want to use the collection of selected approvers to repeat the approval sub-process for each of the selected approvers.

Add a Multi-instance Call Activity

Go back to our Travel Request Main Approval Process and make sure there is some space between the User task and the gateway. See the discussion in the last chapter for a review of the process of adding space in the diagram.

Add a Structural>Call activity by dragging and dropping it directly onto the sequence flow between the User task and the second (joining) gateway and name it Approval subprocesses. Select the Details>Process reference property and use the tab "Reference" and select the Travel Request Approval Process (the one from the first part we previously created).

Of course, we need to adapt that sub-process, but let us first set up the call activity to be multi-instance.

For this, go to the property Multi instance>Multi instance type and set it to Parallel, which means, we start a new sub-process instance in parallel for each of the selected approvers. We could also choose Sequential, which would start them one after the other.

The Multi instance>Element index variable is predefined as loopCounter, which is fine and allows us to use it as the index counting for each of the sub-process instances (starting with 0). We need to define the collection to loop for, so we set the property Multi instance>Collection to ${root.approvers}. That variable was previously created through our multi-entry subform and contains a list (collection) of the selected approver user ids. More data is added for each element through that sub-process later on.

Let us also specify an element variable which we can then use within the sub-process to access our specific element. So set Multi instance>Element variable to approver for that. Next, add some values we need within the sub-process as in-variables. Select the Details>In property and set the following values:

52 sub process in mappings

Now let us adapt the approval sub-process to be ready for multi-instance approvals.

Select that process by clicking on the Travel Request Approval Process link on the Details>Process reference property of the call activity.

First, we remove (delete) the first user task (Select approver for travel request) where we previously selected the approver as this is now done in the main process. We also move the Start event into the second lane of the Travel approver and later remove the travel requestor lane entirely, as we do not need it anymore. If we add a feedback or adjusting loop between the approver and requestor at some point, then it would make sense again to have that lane.

We also change the expression used for the assignee of the approval user task which is just ${approverId} in our previous version, but now needs the value of our local loop element variable.

Select the Approve travel request user task, go to the Assignment>Assignee property and set it to ${approver.approverId}. This uses the approver id variable of the local element data we have on the sub-process scope.

Adapt Approver Task Form

Once done, let us adapt the approver task form, so select the user task, then click on the Approve Travel Request Task Form link to open that task form.

We need to change the binding of the review comments as it is added to each approver within our collection.

So select the Review comments input field and set its General>Value to {{root.approvers[loopCounter].reviewComments}}. Remember: we have our approver collection on the case level; hence, we need root to resolve it. Then we set that review comment at the index of our multi-instance loop counter. That is why we use the loopCounter as the index within the collection. Lastly, we use the dot-notation and add a new field named reviewComments to that collection. Now each comment is found at the same index as the approver user id.

In the first version, we used the outcomes to approve or decline. Let us change this also to see alternative outcomes. First, remove the outcomes by clicking the Details>Outcomes and then remove the two outcomes that are currently there.

Now drag and drop a Selection>Radio button component between the text display and the comment field and name it Review decision. Of course, we want that review decision stored in our approvers' collection as well, so set the General>Value of the radio buttons to {{root.approvers[loopCounter].reviewDecision}}. Now make the component required by selecting Validation>Required and set its Details>Orientation property to Horizontal.

Click the Data source>Items property and add the following values to it:

53 review decision options

We want the required option of the review comment to depend on the decision, so click the lightning icon of the Validation>Required property and set its expression to {{root.approvers[loopCounter].reviewDecision == 'declined'}}. This expression means that whenever we select the declined option, the comment field is mandatory; otherwise, it is optional.

Save the form and go back to the sub-process as we need to deal with the decision of the task (approve or decline).

Collect the Approval Result from Each Approver

Each approval user task produces a decision according to the approver’s selection whether to approve or decline the travel request, which is stored back at the same index within the approvers' collection. We want to add it to the parent process level and combining it with all the other approval results. To do that, add an Flowable Work Activities>Initialize variables service task after the Approve travel request user task. Label the new task, Set approval result and make sure Details>Overwrite if existing is checked and add one variable initialization to the Details>Init variables property:

${parent.approval && flwJsonUtils.getAtIndex(root.approvers, loopCounter).reviewDecision == 'approved'}

54 set approval variable

We use parent as the target because we want to set that approval variable on our parent main process instance. As the variable value, we use an expression, where we use the currently set decision value and combine it with the outcome of the local selection.

The Travel Request Approval Process now looks something like this:

54A travel request approval process

Now the first part of the expression, parent.approval is a variable that is not initialized the first time, so we need to make sure it exists and is set to true, otherwise, we would never be able to approve the case. So go back to the main travel request case model and select the Travel request approval process task and open the Details>In variable mapping and add initialization of our approval variable on the main process instance level:

55 initialize approval var

With the expression ${true}, we create a Boolean true value to initialize the approval variable.

Now for the Details>Out mappings (what comes back from the process and is saved on case level), we remove all the previous mappings and only add approval to the list:

56 approval out mapping

This means we store the final approval variable value from our main process back to the case, once all approval sub-processes are finished. We then have the final approval flag (true or false) back on the case and all the detailed results in our approvers collection.

As our result now is a Boolean value, we need to adapt the entry sentry expression (condition) on the entry criterion of our last stage. So select the entry criterion and set the Details>Condition to ${vars:getOrDefault('approval',false)} to create a Boolean variable value (from a string based in our first version of the approval process).

Improve the Case Overview Form

As we no longer have a single approver, let us modify the case overview (work) form by adding a multi-entry subform with an overview of the approvals.

Go back to the case model and open the work form (Travel Request Work Form). Delete the two existing text display components at the top of the form representing the approval review.

Add a Container>Subform component at the top of the form and name it Approval overview. Then set the value binding of General>Store subform data in single variable to {{approvers}}. Also check the property Details:>Multiple elements as we want to have more than one subform rendered, as the variable approvers is representing a collection, not a single value.

Turn off the Details>Show add button and the Details>Show remove button by unchecking each attribute. We also need the form to be read-only, so uncheck the General>Enabled property as well. Furthermore, we only want to show the form, if the approvers are initialized, so use the lightning icon on the General>Ignored property and set the expression to {{!(approvers[0].approverId)}}. The approvers[0].approverId part is only true, once we have at least one approver set in the collection of approvers.

Now select the Details>Form reference property, set the name for the new subform to Travel Request Approval Overview Subform, hit Tab and then Enter to create a new subform for our overview list.

We want to put the selected approver, the decision, and comments in the form. The easiest way is to go back to the approver selection form we did for the Select approvers task or just by opening the Select Travel Request Approver Subform. Select and copy that Select approver widget and go back to our newly created subform and paste it to the form. Remove its label and make it not required as we do not need it here.

Add a new Data entry>Multiline text component, and do not set a label as it is not needed, but set its General>Value property to {{reviewComments}}.

As the goal is to either display a text saying the review is not yet done or as an alternative, show the review result, add a Container>Panel component on the same row, in between the approver selection widget and the comment widget. Make the selection widget use 3 columns in width, the panel use 4 columns, and the comment use 5 columns.

Now drop a Display>Text display widget onto the panel and set the content to Request was {{reviewDecision}}.. You can even mark the expression and make it bold for highlighting the decision result. The widget is shown once the decision is made, so set the General>Ignored property to {{!reviewDecision}}.

Now drop another Display>Text display in the panel below the existing one and set its content to The request was not yet reviewed and decided.. This widget is visible if the review is not yet completed, so set the General>Ignored property to {{reviewDecision}}.

That subform now looks similar to this:

57A approval overview subform

Save the subform and go back to its parent form. Drag and drop a Display>Text display below the subform and set its content to something like:

The travel request review and approval was not yet started.
So there is no approval result available yet.

Set the General>Ignored property to {{approvers[0].approverId}} so it is only shown if there is no approver set.

Now save everything, and we should be ready for another test.

Deploy and Run the Travel Request Again

Publish the app again, switch to the runtime view, and start a new travel request case as before. Make sure the estimated costs are greater than 1000, so the outcome requires two approvers and then file the case for approval.

After selecting the two approvers and completing that task, you should now see two approval tasks in the list of open tasks of our case:

57B travel request

They are assigned to the approvers you selected in the previous task and are created in parallel.

Of course, you cannot complete those tasks as the travel requestor, so log in with each of the selected approvers, go to their task inbox, select the approval task and make a decision (works like before). When both users approve the task, then the travel request is approved. If either one declines the request, then the case is declined.

If you approved with both approvers, you can go back to the case and see the Organize travel task like before.