Shockingly, when I arrived at my current company, they had basically NO scheduled agents at all. Apparently, someone had decided long ago that scheduled agents were dangerous, that they would overwhelm and crash the servers. So, whenever anything was done, it was done manually. This even extended to user notifications. That is, if I submitted a document for approval, there was some formula language that would populate a new notification message in the client and the user would fill in any extra details before clicking send. I was shocked. As I’ve modified designs, I’ve been adding background notifications and also scheduled agents. Our main project management database, which our field offices use copies of to manage their projects, hasn’t been mine to modify, since it’s already working and there is a team that customizes the design for each field office.
As we’ve been delving further into XPages and as I’ve been spreading the good word about scheduled agents and notifications, we’re now finally putting them into those project management databases. One hurdle though. Our admin team has, quite rightly, limited who can sign agents that will run on production servers.
Concept
Now, I’ve designed dozens or even hundreds of notification and reminder agents in many databases over the decades, but I always designed them from scratch, customizing it to the particular database and the particular recipients. I’d created a basic one and Ariwan Susey, who’s really coming up to speed on LotusScript and XPages, modified it for use in that project management database. This was nice, and Virginia Tauss had started creating copies of it, customized for each notice type. However, every time someone made a change to the half-dozen agents, I had to sign them. Since they were customized for their particular database and the particular recipients, this meant that eventually, I might spend all day signing agents instead of writing code.
Since the agents were almost the same, except for what view they used and who received the message, I realized that if I created a basic agent, they could use configuration documents to customize as many notices as they wanted and I’d never have to sign that configurable agent again!
Configuration Choices
There were a few basic things I knew would be different between each notification: the view, the recipients, the subject, the server to run on and the time to run. After creating some tests, I also realized that I wanted to emulate the scheduling choices of agents themselves and allow the user to select weekly or monthly notifications instead of just daily. I also remembered that sometimes, they would want to mark the document after they sent the notice, so I made that a configuration choice as well. Based on my recent experience in my Excel series (part 1, part 2, and the sample database) and with full-text queries, I realized we could use those full-text queries in these notifications as well.
So, here’s my form:
Since I’ve been fiddling with DXL editing of forms lately, let me include the DXL for that third row for your review. The right cell contains a table for displaying the weekday or day of the month choices, with the hide-whens appropriately.
<tablerow> <tablecell><par def='4'>Day(s) to run:</par></tablecell> <tablecell> <par def='5'> <field borderstyle='none' lookupeachchar='false' lookupaddressonrefresh='false' type='keyword' kind='editable' name='frequency'> <keywords helperbutton='false' recalconchange='true' columns='3' ui='radiobutton'> <textlist><text>Daily</text><text>Weekly</text><text>Monthly</text></textlist> </keywords> </field> </par> <table leftmargin='0' widthtype='fixedleft' refwidth='2.5000in'> <tablecolumn width='1in'/><tablecolumn width='1.5000in'/> <tablerow> <tablecell valign='center' borderwidth='0px'> <pardef id='6' spacebefore='1.5' keepwithnext='true' keeptogether='true'> <code event='hidewhen'><formula>frequency != "Weekly"</formula></code> </pardef> <par def='6'>Day of week: </par> </tablecell> <tablecell valign='center' borderwidth='0px'> <pardef id='7' spacebefore='1.5' keepwithnext='true' keeptogether='true'> <code event='hidewhen'><formula>frequency != "Weekly"</formula></code> </pardef> <par def='7'> <field usenotesstyle='false' height='0.2500in' width='1in' multiline='true' borderstyle='none' lookupeachchar='false' lookupaddressonrefresh='false' type='keyword' kind='editable' name='weekdayToRun'> <keywords helperbutton='false' columns='1' ui='combobox'> <textlist> <text>Sunday|1</text> <text>Monday|2</text> <text>Tuesday|3</text> <text>Wednesday|4</text> <text>Thursday|5</text> <text>Friday|6</text> <text>Saturday|7</text> </textlist> </keywords> </field> </par> </tablecell> </tablerow> <tablerow> <tablecell valign='center' borderwidth='0px'> <pardef id='8' keepwithnext='true' keeptogether='true'> <code event='hidewhen'><formula>frequency != "Monthly"</formula></code> </pardef> <par def='8'>Day of month:</par> </tablecell> <tablecell valign='center' borderwidth='0px'> <pardef id='9' keepwithnext='true' keeptogether='true'> <code event='hidewhen'><formula>frequency != "Monthly"</formula></code> </pardef> <par def='9'> <field type='number' kind='editable' name='monthdayToRun'> <numberformat format='general' digits='2' punctuated='false' parens='false' percent='false' bytes='false'/> <code event='defaultvalue'><formula>1</formula></code> <code event='inputvalidation'><formula>@If ( frequency != "Monthly"; @Success; @ThisValue > 1 & @ThisValue < 29; @Success; @Failure ( "Must be in the first 28 days of the month"))</formula> </code> </field> </par> </tablecell> </tablerow> </table> <pardef id='10' keepwithnext='true' keeptogether='true'> <code event='hidewhen'><formula>frequency != "Monthly"</formula></code> </pardef> <par def='10'><run><font size='1pt'/></run></par> </tablecell> </tablerow>
As I use the source view more in XPages, I get more and more comfortable with just editing code, and checking appearances occasionally. While I have only done a little of that in forms, I have used it several times in views. When I created this form, my initial design of it was done by creating a single in the normal designer form, then saving it, and re-opening it in DXL. Then I added several fields to a form with cut-and-paste for field names. Using the properties boxes just seemed like it would take so much longer – after all, I had the field names in my notepad already.
The Agent
Our agent is set to run hourly, on every server. If there are no autoNotify documents, it doesn’t do anything, but if there are, it checks each one for whether it runs on that server, on that day and at that hour.
Sub Initialize Dim session As New NotesSession ' thisdb is declared in my utilities library, so not declared here ' Dim autoNotifyView As NotesView Dim autoNotifyDoc As NotesDocument Dim serverToRunOn As Variant Dim hourToRun As Variant Dim frequency As Variant Dim weekdayToRun As Variant Dim monthdayToRun As Variant Dim noticeName As Variant Dim hourNow As Integer Dim weekdayToday As Integer Dim monthdayToday As Integer Dim reason As String On Error GoTo errorhandler Set thisdb = session.Currentdatabase Call StartAgentLogging ( session ) Dim serverName As New NotesName ( thisdb.Server ) ' get view of autonotify documents ' Set autoNotifyView = thisdb.Getview("AutoNotify") Set autoNotifyDoc = autoNotifyView.Getfirstdocument() While Not autoNotifyDoc Is Nothing ' check server to run on ' serverToRunOn = autoNotifyDoc.Getitemvalue("serverToRunOn") If ( Ucase ( serverToRunOn (0) ) = Ucase ( serverName.Common ) ) Then ' check frequency and day ' frequency = autoNotifyDoc.Getitemvalue("frequency") weekdayToRun = autoNotifyDoc.Getitemvalue("weekdayToRun") If ( weekdayToRun (0) = "" ) Then weekdayToRun (0) = "0" End If weekdayToday = Weekday ( Today ) monthdayToRun = autoNotifyDoc.Getitemvalue("monthdayToRun") monthdayToday = Day ( Today ) If ( frequency (0) = "Daily" or ( frequency (0) = "Weekly" And CInt (weekdayToRun (0)) = weekdayToday ) Or ( frequency (0) = "Weekly" And CInt ( monthdayToRun (0) ) = monthdayToday ) ) Then ' check hour to run ' hourToRun = autoNotifyDoc.Getitemvalue("schedule") hourNow = Hour (Now) If ( CInt ( hourToRun (0) ) = hourNow ) Then noticeName = autoNotifyDoc.Getitemvalue("NoticeName") If ( sendNotices ( autoNotifyDoc ) ) Then Call agentLog.LogAction ( noticeName (0) & " sent") Else Call agentLog.LogAction ( noticeName (0) & " FAILED") End If End If End If End If Set autoNotifyDoc = autoNotifyView.Getnextdocument(autoNotifyDoc) Wend Call agentLog.LogAction ( "Completed" ) exiting: Exit Sub errorhandler:' report all errors in a messagebox ' reason = "Error #" & CStr (Err) & " (" & Error & ") on line " & CStr (Erl) MessageBox reason, 16, "Error" Call agentLog.LogAction ( reason ) Resume exiting ' transfers control to the exiting label End Sub
The actual notification builds off the values from the configuration document. At MWLUG, speakers recommended making sure to use functions instead of subroutines, partly because functions return a value and partly for forward compatible with other programming languages. So, my sendNotices function is a boolean, indicating success or failure.
The simplest, yet most powerful part of the script is the application of the querystring. By using that, I could create dozens of notifications from a single view, saving myself disk space by avoiding unnecessary view indices.
Ariwan’s great contribution to the basic agent that made it so useful in this configurable design was the use of columnvalues. The agent simply spits out the contents of the view, populating the message with the details of the document regardless of which fields are used. I’d never thought of doing that!
You’ll notice that in the loop, we get a handle to the nextdoc before processing. If the document would be removed from the view by marking one of the fields “Yes” and saving the document, we need to already have a handle to the next document. If we don’t do that, the view won’t be able to find the next document by referring to the current document, as it has no position in the view any more.
Now, since I want each notification to be processed even if I encounter some errors, I added error-handling in the function as well. If I had not, an error would bubble up to the Initialize routine and stop my agent. This way, it only stops that particular notification, but continues to the next one.
Function sendNotices ( autoNotifyDoc As NotesDocument ) As Boolean Dim viewName As Variant Dim recipientGroup As Variant Dim subjectLine As Variant Dim introText As Variant Dim queryString As Variant Dim flagField As Variant Dim workingView As NotesView Dim workingCollection As NotesDocumentCollection Dim doc As NotesDocument Dim nextdoc As NotesDocument Dim memo As NotesDocument Dim body As NotesRichTextItem Dim reason As String Dim count As Integer sendNotices = false ' get viewName ' viewName = autoNotifyDoc.Getitemvalue( "viewName" ) Set workingView = thisdb.Getview ( viewName (0) ) ' apply query string, if there is one ' queryString = autoNotifyDoc.Getitemvalue( "queryString" ) If ( queryString (0)<> "" ) Then Call workingView.Ftsearch(queryString(0), 0) End If Set doc = workingView.Getfirstdocument() count = 0 Set memo = thisdb.Createdocument() Set body = memo.Createrichtextitem("Body") memo.Principal = thisdb.Title ' copy the introductory text from the autoNotify document into the email ' introText = autoNotifyDoc.Getitemvalue( "introText" ) Call body.Appendtext ( introText(0) ) Call body.Addnewline(2) While Not doc Is Nothing Set nextdoc = workingView.Getnextdocument(doc) count = count + 1 Call body.Appendtext( CStr ( count ) & "." ) Call body.Addtab(1) ForAll thing In doc.Columnvalues If ( IsArray ( thing ) ) Then Call body.Appendtext( Implode (thing, ", " ) ) Else Call body.Appendtext( thing ) End If Call body.Addtab(1) End ForAll Call body.Appenddoclink(doc, "Open the doc", "Link") Call body.Addnewline(1) ' if field to mark, then modify field and save doc ' flagField = autoNotifyDoc.Getitemvalue( "flagField" ) If ( Trim ( flagField (0) ) <> "" ) Then Call agentLog.LogAction ( flagField (0) & " field #" & CStr ( count ) ) Call doc.ReplaceItemValue ( flagField (0), "Yes" ) Call doc.Save ( True, False ) End If Set doc = nextdoc Wend subjectLine = autoNotifyDoc.Getitemvalue( "subjectLine" ) memo.Subject = CStr (count) & " " & subjectLine (0) recipientGroup = autoNotifyDoc.Getitemvalue("recipientGroup" ) Call memo.Send(False, DetermineKeyword ( recipientGroup(0)) ) sendNotices = True exiting: Exit Function errorhandler:' report all errors in a messagebox ' reason = "Error #" & CStr (Err) & " (" & Error & ") on line " & CStr (Erl) MessageBox reason, 16, "Error" Call agentLog.LogAction ( reason ) Resume exiting ' transfers control to the exiting label ' End Function
It’s not quite perfect because if the server is down, it won’t run the notification later. I might take that into account in a future version, since many our project servers are in locations where power may not always be 24×7. Similarly, if someone puts too many notifications to run at the same time, the agent could time out, failing to run all of them.
Hopefully, this exercise proves useful to someone else. I can’t believe I spent more than a decade constantly re-writing the same code when I could have saved myself considerable time by just creating a customizable, reusable piece of code back in the day. Live and learn!
Update:
Turns out there was a bug in the code. The simple loop through the columnValues didn’t take into account multi-value fields. So, when the agent ran on a view with a document that had multiple values, it was trying to print a variant as text. So, I added a simple check for IsArray and imploded the multi-value field to build a comma-delimited string. That avoids the type mismatch that our script was throwing when it found those multi-value fields as shown at right in the debugger.
Hey David, good idea. You can also write script to sign agents so if you had a database with an approval process for a scheduled agent created by someone else you could review and then have code that not only signs the agent but also enables it.