Exchange Server allows you to build applications that model and automate business processes. You can hardcode a workflow process by using server events and Visual Basic, but the built-in workflow engine ”Workflow Designer ”and CDO workflow objects of Exchange Server simplify this development task by providing specialized workflow tools.
With these tools, you can build database workflows and e-mail workflows on Exchange Server. You can also enable the user to connect directly to the data or to an application that directly touches the data to update state information in the workflow. The Training application is a database workflow. When a manager approves a user to take a course, an ASP page updates a property on the pending approval message to the student, which in turn triggers the workflow engine to change the state of the pending approval message to Approved . This change of state causes the application to register the student for the course and then notify the student. Database workflows are great if your users have direct access to the process instances of the workflow, and, in effect, to the items undergoing the workflow process, so that they can update the state.
In e-mail workflows, instead of having the user directly interact with the data, notifications and data copies are sent to the user via e-mail. The user can then perform an action, such as approve or edit the data, and return it to the application via e-mail. The Exchange Server workflow engine directs the e-mail to the correct process instance in your workflow folder, and you can update your process instance accordingly . This style of workflow is useful if you can't guarantee that users have direct access to your data or if you want to provide an easy transmission method for data and approval. Most users understand and have ready access to Outlook e-mail, so this solution works well for business processes that extend beyond a corporation and over the Internet. Note that to use e-mail-style workflow, you must be running Outlook on your client.
Exchange Server uses four main components to implement workflow: CDO for Workflow (CDOWF), server event sinks (handlers), action tables, and script files. The workflow engine in Exchange Server is stored in CDOWF. This engine works in conjunction with the other components to evaluate and maintain the workflow state, such as Approved or Rejected . CDOWF also provides the object model you can use to interact with the engine to change states or validate workflow properties.
The workflow engine works in conjunction with the action table, which is basically a finite state machine that describes transitions between states in the workflow. Table 17-4 shows an example of an action table based on the Training application. In this action table, a request for approval of user's enrollment is e-mailed to the user's manager, and the workflow engine waits 15 minutes for a response. If a response isn't received from the manager within 15 minutes, the engine e- mails the user that the request was not processed . If the manager approves the course request, the engine registers the user and sends an e-mail that tells the user of the approval.
State | New State | Event Type | Condition | Action | Expiration Interval |
---|---|---|---|---|---|
Created | OnCreate | CheckValidity | SendMailtoManager | ||
Created | OnEnter | True | 15 minutes | ||
Created | Approved | OnChange | ApprovalStateofItem | RegisterUserSendMailtoUser | |
Created | NoResponse | OnExpiry | True | SendNoResponsetoUser |
To determine what changes have been made to workflow items and to evaluate whether they are valid according to the action table, the workflow engine implements both synchronous and OnTimer event handlers. When you drop a new document into a folder in which a workflow process is enabled, the Workflow event handler in that folder fires before the item is committed to the Exchange Server database. If workflow is enabled, the event handler creates the process instance for your item in the folder and determines the initial state of the item. If a user changes that item, the event handler checks the change against the valid state changes in the action table. If the state change is not valid, it is not committed to Exchange Server. If the time expires for a specific state and there is a state transition for that event in the action table, the OnTimer event will move the workflow item to the next state. Figure 17-11 shows the Workflow event handler that's installed by default as a COM+ application.
The script file is used in conjunction with CDOWF, the server event handlers, and the action tables. Your script implements the conditions for the state transitions and the actions that occur at the time of those transitions. For example, to check the validity of an item before posting it in the workflow folder, you write script to check the necessary properties and return to the workflow engine a Boolean that indicates whether the item is valid. You must use condition functions in your script. If a condition is valid, for example, you might want to send an e-mail to the user who needs to approve the item. You have to write script for the action portion of your workflow in order to send the e-mail.
Enough talk about what workflow is ”let's look at how to write a workflow application with Exchange Server. In this section, I'll concentrate on the Workflow Designer for Exchange because it makes creating workflow applications easier. Everything we'll discuss in this section can be performed programmatically using CDOWF.
To start writing workflow applications, you first need to set up the workflow environment for Exchange Server. I won't go into the gory details of how to set up all the accounts and infrastructure. You can find all this information in the Exchange SDK, in a section called "Adding the Workflow System Account." Essentially, you need to create an account and make it your default workflow system account.
Next you need to add yourself to the Can Register Workflow role for the Workflow Event Sink COM+ application. This role lists all the users or groups that can actually register for the event handlers we discussed earlier. Figure 17-12 shows this COM+ application with the roles populated .
If necessary, you also should add yourself to the Privileged Workflow Authors role. If you are not in this role, your workflow scripts will be sandboxed. In other words, they will only be able to modify the item undergoing workflow, send notification mail, or write to the workflow audit trail. They will not be able create objects at all. If you add yourself to the Privileged Workflow Authors role, you'll be able to create objects and access other items you have permissions for in Exchange Server.
Note | If you are a privileged workflow author, you will be running under the workflow system account you initially specified. Also, Exchange supports ad hoc workflows. This means that items coming into the folder where workflow is enabled must already have a workflow definition on them. Only restricted mode workflows can be run as ad hoc. You cannot allow ad hoc workflows and enable privileged mode. |
As mentioned earlier, the Workflow Designer for Exchange makes building and deploying your workflow applications easier. Its graphical user interface (GUI) simplifies the process of visualizing your workflow while automating the process of creating the event handler registrations and action tables in your workflow-enabled folders.
I won't cover all the GUI elements of the Workflow Designer; you can find that information easily in the Exchange Server documentation, or you can just read the Workflow Designer screen. However, I will cover three important elements that make up your workflow in the designer: states, actions, and script. The states are the boxes in the GUI that indicate whether your workflow is pending, approved, rejected, or expired . The actions provide the transitions between the states. These actions, which I'll detail momentarily, can be found in your action table and include OnEnter , OnExit , and OnCreate , among others. The script implements the condition checking and the actions.
The states in the Workflow Designer are not very interesting because they don't perform any task; they only serve as a destination for the workflow item to move to during the workflow process. Therefore, your only concern with states should be that your scripts allow you to check which state the workflow item is in, to see whether it's being approved or rejected or placed in any other state you specify.
Actions are among the most important aspects of the Workflow Designer because they are associated with the transitions between states. These transitions define how the work in your workflow is performed. Figure 17-13 shows how to create actions in the Workflow Designer.
Table 17-5 describes the actions in the Workflow Designer. Be aware that some actions, such as the OnChange action, can appear multiple times on a state. But each time it appears for the same state, it must have different conditions that make the action valid for a workflow item. For example, one condition might check changes to the subject line and another might check changes to the message body.
Workflow Designer | Action Table | Description |
---|---|---|
Create | OnCreate | An item was created. You must have at least one Create action in your workflow; otherwise , no items can be created in the folder. |
Enter | OnEnter | This action manages the time used by the Expiry action. As soon as the state is entered using this action, the timer is started for the expiration interval you set. Entry into states is implicitly allowed, so you don't have to add this action to every state. You will normally use this action for timer-based workflow requirements only. |
Exit | OnExit | The state is transitioning to a new state. |
Delete | OnDelete | The document is deleted. If you do not have a Delete action, workflow items cannot be deleted. If that's the case, Exchange Server will return an error if an application or user attempts to delete an item. |
Change | OnChange | The document is modified. You can have multiple Change actions on a single state. If you have no Change actions, documents cannot be modified. |
Receive | OnReceive | The workflow has received an e-mail message that correlates to a workflow item. This action allows your workflow to respond to e-mail. |
Expiry | OnExpiry | The document has passed its time limit for the current state. This action is useful for time-based tasks , such as reminder notifications to managers to approve workflow items. |
The following code is the XML representation of a real action table generated by the Workflow Designer. Notice the action and state names in the XML code. You'll see how to use XML action tables to simplify deploying workflow processes later in the chapter.
<xml xmlns:s='uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882' xmlns:dt='uuid:C2F41010-65B3-11d1-A29F-00AA00C14882' xmlns:rs='urn:schemas-microsoft-com:rowset' xmlns:z='#RowsetSchema'> <s:Schema id='RowsetSchema'> <s:ElementType name='row' content='eltOnly' rs:updatable='true'> <s:AttributeType name='ID' rs:number='1' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='Caption' rs:number='2' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='State' rs:number='3' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='NewState' rs:number='4' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='EventType' rs:number='5' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='Condition' rs:number='6' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='Action' rs:number='7' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='ExpiryInterval' rs:number='8' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='RowACL' rs:number='9' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='TransitionACL' rs:number='10' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='DesignToolFields' rs:number='11' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='CompensatingAction' rs:number='12' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='Flags' rs:number='13' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:AttributeType name='EvaluationOrder' rs:number='14' rs:write='true'> <s:datatype dt:type='string' dt:maxLength='4294967295' rs:precision='0' rs:long='true' rs:maybenull='false'/> </s:AttributeType> <s:extends type='rs:rowbase'/> </s:ElementType> </s:Schema> <rs:data> <rs:insert> <z:row ID='1' Caption='Create' State='' NewState='Pending' EventType='OnCreate' Condition='TRUE' Action='' ExpiryInterval='0' RowACL='' TransitionACL='' DesignToolFields='-1:1:' CompensatingAction='' Flags='0' EvaluationOrder='1000'/> <z:row ID='2' Caption='Delete' State='Pending' NewState='' EventType='OnDelete' Condition='true' Action='' ExpiryInterval='0' RowACL='' TransitionACL='' DesignToolFields='1:-2:' CompensatingAction='' Flags='0' EvaluationOrder='7000'/> <z:row ID='3' Caption='StartTimer' State='' NewState='Pending' EventType='OnEnter' Condition='TRUE' Action='sendMailToManager' ExpiryInterval='15' RowACL='' TransitionACL='' DesignToolFields='0:1:' CompensatingAction='' Flags='0' EvaluationOrder=''/> <z:row ID='5' Caption='ManagerApproved' State='Pending' NewState='Approved' EventType='OnChange' Condition='WorkflowSession.Fields ("http://thomriz.com/schema/approvalstatus").value = "Approved"' Action='strCourseName = GetCourseName strStudentEmail = GetStudentEmail strManagerEmail = GetManagerEmail strBody = "Your manager approved you for the course: " & strCourseName sendMail strBody,strStudentEmail & "," & strManagerEmail,"Approved for course: " & strCourseName addregistration sendcalendarmessage' ExpiryInterval='0' RowACL='' TransitionACL='' DesignToolFields= '1:3:' CompensatingAction='' Flags='0' EvaluationOrder='3001'/> <z:row ID='6' Caption='ManagerRejected' State='Pending' NewState='Rejected' EventType='OnChange' Condition='WorkflowSession.Fields ("http://thomriz.com/schema/approvalstatus").value = "Rejected"' Action='strCourseName = GetCourseName strStudentEmail = GetStudentEmail strManagerEmail = GetManagerEmail strBody = "Your manager rejected you for the course: " & strCourseName sendMail strBody,strStudentEmail & "," & strManagerEmail,"Rejected for course: " & strCourseName ' ExpiryInterval='0' RowACL='' TransitionACL='' DesignToolFields= '1:2:' CompensatingAction='' Flags='0' EvaluationOrder='3000'/> <z:row ID='7' Caption='NoResponse' State='Pending' NewState='Expired' EventType='OnExpiry' Condition='TRUE' Action='strCourseName = GetCourseName strStudentEmail = GetStudentEmail strManagerEmail = GetManagerEmail strBody = "Your manager did not approve your attending of the course: " & strCourseName & " in enough time. You will not be registered for this course." sendMail strBody, strStudentEmail & "," & strManagerEmail, "Approval not received for course: " & strCourseName ' ExpiryInterval='0' RowACL='' TransitionACL='' DesignToolFields= '1:4:' CompensatingAction='' Flags='0' EvaluationOrder='5000'/> <z:row ID='8' Caption='' State='Rejected' NewState='' EventType='OnDelete' Condition='TRUE' Action='' ExpiryInterval='' RowACL='' TransitionACL='' DesignToolFields='2:-2:' CompensatingAction='' Flags='0' EvaluationOrder='7001'/> <z:row ID='10' Caption='' State='Approved' NewState='' EventType='OnDelete' Condition='TRUE' Action='' ExpiryInterval='' RowACL='' TransitionACL='' DesignToolFields='3:-2:' CompensatingAction='' Flags='0' EvaluationOrder='7002'/> <z:row ID='11' Caption='' State='Expired' NewState='' EventType='OnDelete' Condition='TRUE' Action='' ExpiryInterval='' RowACL='' TransitionACL='' DesignToolFields='4:-2:' CompensatingAction='' Flags='0' EvaluationOrder='7003'/> </rs:insert> </rs:data> </xml>
Instead of using the Workflow Designer to create your workflow process, you could programmatically create your action table by using just ADO and CDOWF. However, in most cases, you'll want to take advantage of the Workflow Designer and generate your action tables to XML, as shown in the preceding example. You can then import the XML action table into ADO and use that data to programmatically generate your workflow process.
We have a workflow engine, event handlers, and an action table, but we don't have a true workflow application yet. The real foundation of the workflow application is the VBScript code you write for the actions in your action table. Whether you want to send a message, change a property, or update an item, you need to implement this action in your script. Writing your workflow script is pretty straightforward; you'll probably call ADO or CDO to perform functions in Exchange Server. The Workflow Designer includes a script editor, shown in Figure 17-14.
You don't have to use the Workflow Designer script editor to write your scripts. You can use another editor, such as Microsoft Visual Studio .NET, and save the scripts to a common location or even implement the handlers for your actions using COM components. You can then point the Workflow Designer, or a workflow process you programmatically create, to a common script file. The following script file is used in the Training application to implement the workflow process:
Dim strHTMLBody Dim bWroteDebugging Sub AddAuditEntry(strString, lResult) WorkflowSession.AddAuditEntry strString, lResult End Sub Function DebugWorkflow() 'Check to see whether debugging is enabled bWorkflow = cBool(WorkflowSession.Fields( _ "http://thomriz.com/schema/debugworkflow").value) If bWroteDebugging <> True Then If bWorkflow Then AddAuditEntry "Workflow Debugging Enabled", 0 bWroteDebugging = True End If End If DebugWorkflow = bWorkflow End Function Function GetSchema() If DebugWorkflow Then AddAuditEntry "In GetSchema", 0 End If GetSchema = _ WorkflowSession.Fields("http://thomriz.com/schema/schema").value If DebugWorkflow Then AddAuditEntry "In GetSchema -> Schema: " & _ WorkflowSession.Fields("http://thomriz.com/schema/schema").value,0 End If End Function Function GetWorkflowSessionField(strField) If DebugWorkflow Then AddAuditEntry "In GetWorkflowSessionField -> Value: " & strField, 0 End If GetWorkflowSessionField = _ WorkflowSession.Fields("http://thomriz.com/schema/" & strField).value End Function Function GetCourse(bReadOnly) If DebugWorkflow Then AddAuditEntry "In GetCourse", 0 End If Set oRec = CreateObject("ADODB.Record") If DebugWorkflow Then AddAuditEntry "Course URL: " & WorkflowSession.Fields( _ "http://thomriz.com/schema/fullcourseurl").value, 0 End If If bReadOnly Then iAccess = 1 Else iAccess = 3 End If oRec.Open WorkflowSession.Fields("http://thomriz.com/sche" & _ "ma/fullcourseurl").value, WorkflowSession.ActiveConnection, iAccess If DebugWorkflow Then AddAuditEntry "In GetCourse -> CourseName: " _ & oRec.Fields("urn:schemas:httpmail:subject").value, 0 End If Set GetCourse = oRec End Function Function GetCourseName() 'Returns the name of the course Set oRec = GetCourse(True) If DebugWorkflow Then AddAuditEntry "In GetCourseName -> CourseName = " _ & oRec.Fields("urn:schemas:httpmail:subject").value, 0 End If GetCourseName = oRec.Fields("urn:schemas:httpmail:subject").value End Function Sub ReplaceString(strToken, strReplacement) 'Take the token and replace it in the global strHTMLBody strHTMLBody = Replace(strHTMLBody, strToken, strReplacement) End Sub Function GenerateHTMLBody() 'Generates the HTML to send in the message. 'Retrieve the message containing the HTML. 'The HTML template must always be called WorkflowMessage. If DebugWorkflow Then AddAuditEntry "In GenerateHTMLBody", 0 End If strHTMLBody = "" 'Build the SQL statement. 'Query for the e-mail message. strEmailsFolderPath = GetWorkflowSessionField("stremailsfolderpath") strSQL = "SELECT ""urn:schemas:httpmail:textdescription"" FROM " _ & "SCOPE('SHALLOW TRAVERSAL OF """ & strEmailsFolderPath _ & """') WHERE ""DAV:iscollection"" = false AND " _ & """DAV:ishidden"" = false AND " _ & """urn:schemas:httpmail:subject"" LIKE '%WorkflowMessage%'" 'Create a new RecordSet object Set rst = CreateObject("ADODB.RecordSet") With rst 'Open RecordSet based on the SQL string .Open strSQL, WorkflowSession.ActiveConnection End With If rst.BOF And rst.EOF Then GenerateHTMLBody = "" Exit Function End If 'On Error Resume Next rst.MoveFirst strHTMLBody = rst.Fields("urn:schemas:httpmail:textdescription").Value 'Get the course Set oCourse = GetCourse(True) 'Load it into CDO Appointment Set iAppt = CreateObject("CDO.Appointment") iAppt.DataSource.Open oCourse.Fields("DAV:href").value, _ WorkflowSession.ActiveConnection, 1 strSchema = GetSchema 'Replace the tokens with real values ReplaceString "%StudentName%", _ GetWorkflowSessionField("strStudentFullname") ReplaceString "%Name%", _ iAppt.Fields("urn:schemas:httpmail:subject").Value ReplaceString "%Category%", iAppt.Fields(strSchema & "category").Value strDate = Month(iAppt.StartTime) & "/" & Day(iAppt.StartTime) & "/" _ & Year(iAppt.StartTime) ReplaceString "%Date%", strDate ReplaceString "%StartTime%", TimeValue(iAppt.StartTime) ReplaceString "%EndTime%", TimeValue(iAppt.EndTime) ReplaceString "%Location%", iAppt.Location ReplaceString "%Description%", iAppt.TextBody strHTTPURL = GetWorkflowSessionField("strRootDirectory") _ & "workflow.asp?CourseID=" & iAppt.Fields("DAV:href") _ & "&student=" & GetWorkflowSessionField("fullStudentURL") ReplaceString "%URLLink%", strHTTPURL If strHTMLBody <> "" Then GenerateHTMLBody = strHTMLBody Else GenerateHTMLBody = "" End If rst.Close Set rst = Nothing End Function Sub sendMail(strMsg, strAddress, strSubject) set msg = createobject("CDO.Message") msg.To = strAddress msg.From = GetWorkflowSessionField("NotificationAddress") msg.Subject = strSubject msg.TextBody = strMsg If DebugWorkflow Then AddAuditEntry "In SendMail: Address -> " & strAddress & vblf _ & "Subject -> " & strSubject & vblf & "Message: " & strMsg, 0 End If msg.Send End sub Function GetStudentEmail() If DebugWorkflow Then AddAuditEntry "In GetStudentEmail", 0 End If GetStudentEmail = GetWorkflowSessionField("StudentEmail") End Function Function GetManagerEmail() If DebugWorkflow Then AddAuditEntry "In GetManagerEmail", 0 End If GetManagerEmail = GetWorkflowSessionField("ManagerEmail") End Function Sub SendMailToManager() 'Get the manager's e-mail strManagerEmail = GetWorkflowSessionField("ManagerEmail") If DebugWorkflow Then AddAuditEntry "In SendMailToManager -> Manager: " & strManagerEmail, 0 End If set oMsg = CreateObject("CDO.Message") set oRecord = GetCourse(True) oMsg.To = strManagerEmail oMsg.From = GetWorkflowSessionField("NotificationAddress") oMsg.Subject = "Approval Required for course: " _ & oRecord.Fields("urn:schemas:httpmail:subject").value oMsg.AutoGenerateTextBody = True oMsg.MimeFormatted = True oMsg.HTMLBody = GenerateHTMLBody oMsg.Send End Sub Function GetStudent(bReadOnly) If DebugWorkflow Then AddAuditEntry "In GetStudent",0 End If Set oRec = CreateObject("ADODB.Record") If DebugWorkflow Then AddAuditEntry "Student URL: " & WorkflowSession.Fields( _ "http://thomriz.com/schema/fullstudenturl").value, 0 End If If bReadOnly Then iAccess = 1 Else iAccess = 3 End If oRec.Open WorkflowSession.Fields("http://thomriz.com/sche" _ & "ma/fullstudenturl").value, _ WorkflowSession.ActiveConnection, iAccess If DebugWorkflow Then AddAuditEntry "In GetStudent -> StudentName: " _ & oRec.Fields("urn:schemas:httpmail:subject").value, 0 End If Set GetStudent = oRec End Function Sub addRegistration() Set oRecord = GetStudent(False) strSchema = GetSchema strCourseURL = GetWorkflowSessionField("shortCourseURL") oRecord.Fields(strSchema & "registrations") = oRecord.Fields(strSchema _ & "registrations") _ & strCourseURL & "," oRecord.Fields.Update oRecord.Close Set oRecord = Nothing End Sub Sub sendCalendarMessage() If DebugWorkflow Then AddAuditEntry "In SendCalendarMessage", 0 End If 'Get the original appointment Set oOriginalAppt = CreateObject("CDO.Appointment") oOriginalAppt.Datasource.Open GetWorkflowSessionField("fullCourseURL"), _ WorkflowSession.ActiveConnection, 1 'Create a throwaway appointment Set oAppt = CreateObject("CDO.Appointment") set oConfig = CreateObject("CDO.Configuration") strNotificationAddress = GetWorkflowSessionField("NotificationAddress") oConfig.Fields("http://schemas.microsoft.com/cdo/config" _ & "uration/sendemailaddress") = strNotificationAddress oConfig.Fields.Update oAppt.Configuration = oConfig oAppt.StartTime = oOriginalAppt.StartTime oAppt.EndTime = oOriginalAppt.EndTime oAppt.Subject = "Course: " & oOriginalAppt.Subject oAppt.Location = oOriginalAppt.Location strSchema = GetSchema oAppt.TextBody = "The Instructor is " _ & oOriginalAppt.Fields(strSchema & "instructoremail").Value 'Don't ask for a response since we don't care if they accept or decline oAppt.ResponseRequested = False Set oAttendee = oAppt.Attendees.Add strEmail = GetStudentEmail oAttendee.Address = strEmail oAttendee.Role = 0 Set oMtg = oAppt.CreateRequest oMtg.Message.Send End Sub
This script uses ADO and CDO to perform its functions. However, another object is at work in the script: WorkflowSession . This intrinsic object (which you don't have to create) is passed to your script by the workflow engine. It allows you to access properties on the process instance as well as the audit trail specified for the workflow. Table 17-6 shows the most important properties and methods of this object. For more information on the properties and methods , refer to the Exchange Platform SDK.
Property or Method | Description |
---|---|
ActiveConnection | A property that returns an ADO Connection object. You should use this Connection object in your script's ADO and CDO functions, especially if you want them to take part in transactions. |
AddAuditEntry | A method that allows you to add an audit entry to the selected audit entry provider of the workflow process. You pass a string and a long value to specify what the entry should say and the custom result you want for the value. By default, Exchange Server ships with one audit trail provider, which writes to the Windows Event Log. You can create custom audit trail providers by creating COM components that implement the IAuditTrail interface. |
DeleteReceivedMessage | A method that deletes the received e-mail, if one exists, for the workflow item. You usually call it in the Receive action. |
DeleteWorkflowItem | A method that deletes the workflow item. |
Domain | A property that returns the domain of the server. This property works in conjunction with the Server property to make it easier for you to generate file:// or http:// URLs. |
ErrorDescription | A property used in conjunction with the ErrorNumber property. ErrorDescription contains a description of the error to report to the audit trail provider. |
ErrorNumber | A property that holds the number of the errors to report to the client and the audit trail provider. |
Fields | A property that returns the ADO Fields collection for the workflow item. Using Fields , you can access built-in and custom schemas on the workflow item. |
GetNewWorkflowMessage | A method that creates and returns a new WorkflowMessage object. The object allows you to send e-mail messages from restricted workflows because you cannot create a CDO Message object in a restricted workflow. Also, the CDO message is created in the context of the workflow transaction so if the state transition fails, the e-mail created using this method never gets sent. |
GetUserProperty | A method that gets an Active Directory attribute off an Active Directory object. |
IsUserInRole | A method that checks to see whether a user is in a folder role. You pass to this method the user's e-mail address and the name of the role. The method returns a Boolean indicating whether that user is in that particular folder role. A folder role is a grouping of users who perform a particular function that you define for the folder. The roles are stored on the folder, so to implement roles-based workflow, you do not need permissions to modify or add properties to Active Directory. |
ItemAuthors | A property that contains a collection representing a list of all users with authoring ability on the workflow item. Exchange Server supports item-level permissions, so you might want to set such permissions on workflow items. |
ItemReaders | A property that contains a collection of users who should have Reader permissions on the workflow item. |
Properties | A property that returns an ISessionProps interface so you can add properties you need persisted for a single session that lasts for one ProcessInstance transition. Here's a good example of using this property: Suppose you have multiple actions that need to be evaluated to make a state transition. You do not want each action to check multiple times whether a certain property on the item already exists as part of the evaluation criteria. So you use this property to cache the value and share the value between multiple condition scripts. |
ReceivedMessage | A property that returns the e-mail message that was received in correlation to a workflow item. |
Sender | A property that contains the SMTP address of the person who initiated the state transition. |
Server | A property that contains the name of the server and is used in conjunction with the Domain property. |
StateFrom | A property that contains the name of the state before the current process transition. |
StateTo | A property that holds the name of the state after the current transition. |
TrackingTable | Used with e-mail workflows, this property contains a RecordSet object that has a number of properties relating to the current workflow item. Refer to the Exchange Platform SDK for more information on this property. |
The GetUserProperty method is very useful. It takes three parameters. The first is the distinguished name of the object in Active Directory, which can be either the Active Directory path to the object or the unique e-mail address of the object. The second parameter is the Active Directory attribute you want to get off the object. The third parameter works in conjunction with the first and tells CDO whether the first parameter is an Active Directory path ( 1 ) or an e-mail address ( ). Probably the most common use for this method is to retrieve the manager of the owner of the item that is undergoing the workflow to get approval. You retrieve the manager by getting the manager property off the current user's Active Directory object. You can get the e-mail address of the current user by using the WorkflowSession.Sender property. You can then retrieve the mail property from the manager's Active Directory object. The manager property returns to you the Active Directory path to the manager. The following example illustrates this scenario:
With WorkflowSession strUserAddress = "username@company.com" mgrDN = .GetUserProperty(strUserAddress, "manager", 0) strUserMgrEmail = .GetUserProperty(mgrDN, "mail", 1) End With
Two other properties in Table 17-6, ItemAuthors and ItemReaders , also demand more explanation. ItemAuthors is a collection used to specify per-item modify and delete permissions. If you add any users to this collection, only those users can modify or delete the item as well as read it. If you remove all users from this collection, the default permissions on the folder apply.
With ItemReaders , you can specify per-item read access. If you add users to this collection, only those users can read or view the item, but they cannot necessarily modify the item. This means that even when other users query, know the URL of the item, or try to retrieve a specific property on the item, they cannot modify the item unless they are in the ItemReaders collection. When you clear the collection, default folder permissions will apply.
Both ItemAuthors and ItemReaders return an IMembers interface. This interface supports one property and three methods: the Count property, and the Add , Clear , and Delete methods. Count returns the number of members in the collection. Add adds a new member by taking two parameters, Name and Type . Name must be a string that specifies the e-mail address of the user or a role. Exchange supports the string literals " Role 1" through " Role 16" for adding roles. The Type parameter is an integer that specifies the type of user you are adding, whether it is an e-mail address ( ) or a role ( 1 ). The Clear method clears all members from the collection. Finally, the Delete method deletes a member from the collection. You must pass a numbered index into the collection or a string that uniquely identifies a member of the collection. This string can be a role name such as " Role 1" or the e-mail address of the user you want to remove from the collection. The following example shows how to add two different users to the ItemAuthors and ItemReaders collections on a workflow item:
strAddress = "user@domain.com" WorkflowSession.ItemAuthors.Add strAddress, 0 'cdowfEmailAddress strAddress = "user2@domain.com" WorkflowSession.ItemReaders.Add strAddress, 0 'cdowfEmailAddress
Even though you do not want state transitions to fail because of intermittent computer issues or people attempting transitions when they do not have permissions to, these situations can occur. The Workflow Designer gives you the ability to run compensating actions if a state transition fails. The compensating action is VBScript code. A good example of using a compensating action is when you update a SQL database in the beginning of your state transition ”for some reason, the state transition fails. You can use the compensating script to undo your changes to the SQL server since the state transition failure. Figure 17-15 shows where to set your compensating actions. Compensating actions are not required. There is no need to create them unless you need to for your application.
One other requirement of your workflow might be to change the inherent file-based URL that you receive in your workflow to an HTTP URL that you can e-mail to the end user to open the work item. The file URL will look something like file://./backofficestorage/yourdomain/public folders/folder/myitem.eml . Microsoft Internet Explorer cannot use this URL to browse to the item. You could manually convert the item file URL to an HTTP URL, but the OLE DB provider for the Web Storage System provides this capability as a core part of its functionality in an object called URLMAPPER . The URLMAPPER can take file URLs and map them to HTTP, and vice versa.
The methods of URLMAPPER are shown in Table 17-7.
Method | Description |
---|---|
ExoledbFileURLtoFilePath | Takes a file URL in the form file://./backofficestorage/yourdomain and changes it to a file path. You can then turn the file path into an HTTP URL by using the FilePathtoHTTPURLs method. |
FilePathtoEXOLEDBFileURL | Takes a file path in the form \\.\backofficestorage \domain\folder\folder and converts it to file://./backofficestorage/domain/folder/folder so you can use the path with EXOLEDB. |
FilePathtoHTTPURLs | Takes a file path and returns a variant array that contains all combinations of the HTTP URL, including http://server/foldertree/folder/item , https ://server/foldertree/folder/item , http://server/exadmin/folder/item , and https://server/exadmin/folder/item . |
HttpURLtoFilePath | Takes an HTTP URL to an item and converts it to a file path. |
Therefore, to convert the standard file URL that is passed to you via the workflow engine, you can use the URLMAPPER . The following code converts the URL to an HTTP URL and sends it to the user who just submitted the item:
Sub URLMAPPER strURL = WorkflowSession.Fields("DAV:href").value set oMapper = CreateObject("Exoledb.UrlMapper") strFilePath = oMapper.ExoledbFileUrlToFilePath(strURL) arrHTTP = oMapper.FilePathtoHTTPURLs(strFilePath) set oMsg = CreateObject("CDO.Message") oMsg.From = "Workflow" oMsg.To = WorkflowSession.Sender oMsg.Subject = "HTTP URLs" For i = LBound(arrHTTP) To UBound(arrHTTP) 'Strip out the https and exadmin URLS If InStr(1, arrHTTP(i), "https") = 0 Then 'Not HTTPS, check for exadmin If InStr(1, arrHTTP(i), "exadmin") = 0 Then 'Not exadmin, send it to the user strText = "The URL of the item is: " & arrHTTP(i) Exit For End If End If Next oMsg.TextBody = strText oMsg.Send End Sub
A couple of things about URLMAPPER . First, you must be running in privileged mode to use it because you need to use CreateObject to create the component. Using CreateObject in your script requires privileged mode. Second, on a creation event, the URL for an item does not exist because the workflow engine uses synchronous events. This means that before the item is even committed to the database, your code will be running against that item. An item might already exist in the folder where the application is trying to place the new item. No two items in the Exchange database can have the same URL. Exchange will append a number to the URL, as in myitem-2.eml . For this reason, pulling the URL of the item and using it with URLMAPPER to determine the HTTP URL in order to send this URL in an e-mail is not recommended in Create events. Most times, the URL you can get for the item is probably the final URL, but if it is not, your application might show unexpected results.
One neat thing you can do to get around the privileged mode requirement for URL mapping is to use the workflow session object to retrieve the IExOLEDBURLMapper interface. To do this, you use the following code in your workflows:
Function GetHTTPUrl() On Error Resume Next Dim oMap, strFileURL strFileURL = WorkflowSession.Fields("DAV:href").Value Set oMap = WorkflowSession.Properties.Get("IExoledbUrlMapper") GetHTTPUrl = oMap.FilePathtoHttpUrls( _ oMap.exoledbFileUrlToFilePath(cstr(strFileURL)))(0) Set oMap = Nothing End Function
You might be wondering how you can use ADO and OLE DB transactions inside of your workflow code. The Web Storage System supports transactions, which you can use to make sure that all your code commits to the Web Storage System or rolls back. One thing to note is that transactions are supported across a single ADO connection. So, if you have multiple ADO connections, you will have multiple transactions. For this reason, you should use a single ADO Connection if you want a single transaction context. You can either use the built-in WorkflowSession ActiveConnection property and its transaction context or create your own new Connection object with your own transaction context. Let's take a look at the pros and cons of these two approaches.
When you use the built-in WorkflowSession ActiveConnection object's transactions, you can do all of your work in a transacted state. This means that if you use ADO to add a new item or delete an item, if the state transition fails, all of your work will be rolled back. This is a great benefit. However, there is one drawback. Connections in the Web Storage System are per database. This means that if you want to connect to another database and perform work, you must set up a new Connection object. This new Connection object will have its own transaction context. Therefore, any work you do over the new connection will not be in the transaction context, and the rest of the WorkflowSession commands you perform over this Connection object will not be rolled back in the case of a transition failure.
The benefit of creating a new Connection object and its own transaction context is that you might want to commit your transaction before the state transition is complete. Because the workflow engine will not commit your transaction until the state transition is complete, you cannot commit your commands before the state transition is complete. Both concepts might be made clearer with some sample code.
The following code shows how you can use the built-in WorkflowSession transaction context. The first example sends an e-mail using CDO for Exchange and does this on the transaction context of the WorkflowSession . If something were to happen so that the state transition did not occur, the e-mail would not be sent.
Sub SendEmail(strAddress,strSubject,strBody) 'Create a CDO Message Object 'You could also use the WorkflowSession object 'GetNewWorkflowMessage method 'However, this is to show explicit transaction use. 'Create a new CDO Configuration object 'so we can set the ActiveConnection Set oConfig = CreateObject("CDO.Configuration") oConfig.Fields("http://schemas.microsoft.com/cdo/configuration" _ & "/activeconnection") = WorkflowSession.ActiveConnection Set oMessage = CreateObject("CDO.Message") oMessage.Configuration = oConfig oMessage.Sender = "workflow" oMessage.To = strAddress oMessage.Subject = strSubject oMessage.TextBody = strBody oMessage.Send End Sub
The next example shows how to create your own transaction using the ADO Connection object. Here an item is created in a separate folder, but the transaction is rolled back so it never occurs. When you build applications, you should attempt to use the built-in transaction context because it provides the best way to roll back if errors occur.
Sub PostMessage(strPath) 'Create a new ADO connection to the workitem Set oConn = CreateObject("ADODB.Connection") oConn.Provider = "Exoledb.Datasource" oConn.Open "URL=" & WorkflowSession.fields("DAV:href").value oConn.BeginTrans Set oRecord = CreateObject("ADODB.Record") 'Open a new item in the folder as read/write oRecord.Open strPath, oConn, 3, 0 oConn.RollbackTrans End Sub
Besides supporting database-style workflow in which the user interacts with the work item directly through an application to manipulate data, the Web Storage System also supports e-mail-style workflow, with some special requirements. First, the folder you are sending from must be mail-enabled ”otherwise, the folder cannot receive any e-mail responses. You mail-enable a folder through the Exchange System Manager or through the CDO for Exchange Management objects. By default, top-level folders in the default Public Folder hierarchy are mail-enabled. However, folders in other folder trees are not mail-enabled. Second, the Windows account you use for the workflow event sink to run under must have a mailbox on the local Exchange server. Finally, you cannot send e-mail on an OnCreate action and have the workflow engine correlate and track that e-mail back onto your workflow process. If you need to do this, you must advance the workflow from an OnCreate event and get an OnChange event to fire, from which you send your workflow message. The following example creates a workflow CDO message and sends it to a user:
Sub CreateWFMessage() set oWFMsg = WorkflowSession.GetNewWorkflowMessage With oWFMsg .From = WorkflowSession.Sender .To = WorkflowSession.Sender .Subject = "Workflow Message" .TextBody = WorkflowSession.StateFrom & " -> " _ & WorkflowSession.StateTo .Fields("http://schemas.microsoft.com/exchange/" _ & "outlookmessageclass").value = "IPM.Note.Workflow" .Fields.Update .SendWorkflowMessage 2 'cdowfAdd (not strict) End With End Sub
E-mail-based workflow is supported only by clients that understand how to send back MAPI-based messages because custom properties need to be set on the reply message. For example, you must set the workflowmessageid and parentprocessinstance on your reply so the workflow engine knows to correlate the e-mail response rather than create an entirely new work item. The easiest way to add these properties to your responses is to use a custom Outlook or Web application that adds these properties. If you are going to use a custom Outlook form, you add code such as the following to your Outlook form to take the original properties from the e-mail sent by the workflow engine and add the needed properties to the response. If the code does not work for you, you might need to use CDO 1.21 in your Outlook form to add these custom properties to the item. The Outlook object model sometimes does not work correctly with these properties, but CDO 1.21 does. (The Outlook object model is covered in Chapter 6, and CDO is covered in Chapter 11.)
Sub Item_Reply(ByVal Response) set CurrentProp = Response.UserProperties.Add( "http://" _ & "schemas.microsoft.com/cdo/workflow/" _ & "parentprocinstance", 1) CurrentProp.Value = Item.UserProperties("http://" _ & "schemas.microsoft.com/cdo/workflow/" _ & "parentprocinstance").Value set CurrentProp = Response.UserProperties.Add ("http://" _ & "schemas.microsoft.com/cdo/workflow/" _ & "workflowmessageid", 1) CurrentProp.Value = Item.UserProperties("http://" _ & "schemas.microsoft.com/cdo/workflow/" _ & "workflowmessageid").Value End Sub
The workflow engine, when you use the SendWorkflowMessage method, automatically adds the properties shown in the previous code to the outgoing message. When you receive the response back to the folder, you must tell the workflow engine to correlate the response back onto the work item. You can have the engine automatically do this for you by setting the http://schemas.microsoft. com/cdo/workflow/response in your custom form. If you do not set this property, you must update the TrackingTable in your workflow yourself. The TrackingTable is a record that the workflow engine keeps for each response. It contains standard properties for each response, such as date, state, e-mail address, and the tracking ID. It also contains custom fields that you can use for your custom data called custom0 to custom9 . The following code manually updates the TrackingTable for a response:
With WorkflowSession .TrackingTable.Fields("custom0") = .TrackingTable.Fields("custom0") _ & vbCrLf &.ReceivedMessage.TextBody .TrackingTable.Fields.Update End With
Note | You can debug your workflow solutions by using either the audit trail provider included with Exchange or script debugging. To enable script debugging, you must either select the script debugging option in the Workflow Designer or set to True the property on your workflow's event handler registration called http://schemas.microsoft.com/cdo/ workflow/enabledebug . For the debugger to work, you must make sure that just-in-time (JIT) debugging is enabled in Windows. You can do this by modifying a key in the registry under HKCU/Software/Microsoft/Windows Script Host/Settings/ActiveDebugging and setting it to 1 . |
You might want to graphically show or provide a text list of all the states in a workflow, and then perhaps display a list of current work items and finally display the state that the current work item is in. Retrieving and displaying the list of states ”or, for that matter, doing this with state transitions or other workflow elements ”is just a matter of retrieving the .wfd file for the workflow, which is stored in the folder. Then you just parse the XML that is contained in that file, looking for the specific XML elements you are interested in. The following code finds all row nodes in the XML that correspond to the different states in the workflow:
Function StateExist(strState, strStateArray) As Boolean Dim i, lo, hi As Integer StateExist = False lo = LBound(strStateArray) hi = UBound(strStateArray) For i = lo To hi If strStateArray(i) = strState Then StateExist = True Exit Function End If Next End Function Sub PopulateStates() ' Return all defined actions from specified workflow definition Dim cnn As ADODB.Connection Dim rec As ADODB.Record Dim fld As ADODB.Field Dim urlResource, urlFolder 'As String Dim xmlDoc As MSXML.DOMDocument Dim nodelist As MSXML.IXMLDOMNodeList Dim n As MSXML.IXMLDOMNode Dim strStateArray() As String Dim strState As String Dim intNrStates As Integer ReDim strStateArray(0) urlFolder = "http://thomriznt52/public/workflow/showstates/" urlResource = "showwfstate.wfd" ' Open the resource Set cnn = CreateObject("ADODB.Connection") With cnn .Provider = "Exoledb.Datasource" .Open urlFolder End With Set rec = CreateObject("ADODB.Record") rec.Open urlResource, cnn, adModeRead, adOpenIfExists ' Get the action table as XML Set fld = rec.Fields("http://schemas.microsoft.com/" _ & "cdo/workflow/actiontable") Set xmlDoc = CreateObject("MSXML2.DOMDocument") xmlDoc.validateOnParse = False ' Load the Action Table xmlDoc.loadXML fld.Value ' Get the row node instances Set nodelist = xmlDoc.documentElement.selectNodes("//z:row") ' Return the name of each state and remove duplicates intNrStates = 0 For Each n In nodelist strState = n.Attributes.getNamedItem("NewState").Text If strState <> "" And ((intNrStates = 0) Or _ Not StateExist(strState, strStateArray)) Then intNrStates = intNrStates + 1 ReDim Preserve strStateArray(intNrStates - 1) strStateArray(intNrStates - 1) = strState strStateList = strStateList & " " & strStateArray(intNrStates - 1) End If Next MsgBox "The states are: " & strStateList ' Clean up rec.Close cnn.Close Set rec = Nothing Set fld = Nothing Set cnn = Nothing Set xmlDoc = Nothing Set nodelist = Nothing Set n = Nothing End Sub Private Sub Command1_Click() PopulateStates End Sub
Once you've drawn out your process, implemented your conditions and actions, and written your script, the next step is to deploy your workflow process to a folder. The Workflow Designer makes this step easy because you can save the workflow process into any folder in which you have permissions to create a workflow. Figure 17-16 shows how to select a public folder via the Save Workflow Process To Folder dialog box in the Workflow Designer.
In some cases, you might need to programmatically deploy your solutions. Unfortunately, the Workflow Designer doesn't have an object model that you can automate to use the Save Workflow Process To Folder feature. Instead, you have to write some code to deploy your workflow process. If you do so, you should first use the Workflow Designer to export to XML the action table for your workflow process.
The following code, taken from the Training application setup program, shows how to deploy your workflow application programmatically. You'll notice the following steps in the code:
Create your common script file (if necessary).
Create a workflow ProcessDefinition object.
In the ProcessDefinition object, add your action table by creating a new RecordSet object and using the XML features of ADO to load the XML version of the action table that the Workflow Designer saved for you.
As part of creating your ProcessDefinition object, select your audit trail provider, set the location of your common script file, and set the mode that the workflow process should run under (restricted or privileged).
Save the ProcessDefinition object into the folder.
Create the event registration items for the OnSyncSave , OnSyncDelete , and OnTimer events. On the server events registration item for OnSyncSave and OnSyncDelete , set the properties in the http://schemas.microsoft.com/ cdo/workflow/ namespace ”for example, the pointer to the default process definition for the folder, whether ad hoc workflows are allowed in the folder, whether to enable script debugging, and whether to log successful state transitions to the audit trail provider.
Private Sub AddWorkflowProcess() Dim oRS As New ADODB.RecordSet Dim oPD As New CDOWF.ProcessDefinition On Error GoTo errHandler 'Add the common script file Dim oScriptRec As New ADODB.Record Dim oStream As New ADODB.Stream 'Load the script file Dim fso As New Scripting.FileSystemObject Dim ofile As TextStream Set ofile = fso.OpenTextFile(App.Path & "\commonscript.txt") strCommonScript = ofile.ReadAll oScriptRec.Open strPath & "/Pending/commonscript", oConnection, _ adModeReadWrite, adCreateNonCollection oStream.Open oScriptRec, adModeReadWrite, adOpenStreamFromRecord With oStream .Charset = "unicode" .Type = adTypeText .Position = 0 .SetEOS .WriteText strCommonScript .Position = 0 .Flush .Close End With strScriptURL = oScriptRec.Fields("DAV:href").Value 'Load the action table oRS.Open App.Path & "\actiontable.xml" With oPD .ActionTable = oRS .AuditTrailProvider = "CDOWF.AuditTrailEventLog" .CommonScriptURL = strScriptURL .Mode = cdowfPrivilegedMode .Fields("DAV:ishidden") = True End With oPD.DataSource.SaveTo strPath & "/Pending/WFDEF", oConnection, _ adModeReadWrite, adCreateNonCollection strPDHREF = oPD.Fields("DAV:href").Value 'Create the event registrations 'First create the timer event arrRequired = GenerateRequiredEventArray("", "ontimer", _ "CdoWfEvt.EventSink.1", "","") strNow = Now arrOptional = GenerateOptionalEventArray("", "", "", "", 15, _ strNow, "") CreateEvtRegistration oConnection, strPath & "Pending/timer", _ arrRequired, arrOptional, True 'Create the OnSyncSave and onSyncDelete registration arrRequired = GenerateRequiredEventArray("WHERE ""DAV:ishidden"" = " _ & "false AND ""DAV:isfolder"" = false", _ "onsyncsave;onsyncdelete", _ "CdoWfEvt.EventSink.1", "", "") 'Create a new array and add some further properties for workflow Dim arrWorkflowRequired(6, 1) For i = LBound(arrRequired) To UBound(arrRequired) arrWorkflowRequired(i, 0) = arrRequired(i, 0) arrWorkflowRequired(i, 1) = arrRequired(i, 1) Next 'Add workflow properties arrWorkflowRequired(3, 0) = _ "http://schemas.microsoft.com/cdo/workflow/defaultprocdefinition" arrWorkflowRequired(3, 1) = strPDHREF arrWorkflowRequired(4, 0) = _ "http://schemas.microsoft.com/cdo/workflow/adhocflows" arrWorkflowRequired(4, 1) = 0 arrWorkflowRequired(5, 0) = _ "http://schemas.microsoft.com/cdo/workflow/enabledebug" arrWorkflowRequired(5, 1) = False arrWorkflowRequired(6, 0) = _ "http://schemas.microsoft.com/cdo/workflow/disablesuccessentries" arrWorkflowRequired(6, 1) = False 'Enable success entries CreateEvtRegistration oConnection, strPath & "Pending/workflowreg", _ arrWorkflowRequired, arrWorkflowOptional, True Exit Sub errHandler: MsgBox "Error in AddWorkflowProcess. Error " & Err.Number & " " & _ Err.Description End End Sub
One gotcha you should be aware of when you deploy workflow solutions is a feature that can trip you up if you don't understand it. The workflow event handlers have two COM+ roles that they implement, which you saw earlier: CanRegisterWorkflow and PrivilegedWorkflowAuthors . If you don't understand what these roles are used for and how the workflow engine uses them, you might run into some issues. This section outlines how these two roles and the workflow engine work together.
The CanRegisterWorkflow role is used when someone attempts to register for the Workflow event handler. The Workflow event handler implements ICreateRegistration , so when someone attempts to register for the Workflow event handler, the event handler is called to verify whether it wants to allow the registration to go through. The Workflow event handler calls the COM+ method IsUserInRole(CanRegisterWorkflow) to determine whether the user attempting the registration is authorized to do so. If this call returns True , the Workflow event handler allows the registration to go through.
The PrivilegedWorkflowAuthors role is used to ensure that any executable workflow code to be run in Privileged mode has not been tampered with by an unauthorized person. Here's the scenario: User A has privileged permissions and registers a new workflow that contains a script to run in privileged mode. User B is allowed to write only sandboxed workflows, but he does have write access to user A's script file. User B later inserts malicious script into these workflow files, knowing it will be run in Privileged mode because User A has privileged workflow permissions.
To prevent this, at run time, the Workflow event sink checks to see which user last modified and saved the process definition, the script, and the event handler registration item. For each of these SIDs, the event sink calls COM+ IsUserInRole(PrivilegedWorkflowAuthors) . If any of these documents were last modified by a nonprivileged person, the workflow engine knows that the files were tampered with. The workflow engine immediately stops execution and logs a security error.
So, if you want to run privileged mode workflows, you must make sure that the account used to save all the critical documents, such as the process definition, scripts, and event registrations, is a member of the PrivilegedWorkflowAuthors role.