Andrew Wallace
Microsoft Corporation
April 1998
Click to view or copy the sample files associated with this technical article.
So you want to send a document to a sequence of destinations for review, and track the progress of the document through the cycle. Or maybe you just want to send a Web page to a group of editors for approval before putting it out on the Web. Or you want to develop a sales automation application, or even just a means of passing expense reports to the appropriate managers for review.
Traditionally, you can either build a sophisticated application on a database engine, which is great if the application justifies the cost. Or you can try using the very simple routing slips that come with Microsoft® Office. The routing slip in Office is an example of client-side routing. This approach suffers from a number of drawbacks. For example, there is no reliable way to ensure execution in a prescribed period of time or to guarantee that execution will complete. It is also difficult to get an overview of the status of such routes.
The only way around these reliability, timing, and administration issues is with a server-side solution. The trick is to do it without incurring a high development cost.
The Microsoft Exchange Server Routing Objects and Routing Engine have been designed to address these issues. The rest of this article describes the Exchange Server routing solution and gives an example of how to use it.
From a high-level or architectural viewpoint the Exchange Server routing solution is a hub-and-spoke architecture. An Exchange Public Folder (or a mailbox) acts as the hub, and the flow of messages to and from each participant in the route appears as the spokes. Figure 1 shows the logical view of a simple route on the left. The diagram on the right shows the actual movement of messages.
Figure 1. Logical view of route compared to the actual movement of the messages
From an implementation perspective the Exchange Server routing solution could be conceived of as a stateful scripting agent. There are four components:
The Routing Engine has been implemented as a custom agent that runs under the control of the Exchange Event Service. It is configured on a per-folder basis and responds to all the events generated by the Event Service on that folder.
The Routing Map is a low-level description of the logic involved in the route. A task such as "Get Manager's Approval" is broken down into its basic logic and functions. An example of such a breakdown would be:
Logic at this level can be encoded more or less directly into the Routing Map as shown in Table 1.
Table 1. A Map to Get Approval from a Manager
Activity ID | Action | Argument |
10 | Send | Manager |
20 | Wait | 24 hours |
30 | ORSplit | IsTimeout |
40 | Goto | Timeout |
50 | Receive | |
60 | ORSplit | IsApprove |
70 | Goto | Approved |
80 | Goto | Rejected |
The Routing Map contains two kinds of activity:
Table 2 lists the set of activities supplied with Exchange Server version 5.5 Service Pack 1.
Table 2. Routing Activities with Exchange Server Version 5.5 Service Pack 1
Type | Activity | Description |
Flow Control | ||
New | Instantiates a new route process instance. | |
Terminate | Terminates a route process instance. | |
Wait | Waits for an event or an expiry timer. | |
ORSplit | Evaluates a VBScript function and skips a line if it returns false. The logic branches one way or the other. | |
AndSplit | Creates a set of child process instances. The logic branches in parallel. | |
Goto | Branches execution to a particular activity. | |
Evaluation | ||
IsTimeout | Did this process instance leave its previous state due to expiry? | |
IsApprove | Did the participant approve the process instance? | |
IsApproveTable | Evaluates the complete list of votes. | |
Functional | ||
Send | Sends a message. | |
Receive | Updates status and properties when a valid message is received. | |
Consolidate | Moves properties from a work item onto the process instance. | |
FinalizeReport | Generates a summary report on a process instance. | |
AutoSet | Provides default Approval or Rejection behavior for participants. |
Note The Microsoft Exchange Server version 5.5 Service Pack 1 is a forthcoming Service Pack, however, at the time of this writing (April 1998) a beta version of The Microsoft Exchange Server Routing Objects is available, after registration, on the Microsoft TechNet IT Home Web site at http://technet.microsoft.com/reg/download/exchange/default.htm.
Clearly, most users are not going to describe a route at this level. Rather, users will have their choice of design tools or authoring environments that take a higher level description of the route and translate it into a routing map that the routing engine can interpret. The low level of the map allows for a great deal of flexibility in the behavior of the route.
Exchange Server 5.5 Service Pack 1 includes a Routing Wizard, which is an example of a fairly simple authoring tool that can be used to create short sequential and parallel routes. The source code is included on the Service Pack 1 CD. The wizard creates a route and saves it on the folder, such that any message that is dropped or sent to this folder will be routed according to the map.
The sample described below is an even simpler authoring tool that is hosted as a Microsoft Outlook™ form. The form creates a map, saves it onto the message being composed, and posts it into a folder for routing. Observe the distinction between this approach and the Routing Wizard, in that the route defined with this form is applied only to the particular message being created. These are referred to as one-off maps.
The Routing Objects are used to manipulate the map, and process instances. A list of the objects and their most interesting properties and methods are given in Table 3.
Table 3. Exchange Routing Objects
Object | Property/Method | Description |
Routing Map | ||
Open/Save | Opens and saves routing maps to a folder or message. | |
Insert/Get Activity | Inserts and gets routing activities on and from the map. | |
Process Instance | ||
RUI | Routing Unique Identifier. Uniquely identifies a particular process instance. | |
Open/Save | Opens or saves a process instance. | |
Status | The current high level status of process instance. | |
Timeout | The time when the process instance will expire (that is, move to next state). | |
ParentProcID | The ID of the parent process—used when this process instance is a child of another process. | |
Log | Returns the Log property of the process instance. | |
Work Item | ||
Correlate | Correlates a work item to its parent process instance. | |
Consolidate | Moves properties from the work item to the process instance. | |
EmbedMsg | Embeds a message on the work item. | |
Routing Activity | ||
ActivityID | Identifies the activity in the map. | |
Action | The action to be taken for this activity. | |
Flags | ||
Args | The arguments to be passed to a function as part of an action. | |
Routing Participant | The participant in a route. Anything with an email address can be a participant. | |
RoleName | The name of the participant. | |
MemberName | The address of the participant. | |
ResolveRole | Resolves the role of the participant to an address. | |
Routing Log | Builds a log for the process instance. Entries in the log can come from the engine, script, or even be made at the client if a form is used. | |
Open/Save | Opens and saves the log for this process instance. | |
Get/AddLogEntry | Gets and adds log entries. | |
Routing Voting | Models the properties used to enable the Outlook client's voting buttons and interpret the results. | |
PopulateVoteMessage | This sets the voting properties on the message. | |
Item | A recipient entry on the voting message. | |
Count | The number of recipient entries on the voting message. | |
Recipient Entry | Models the vote that a recipient gave on a message. | |
Recipient | The name of recipient. | |
Status | The status of the recipient's vote. | |
Date | The date of the recipient's last change in status. |
Some of the terminology being used here probably bears explanation. The four key concepts are described below:
Lists all the possible states in a route. It is traversed from top to bottom, much like a program, and branches when indicated by a particular state.
Defines what happens in the map at a particular state. Examples would be "SendMessage", "Goto X", "Wait", and so on.
Models a particular route and stores all the information for that route. For example, dropping a spreadsheet into a folder will cause a process instance to be created and the spreadsheet to be embedded in it.
Any new item in a folder is modeled as a work item until the process instance it relates to is found, or, if none is found, it is used as the basis from which to create a new process instance.
Exchange Server 5.5 Service Pack 1 includes several components for routing. The Routing Engine and the Routing Objects are installed to the server by default if the Exchange Scripting Agent is present. The Routing Wizard is on the CD and can be installed on any client machine. The Routing Wizard can be used to either build the bindings and scripts for the Event Service, or to create some simple approval routes.
By way of a sample to demonstrate the use of the Exchange Routing objects, we have created an Outlook form that creates a message, with a one-off map, and drops it in a routing folder. When opened in read mode, the form also shows a tracking tab to enable the user to view the status of the route.
Figure 2 shows a picture of the form in compose mode. The user is able to select the recipients to receive the form, enter subject, and some text. For simplicity, the order in which the recipients receive the form is given by the order in which they are selected from the address book.
Figure 2. The sample form in compose mode
The following code creates a message and instantiates a map object. It also populates the recipient list with resolved names.
RTMap.Message = cdoMessage
RTMap.openmap 1
Set cdoTemp = cdoSession.inbox.messages.Add
cdoTemp.Recipients.addmultiple txtRecip.Text
cdoTemp.Recipients.Resolve
Set Recips = cdoTemp.Recipients
If cdoTemp.Recipients.Resolved = False Then
For k = 1 To cdoTemp.Recipients.Count
On Error Resume Next
cdoTemp.Recipients.Item(k).Resolve True
On Error GoTo 0
If Err Then Exit Function
Next
End If
MakeMap RTMap, Recip
RTMap.savemap
MakeMap is the function that does most of the work for the route. First it inserts some preprocessing logic in the map to handle arrival NDRs and so on and then it enters a loop on MakeUserSnippet to create all rows necessary for each participant. It ends with a series of loops writing the different sections of participant code onto the actual map.Function MakeMap(RTMap, ByRef Recips)
Dim TimeHandlers, RejectHandlers, MainBodies
'COLLECTIONS TO HOLD ALL THE USERS ROW CATEGORIES
Dim UserArray, k, j, TempAr
'INSTANCIATE THE OBJECT
Set TimeHandlers = CreateObject("Scripting.Dictionary")
Set RejectHandlers = CreateObject("Scripting.dictionary")
Set MainBodies = CreateObject("scripting.dictionary")
'INSERT START UP ROWS, CHECK FOR RECEIPITS OR NON RELEVANT MESSAGES
RTMap.InsertActivity -1, MakeRow(1, "ORSplit", 0, Array("IsNDR"), 1)
RTMap.InsertActivity -1, MakeRow(2, "Goto", 0, Array(60110), 1)
RTMap.InsertActivity -1, MakeRow(3, "OrSplit", 0, Array("IsReceipt"), 1)
RTMap.InsertActivity -1, MakeRow(4, "Goto", 0, Array(60110), 1)
RTMap.InsertActivity -1, MakeRow(9, "PreProcessing", 2, Array(False), 1)
'CREATE USER ROWS FOR EACH RECIPIENT AND ADDS THEM TO THE COLLECTIONS
For k = 1 To Recips.Count
UserArray = MakeUserSnippet(Recips, k)
MainBodies.Add CStr(k), UserArray(0)
TimeHandlers.Add CStr(k), UserArray(1)
RejectHandlers.Add CStr(k), UserArray(2)
Next
'INSERT THE MAIN BODIES OF EVERY USER'S ROWS IN THE MAP
For k = 1 To MainBodies.Count
TempAr = MainBodies.Item(CStr(k))
For j = 0 To UBound(TempAr)
RTMap.InsertActivity -1, TempAr(j)
Next
Next
'INSERT THE TIMEOUT HANDLERS OF EVERY USER'S ROWS IN THE MAP
For k = 1 To TimeHandlers.Count
TempAr = TimeHandlers.Item(CStr(k))
For j = 0 To UBound(TempAr)
RTMap.InsertActivity -1, TempAr(j)
Next
Next
'INSERT THE REJECT HANDLERS OF EVERY USER'S ROWS IN THE MAP
For k = 1 To RejectHandlers.Count
TempAr = RejectHandlers.Item(CStr(k))
For j = 0 To UBound(TempAr)
RTMap.InsertActivity -1, TempAr(j)
Next
Next
'INSERT IF MESSAGE REJECTED CLEANUP CODE
RTMap.InsertActivity -1, MakeRow(60000, "FinalizeReport", 2,
Array(False, False), 2)
RTMap.InsertActivity -1, MakeRow(60005, "Send", 2, Array("<BLANK>", "",
False, "<FINALIZED>", "<ATTACH>", False,
False), 7)
RTMap.InsertActivity -1, MakeRow(60010, "Terminate", 0, Null, 0)
'INSERT IF MESSAGE APPROVED CLEANUP CODE
RTMap.InsertActivity -1, MakeRow(60100, "FinalizeReport", 2, Array(True,
False), 2)
RTMap.InsertActivity -1, MakeRow(60105, "Send", 2, Array("<BLANK>", "",
False, "<FINALIZED>", "<ATTACH>", False,
False), 7)
RTMap.InsertActivity -1, MakeRow(60110, "Terminate", 0, Null, 0)
End Function
MakeUserSnippet creates three sections of Routing Map for each participant. The first and largest section is the list of activities that send a message and evaluate the response. The second and third sections handle timeouts and rejections respectively.
Function MakeUserSnippet(ByRef Recipients, k)
Dim TimHan, RejHan, Body
'ARRAYS OF ROWS FOR EACH OF THE THREE PARTS OF A RECIPIENTS DATA
'ROW VARS FOR THE MAIN BODY
Dim SendRow, WaitRow, TimeOutSplitRow, GotoTimeHandlerA, IsNDRRow,
GotoTimeHandlerB, IsRecRow, GotoTimeHandlerC
Dim ReceiveRow, ConsolidateRow, ApproveSplitRow, GotoNextRecipRow,
GotoRejectHandlerRow
'ROW VARS FOR TIMEOUT HANDLING
Dim TimeoutHandlerRow, GotoTerminRowA
'ROW VARS FOR REJECT HANDLING
Dim RejectHandlerRow, GotoTerminRowB
'BEGIN CREATING THE MAIN BODY ROWS
ID = k * 100
Set SendRow = MakeRow(ID, "Send", 2, Array(Recipients.Item(k).Name,
Recipients.Item(k).address, False, cdoMessage.Text,
"<ATTACH>", False, False), 7)
Set WaitRow = MakeRow(ID + 10, "Wait", 0, Array(24 * 60), 1)
Set TimeOutSplitRow = MakeRow(ID + 15,"OrSplit",0,Array("IsTimeout"), 1)
Set GotoTimeHandlerA = MakeRow(ID + 20, "Goto", 0, Array(k * 1000), 1)
Set IsNDRRow = MakeRow(ID + 25, "OrSplit", 0, Array("IsNDR"), 1)
Set GotoTimeHandlerB = MakeRow(ID + 30, "Goto", 0, Array(ID + 10), 1)
Set IsRecRow = MakeRow(ID + 35, "OrSplit", 0, Array("IsReceipt"), 1)
Set GotoTimeHandlerC = MakeRow(ID + 40, "Goto", 0, Array(ID + 10), 1)
Set ReceiveRow = MakeRow(ID + 45, "Receive", 2, Array(False), 1)
Set ConsolidateRow = MakeRow(ID + 50, "Consolidate", 2, Array(False), 1)
Set ApproveSplitRow = MakeRow(ID + 55, "OrSplit", 0 ,
Array("IsApprovalMsg"), 1)
If k < Recipients.Count Then
Set GotoNextRecipRow = MakeRow(ID + 60, "Goto", 0, Array((k + 1) *
100), 1)
Else
Set GotoNextRecipRow = MakeRow(ID + 60, "Goto", 0, Array(60100), 1)
End If
Set GotoRejectHandlerRow = MakeRow(ID + 65,"Goto", 0,Array(k * 10000), 1)
'BEGIN CREATING TIMEOUT HANDLER ROWS
ID = k * 1000
Set TimeoutHandlerRow = MakeRow(ID, "Goto", 0, Array(60000), 1)
'BEGIN CREATING REJECT HANDLER ROWS
ID = k * 10000
Set RejectHandlerRow = MakeRow(ID, "Goto", 0, Array(60000), 1)
'BUNDLE THE ROWS IN THEIR OWN ARRAYS
Body = Array(SendRow, WaitRow, TimeOutSplitRow, GotoTimeHandlerA,
IsNDRRow, GotoTimeHandlerB, IsRecRow, GotoTimeHandlerC,
ReceiveRow, ConsolidateRow, ApproveSplitRow,
GotoNextRecipRow, GotoRejectHandlerRow)
TimHan = Array(TimeoutHandlerRow)
RejHan = Array(RejectHandlerRow)
'BUNDLE UP ALL THREE ARRAYS IN ONE AND RETURN IT
MakeUserSnippet = Array(Body, TimHan, RejHan)
End Function
Figure 3 shows the map generated when the user selects just two users. The code to load the map into the form has been included purely for the purposes of demonstration.
Figure 3. The map generated by the sample form for two users
When the form is loaded in read mode, perhaps by the user choosing to check on the status of the route, the tracking table is visible. The tracking table is shown in Figure 4.
Figure 4. The tracking table for a process instance
The following code is executed to load the tracking table from the process instance message into the form.
Function OpenAsRead()
Dim RTMap, RTRow, k, RowAr
Dim RTRecipient, RTVote, RecipName, Argv
SetObjects
cmdRecip.Enabled = False
Set RTMap = CreateObject("ExRt.Map")
Set cdoMessage = cdoSession.GetMessage(Item.EntryID,
olkActEx.CurrentFolder.StoreID)
RTMap.Message = cdoMessage
RTMap.openmap 1
'LIST ALL THE MAP ROWS IN THE LISTBOX ON THE "Map" PAGE
If RTMap.ActivityCount > 0 Then
ReDim RowAr(RTMap.ActivityCount, 3)
For k = 1 To RTMap.ActivityCount
Set RTRow = CreateObject("ExRt.Row.1")
RTMap.GetRow k - 1, RTRow
RowAr(k - 1, 0) = CStr(RTRow.ActivityID)
RowAr(k - 1, 1) = RTRow.Action
RowAr(k - 1, 2) = CStr(RTRow.Flags)
RowAr(k - 1, 3) = ParamsToString(RTRow)
If RTRow.Action = "Send" Then
RTRow.GetArgs 1, Argv
If Not (Argv(0) = "<BLANK>")
Then txtRecip.Text = txtRecip.Text & Argv(0) & ";"
End If
Next
lbMap.List() = RowAr
End If
'LIST STATUS OF ANSWERED RECIPIENTS IN THE "Recipients Table" PAGE
Set RTVote = CreateObject("ExRt.VoteTable")
RTVote.PIMessage = cdoMessage
ReDim RowAr(RTVote.Count, 2)
For k = 1 To RTVote.Count
Set RTRecipient = RTVote.Item(k)
RowAr(k - 1, 0) = RTRecipient.Recipient
RowAr(k - 1, 1) = RTRecipient.status
RowAr(k - 1, 2) = RTRecipient.Date
Next
lbRecipTrack.List() = RowAr
lbMap.Enabled = True
End Function
To use this form requires two steps:
Participants in the route can use any e-mail client that supports "mailto" URLs. This covers most of the e-mail clients available on the market today. Participating in a route usually involves simply selecting the appropriate mailto URL for approval or rejection. If the participant is using the Outlook messaging and collaboration client as the client, then the Outlook voting buttons can be used instead. The participant will also be able to go to the routing folder and examine the status of all the process instances using a view of the folder. Or the participant can drill down to see the status of all the individual participants in one particular process by looking at its Outlook tracking table.
There is a temptation to call the Exchange Server routing solution a workflow system. It is not. Rather it is just one component of a larger set that would have to be assembled to build such a system. For example, a complete system requires a serious design tool, sophisticated auditing, and, usually, interaction with other databases.
The samples and Routing Wizard provided do not exercise many of the capabilities of the Routing Engine. For example, one capability not exercised is the AndSplit function. This is implemented by the engine spawning a set of child processes, each as capable as the parent. Between this and the OrSplit, it is possible to create arbitrarily complex routes. The use of script for evaluation and functional activities allows scope for interaction with other workflow systems—for example, to handle exception processing while keeping the master workflow process up to date with tracking messages, and, as far as Exchange allows, transaction processing systems, such as Microsoft Transaction Server.
There are also several other features of Exchange Server routing that are not demonstrated in this sample: