Brief Task Engine Overview
In order to use Joins and Junctions effectively, having some fundamental concepts about how the Task Engine works will be extremely valuable. So, let's go over a few points. If you're already familiar with the Task Engine's behavior, feel free to skip to the next section.
- The Task Engine processes a flowchart like structure called a Task Tree.
- A Task Tree represents a Business Process.
- Task Trees consist of Nodes and Connectors.
- Nodes are bits of code that process data.
- Connectors are logic that control the flow of Nodes.
- Connectors can have Qualifications that selectively control the path of processing.
When you lay out Nodes and Connectors and create a Task Tree in the Task Builder, this is considered design time. When a Request triggers processing of a Task Tree, this is considered run time.
When the Task Engine processes a Task Tree at run time, it follows the path of the Connectors. When a Node is encountered along a path, the Task Engine creates a Task Instance that corresponds to that Node and executes the code contained in the Node.
When the Node completes processing, the Task Instance is set to Complete and the Task Engine continues following the Connector Path to the next Node on the Branch. If a Node is Deferred, meaning it does not execute right away, that Task Instance waits for a Task Trigger to be created before being set to Complete.
Join and Junction Overview
With this in mind, let's discuss what Joins and Junctions are. Much like in a flowchart, Task Trees can represent very simple or very complex paths of logic for a process. Joins and Junctions serve the purpose of combining multiple paths into a single path. For example, making sure all parts of a process are completed before Closing a Request, or ensuring all Network Accounts, Equipment and Software is available before issuing a request to support personnel to place a computer for a new hire.
Joins and Junctions are most useful in cases where your Task Tree is more complex, involves parallel branches of processing, has branches of processing that are selectively executed or can be bypassed, or requires prerequisite branches be complete before continuing processing.
I will demonstrate this by starting with a simple example case (Computer Setup) and adding complexity to the Task Tree to illustrate the types of situations where Joins and Junctions become useful.
Let's start with a case where using a Join or Junction is not necessary.
In the below process, we have a very linear flow - 1 Request, 1 Approval, and then either alerting the submitter of a denied Approval or purchasing the equipment.
With a design like this, neither a Join or Junction is necessary. Regardless of whether the Approval is Denied or Approved each Branch will lead to the Close Request node and the Close Request node only needs to be executed once. So, even though we have a split in processing, both Branches terminate in the same place and we have no need to check that any particular Branch is complete.
Now, let's add a situation where a Join would come in handy. Joins have the ability to determine how many of the connecting nodes have completed before continuing.
The options Any, All and Some determine the conditions for the Join Node to continue processing.
- All - Every Node attached to the Join must complete..
- Any - At least 1 Node attached to the Join must complete.
- Some - A configurable number of Nodes attached to the Join must complete
So, now that we know a little more about what options a Join provides, here is an example Task Tree showing the use of a Join Node with the "All" option set. We've added steps to our Task Tree to Purchase Software and Install Software. Obviously, in order to install the software, both the software and hardware have to be available. So, we want to wait until both Software and Hardware purchasing are done before issuing an Install Software task.
A couple of items to note on this design. First of all, note the Approval Process node has been added. This is a No Operation node and does nothing except make the Task Tree more readable. On an Approved Request, the Approval Process node is called by the Task Engine, automatically completes, and continues processing any Nodes connected to it. This makes it so we only have to manage the qualification on the "Approved" Connector in one place. We could run two Connectors with the same qualification, but that increases maintenance if something changes. Remember: as complexity increases, small changes can accumulate into significantly more work to maintain.
You might think of this technique as a reverse Join. Where a Join consolidates multiple Branches into a single Branch of processing, a No Op can be used to take a single Branch and expand processing into multiple Branches.
Secondly, it would be possible to bypass the need for a Join completely by making the Purchasing steps linear, like this:
While this would be a viable design, it is not particularly efficient. Both Purchasing Tasks can occur in parallel, meaning they can execute simultaneously, so there's no need to wait for one to complete before beginning the other. The Tasks are not dependant on each other. Being able to have Tree Branches execute in parallel is a powerful feature of the Task Engine and can provide great flexibility and efficiency in how you arrange your processes.
So, back to our example Task Tree.
The Request is sent, Approvals made, and Purchasing tasks are created. At this point in the Task Tree, we want to wait for everything above the "All Join - Purchasing Complete" node to finish before continuing. Once both of the Purchasing Tasks are completed, we issue the Install Software request, and when that is complete, we can close the Request.
At this point you may wonder where and when you would need to use an Any or Some Join.
Using Any and Some Joins is largely a function of business rules. A typical scenario is that of Approvals where multiple Approvals are created for a particular Request. Your process may require that All, Any or Some of the Approvals sent are Approved before continuing a process. For our example Task Tree, we'll assume that at least 1 of the 3 Approvals created is Approved.
Once any of the 3 Approvals are Approved, the Join executes and passes Tree processing to the "No Op - Approved Process" Node.
If we needed at least 2 Approvals, a Some Join can be used. When you select a Some Join, you have an additional parameter called Number made available.
The Number parameter can be a static value, meaning it's entered directly into the Number parameter, or it can be a reference to an Answer value, a Base Value, a Service Item Attribute, or a Task Result. When you use a Some Join, after the specified number of Nodes leading to the Join are completed, the Join executes and continues processing down the Branch.
A caveat to using the Any and Some Joins - after the first attached Node to an Any Join or the requisite number of nodes of a Some Join complete, subsequent Nodes completing do not further any Task Tree processing. Meaning, the first Approval leading to the Any Join or after the 2nd Approval leading to our example Some Join completes, the Join executes and continues. Any subsequent Approvals do not process the Join - it's job has already been done.
As spiffy as Joins seem to be, you might ask why do we need Junctions?
The need for Junctions comes from a limitation to the way Joins manage the Branches that lead to them. All of the Branches leading to a Join must be created at run time. A relatively common scenario is where a Task Tree has some optional Branches to process that may or may not be created at run time. We'll discuss this more shortly, as well as expand our example to show situations where a Junction is necessary.
So, before continuing on, let's review briefly.
- Joins consolidate Task Tree Branches into a single Branch of processing.
- Joins are most useful when Task Trees become more complex, involve parallel Branch processes, or there is a need to determine if Any, All or Some of the preceeding Branches are complete before proceeding.
- Any and Some Joins execute just once when their conditions are met and subsequent Node completions leading to the Join do not execute any further processing..
- Some Joins provide a Number parameter that determines how many of the attached Nodes must complete before continuing processing.
- As a rule of thumb, if you have Branches of processing that can execute in parallel, Joins and Junctions are very likely to be involved.
Junctions exist to coalesce Task Tree Branches that may or may not be active.
That statement may not make sense at the moment, but consider this scenario: you have a Computer Request where a customer can request either a Laptop or a Desktop, but not both. The Task Tree for such a Request may look like this:
Similar to our first linear Approval example, at least one Branch will execute the Install Software Node - it's an either/or decision point and very linear. There is a binary division in the logic - if one side processes the other side does not process - but both sides go to the same place and will never execute simultaneously. This makes the logic fairly simple as nothing is occurring in parallel.
If we want to ensure that we continue regardless of which option is selected, but not process Install Software until we're sure the Setup is complete, we may be inclined to use a Join here like this:
But there's a catch! In order for Joins to provide the ability to qualify execution using Some, All or Any, they have to know how many Nodes point to them. When the Task Tree is designed, Join Nodes keep track of this number. Then, when the Task Tree is executed and the Branch logic traverses the Tree, Task Instances are created for each Node as they get activated.
Some insider techie stuff...
If you look under the hood at a Join Task Instance, you'll find that the Join handler returns quite a bit of information. A typical Result from a Join Node looks like this:
<results> <result name="complete" type="FalseClass">false</result> <result name="mode">2</result> <result name="size" type="Fixnum">3</result> <result name="threshold" type="Fixnum">3</result> <result name="type">All</result> </results>
Of special note are the "size", "threshold" and "complete" values. Size refers to the number of Nodes the Join expects. Threshold is the number of Nodes that must complete before the Join executes. Complete is a boolean value that displays whether all of the conditions for the Join have been met or not. When the Join Node is created, these values are set and as Nodes reach the Join upon completion, the Join keeps an internal count. When the count equals the threshold, the Join considers itself complete, and continues processing.
Now, let's compare that to the Result from a Junction.
<results> <result name="complete" type="FalseClass">false</result> <result name="mode">2</result> <result name="size" type="Fixnum">3</result> </results>
Junctions operate logically as an "All" operation. However, it's interesting to note that Junctions do not have a threshold Result. Why? Because Junctions don't depend on an exact number of Nodes to be completed. Rather, they depend on the number of Nodes that are created that complete. The first time the Task Engine "touches" a Junction and the Junction Task Instance is created, the Task Engine determines what Nodes connect to the Junction and then traces the Task Tree to see what Nodes will be viable. If the Task Tree bypasses a particular Node that's connected to the Junction, the Junction ignores that Node and continues.
And now back to our example...
In the above example, let's say that the customer only selected Laptop. The Join Node knows to expect 2 Task Instances to complete before continuing. However, only 1 Task Instance will ever be created!
This means that not only does the Desktop Branch not execute, the Task Instance attached to it is never created. As far as the Join node is concerned it expects 2 branches to be processed. The "Join - PC Setup Complete" Node will be perpetually waiting for the Setup Desktop Task Instance to be created and completed before continuing. Consequently, the Install Software Node will never execute.
So here's the important concept that bears repeating - Joins are aware at design time how many Nodes point to them and expect an equal number of Task Instances to be created and active at run time. If a Branch is bypassed, the Task Engine does not create Task Instances for those Nodes so the Join never continues.
You may think you can work around this using an Any Join, and this would work. As we learned earlier, it only takes 1 Node to complete to execute an Any Join.
But what if a customer can request either a Laptop or a Desktop or both? Suddenly we're faced with 3 possibilities and it's no longer an either/or situation. An Any Join won't work. We have to be able to accommodate cases where it's either one or both options.
And this is where Junctions come to the rescue!
Unlike a Join, Junctions do not have All, Any and Some properties. They are, logically speaking, All Joins but with a difference. Junctions are more aware of the Task Tree. They know when Task Instances are created or not and accommodate Branches that may not have Task Instances created for them.
So, changing our Join to a Junction, the tree looks remarkably the same:
However, the behavior is quite different. If the customer selects just a Desktop, the Junction will execute once the Setup Desktop Task is complete. If the customer selects both a Desktop and a Laptop, the Junction will wait for both Setup Desktop and Setup Laptop to complete before continuing. In other words, a Junction will always continue processing when everything pointing to it is complete.
You may be tempted to use Junctions in all cases. Given the flexibility of a Junction in being able to determine how many Nodes are viable, this seems like an easy way to combine multiple Branches without having to worry too much about Task Tree design, whether Nodes are created or not, or how multiple parallel Branches are timed.
Keep in mind that logically Junctions mean All. Waiting for All Nodes is not always what you want. Refer to the Approval example above - if your Business Rules dictate that only 1 of 3 Approvals is necessary to continue processing your Task Tree, using a Junction would not work.
To summarize some ideas on Junctions:
- Junctions, like Joins, are used to combine multiple Branches of processing into a single path.
- Junctions are logically "All" operations and wait for all connecting Nodes to complete before continuing.
- Junctions accommodate connecting Branches that may not be created at run time.
One Last Example
To really get a good contrast between Joins and Junctions, let's devise a complex example where you might use them both together. Consider this set of Business Rules for a Computer Setup Request:
- A customer may request a Desktop, Laptop or both.
- A customer automatically gets a Standard Software Package.
- A customer may optionally request a Custom Software Package.
- Custom Software Packages require Manager and Technical Approval or Director Approval.
- Both the hardware and software procurements must be complete before Desktop Support is given an Install Work Order.
In order to keep the example readable (and small enough for a screen capture!), I've omitted what happens if an Approval is Denied. For our example purposes, assume that all Approvals are eventually Approved. The resulting Task Tree would look similar to this:
Take a moment and trace processing through the Task Tree keeping in mind that our hypotehtical customer can select multiple options. Any given selection will process the Task Tree differently, and accommodating those inputs is where Joins and Junctions really prove their worth. As you trace the Task Tree, think of these use cases:
- Customer selects a Desktop OR Laptop only and no Custom Software.
- Customer selects BOTH a Desktop AND Laptop and no Custom Software.
- Customer selects a Desktop OR Laptop AND Custom Software.
- Customer selects BOTH a Desktop AND Laptop AND Custom Software.
And for cases of Custom Software, trace these use cases:
- Custom Software gets a Standard (Manager and Technical) Approval.
- Custom Software gets Director Approval.
And now for one last summary concept - if you're unsure about where to use a Junction or Join, consider this rule of thumb: Joins are useful for Branches that must happen, Junctions are useful for Branches that might happen.
Hopefully this article has cleared up this sometimes mystifying subject and given you a better grasp on a very powerful tool. As always, you are encouraged to test out techniques and play with options to see where you can add efficiency and robustness to your Task Tree designs.
If you have any questions about this article, feel free to email firstname.lastname@example.org and I will be happy to respond.