Create a custom integration to sync your workforce management system with Shifts
Overview
Integrate Shifts, the schedule management app in Microsoft Teams, with your workforce management (WFM) system. This integration allows your frontline workforce to view and manage their schedules directly within Shifts.
This article guides you through how to create a connector using the Microsoft Graph API to facilitate this integration.
You can set up your integration for either a one-way data sync or a two-way data sync.
One-way sync (WFM system to Shifts): In this setup, schedule data in your WFM system is synced to Shifts. The connector reads the data in your WFM system and writes it to Shifts. However, any changes made in Shifts by users aren't reflected back in your WFM system.
Two-way sync (WFM system and Shifts): This setup allows for a bidirectional sync. Schedule data in your WFM system is synced to Shifts, and any changes made in Shifts by users are synced back to your WFM system. The connector validates and approves the changes users make in Shifts according to business rules enforced by your WFM system before the changes are written to Shifts.
Note
If you're using UKG Pro WFM, Blue Yonder WFM, or Reflexis WFM, you can also use a managed connector to integrate Shifts with your WFM system. To learn more, see Shifts connectors.
Terminology used in this article
Term | Description |
---|---|
connector | An app that syncs schedule data between your WFM system and Shifts. |
workforce integration | An entity that defines the encryption method for communication, the callback URL for your connector, and the Shifts entities to be synced. |
Before you begin
Prerequisites
- Determine what data you want to sync according to your business needs.
- Understand the authentication and authorization concepts in the Microsoft identity platform. See Authentication and authorization basics.
- Admin roles required:
- At least a Cloud Application Administrator to register an app in the Microsoft Entra admin center
- Global Administrator to register the workforce integration
Get familiar with the integration process
Here's an overview of the integration steps. Review this information to get an understanding of the overall process, including who performs each step.
Step | One-way sync | Two-way sync | Who performs this step |
---|---|---|---|
1 | Create your connector:
|
Create your connector:
|
Developer |
2 | Register an app in the Microsoft Entra admin center | Register an app in the Microsoft Entra admin center | An account that is at least a Cloud Application Administrator |
3 | Create teams and schedules for syncing | Create teams and schedules for syncing | Developer or Teams Administrator |
4 | Register and enable the workforce integration: | Register and enable the workforce integration: | Step 4a: Global Administrator Step 4b: Developer |
Step 1: Create your connector
To create your connector, complete the following steps:
- Step 1a: Sync changes made in Shifts to your WFM system
- Step 1b: Sync data from your WFM system to Shifts
Step 1a: Sync changes made in Shifts to your WFM system
To set up your connector to receive and process requests from Shifts, you need to implement the following endpoints:
Determine your base URL and endpoint URLs
The base URL (webhook) is {url}/v{apiVersion}
, where url and apiVersion are the properties you set in the workforceIntegration object when you register the workforce integration.
The relative paths of the endpoint URLs are as follows:
- /connect:
/connect
- /update:
/teams/{teamid}/update
- /read:
/teams/{teamid}/read
For example, if url is https://contosoconnector.com/wfi
and apiVersion is 1
:
- The base URL is
https://contosoconnector/com/wfi/v1
. - The /connect endpoint is
https://contosoconnector/wfi/v1/connect
. - The /update endpoint is
https://contosoconnector/wfi/v1/teams/{teamid}/update
. - The /read endpoint is
https://contosoconnector/wfi/v1/teams/{teamid}/read
.
Encryption
All requests are encrypted using AES-256-CBC-HMAC-SHA256. You specify the shared secret key when you register the workforce integration. Responses sent back to Shifts shouldn't be encrypted.
Endpoints
POST /connect
Shifts calls this endpoint to test the connection when you register the workforce integration. A success response is returned only if this endpoint returns an HTTP 200 OK
response.
Example
Request
ConnectRequest
{
"tenantId": "a1s2s355-a2s3-j7h6-f4d3-k2h9j4mqpz",
"userId": "4fbc12d7-1234-56ef-8a90-bc123d45678f"
}
Response
Return HTTP 200 OK
POST /teams/{teamid}/update
Shifts calls this endpoint to get approval when a change is made to a Shifts entity in a schedule that's enabled for the workforce integration. If this endpoint approves the request, the change is saved in Shifts.
As your WFM system is the system of record, when the connector receives a request to this endpoint, it should first attempt to make the change in the WFM system. If the change is successful, return success. Otherwise, return failure.
Shifts calls this endpoint for every change (including changes initiated from the connector/WFM system). If the connector sent an update to Shifts using Graph API and added the X-MS-WFMPassthrough: workforceIntegratonId
header, the request coming to this endpoint will have the same header, which allows you to identify and handle these requests appropriately. For example, return success without making the same change in the WFM system as it would be redundant and can cause the connector to get stuck in an infinite loop.
The following diagram shows the flow of data.
Note
For more information on Request and Response models, see WfiRequest in the Endpoint reference section of this article.
Return response code
Any response from the integration, including an error, must have an HTTP response code 200 OK
. The response body must have the status and error message that reflects the appropriate sub call error state. Any response from the integration other than 200 OK
is treated as an error and returned to the caller (client or Microsoft Graph).
If you want to set up a one-way sync, make Shifts read-only
For a one-way sync, you must make Shifts read-only so that users can't make changes in Shifts. To make Shifts read-only, return a failure response for all requests from Shifts.
For example, to block users from making changes to shifts in the schedule, this endpoint must return a failure response whenever it receives a request regarding a shift
entity.
Example
Request
WfiRequestContainer
The following example shows a request from Shifts that asks whether a shift, whose ID is SHFT_12345678-1234-1234-1234-1234567890ab and has the properties listed in body, can be saved in Shifts. This request was triggered when a user creates a shift in Shifts.
{
"requests": [
{
"id": "SHFT_12345678-1234-1234-1234-1234567890ab",
"method": "POST",
"url": "/shifts/SHFT_12345678-1234-1234-1234-1234567890ab",
"headers": {
"X-MS-Transaction-ID": "1",
"X-MS-Expires": "2024-10-11T21:27:59.0134605Z"
},
"body": {
"draftShift": {
"activities": [],
"isActive": true,
"startDateTime": "2024-10-12T15:00:00.000Z",
"endDateTime": "2024-10-12T17:00:00.000Z",
"theme": "Blue"
},
"isStagedForDeletion": false,
"schedulingGroupId": "TAG_a3e0b3f1-4a5c-4c2e-8eeb-5b8c3d1e3f8b",
"userId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"createdDateTime": "2024-10-11T21:27:28.762Z",
"lastModifiedDateTime": "2024-10-11T21:27:28.762Z",
"lastModifiedBy": {
"user": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"displayName": "Adele Vance"
}
},
"id": "SHFT_12345678-1234-1234-1234-1234567890ab"
}
}
]
}
Response
WfiResponse
Success: Return HTTP 200 OK
This example shows the response returned if the endpoint approved the request. In this scenario, the shift is saved in Shifts, and the user can see the shift in the schedule.
{
"responses": [
{
"id": "SHFT_12345678-1234-1234-1234-1234567890ab",
"status": 200,
"body": {
"eTag": "3f4e5d6c-7a8b-9c0d-1e2f-3g4h5i6j7k8l",
"error": null,
"data": null
}
}
]
}
Failure: Return HTTP 200 OK
This example shows the response returned if the endpoint denied the request. In this scenario, the user receives a “Could not add the shift” error message in Shifts.
{
"responses": [
{
"id": "SHFT_12345678-1234-1234-1234-1234567890ab",
"status": 500,
"body": {
"error": {
"code": "500",
"message": “Could not add the shift”
},
"data": null
}
}
]
}
POST /teams/{teamid}/read
This endpoint handles requests from Shifts to fetch eligible time-off reasons or eligible shifts for swap requests for a user.
Note
As of October 2024, this endpoint is supported only in the beta version of the Microsoft Graph API. You must also specify values for the eligibilityFilteringEnabledEntities property when you register the workforce integration.
The following diagram shows the flow of data.
Return response code
Any response from the integration, including an error, must have an HTTP response code 200 OK
. The response body must include the status and error message that reflects the appropriate sub call error state. Any response from the integration other than 200 OK
is treated as an error and returned to the caller (client or Microsoft Graph).
Example: TimeOffReason
Request
The following example shows a request from Shifts that asks which time off reasons a user (user ID aa162a04-bec6-4b81-ba99-96caa7b2b24d) is eligible for. This request was triggered when the user requests time off in Shifts.
{
"requests": [
{
"id": "aa162a04-bec6-4b81-ba99-96caa7b2b24d",
"method": "GET",
"url": "/users/aa162a04-bec6-4b81-ba99-96caa7b2b24d/timeOffReasons?requestType=TimeOffReason"
}
]
}
Response
Success: Return HTTP 200 OK
The following response shows that the eligible time off reason IDs for the user are "TOR_29f4a110-ae53-458b-83d6-00c910fe2fbc" and "TOR_8c0e8d07-ac1a-48dc-b3af-7bc71a62ff7d". In this scenario, the user sees the corresponding time-off reasons to choose from in Shifts.
{
"responses": [
{
"id": "aa162a04-bec6-4b81-ba99-96caa7b2b24d",
"status": 200,
"body": {
"data": [
"TOR_29f4a110-ae53-458b-83d6-00c910fe2fbc",
"TOR_8c0e8d07-ac1a-48dc-b3af-7bc71a62ff7d"
],
"error": null
}
}
]
}
Failure: Return HTTP 200 OK
In this example, an error response is returned because the connector couldn't reach the WFM system to retrieve time off reasons for the user.
{
"responses": [
{
"id": "aa162a04-bec6-4b81-ba99-96caa7b2b24d",
"status": 503,
"body": {
"data": null,
"error": {
"code": "503",
"message": "Could not reach WFM"
}
}
}
]
}
Example: SwapRequest
Request
The following example shows a request from Shifts that asks which shifts are eligible for a swap with the shift whose ID is SHFT_5e2b51ac-dc47-4a66-83ea-1bbbf81ac029 between 2024-10-01T04:00:00.0000000Z and 2024-11-01T03:59:59.9990000Z.
{
"requests": [
{
"id": "SHFT_5e2b51ac-dc47-4a66-83ea-1bbbf81ac029",
"method": "GET",
"url": "/shifts/SHFT_5e2b51ac-dc47-4a66-83ea-1bbbf81ac029/requestableShifts?requestType=SwapRequest&startTime=2024-10-01T04:00:00.0000000Z&endTime=2024-11-01T03:59:59.9990000Z"
}
]
}
Response
Success: Return HTTP 200 OK
The following response shows that the shift can be swapped with the shift whose ID is SHFT_98e96e23-966b-43be-b90d-4697037b67af.
{
"responses": [
{
"id": "SHFT_5e2b51ac-dc47-4a66-83ea-1bbbf81ac029",
"status": 200,
"body": {
"data": ["SHFT_98e96e23-966b-43be-b90d-4697037b67af"],
"error": null,
}
}
]
}
Failure: Return HTTP 200 OK
In this example, an error response is returned because the connector couldn't reach the WFM system to retrieve eligible shifts for a swap request for the user.
{
"responses": [
{
"id": "SHFT_5e2b51ac-dc47-4a66-83ea-1bbbf81ac029",
"status": 503,
"body": {
"data": null,
"error": {
"code": "503",
"message": "could not reach WFM"
}
}
}
]
}
Step 1b: Sync data from your WFM system to Shifts
Use Shifts APIs in Microsoft Graph to read schedule data from your WFM system and write the data to Shifts.
For example, to add a shift to Shifts, use the Create shift API.
See the Microsoft Graph API v1.0 reference for Shifts APIs, which are listed under Shift management.
Note
The MS-APP-ACTS-AS
header is required in requests and must contain the ID (GUID) of the user your app is acting on behalf of. We recommend you use the user ID of a team owner when updating the schedule.
The following diagram shows the flow of data.
Initial sync
For the first sync, the connector should read data in your WFM system and write the data to Shifts. We recommend you sync two weeks of future data.
After the initial sync
After the first sync, you can choose to:
Synchronously update Shifts with changes in your WFM system: Send an update to Shifts for every change made in your WFM system.
Asynchronously update Shifts with changes in your WFM system: Perform a periodic sync by writing all changes that occurred in your WFM system within a certain timeframe (for example, 10 minutes) to Shifts.
All write operations to Shifts, including write operations initiated by the connector, trigger a call to the connector’s /update endpoint. We recommend you include the
X-MS-WFMPassthrough: workforceIntegratonId
header to all write calls so the connector can identify and handle them appropriately. For example, if your WFM system initiated the change, approve it without applying an update to your WFM system.Note
If you're setting up your connector for a two-way sync of data between your WFM system and Shifts, exclude changes initiated from Shifts in the periodic sync. These changes are already written in Shifts.
Step 2: Register an app in the Microsoft Entra admin center
Follow these steps to register an app for your connector in the Microsoft identity platform, configure permissions for the app to access Microsoft Graph, and get an access token.
Sign in to the Microsoft Entra admin center as at least a Cloud Application Administrator.
Register your app. For steps, see Register an application with the Microsoft identity platform.
Assign the Schedule.ReadWrite.All application permissions to the app for app-only access and get an access token.
For step-by-step guidance, see Get access without a user.
The access token verifies that your app is authorized to call Microsoft Graph using its own identity using the Schedule.ReadWrite.All permission. It must be included in the Authorization header of requests.
Step 3: Create teams and schedules for syncing
Set up the teams in Teams that you want to sync. You can use existing teams or create new teams.
Set up teams in Teams to correspond with the teams and locations in your WFM system. Ensure you add the following people to each team:
- Frontline managers as team owners. Make sure you add the user in the
MS-APP-ACTS-AS
header as a team owner of each respective team. - Frontline workers as team members.
- Frontline managers as team owners. Make sure you add the user in the
Create a schedule in Shifts for each team. To learn more, see Create or replace schedule.
Add schedule groups to the schedule on each team. Schedule groups are used to group employees based on common characteristics within a team. For example, schedule groups can be departments or job types. To learn more, see schedulingGroup resource type.
Add employees to each schedule group. To learn more, see Replace schedulingGroup.
Note
You can also use the Teams admin center to set up your teams and deploy Shifts to the teams. To learn more, see:
Step 4: Register and enable the workforce integration
A workforce integration defines the encryption settings for communication between Shifts and the connector, the URL for callbacks from Shifts, and the types of entities to sync.
To register and enable the workforce integration, complete the following steps:
- Step 4a: Register the workforce integration
- Step 4b: Enable the workforce integration for your team schedules
Step 4a: Register the workforce integration in your tenant
You must be a Global Administrator to perform this step.
Use the Create workforceIntegration API to register your workforce integration in your tenant.
Here's an example of a request.
POST https://graph.microsoft.com/v1.0/teamwork/workforceIntegrations/
{
"displayName": "Contoso integration",
"apiVersion": 1,
"encryption": {
"protocol": "sharedSecret",
"secret": "secret-value"
},
"isActive": true,
"url": "https://contosoconnector.com/wfi",
"supportedEntities": "Shift,SwapRequest,UserShiftPreferences,Openshift,OpenShiftRequest,OfferShiftRequest”,
}
See the following table for details. To learn more, see workforceIntegration resource type.
Property | More information |
---|---|
apiVersion | API version for the callback URL. Your base URL is comprised of the url property and this property. |
encryption | Set protocol to sharedSecret . The secret value must be exactly 64 characters. Use the secret to decrypt the encrypted JSON payloads that are sent to your connector's endpoint from Shifts. The payload is encrypted using AES-256-CBC-HMAC-SHA256. Your app should safely persist this secret. For example, in a key vault. |
supportedEntities | Specify the Shifts entities you want the connector to support for syncing. Shifts calls your connector's /update endpoint when any of these entities change so that you can approve or reject the change. For the list of the possible values, see workforceIntegration resource type Note This list is an evolvable enumeration. You must use the Prefer: include-unknown-enum-members request header to get all the values. |
eligibilityFilteringEnabledEntities | Note: As of October 2024, this endpoint is supported only in the beta version of the Microsoft Graph API. Specify the Shifts entities that you want to connector to support for eligibility filtering. Possible values are:
Prefer: include-unknown-enum-members request header to get all the values. |
url | The workforce integration URL for callbacks from Shifts. Your base URL is comprised of this property and the apiVerson property. |
Step 4b: Enable the workforce integration for your team schedules
Enable your workforce integration on the schedules you want to manage. To do this, use the Create or replace schedule API to create or update the schedule for your teams.
Here's an example of a request.
POST https://graph.microsoft.com/v1.0/teams/{teamId}/schedule
{
enabled: true,
timezone: “America/New_York”,
workforceIntegrationIds: [ “workforceIntegrationId”]
}
- Specify the workforceIntegrationId that was generated when you registered the workforce integration.
- You can enable a maximum of one workforce integration on a schedule. If you include more than one workforceIntegrationId in the request, the first one is used.
Troubleshooting
Connector
When the connector responds to a request from Shifts, what happens if it returns a response code other than 200? Does it make a difference if it returns a status other than 200 in the response body?
There's a difference between these two scenarios.
- If the connector returns a response code other than 200, Shifts attempts to retry the /read and /update endpoints multiple times. Eventually, Shifts displays a "Something went wrong. The workforce integration setup on your team has responded with invalid data." error message.
- If the connector returns a status other than 200 in the response body, Shifts displays a "Something went wrong. Sorry, your change couldn’t be completed," error message and stops retrying the endpoints.
What happens if the connector returns invalid data in the response body?
Shifts attempts to retry the /read and /update endpoints multiple times. Eventually, Shifts displays a "Something went wrong. The workforce integration set up on your team has responded with invalid data." error message.
How do I identify whether the request was originally made in Shifts or in the WFM system to prevent an infinite loop?
Add the X-MS-WFMPassthrough: workforceIntegratonId
header to all write and update calls to identify/ignore the changes triggered by connector. This header is used to indicate that the request is made because of a preceding call that was made by the connector to Graph API to sync data from your WFM system to Shifts.
Workforce integration registration
I registered the workforce integration and specified "eligibilityFilteringEnabledEntities" including "SwapRequest, OfferShiftRequest, and TimeOffReason' but the response body doesn't show the "eligibilityFilteringEnabledEntities" list.
Eligibility filtering is currently supported through the https://graph.microsoft.com/beta
endpoint, not the https://graph.microsoft.com/v1
endpoint.
I registered the workforce integration and added "supportedEntities" but receive a 400 Bad Request response and an "Invalid payload: Requested value 'shift, ....' was not found." message
Make sure that every Shifts entity in the supportedEntities
list request body starts with an uppercase letter. For example,
"supportedEntities":"Shift,SwapRequest,OpenShift"
.
I registered the workforce integration and implemented the /connect, /update, and /read endpoints, but the webhook isn't working.
Make sure your workforce integration is enabled for your team schedule. Additionally, check that the url and apiVersion properties are correct.
Endpoint reference
Request
ConnectRequest
Property | Type | Description |
---|---|---|
tenantId | String | ID of the tenant for the workforce integration |
userId | String | ID of the user for the workforce integration |
{
"tenantId": "string",
"userId": "string"
}
WfiRequestContainer
Property | Type | Description |
---|---|---|
requests | WfiRequest collection | List of WfiRequests |
{
"requests": [
{
"id": "string",
"method": "string",
"url": "string",
"headers": {
"X-MS-Transaction-ID": "string",
"X-MS-Expires": "string (DateTime)"
},
"body": "ShiftsEntity"
}
]
}
Number of elements in a request:
- In most cases, a request has one element.
- Some requests, such as swap shift request approvals, have five elements: one PUT swap request, two DELETE shifts (existing shifts), and two POST shifts (new shifts).
WfiRequest
Property | Type | Description |
---|---|---|
id | String | ID of the entity |
method | String | The method invoked on the item. For example, POST , PUT , GET , DELETE . |
url | String | Indicates the type of entity and operation details. |
headers | WfiRequestHeader | Headers |
body | ShiftsEntity | Body of the entity related to the request. |
For POST /teams/{teamId}/update
Property | Type | Description |
---|---|---|
id | String | ID of the entity |
method | String | POST to create an entity, PUT to update an entity, DELETE to delete an entity. |
url | String | The format is /{EntityType}/{EntityId} . Possible values for {EntityType} are shifts , swapRequests , timeoffReasons , openshifts , openshiftrequests , offershiftrequests , timesoff , timeOffRequests . For example, /shifts/SHFT_12345678-1234-1234-1234-1234567890ab . |
header | WfiRequestHeader | Header |
body | ShiftsEntity | Must match {EntityType} in the url property. Use one of shift, swapShiftsChangeRequest, timeOffReason, openshift, openShiftChangeRequest, offerShiftRequests, timeOff, timeOffRequest. For example, /shifts/SHFT_12345678-1234-1234-1234-1234567890ab . |
For POST /teams/{teamsId}/read
Property | Type | Description |
---|---|---|
id | String | ID of the entity |
method | String | Is always GET . |
url | String |
|
header | WfiRequestHeader | Header |
body | ShiftsEntity | Is always null . |
WfiRequestHeader
Property | Type | Description |
---|---|---|
X-MS-Transaction-ID | String | Transaction ID |
X-MS-Expires | String (DateTime) | Transaction expiration DateTime |
X-MS-WFMPassthrough: workforceIntegratonId
won't be included in WfiRequestHeader. It should be extracted from the HttpRequest.
Response
WfiResponseContainer
Property | Type | Description |
---|---|---|
responses | WfiResponse collection | List of WfiResponses |
{
"responses": [
{
"id": "string",
"status": "string",
"body": {
"eTag": "string",
"error": {
"code": "string",
"message": "string"
},
"data": ["string1", "string2"]
}
}
]
}
WfiResponse
Property | Type | Description |
---|---|---|
id | String | ID of the entity |
status | String | Result of the operation |
body | WfiResponseBody | WfiResponseBody |
WfiResponseBody
Property | Type | Description |
---|---|---|
eTag | String | eTag |
error | WfiResponseError | Details about the error |
data | String | The requested data (for read requests) |
WfiResponseError
Property | Type | Description |
---|---|---|
code | String | Error code |
message | String | Error message |