In BPMN, the most common way information is provided to a process from an external source is via a message. Incoming messages are indicated in the diagram by a dashed arrow connector called a message flow, with its tail on a black-box pool or a message node in another process pool and its arrow on a message node in the receiving process, either an activity or message event. In our BPMN Method and Style training, which focuses on non-executable processes, we discuss at length the semantics and usage patterns of messages and message events. But when we take the next step of making our BPMN model executable, we need to think about messages in a new way. This post explains how it works.
Below is a variant of an example from the training, illustrating the use of message boundary events.
The scenario is as follows:
- The Customer sends an Order message to the start event, creating a new instance of the process. Initiation by a request from an external entity is the most common way processes are started, i.e., a message flow to the process start event.
- At some time before completion of the process, the Customer could possibly send a Cancellation message, cancelling the order.
- The handling of this cancellation depends on the state of the process when this message is received. If payment has not yet been completed, all process activity is immediately terminated and the process ends in the end state Order cancelled. If payment has been completed but fulfillment has not, fulfillment is terminated, a refund of the payment is issued, and again the process ends in the state Order cancelled. If fulfillment completes without receiving a Cancellation message, the process ends in the state Order complete.
When we make a model like this one executable, we need to think about messages from a new angle. An executable process is a service, and incoming messages are really API calls from an external client. On the Trisotech platform, for example, when we click Cloud Publish in a BPMN model, the tool compiles and deploys each process in the model - there could be more than one - as a REST service accessible through the Service Library. If the only interaction with the Customer were the initial Order, we might not draw the Customer pool and message flows at all. Instead we would make Order a process data input, which is equivalent to the start message. But when we have subsequent interactions, as in this model, we should use the message flow representation, because it implies that Cancellation messages addressed to this process are meant to cancel only this particular instance - this particular order - not all instances of the process.
Maybe this has got you thinking...
- How do we address a message to a particular process?
- How do we address a message to a particular instance of the process?
- How does the instance know which message event receives the message?
The key difference between non-executable and executable BPMN is the latter defines process data in the form of data objects and defines mappings between process activities or events and these data objects. I have to tell you at the start that while all BPMN products generally follow the semantics defined in the spec, the implementation details of executable BPMN vary from product to product. What follows is how it works in the Trisotech Digital Enterprise Suite. I like this platform because it is designed for non-programmers, borrowing from DMN a business-friendly way of modeling and mapping data objects.
When we add the data objects to the diagram, it looks like this:
The diagram now looks cluttered and hard to read, but you can right-click and Hide the data objects, replacing them with three dots at the bottom of each activity mapped to or from the data object. The data objects with black arrows are data outputs, meaning their values are returned upon execution. In this case we simply want to capture in the output the Status of the order - "Received", "Completed", or "Cancelled" - and timestamps indicating precisely when the order was Received, Completed, or Cancelled, along with any Refund info. The reason we need to break these out as separate data outputs as opposed to components of a single structured output is that a data output association - that dotted arrow connector from a task or event to the data object - rewrites the entire data object; it does not simply update selected components.
So let's see how to make this executable. We start with the datatypes, defined on the BPMN ribbon. The Order message is type tOrder, a structure that looks like this:
We assign the message flow Order to this datatype, and this associates the message flow with a message named Order, type tOrder. We do something similar to the message flows named Cancellation, using the type tCancellation.
Later we'll see how the OrderID component of Cancellation is used to select a particular instance of the Order process.
We also assign the type tOrder to the message start event Receive order. With an incoming message, we always need to save the message payload to one or more data objects, here represented in the diagram as data associations to Order info (type tOrder) and Received, which captures the timestamp of receival. The data mapping from the start event is shown below. Received is populated with the current timestamp, using the Trisotech FEEL extension function now(), and Status is populated with the value "Received". Order info passes order details to the other activities in the process.
Following the scenario described previously, we collect payment, fulfill the order, and complete. If a Cancellation message for this order is received while in the activity Collect payment, we set the Status to "Cancelled" and end the process in the state Order cancelled. If the Cancellation is received while in the activity Fulfill order, we again set the Status to "Cancelled", calculate the amount to refund, and end in Order Cancelled. If Fulfill order completes with no Cancellation, we set the Status to "Completed" end in Order complete.
We model the Cancellation message flows in a similar way: Assign them and their catching boundary events to the type tCancellation and map the message content to Cancel info, Status, and Cancelled as we did with Order. For this example we ignore the details of the tasks Process payment and Fulfill order, except that completion of Fulfill order sets the Status and Completed timestamp, as shown by the output data mapping below.
Process Refund calculates the refunded amount. Here we are using simply Order info.Amount, but we made it a decision task to allow for more complex logic, such as a change in the price between time of ordering and cancellation. The data output Refund info reports the refunded amount and a timestamp.
Now we also need to provide a way to identify this particular instance of the order process. The primary method used by Trisotech is called instance tagging. At the process level you define one or more strings that uniquely identify the instance. These must be strings not structured data, so if you want to associate a value, such as the Order ID, with a label, you need to concatenate the label and the value. In the BPMN diagram, right-click Attributes/Process Instance Tags and define one or more label-value strings that identify a single process instance or possibly a small collection of instances:
Since each Order has a unique ID, this tag always identifies a single instance.
We want to tell the Receive cancellation events to accept only Cancellation messages for the current instance. Right-click the boundary event and select Message correlation. As you see below, Trisotech uses a three-stage filter for correlation, although typically one or more of the stages is not used.
The first stage, a message content filter, is a boolean expression of message data that makes no reference to the process instance. The second stage, instance tag matching, is the most common, and what we use in our example. It is a FEEL expression of type Text referencing attributes of the incoming message (here, Cancellation). Only strings matching a process instance tag are received, i.e., Cancellation messages in which the component OrderID matches Order info.ID. Because the instance tags are indexed, the runtime can do this correlation matching very quickly.
The third stage is a boolean expression of process instance data. It is very flexible but unless you have cut down the list of available messages using the first two filters, it could be slow. It should be used only in special cases.
So let's now test our executable process. To do that, on the Execution ribbon click Cloud Publish, which compiles the logic and deploys it as a REST service accessible through the Service Library. There you see the various endpoints created for this service:
This answers the first question posed at the start of this post: How do we address a message to a particular process? It's via the endpoint URL. In the modeling tool, my process has the name message flows5 executable, and this is version 1.0 of that process. So a POST to [base]/bpmn/api/run/test/bpmn/message-flows5-executable/1.0 is equivalent to sending the message Order to the start event. You can expand that box to see the required input data and the data returned when the process completes for various response codes:
As you can see, these are the json equivalents of the FEEL message definitions. Also note that POSTing an API call to [base]/trigger/test/bpmn/message-flows5-executable/1.0/default/Cancellation is equivalent to sending the Cancellation message to the boundary events. Here the name of both the process and the message event are part of the endpoint URL. Correlation to the particular instance of the process - this specific order - is achieved via the Process Instance Tag.
We can test execution of this process from the Service Library. The TryIt button exposes a form where you can input Order data.
Clicking Run advances the process to Process payment.
Here the Status is "Received" and the data output Received contains the timestamp of receival. The Continue service section lets you select either Process payment, meaning run this activity, or Receive cancellation, the boundary event. If we click Process payment, we see the data mapped to this task, and we can define any output data values. Clicking Run again advances to Fulfill order. Again the Continue service section lets us either run the task or receive the Cancellation message. This time let's click Receive cancellation. We are prompted to define the Cancellation message:
Clicking Run now should exit on the exception flow, run the decision task, and end with Status "Cancelled", end state "Order cancelled", with a Cancellation timestamp and Refund info populated. You can see from the data outputs that it does exactly that.
The answer to the third question posed at the top - How does the instance know which event catches the message? - is simply based on timing. It can be received only by an active node at runtime.
Let's recap. As you can see, executable BPMN is a bit more involved than normal non-executable Method and Style. Modeling real-world processes almost always involves incoming messages, which in executable BPMN are API calls to the deployed BPMN service. We model message data in the normal way, via FEEL datatypes, and use data mapping to save that data to data objects used in the process. At runtime, messages are routed to the right process and message event via the endpoint URL, and correlated to a particular instance via Process Instance Tags. We can test and debug the logic, including waiting for message events, via the Service Library. And best of all, none of this requires programming.
If this interests you - and it should! - you will need a Trisotech account with the Automation features enabled. Contact Trisotech for that.