BPMN has a way to say an activity should be performed more than once. In fact, it has multiple ways, and students in my BPMN Method and Style training sometimes get them confused. This post will clear things up.
A Loop activity is like a Do-While loop in programming. It means perform the activity once - it could be either a task or subprocess - and then test the loop exit condition, a Boolean expression of process data. If the condition is false, perform the activity again and evaluate the condition once more. If the condition is true, exit the activity on the normal outgoing sequence flow. Repeat until the exit condition is satisfied. It's quite handy for activities that might require multiple tries to complete successfully. It is possible to set a maximum number of allowed iterations.
The semantics of a Loop activity are exactly the same as a non-Loop activity followed by an XOR gateway that either loops back to the activity or continues on. A Loop activity is indicated in the diagram by a circular arrow at the bottom center. This takes up less space in the diagram than adding the gateway and loopback, but it hides the loop exit condition. For that reason, Method and Style asks the modeler to insert a text annotation on any Loop activity, labeled "Loop until [condition]".
A Multi-instance (MI) activity is like a For-Each loop in programming. It means perform the activity once - again, either a task or subprocess - for each item in a list, a process data element that contains multiple items. Processing each list item is a separate instance of the activity, and the MI activity as a whole is complete only when all the item instances are complete.
A Multi-instance activity is indicated by three parallel bars at bottom center. If the bars are vertical, it means the instances are performed at the same time, in parallel. If the bars are horizontal, which you often see with human activities, it means the instances are performed sequentially. With an MI activity, Method and Style asks the modeler to insert a text annotation, "For each [item]".
Many beginners are confused by the difference between Loop and MI-sequential activities. They are not the same!
The key points about Loop activities are:
- The iterations are strictly sequential. Iteration 2 does not begin until iteration 1 has completed.
- You don't know how many iterations are required when you begin. The activity simply repeats until its loop exit condition is satisfied, or the maximum number of iterations is reached.
- The loop condition is the same expression with each iteration, but each iteration can change data values used in the loop exit condition. Without that, the loop exit condition could never go from false to true!
- As with Loop, the iterations are strictly sequential. Iteration 2 does not begin until iteration 1 has completed.
- You do know how many iterations are required when you begin. It's the number of items in the list!
- Each iteration is an independent instance, so the data values of one iteration generally do not affect the logic of other instances, although it is possible they could interact via a shared data store.
When the order is received, the process first checks whether the items are in stock. Because an order may contain multiple items, Check stock must be done independently for each order item. In the diagram we indicate this by the text annotation. Let's say an item is either in stock or out of stock, ignoring the case where the available stock only partially satisfies the order. Since this is processing a list, it's MI - not Loop! The number of iterations is the number of items in the order.
Normally, in Method and Style, when an activity has two possible outcomes - in stock or out of stock - we follow it with a gateway with two gates, in stock and out of stock. But remember, with an MI activity each instance has those two outcomes, so the MI activity as a whole has more than two possible end states. In practice, it is common to follow such an MI activity with a gateway that tests whether any item is in stock, or possibly if any item is out of stock.
Here we will continue the process if some order items are in stock. Otherwise we end the process in the end state Out of stock.
Now we Collect payment, which uses the customer's credit card on file. That could fail. Maybe the card is expired, in which case the customer is advised to enter a valid credit card number. Collect payment is thus a Loop activity. You don't know, when you start, how many iterations are required. You keep trying it until either it succeeds or you exhaust the allowed number of attempts. Again the loop exit condition and maximum iterations are indicated by a text annotation. There is only one instance of this activity. In the end it either succeeds or fails, so we follow it with a gateway with those two gates.
Here are some common mistakes beginners make with repeating activities:
- Loop within a loop. A Loop activity is the same as a non-Loop activity followed by a gateway that tests the exit condition and possibly loops back to the activity. But if you follow a Loop activity by such a gateway, that's a loop within a loop... usually not what was intended. A second case of loop within a loop is a subprocess that contains a Loop marker on the collapsed shape in the parent level and a gateway loopback in the child level... again usually not what was intended.
- MI within an MI. Similar to the second case of loop within a loop, an MI subprocess has activities in the child level also marked MI. Again, this is not usually what was intended.
- Error boundary event on MI subprocess. In a Multi-instance subprocess, there are N separate instances of the child level flow. If any one of them throws an error, this terminates all N instances. That may be what you intended, but if you just want to handle the exception for that one instance, you need to do that within the child level, not throw it to the parent level subprocess boundary.
The input is an Order, type tOrder shown below. It is a collection of Order items, each defined by a SKU, Quantity, and UnitPrice.
In a real process, each instance of Check stock would be a database lookup, but to keep it simple here we just use a script task against the process data input Stock table, a collection of items defined by SKU and QuantityAvailable. The data output In stock items is a list of Order items that are in stock.
We first need to map the data inputs to variables in the script task context using its Data Mapping attribute.
A Multi-instance activity always iterates over a collection, so we need to select it from those supplied by input data associations, here Order. We need to assign a name to the range variable or iterator, meaning one item in the collection. It can be anything we want, so here we name it OrderItem. In the Mapping section we define the variables used in the script task logic and their mappings from the input data associations and the iterator. So variable Stock table is just the input data association of that name, and we define a new variable CurrentItem mapped from the iterator OrderItem. The mapping expression of the iterated variable must be the iterator.
The script task logic is the FEEL expression
if Stock table[SKU = CurrentItem.SKU].QuantityAvailable[1] >= CurrentItem.Quantity then CurrentItem else nullFor those unfamiliar with FEEL, here is how it works. Remember we are executing the script independently over each OrderItem, and collecting the results of all the instances.
Stock table[SKU=CurrentItem.SKU].QuantityAvailableselects the row of Stock table where the SKU matches the CurrentItem.SKU, and finds the QuantityAvailable for that row. Since SKU is a primary key, the filter selects a single row, but technically a FEEL filter always returns a list, so we need to append [1] to extract the item. Now the script says, if that QuantityAvailable is greater than or equal to the corresponding OrderItem quantity, return the CurrentItem, otherwise return null. We run this script for each OrderItem.
Now we define the data output mapping.
A script task always has a single output. We need to assign it to a variable, here In stock item. For Resulting collection, we select one of the output data associations, here the process data output In stock items. Collected variable says that each item in that collection is assigned the script task output In stock item. Thus the data output In stock items is a collection containing the original Order item if in stock or null if out of stock.
We use In stock items in the logic of the gateway Any in stock? The yes gate condition is defined by the FEEL Boolean expression:
nn count(In stock items)>0,where nn count(list) is a Trisotech extension function equivalent to count(list[item!=null]).
When we test the logic in Service Library TryIt, we get the expected result. Order item A001 is in stock and Order item A003 is out of stock. Thus In stock items has two items, one of them null, and the process end state is "Some in stock".
To recap, Loop and MI activities are frequently used in BPMN and they are not the same thing. Use Loop when you need to retry an activity until it succeeds. Use MI - either sequential or parallel - when you need to perform an action for each item in a list.