· May 9 9m read

Microsoft 365 - Events and Tasks

 In our previous article, we explored how to send emails through Microsoft 365 using the Graph API. Since then, an anonymous client reached out to me about setting up some other methods of notifications through that API. He was particularly interested in Outlook’s tasks and calendar events. 

If you still have your client ID, client secret, and application ID from the last exercise, you may continue utilizing them. We will reuse the globals we stored from before with the GetToken method. Most of the setup in Microsoft Entra will not need to be repeated. The only exception would be that you will have to go back to your application permissions and add the correct permissions for each item. We will start with adding a Task that requires permission Tasks.ReadWrite.All. Add that permission and grant admin consent employing the same process we described in the previous article.

As always, our API request will be built from a %Net.HttpRequest object. In this case, we will have to make two additional requests besides the one to get the token. Since Tasks in Outlook currently use Microsoft ToDo, your tasks can be organized into lists now. To create a task we must know the ID of the list to which we should send it. We will utilize the default task list for that. To find out its ID we need to create a request to the ToDo lists API. It will be a simple HTTP get request with no entity body, so it will be very easy.

set listRequest = ##class(%Net.HttpRequest).%New()
set listRequest.Server = ""
set listRequest.SSLConfiguration = "O365"
set listRequest.Https = 1
set listRequest.Location = "/v1.0/users/"
do listRequest.SetParam("$filter","displayName eq 'Tasks'")
set sc = ..GetToken(.token)
do listRequest.SetHeader("Authorization","Bearer "_token)
set sc = listRequest.Get()

Pay attention to the parameter we set. On HTTP get requests, the graph API supports some OData filtering. By adding it, we specify that we are looking for the task list with the display name “Tasks,” which should be the default task list for most users. Since my client is not very tech savvy - he says he only ever accepts cookies with milk - he hasn’t changed the name of his default task list.

Note that we are going to employ the same basic setup that we operated for the email API request. The server will be We will  utilize the O365 SSL configuration. We will also get the bearer token and add it to the header. Then we will send the response to a location that includes the target user’s email address. This time we will operate the request’s Get() method rather than Post(). It will return a JSON response that will look similar to the one below:

    "@odata.context": "$metadata#users('########-####-####-####-############')/todo/lists",
    "value": [
            "@odata.etag": "#####################################",
            "displayName": "Tasks",
            "isOwner": true,
            "isShared": false,
            "wellknownListName": "defaultList",
            "id": "There will be a big long string here."

Without the filter parameter, we would get all user’s task lists. They often have at least two lists: a default task list and a separate one for flagged emails. If they set up other lists in ToDo, there will be even more. We are only interested in the default task list today. To get its ID, we should do the following:

set respObj = ##class(%Library.DynamicObject).%FromJSON(listRequest.HttpResponse.Data)
set respArray = respObj.%Get("value")
set listObj = respArray.%Get(0)
set ListID = listObj.%Get("id")

We need to use it in the task location to complete the request, so let's set it up similarly to the previous request.

set taskRequest = ##class(%Net.HttpRequest).%New()
set taskRequest.SSLConfiguration = "O365"
set taskRequest.Https = 1
set taskRequest.Server = ""
set taskRequest.ContentType = "application/json"
set taskRequest.Location = "/v1.0/users/"_ListID_"/tasks"

The body of this request will have a JSON structure. We could set just the title property of this JSON object and send the request. If we did it, it would give the user a new task with only that title and nothing else.

set mytask = ##class(%Library.DynamicObject).%New()
do mytask.%Set("title","Merry Christmas!")

However, my client’s needs are more advanced than that! He has a recurring annual task that requires a bit more information, including a small description, a due date, a reminder, and some recurrence. We will start with the body which will contain a very basic reminder. He is getting old, you know. So, the body will be a JSON object itself. Just like the email body, it will have two properties. One of them will be contentType which can be either “text” or “html”, and the other will be content, which will consist of the actual content of the body. Let's stick to a simple text.

set body = ##class(%Library.DynamicObject).%New()
do body.%Set("content","Don't forget to deliver!")
do body.%Set("contentType","text")
do mytask.%Set("body",body)

Since my client is a very influential man in charge of some crucial work, we will also set the importance of this task.

do mytask.%Set("importance","high")

He has also requested to add a due date and a reminder on the task. Additionally, his dates should be easy enough to remember. The dueDateTime property of the task should also be a JSON object with both a timestamp and a time zone. The reminder will likewise be the same kind of object, so we should set the isReminderOn property to true.

set duedate = ##class(%Library.DynamicObject).%New()
do duedate.%Set("dateTime","2024-12-25T00:00:00")
do duedate.%Set("timeZone","Etc/GMT")
do mytask.%Set("dueDateTime",duedate)
set reminder = ##class(%Library.DynamicObject).%New()
do reminder.%Set("dateTime","2024-12-24T00:00:00")
do reminder.%Set("timeZone","Etc/GMT")
do mytask.%Set("reminderDateTime",reminder)
do mytask.%Set("isReminderOn","true","Boolean")

Next, we need to deal with the recurrence. The recurrence object typically contains two more objects: a pattern and a range. The pattern tells us when this task recurs, and the range suggests how long it recurs. My client’s task is annual and will continue without an end, so I guess he will be doing this forever! 

We will start with the pattern. The type of pattern can be daily, weekly, relativeMonthly, absolutelyMonthly, relativeYearly, or absoluteYearly. For the monthly and yearly options, the absolute indicates that the event will occur on the exact same date, for example, September third of every year or on the tenth of every month. Relative means a certain occurrence of a day, like the third Thursday of every month or the fourth Thursday of every November. In my client’s case, it is an absolute yearly recurrence. The interval is based on the type. If it is a yearly event, 1 means this event will repeat every year. If it were a weekly event, an interval of 1 would mean one week. We should also set the month and dayOfMonth. For other types of recurrence, you may need to set the daysOfWeek property as well. It is a collection that can contain any day of the week. However, it does not apply in our case.

set pattern = ##class(%Library.DynamicObject).%New()
do pattern.%Set("type","absoluteYearly")
do pattern.%Set("interval",1)
do pattern.%Set("month",12)
do pattern.%Set("dayOfMonth",25)

The range can have one of three types: noEnd, endDate, and numbered. Since we are going to use the noEnd type, we only need to specify a start date and the type. If we were using the endDate type, we would have to specify an endDate, and if we were employing the numbered type, we would need to specify numberOfOccurrences as a positive integer telling us how many times the event will repeat.

set range = ##class(%Library.DynamicObject).%New()
do range.%Set("type","noEnd")
do range.%Set("startDate","2024-12-25")

Then we need to add the following code to a recurrence object and later add that object to our task. 

set recurrence = ##class(%Library.DynamicObject).%New()
do recurrence.%Set("pattern",pattern)
do recurrence.%Set("range",range)
do mytask.%Set("recurrence",recurrence)

Now we are finally ready to send off our request!

do taskRequest.EntityBody.Write(mytask.%ToJSON())
do taskRequest.Post()

If we have done everything correctly, we should get an HTTP response with a Status Code of 201 telling us that the task was created. The user should be able to see it in Outlook as soon as their Outlook syncs again.


After my client’s big annual event, they usually have a company party. He would like me to create an event on his Outlook calendar for it. To do it, you will need to go back to Entra and give your application the Graph API permission Calendars.ReadWrite. Simply sending an event request without specifying a calendar ID will add the event to the user’s default calendar. Once again, we will create a %Net.HttpRequest with our usual setup.

set evtRequest = ##class(%Net.HttpRequest).%New()
set evtRequest.Server = ""
set evtRequest.SSLConfiguration = "O365"
set evtRequest.Https = 1
set evtRequest.Location = "/v1.0/users/"_..UserAddress_"/calendar/events"
set evtRequest.ContentType = "application/json"
set sc = ..GetToken(.token)
do evtRequest.SetHeader("Authorization","Bearer "_token)

Next, we have to set up the body of the request. This object will contain some properties that should look very familiar to you. The subject will be a simple string with a description of the event. The body will be the same kind of object as email and task bodies. There we must specify the content type as either "html" or "text". Additionally, remember to mention the start and end dates and times with the time zone included.

set evtObj = ##class(%DynamicObject).%New()
do evtObj.%Set("subject","After Party")
set bodyObj = ##class(%DynamicObject).%New()
do bodyObj.%Set("contentType","html")
do bodyObj.%Set("content","Party after the big day! <br /><b>BRING YOUR OWN NOG!</b>")
do evtObj.%Set("body",bodyObj)
set start = ##class(%Library.DynamicObject).%New()
do start.%Set("dateTime","2024-12-26T20:00:00")
do start.%Set("timeZone","Etc/GMT")
do evtObj.%Set("start",start)
set end = ##class(%Library.DynamicObject).%New()
do end.%Set("dateTime","2024-12-27T00:00:00")
do end.%Set("timeZone","Etc/GMT")
do evtObj.%Set("end",end)

There is a reminderMinutesBeforeStart field that will set a reminder for an event.

do evtObj.%Set("reminderMinutesBeforeStart",60)

We can also add a location, which is an object that can contain a name, address, coordinates, and some contact information.

set locObj = ##class(%Library.DynamicObject).%New()
do locObj.%Set("displayName","SeasonalSpirits")
set coordObj = ##class(%Library.DynamicObject).%New()
do coordObj.%Set("latitude",90)
do coordObj.%Set("longitude",0)
do locObj.%Set("coordinates",coordObj)
do evtObj.%Set("location",locObj)

It will not be much of a party if no one comes, so we should add some attendees too. It will be the same style of email address we used before, plus a type field specifying whether the attendee is required, optional, or a resource.

set attendees = ##class(%Library.DynamicArray).%New()
set myatt1 = ##class(%Library.DynamicObject).%New()
do myatt1.%Set("type","required")
set myemail1 = ##class(%Library.DynamicObject).%New()
do myemail1.%Set("name","Mr C.")
do myemail1.%Set("address","")
do myatt1.%Set("emailAddress",myemail1)
do attendees.%Push(myatt1)
set myatt2 = ##class(%Library.DynamicObject).%New()
do myatt2.%Set("type","optional")
set myemail2 = ##class(%Library.DynamicObject).%New()
do myemail2.%Set("name","Mrs. C.")
do myemail2.%Set("address","")
do myatt2.%Set("emailAddress",myemail2)
do attendees.%Push(myatt2)
do evtObj.%Set("attendees",attendees)

Finally, we are ready to write the request body and post it.

do evtRequest.EntityBody.Write(evtObj.%ToJSON())
do evtRequest.Post()

Once again, if we have done all of the abovementioned correctly, we should get an HTTP response with a status code of 201, and the user should now have the event on their default calendar.


That is all for now! However, it seems to me that I have just got another email from my mysterious client. It is something related to managing big and busy teams of builders, stable managers, etc. This guy’s business is really weird! I have a feeling I will be back soon, talking about Teams.  

Discussion (0)1
Log in or sign up to continue