What is Microsoft Teams Shifts and how you can customize it – Part 2

What is Microsoft Teams Shifts and how you can customize it – Part 2

Last updated on October 16, 2021

Are you interested in knowing what kind of customizations you can build for Microsoft Teams Shifts? You’ve come to the right place!

Our developer lives are not so simple that it’d be enough if we just know how to code. When implementing customizations on top of a product, we first need to understand how the product works. Only after we are aware of what we can do out-of-the-box, can we avoid doing something stupid. If you haven’t yet got a chance to use Shifts, I recommend that you read the first part of this series before diving deeper into this article, so you’ll have a better idea of what the heck I am talking about here. 🙂

Published articles in this blog post series

Table of contents

  1. How we can customize Shifts
  2. Examples of useful processes
  3. Use case: Requesting for a substitute employee
    1. Setting up scheduling groups
    2. An order form in extranet
    3. Reviewing and approving the order
  4. The current significant problems of the Shifts API in Microsoft Graph
  5. The conclusion

How we can customize Shifts

Just like you can’t modify the user interface of Teams (except inside a custom tab), you can’t remodify the UI of Shifts either. However, you can implement automated business processes for Shifts by utilizing the Microsoft Graph API.

Whenever there is an API available for doing the same things we can do via a graphical user interface, I always think what are the common processes we can automate so the user doesn’t need to do them manually, and can instead spend their time doing something else more meaningful?

Microsoft Graph offers a lot of operations for Shifts and new operations are regularly introduced. Instead of attempting to keep a track of them here, I kindly ask you to check the Microsoft Graph documentation for Shifts for the latest information.

Examples of useful processes

Let’s see what we can build with the operations available on Microsoft Graph! So far, I’ve come up with the following ideas for Shifts:

  1. If you have an automated Teams provisioning process in place, you could add Shifts to be a part of it.
    • Automatically add the Shifts schedule for the team. This way the feature is there from the beginning and no one needs to create it for each team by hand.
    • You could also think if there is a common scheduling group structure all teams could benefit from. By provisioning the group structure for all schedules automatically, you ensure they are consistent across all teams.
    • Before you can assign shifts to the team members, you always need to add them to the scheduling groups first. Does your provisioning pipeline already add owners and members to the team? Continue by adding members to the scheduling groups as well so the manager doesn’t need to include all of them manually before they are able to assign shifts.
    • Are there some shifts that are commonly used across all teams? If yes, provision one of each, and the managers can then easily copy them when they start constructing a new schedule.
    • Or better yet, you could even create an entire schedule template. You can copy a schedule (shifts within a certain timespan) through the GUI. However, you can only do that within that one team. You can’t copy schedules between teams. This means that even if you had already created a great schedule for one team, you can’t copy-paste it to another. You always need to start from scratch. However, what you could do is to automatically create that awesome schedule for all teams as they get provisioned. After that, the team managers have access to that thought-out shift structure in all new teams.
  2. A person requests for time off in Shifts, and after the request has been approved, information about the time off is synchronized to an HR/salary system automatically.
  3. Someone else than the team manager needs to create a shift for the team. For this purpose, you can create, for example, an order form, and upon submitting, a background process creates the shift based on the provided information. I had a real customer case about this scenario, so let me tell you a little bit more about that.

Use case: Requesting for a substitute employee

This example is based on a real-life PoC request from a customer. They are in the business of offering temporary employees to all shops in a particular field. The shops can request for a substitute employee if the regular employee falls ill, goes on vacation, or if extra staffing is seasonally required, for example. The customer already has their own system with shift scheduling and booking processes, but since they are moving to Office 365, and will soon start to using SharePoint Online and Teams anyway, they were interested to know if they could replace their old system with Shifts.

Note that what I am about to show you is a PoC and far from a complete, polished solution. Its purpose is purely to demonstrate what we can do in relation to Shifts.

Setting up scheduling groups

Microsoft Teams Shifts offers all the tools the customer needs for managing the requested shifts. But before that can happen, we need to set up the team and schedule structure.

  • First, we want to create teams based on the service areas or regions. The substitute employees need to be physically present to perform the shifts, so they need to live in fairly close proximity.
  • Then, we need to add scheduling groups for all the different shops in that area (team).
  • We should also add all the employees who are eligible to work at the shops as the as members in those scheduling groups. Only after that, we can assign shifts to them.
  • Finally, we’ll also add a “Shift Orders” service account to all of the scheduling groups; more about this later.

Creating several teams like this isn’t necessarily required though: you could have just one team for everything as the number of scheduling groups you can have within a team seems to be unlimited. In fact, having only one team probably works best when there are not a lot of shops in total — and all of the team members are allowed to see the schedules of all the other shops. However, in this case, with large quantities of shops and employees, it could potentially become challenging to manage if everything was placed in just one team. If the organization already has certain regions with their own regional managers, etc., following that structure for teams can work really well.

An order form in extranet

Now the only feature we are missing is the booking process, and that we need to build by ourselves. As with many other provisioning processes, this one starts with an order form with the following fields:

  • Title: Title of the order. Appears in the emails we are going to send.
  • City: The team to which schedule the shift will be created in.
  • Store: The scheduling group in which the shift will be created in.
  • Special requirements: Something the team manager needs to take into consideration when assigning the shift to someone. Appears in the shift description.
  • Start time: The start time of the shift.
  • End time: The end time of the shift.
  • Lunch at: The start time of lunch or some other meal/break during the shift. The duration is always 30 minutes as set by our process below.

This form is elementary in its current state. It is an out-of-the-box list form and is currently meant only for booking single shifts. Notably, the date pickers that are related to the DateTime fields are not the most suitable for this case. But the form is sufficient for our PoC. For the real solution, I’d implement the form as an SPFx web part, so we can have more flexibility with the form controls. For example, not all cities have the same stores, and hence those dropdown values need to be populated depending on the selected city.

Whether the form is the out-of-the-box list one or a custom SPFx web part, we can add it on a page in our SharePoint Online extranet, or pin it as a tab in a Teams team where our external users can access it. As the user submits the filled form, the information gets saved to a SharePoint list, which again triggers a Power Automate flow.

Reviewing and approving the order

The background process is implemented with Power Automate, in this case. However, after the Power Automate licensing changes, you might rather want to implement this as an Azure Logic App instead. Be mindful about the trigger though: You need to set the trigger to check for the list for new items on an interval, and whenever this polling happens, it will cost money. This means that if you poll very often, it will end up being a big sum of money. Either poll less frequently, or consider having an Azure function do the polling, which will then trigger your logic app whenever there is a new item to process.

Now, if this was an actual polished production process, I’d adjust the permissions to the list item at this point so that only the person who made the order and the approvers have access to it. However, this is just a PoC, so we’ll skip that part and proceed to fetch the team information for the selected “City” immediately. Here we’ll simply do that based on the dropdown option chosen, and trust that it matches the team name.

In my flow, I am using a custom batch connector for making the calls to Microsoft Graph. If you want to set one up yourself, there is a tutorial for that.

View the full JSON schema for the team response
{
    "type": "object",
    "properties": {
        "responses": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "status": {
                        "type": "integer"
                    },
                    "headers": {
                        "type": "object",
                        "properties": {
                            "Cache-Control": {
                                "type": "string"
                            },
                            "OData-Version": {
                                "type": "string"
                            },
                            "Content-Type": {
                                "type": "string"
                            }
                        }
                    },
                    "body": {
                        "type": "object",
                        "properties": {
                            "@@odata.context": {
                                "type": "string"
                            },
                            "value": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "id": {
                                            "type": "string"
                                        }
                                    },
                                    "required": [
                                        "id"
                                    ]
                                }
                            }
                        }
                    }
                },
                "required": [
                    "id",
                    "status",
                    "headers",
                    "body"
                ]
            }
        }
    }
}
 

Now that we have the team information, we can pick the team ID and use it for fetching the scheduling groups in that team’s schedule. After that, we’ll filter the received array and choose the group that matches the store that was selected on the form.

View the full JSON schema for the scheduling group response
{
    "type": "object",
    "properties": {
        "@@odata.context": {
            "type": "string"
        },
        "@@odata.count": {
            "type": "integer"
        },
        "value": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "@@odata.etag": {
                        "type": "string"
                    },
                    "id": {
                        "type": "string"
                    },
                    "createdDateTime": {},
                    "lastModifiedDateTime": {
                        "type": "string"
                    },
                    "displayName": {
                        "type": "string"
                    },
                    "isActive": {
                        "type": "boolean"
                    },
                    "userIds": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "lastModifiedBy": {
                        "type": "object",
                        "properties": {
                            "application": {},
                            "device": {},
                            "conversation": {},
                            "user": {
                                "type": "object",
                                "properties": {
                                    "id": {
                                        "type": "string"
                                    },
                                    "displayName": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "required": [
                    "@@odata.etag",
                    "id",
                    "createdDateTime",
                    "lastModifiedDateTime",
                    "displayName",
                    "isActive",
                    "userIds",
                    "lastModifiedBy"
                ]
            }
        }
    }
}
 

Unfortunately, we can’t do a $select for the scheduling groups. We also can’t do $filter, which is why we need to filter the array in our flow separately.

Even though our filtered array should have just one item, our flow will want to add an “Apply to each” loop, and hence we need to create a couple of helper variables before that happens. The defaultUserId is the ID of my “Shift Orders” user, and that will be receiving all new orders. The approvalMessage will derive its value based on the approval outcome and is then included in the email sent at the end.

Also, we’ll add all team owner emails to a variable, and use that for sending the approval requests.


View the full JSON schema for the owner response

{
    "type": "object",
    "properties": {
        "responses": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "status": {
                        "type": "integer"
                    },
                    "headers": {
                        "type": "object",
                        "properties": {
                            "Cache-Control": {
                                "type": "string"
                            },
                            "OData-Version": {
                                "type": "string"
                            },
                            "Content-Type": {
                                "type": "string"
                            }
                        }
                    },
                    "body": {
                        "type": "object",
                        "properties": {
                            "@@odata.context": {
                                "type": "string"
                            },
                            "value": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "@@odata.type": {
                                            "type": "string"
                                        },
                                        "mail": {
                                            "type": "string"
                                        }
                                    },
                                    "required": [
                                        "@@odata.type",
                                        "mail"
                                    ]
                                }
                            }
                        }
                    }
                },
                "required": [
                    "id",
                    "status",
                    "headers",
                    "body"
                ]
            }
        }
    }
}

And finally, all the times that got saved via the form are in UTC (regardless of the site location settings), so we need to convert those to the local time before we include them in any emails that we are about to send.

Now that all preparations have been made let’s create the shift! Note that I’m not assigning the Title as the display name of the shift. This is because the shift display name is limited to 15 characters. Microsoft Graph still creates the shift, no problem, but problems will arise when you attempt to manage that shift through the UI. If you want to assign the title, make a check for the max length. If you leave the display name blank, as I have in this case, the shift time will be shown on the schedule instead of the title, which can often even be more informative to the viewer.

Now the shift has been created in the team schedule and waits for input from the manager. We’ll use the traditional approval action in Power Automate which will send the manager an email with information about the order, a link to Shifts, and buttons as well as a comment field for either approving or rejecting the request.

After the manager receives the email, they can click the link to get directed to Shifts on the Teams web app. It’d be great if we could construct a deep-link to the schedule, so it’d open in the Teams client, but for the time being, I haven’t figured out how to do that.

The manager can now check the current schedule, and assign the order to a member in the scheduling group who is able to perform the shift.

After they have sorted out the shift order and it fits nicely in the schedule, they’ll go back to the email and let the person who made the order know that it has been taken care of. Alternatively, they could reject the request if the team is already too busy to handle it.

Let’s get back to our flow, which will now create a message for the person who made the order based on the approval outcome (click the image to make it bigger).

And that message about the approval outcome, along with the original shift information, will get sent to the person who made the order.

And that’s it. The process could be a lot smoother, though. What if our manager would, for example, like to change the shift information before approving it? Let’s dig deeper into the current shortcomings of the API, and what kind of pitfalls you need to be aware of when designing automated business processes for Shifts.

The current significant problems of the Shifts API in Microsoft Graph

Now, why did I create the shift before for my “Shift Orders” user? Simply because you can’t create an open shift via Microsoft Graph. You always need to create a shift for a user. Wouldn’t it be great to create open shifts automatically and then let the manager decide who they will be assigned to? Unfortunately, you can’t do that. However, you can set up a “service account” to act as this kind of an open shift pool, and assign all new shifts to that account when you create them. The manager can then move the shifts to the actual Open shifts and reassign them to the person who should be doing the shift.

Microsoft Graph also only returns shifts that have been assigned to someone. It doesn’t return any of the shifts that are in Open shifts. Even if you try to get all shifts, you’ll only receive those that are currently assigned to a user. It is as if a shift gets deleted whenever it is moved to Open shifts, and then recreated when it is assigned to a person. There are further indications of this: Whenever a shift is reassigned via Open shifts (there is no other way), its ID changes. This behavior makes it impossible to track a shift between reassignments reliably. There are a couple of things we can implement as workarounds, but both of these are far from bulletproof.

In our substitute booking process described above, we get the original shift ID back when we create the shift, but when the shift is reassigned by the manager from “Shift Orders” to an actual person, the shift ID changes. Because of this, we can’t simply fetch the updated shift information anymore via Microsoft Graph, and instead, just end up sending the original order information in the email. If we really want to get the updated shift information, we could try to work around this problem in two ways:

  • Manager updates the order information on the form to match the new shift, and our flow gets the updated information from the list item. Very clumsy, let’s not do this.
  • Let’s randomly generate a separate 15 characters long ID and insert it in the shift label/display name. Then in our flow, we can get all shifts and filter them based on whether their display name contains this ID or not, and include its information in the email. Still, this is very prone to error: what if the manager edits or deletes the display name of the shift, or what if we get unlucky and get a duplicate ID? Then we are really out of luck.

There are a couple of more (at least) downsides to the Shifts API. The first one is that odata query string parameters such as $filter never work. Either they are not allowed and you get an error, or the request returns a response successfully, but the results are not valid. This is why we had to do the schedule group filtering in Power Automate. We’d need to do the same thing for the shifts in our latter workaround: get all (assigned) shifts first and do the filtering based on the display name in our flow after we’ve received the results.

Sometimes a shift gets deleted completely when moved it to Open shifts. Poof, into the void! This happens if the shift has invalid information, such as a too-long label/title (max 15 characters). Microsoft Graph doesn’t make a check for whether the information is valid or not, it just creates the shift. If the info is not valid, you’ll carry the consequences later, so please do those validations before making the call to the API.

The fact that there is no real “update” operation for shifts — or for the other entities in Shifts either, for that matter — makes using the API feel really clumsy. Whenever you want to update a shift, you need to use the “recreate” operation, and provide all the information you want to keep on the shift all over again in addition to the thing that you actually want to change.

The conclusion

Microsoft Teams Shifts is still such a new feature in Teams that I haven’t yet encountered many organizations who are actively using it. A lot of companies seem to be interested in it but want others to “lead the way” first.

On the one hand, it is a good thing to be cautious. The Shifts API is still in beta and very much a work in progress. Generally, Graph operations that are only available in beta work really well, and I’ve used them in customer projects without any issues (after discussing with the customers and ensuring that they understand the risks). However, in their current state, I’m not sure I can say the same about the Shifts operations. Yes, you can build some customizations with them, but you can’t do everything, so the automated processes can quickly become quite lackluster. Especially the fact that a shift gets recreated with a new ID whenever it is reassigned is a significant issue that makes it impossible to automate a lot of processes.

On the other hand, if no one starts using Shifts, who is going to be there to give valuable feedback about the things that should be improved? Sometimes we need those brave, adventurous, and curious souls do the required legwork. May this blog post serve as one such contribution. I’m hoping that by the time these operations get moved to the v1.0 endpoint, at least the ID issue is fixed and open shifts get returned when querying for all shifts.

 

I hope you enjoyed reading this article and that the information presented here will help you in your Microsoft Teams Shifts adventures. If you’d like to read more articles from me in the future, make sure to follow me on Twitter. Other than that, until next time!

Laura

Congratulations, you’ve just finished reading one of my blog post classics! Please note that I’ve personally stopped answering questions left in the comments section of this article because I no longer actively work with the topic. Still, you are more than welcome to comment and ask questions as other readers also often offer their help.


18 thoughts on “What is Microsoft Teams Shifts and how you can customize it – Part 2”

  • Hi Laura, just came across this and thought you may be the right person to help. I want to change the default shifts am/pm time format to 24 hour format because we work in the shipping industry where everything is tracked in 24 hour time. I cannot find a simple solution.

  • Is there an option to view the schedule differently? Rather than seeing a week by person – looks like random times all over the place, is there a way to list the hours of the day on the left and the time blocks with person on the schedule? I find this view of open shifts at top and schedule organized by person instead of shift, very confusing.

  • Hey Laura, great blog!

    I am just looking to use Shifts for corporate leave approvals. Each Group within the broader team can only have X amount of staff off per day, so i would like to have Open shift slots for different leave types and for staff to be able to take those slots to equate to their leave. But would it be worth having it then just as a shift which is titled with the different types of Leave instead of using the specific leave function?

    I would just use the “Leave” function, but i want to be able to advertise to staff how many availabilities we have for leave on that specific day left (especially with Christmas coming up) so I am not sure how else we could do it?

    Cheers,
    Sarah

  • I am looking also at a method of being able to report on shifts that shows the slots and the users where the slots are taken. Because the ID of the shift itself changes there is no way to track the shift like you said. I was thinking of concatenating a few parts together as I need the shift display name to be a certain thing. ie DDMMYYYHHMM+SHIFTDISPLAYNAME would work perfectly I think. However, one of the outputs appends a “Z” to the date time for some reason while the other doesnt. This is possibly happening in the date time conversion (from UTC to ADELAIDE +9:30 hrs.
    Any ideas on how to get these date times to be consistent?
    Is it possible to LEFT() them perhaps???

    Tony

  • Hi Laura,
    Is it possible auto approve request from specific SHIFTS scheduling groups?
    Thank you for your help …and sorry for my bad english !

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.