How to connect and sync Bryntum Gantt to Microsoft Project

Bryntum Gantt is a fast and customizable Gantt chart component. Built using vanilla JavaScript, it can easily be used with your framework of choice: React, Angular, or Vue. In this tutorial, we’ll connect and sync Bryntum Gantt to a Microsoft Project plan by doing the following:
- Creating a client-side Bryntum Gantt JavaScript app, which users can access using their Microsoft Project login credentials.
- Using the Microsoft Dynamics 365 (D365) Project schedule APIs to get users’ Microsoft Project tasks and task dependencies.
- Displaying the Microsoft Project tasks and task dependencies in the Bryntum Gantt.
- Using the D365 Project schedule APIs to sync data changes in the Bryntum Gantt to the users’ Microsoft Project plan.
Getting started
We’ll begin by cloning the starter repository on GitHub, which contains the client-side Bryntum Gantt app that we will sync with the Microsft Project.
If you want to consult the final code, you can find the app that syncs with Microsoft Project in the completed-gantt
branch of the repository.
The starter repository uses Vite, which is a development server and JavaScript bundler. You need Node.js version 18+ for Vite to work. Install the Vite dev dependency by running the following command:
npm install
Next, we’ll install the Bryntum Gantt component by following the guide to using the Bryntum npm repository and step four of the guide to setting up Bryntum Gantt with vanilla JavaScript and npm.
Run the local dev server using the following command:
npm run dev
You’ll see a Bryntum Gantt with two tasks and a dependency between the tasks:
Now, let’s create a Microsoft Project plan.
Accessing Microsoft Project
To use Microsoft Project, which is a Microsoft 365 app, you need to subscribe to one of the paid Microsoft Planner and Project plans. You can try the Planner Plan 1 or the Planner and Project Plan 3 for free for one month.
You also need to be the system administrator of the Power Platform environment that uses your Microsoft Planner subscription.
Navigate to Power Platform, click the ellipsis icon next to your Environment name, select the Membership option from the dropdown, and then assign the System Administrator role to your account.
Creating a Microsoft Project plan
Sign in to Microsoft Project. Click the + New plan button in the top-left corner of your screen.
Select the + Blank Plan option in the Create dialog that opens.
The plan has four different views: Grid, Board, Timeline, and Charts.
Open the Timeline tab to see the data view that most closely resembles a Gantt chart.
Create some example tasks.
To create a task, click on the + Add new task button in the left column of the Gantt chart. Name the task, then hover your cursor over (or focus on) the task name, and click the “i” button (also known as the “Open details” button) to the right of the task name to open the task details. Give the task a Start and Finish date.
Create a dependency between two of the tasks by hovering your cursor over a task bar, and clicking and dragging the white circle on the side of the task bar until it connects with another task.
Next, let’s set up our Bryntum Gantt web app authentication so that we can access our Microsoft Project data.
Creating a Microsoft Entra app to connect to D365
We’ll use the D365 Project schedule APIs to perform CRUD (create, read, update, delete) operations on Microsoft Project tasks from the Bryntum Gantt app. The D365 Project schedule APIs are part of the D365 Project Operations.
We need an access token to use the API. Let’s create one by registering a Microsoft 365 app in the Microsoft Entra admin center. Microsoft Entra ID is the new name for Azure Active Directory.
Now, create a Microsoft Entra application:
- Sign in to Microsoft Entra using the same email address you used for Microsoft Project.
- In the left navigation menu, select Applications and then App registrations.
- Click the New registration button to create a new app registration.
- Name your app and select the Single tenant option under Supported account types.
- Set the Redirect URI to
http://localhost:5173
and select Single page application. - Finally, click the Register button.
After registering your application, take note of the Application (client) ID and the Directory (tenant) ID, as you’ll need them later to set up authentication for your Bryntum Gantt web app.
Next, configure the API permissions:
- Navigate to API permissions in the left menu, click Add a permission, and select Dynamics CRM from the APIs list.
- Under Permissions, check the user_impersonation permission.
- Finally, click Add permissions to save your changes.
The user_impersonation permission is necessary because it allows your application to access the Common Data Service on behalf of authenticated D365 users, enabling it to perform CRUD operations on project tasks and dependencies via the D365 Project schedule APIs.
Setting up D365 authentication in the Bryntum Gantt app
To get data using the D365 Project schedule APIs, your app needs to prove that you are the owner of the app that you just registered in Microsoft Entra. Your application will get an access token from Microsoft Entra and include it in each request to the D365 Project schedule APIs. This allows users to sign in to the Bryntum Gantt with their Microsoft 365 accounts, so you don’t have to maintain users’ credentials or implement authentication in your app.
The following diagram outlines how you’ll access the Microsoft Project data from the Bryntum Gantt app. You’ll log in to your app, using Microsoft Entra ID to authenticate your user account. Then, you’ll use the access token returned by Microsoft Entra ID to connect to Microsoft Project via the D365 Project schedule APIs.
First, we’ll create the variables and functions we need for authentication and for retrieving tasks from Microsoft Project. Then, we’ll add the Microsoft Authentication Library (MSAL), which we’ll need for authentication and for using the D365 Project Schedule APIs.
Create a .env.local
file in the root directory of your Bryntum Gantt app and add the following environment variables to it:
VITE_MICROSOFT_ENTRA_APP_ID=""
VITE_MICROSOFT_ENTRA_TENANT_ID=""
VITE_MSDYN_PROJECT_ID=""
VITE_MICROSOFT_DYNAMICS_ORG_ID=""
VITE_MSDYN_PROJECTBUCKET_VALUE=""
We prefix the variables with VITE_
to expose the variables to the JavaScript app. Vite only exposes environment variables prefixed with VITE_
to Vite-processed code. The JavaScript app runs client side, so the environmental variables will be exposed to the user, which is fine in this case.
Add the relevant value to each environment variable:
VITE_MICROSOFT_ENTRA_APP_ID
andVITE_MICROSOFT_ENTRA_TENANT_ID
: You retrieved the Application (client) ID and the Directory (tenant) ID of your registered Microsoft Entra app in the previous step.VITE_MSDYN_PROJECT_ID
andVITE_MICROSOFT_DYNAMICS_ORG_ID
: You can find your Microsoft Project ID and Microsoft Dynamics organization ID by signing in tohttps://www.microsoft365.com/
, opening your Project plan from the Quick access table, and coping both IDs from the URL.VITE_MSDYN_PROJECTBUCKET_VALUE
: Once you have your Microsoft Project ID and Microsoft Dynamics organization ID, you can get your Microsoft Project bucket value. Log in tohttps://YOUR_MICROSOFT_DYNAMICS_ORG_ID.api.crm4.dynamics.com/
, then openhttps://YOUR_MICROSOFT_DYNAMICS_ORG_ID.api.crm4.dynamics.com/api/data/v9.1/msdyn_projecttasks
to view the tasks from all of your Project plans, and find a task object with a_msdyn_project_value
that is equal to your Microsoft Project ID. In the same task object, find and copy the_msdyn_projectbucket_value
, then save it as yourVITE_MSDYN_PROJECTBUCKET_VALUE
variable.
Your Microsoft Project bucket value is the value of the default project bucket in Microsoft Project, Bucket 1. Project buckets allow you to sort tasks into buckets that represent, for example, different development phases, types of work, or departments. You can create new buckets in the Board view in Microsoft Project.
Now install the MSAL for single-page JavaScript web apps, MSAL.js
, in your local project.
npm i @azure/msal-browser
Create a file called auth.js
in your project’s root directory and add the following code to it:
const msalConfig = {
auth : {
clientId : import.meta.env.VITE_MICROSOFT_ENTRA_APP_ID,
authority : `https://login.microsoftonline.com/${
import.meta.env.VITE_MICROSOFT_ENTRA_TENANT_ID
}`,
redirectUri : 'http://localhost:5173'
}
};
This code configures MSAL for authentication with Microsoft Entra ID. The clientId
is the unique ID of your Microsoft Entra app. The authority
is the endpoint that MSAL will use to authenticate users. The redirectUri
is the URL that users will be redirected to after they’ve been authenticated.
Add the following imports to the top of the auth.js
file:
import {
PublicClientApplication,
InteractionRequiredAuthError
} from '@azure/msal-browser';
Add the following code to the bottom of the file:
// Initialize a PublicClientApplication object
const msalInstance =
await PublicClientApplication.createPublicClientApplication(msalConfig);
const msalRequest = { scopes : [`https://${
import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID
}.api.crm4.dynamics.com/.default`] };
export function ensureScope(scope) {
if (
!msalRequest.scopes.some((s) => s.toLowerCase() === scope.toLowerCase())
) {
msalRequest.scopes.push(scope);
}
}
// Log the user in
export async function signIn() {
const authResult = await msalInstance.loginPopup(msalRequest);
sessionStorage.setItem('msalAccount', authResult.account.username);
}
export async function getToken() {
const account = sessionStorage.getItem('msalAccount');
if (!account) {
throw new Error(
'User info cleared from session. Please sign out and sign in again.'
);
}
try {
// First, attempt to get the token silently
const silentRequest = {
scopes : msalRequest.scopes,
account : msalInstance.getAccountByUsername(account)
};
const silentResult = await msalInstance.acquireTokenSilent(silentRequest);
return silentResult.accessToken;
}
catch (silentError) {
// If silent request fails with InteractionRequiredAuthError,
// attempt to get the token interactively
if (silentError instanceof InteractionRequiredAuthError) {
const interactiveResult = await msalInstance.acquireTokenPopup(
msalRequest
);
return interactiveResult.accessToken;
}
else {
throw silentError;
}
}
}
This code instantiates a PublicClientApplication
object to use the MSAL.js
library. The msalRequest
variable stores the current MSAL request. It initially contains the .default
scope for the D365 API.
Using .default
means that when your app requests an access token from Microsoft Entra ID, the user is prompted to consent to the required permissions listed during the application registration. The list of permissions granted to your app forms part of the access token returned when a user logs in to the app.
The ensureScope
function checks the user’s permissions. We’ll use this function when making requests to the D365 Project schedule APIs to perform CRUD operations on tasks in the user’s Microsoft Project plan.
The signIn
function grants the user access and stores their access token in the browser’s session storage, and the getToken
function gets the user’s access token from session storage.
Using D365 Project schedule APIs to access Microsoft Project tasks and dependencies
We’ll use the D365 Project schedule APIs to perform CRUD operations on tasks in the user’s Microsoft Project plan. The D365 Project schedule APIs use the Project Scheduling Service to make requests.
The D365 Project schedule REST API uses Dataverse actions, so you can use the Dataverse Web API to make requests. The Dataverse Web API uses the Open Data Protocol (OData). You can use an OData library to simplify OData REST API requests. In this tutorial, we’ll use the Fetch API.
Note that there are project and task limitations, as well as some specific project task restrictions, when using the D365 Project schedule APIs. The Progress, EffortCompleted, and EffortRemaining fields cannot be edited using the APIs.
Create a file called crudFunctions.js
in your project’s root directory, then add the following lines of code to it:
import { ensureScope, getToken } from './auth';
export async function getProjectTasks() {
try {
const accessToken = await getToken();
ensureScope(`https://${
import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID
}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${
import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID
}.api.crm4.dynamics.com/api/data/v9.1/msdyn_projecttasks`;
const response = await fetch(apiUrl, {
method : 'GET',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const modData = data.value.filter((item) => item._msdyn_project_value === import.meta.env.VITE_MSDYN_PROJECT_ID);
return modData;
}
catch (error) {
console.error('Error fetching project tasks:', error);
throw error;
}
}
The getProjectTasks
function uses the D365 Project schedule msdyn_projecttasks
API to fetch the user’s tasks from all their Microsoft Project plans. In the code above, we ensure that the user has access, then we fetch the tasks using the user’s D365 organization ID. In the 'Authorization'
header, we pass in the access token from their Microsoft Entra ID login. The returned tasks are filtered by their Microsoft Project IDs.
Add the following function to the bottom of the crudFunctions.js
file:
export async function getProjectTaskDependencies() {
try {
const accessToken = await getToken();
ensureScope(`https://${
import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID
}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${
import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID
}.api.crm4.dynamics.com/api/data/v9.1/msdyn_projecttaskdependencies`;
const response = await fetch(apiUrl, {
method : 'GET',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const modData = data.value.filter((item) => item._msdyn_project_value === import.meta.env.VITE_MSDYN_PROJECT_ID);
return modData;
}
catch (error) {
console.error('Error fetching project task dependencies:', error);
throw error;
}
}
The getProjectTaskDependencies
function uses the D365 Project schedule msdyn_projecttaskdependencies
API to fetch the user’s task dependencies from all of their Microsoft Project plans. The returned task dependencies are filtered by their Microsoft Project IDs.
Adding Microsoft Project tasks and dependencies to Bryntum Gantt
Let’s add a Microsoft 365 sign-in link to our Bryntum Gantt app.
Replace the contents of the <main>
HTML tag in the index.html
with the following:
<div id="content" style="display: none">
<div id="gantt"></div>
</div>
<a id="signin" href="#">
<img
src="./images/ms-symbollockup_signin_light.png"
alt="Sign in with Microsoft"
/>
</a>
Initially, our app only displays the sign-in link. When a user signs in, it displays the Bryntum Gantt.
In the main.js
file, add the following line to store the Sign in with Microsoft link element object in a variable:
const signInLink = document.getElementById("signin");
Now insert the following function at the bottom of the file:
async function displayUI() {
await signIn();
// Hide sign in link and initial UI
signInLink.style = 'display: none';
const content = document.getElementById('content');
content.style = 'display: block';
try {
const projectTasksPromise = getProjectTasks();
const getProjectDependenciesPromise = getProjectTaskDependencies();
const [projectTasks, projectDependencies] = await Promise.all([
projectTasksPromise,
getProjectDependenciesPromise
]);
const ganttTasks = [];
projectTasks.forEach((event) => {
const startDateUTC = new Date(event.msdyn_start);
// Convert to local timezone
const startDateLocal = new Date(
startDateUTC.getTime() - startDateUTC.getTimezoneOffset() * 60000
);
const finishDateUTC = new Date(event.msdyn_finish);
// Convert to local timezone
const finishDateLocal = new Date(
finishDateUTC.getTime() - finishDateUTC.getTimezoneOffset() * 60000
);
ganttTasks.push({
id : event.msdyn_projecttaskid,
parentId : event._msdyn_parenttask_value,
name : event.msdyn_subject,
startDate : startDateLocal,
endDate : finishDateLocal,
percentDone : event.msdyn_progress * 100,
effort : event.msdyn_effort,
msdyn_displaysequence : event.msdyn_displaysequence,
manuallyScheduled : true,
msdyn_outlinelevel : event.msdyn_outlinelevel,
note : event.msdyn_descriptionplaintext
});
});
ganttTasks.sort((a, b) => a.msdyn_displaysequence - b.msdyn_displaysequence);
gantt.project.tasks = ganttTasks;
const ganttDependencies = [];
projectDependencies.forEach((dep) => {
ganttDependencies.push({
id : dep.msdyn_projecttaskdependencyid,
from : dep._msdyn_predecessortask_value,
to : dep._msdyn_successortask_value
});
});
gantt.project.dependencies = ganttDependencies;
}
catch (error) {
console.error('Error:', error);
}
}
Remove the example inline tasksData
and dependenciesData
arrays from project
config of the gantt
object. Then, add the signIn
and getTasks
function imports to the top of the main.js
file:
import { signIn } from './auth.js';
import { getProjectTaskDependencies, getProjectTasks } from './crudFunctions.js';
In the main.js
code, we use the displayUI
function to sign the user in to the app by calling the signIn
function in auth.js
. Once the user is signed in, the sign-in link is hidden and the Bryntum Gantt is displayed. We use the getProjectTaskDependencies
and getProjectTasks
functions to get the Microsoft Project tasks and task dependencies, then use the retrieved tasks and dependencies to create the tasks and dependencies for the Bryntum Gantt, and add them to the data stores. The Bryntum Gantt project holds the task and dependency data. Bryntum Gantt manages the project’s data using the Bryntum Scheduling Engine.
This populates some of the Bryntum task and dependency fields with data from the user’s Microsoft Project plan.
Now, add a click event listener to the Sign in with Microsoft link by inserting the following line of code at the bottom of the main.js
file:
signInLink.addEventListener('click', displayUI);
There are some Microsoft Project task fields, such as msdyn_outlinelevel
and msdyn_displaysequence
, that don’t have equivalent Bryntum Gantt task fields. We need to create a custom Bryntum Gantt task model to add these fields.
Create a lib
folder in the root directory, and create a CustomTaskModel.js
file inside it.
Add the following lines of code to the lib/CustomTaskModel.js
file:
import { TaskModel } from '@bryntum/gantt';
export default class CustomTaskModel extends TaskModel {
static $name = 'CustomTaskModel';
static fields = [
{ name : 'msdyn_outlinelevel', type : 'number' },
{ name : 'msdyn_displaysequence', type : 'number' }
];
// disable percentDone editing
isEditable(field) {
return field !== 'percentDone' && super.isEditable(field);
}
}
In this code block, we extend the Bryntum Gantt TaskModel
to include the Microsoft Project-specific task fields. We also disable editing for the percentDone
Bryntum Gantt field, because although the Progress
, EffortCompleted
, and EffortRemaining
fields of a Microsoft Project project can be edited in the Microsoft Project UI, they can’t be edited using the D365 Project schedule APIs.
There are other Microsoft Project-specific task fields, but we have excluded them from the guide for simplicity. You can view all of the available task fields by opening https://YOUR_MICROSOFT_DYNAMICS_ORG_ID.api.crm4.dynamics.com/api/data/v9.1/msdyn_projecttasks
, which displays your Microsoft Project tasks.
Import the custom task model in the main.js
file:
import CustomTaskModel from './lib/CustomTaskModel.js';
Set the gantt
to use this task model for its task store:
project : {
taskStore : {
transformFlatData : true,
modelClass : CustomTaskModel
},
writeAllFields : true
},
The task store is configured to transform flat data, as the task data we receive from the D365 Project schedule APIs has a flat data structure that needs to be changed to a tree data structure.
We set writeAllFields
to true
, so that when a task or dependency is updated, we get all the task or dependency record fields and not just changed fields. We need all of the fields because updating a Microsoft Project task dependency requires us to delete the old task and then create a new task.
Set the Gantt chart date
property based on the task dates in your Microsoft Project plan.
Add the following features
config below the project
config in the gantt
:
features : {
taskMenu : {
items : {
// Hide items from the `edit` menu
copy : false,
indent : false,
outdent : false,
convertToMilestone : false
}
},
taskEdit : {
items : {
generalTab : {
items : {
percentDone : {
disabled : true
},
effort : {
disabled : true
}
}
},
resourcesTab : false,
advancedTab : false
}
}
},
Here, we disable the percentDone
and effort
fields in the task editor widget because they cannot be edited using the D365 Project schedule APIs. We also set resourcesTab
and advancedTab
to false
to remove the Resources and Advanced tabs from the task editor, as they contain fields that we won’t sync with Microsoft Project.
Run your dev server using npm run dev
. You should see the sign-in link:
Sign in with the same email address that you used to log in to Microsoft Project and Accept the permissions request:
Once you’ve signed in and given the app the necessary permissions, you’ll see your Microsoft Project tasks and dependencies in your Bryntum Gantt:
Next, we’ll sync the Microsoft Project plan with the Bryntum Gantt by implementing CRUD functionality via the D365 Project schedule APIs. Updates to Bryntum Gantt tasks and dependencies will update the corresponding tasks and dependencies in the Microsoft Project plan.
Using D365 Project schedule APIs to perform CRUD operations on a Microsoft Project plan
Now that we’ve connected our Gantt chart to the D365 Project schedule APIs, let’s create the functions needed to implement the rest of the CRUD functionality.
Creating tasks
In the crudFunctions.js
file, add the following createProjectTask
function:
export async function createProjectTask(projectId, projectBucketId, operationSetId, record, msdyn_displaysequence) {
const accessToken = await getToken();
ensureScope(`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/api/data/v9.1/msdyn_PssCreateV1`;
const payload = {
'Entity' : {
'msdyn_subject' : record.name,
'msdyn_start' : `${record.startDate.toISOString()}`,
'msdyn_finish' : `${record.endDate.toISOString()}`,
'msdyn_description' : record.note,
'msdyn_outlinelevel' : record.childLevel + 1,
'@odata.type' : 'Microsoft.Dynamics.CRM.msdyn_projecttask',
'msdyn_project@odata.bind' : `msdyn_projects(${projectId})`,
'msdyn_projectbucket@odata.bind' : `msdyn_projectbuckets(${projectBucketId})`,
'msdyn_progress' : record.percentDone / 100,
'msdyn_displaysequence' : msdyn_displaysequence
},
'OperationSetId' : operationSetId
};
if (record.parentId) {
payload.Entity['msdyn_parenttask@odata.bind'] = `msdyn_projecttasks(${record.parentId})`;
}
const response = await fetch(apiUrl, {
method : 'POST',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
body : JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
In this function, we first check that the user has access, then make a POST request to the D365 Project schedule msdyn_PssCreateV1
API. This API is used to create one of the Microsoft Project scheduling entities that support the create operation (including Project tasks and Project task dependencies).
The API call requires an operationSetId
in the payload, as all D365 Project schedule API requests that change data must be part of an operation set.
Creating operation sets
The D365 Project schedule APIs use operation sets to execute API requests together. Operation sets provide database integrity by ensuring that the database transactions are atomic. This means that all the operations in a set must succeed, or the entire operation set fails.
We need to create an operation set before we can create a task.
In the crudFunctions.js
file, add the following createOperationSet
function:
export async function createOperationSet(projectId, description) {
const accessToken = await getToken();
ensureScope(`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/api/data/v9.1/msdyn_CreateOperationSetV1`;
const payload = {
'ProjectId' : projectId,
'Description' : description
};
const response = await fetch(apiUrl, {
method : 'POST',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
body : JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.OperationSetId;
}
This create function uses the D365 Project schedule msdyn_CreateOperationSetV1
API to initialize an operation set.
Executing operation sets
We need to call another API to execute the operation set.
Add the following executeOperationSet
function to the bottom of the crudFunctions.js
file:
export async function executeOperationSet(operationSetId) {
const accessToken = await getToken();
ensureScope(`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/api/data/v9.1/msdyn_ExecuteOperationSetV1`;
const payload = {
'OperationSetId' : operationSetId
};
const response = await fetch(apiUrl, {
method : 'POST',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
body : JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
This function uses the D365 Project schedule msdyn_ExecuteOperationSetV1
API to execute an operation set.
Abandoning operation sets
If the execution of an operation set fails, you need to abandon it, or you’ll get an error because each user can only have a maximum of ten open operation sets.
Add the following abandonOperationSet
function to the bottom of the crudFunctions.js
file:
export async function abandonOperationSet(operationSetId) {
const accessToken = await getToken();
ensureScope(`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/api/data/v9.1/msdyn_AbandonOperationSetV1`;
const payload = {
'OperationSetId' : operationSetId
};
const response = await fetch(apiUrl, {
method : 'POST',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
body : JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
Viewing operation sets can be useful for debugging. You can log in to https://YOUR_MICROSOFT_DYNAMICS_ORG_ID.crm4.dynamics.com/
and open https://YOUR_MICROSOFT_DYNAMICS_ORG_ID.api.crm4.dynamics.com/api/data/v9.1/msdyn_operationsets
to view your operation sets.
Updating tasks
Add the following updateProjectTask
function to the bottom of the crudFunctions.js
file:
export async function updateProjectTask(operationSetId, record, msdyn_displaysequence, isReorder, isParentTask) {
const accessToken = await getToken();
ensureScope(`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/api/data/v9.1/msdyn_PssUpdateV1`;
const payloadObj = {
Entity : {
msdyn_projecttaskid : record.id,
'@odata.type' : 'Microsoft.Dynamics.CRM.msdyn_projecttask',
'msdyn_outlinelevel' : record.childLevel + 1
},
OperationSetId : operationSetId
};
if (record.parentId) {
payloadObj.Entity['msdyn_parenttask@odata.bind'] = `msdyn_projecttasks(${record.parentId})`;
}
if (record.name) {
payloadObj.Entity.msdyn_subject = record.name;
}
if (record.startDate) {
payloadObj.Entity.msdyn_start = `${record.startDate.toISOString()}`;
}
// exclude end date for reorder operation and when updating parent task
if (record.endDate && !isReorder && !isParentTask) {
payloadObj.Entity.msdyn_finish = `${record.endDate.toISOString()}`;
}
if (record.note) {
payloadObj.Entity.msdyn_description = record.note;
}
if (msdyn_displaysequence) {
payloadObj.Entity.msdyn_displaysequence = msdyn_displaysequence;
}
// The Progress, EffortCompleted, and EffortRemaining fields can be edited in Project for the Web, but they can't be edited in Project Operations: https://learn.microsoft.com/en-us/dynamics365/project-operations/project-management/schedule-api-preview
const response = await fetch(apiUrl, {
method : 'POST',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
body : JSON.stringify(payloadObj)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
This update function makes a POST request to the D365 Project schedule msdyn_PssUpdateV1
API, which is used to update a Project scheduling entity. If the updated task is a parent task or the update is a reorder, the end date cannot be altered, as it is a read-only value in Microsoft Project.
Deleting tasks
Add the following deleteProjectTask
function to the bottom of the crudFunctions.js
file:
export async function deleteProjectTask(operationSetId, recordId) {
const accessToken = await getToken();
ensureScope(`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/api/data/v9.1/msdyn_PssDeleteV1`;
const payload = {
'EntityLogicalName' : 'msdyn_projecttask',
'RecordId' : recordId,
'OperationSetId' : operationSetId
};
const response = await fetch(apiUrl, {
method : 'POST',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
body : JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
This delete function makes a POST request to the Project schedule msdyn_PssDeleteV1
API, which is used to delete a Project scheduling entity.
Creating dependencies
Add the following createProjectTaskDependency
function to the bottom of the crudFunctions.js
file:
export async function createProjectTaskDependency(projectId, operationSetId, record) {
const accessToken = await getToken();
ensureScope(`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/api/data/v9.1/msdyn_PssCreateV1`;
const payload = {
'Entity' : {
'msdyn_PredecessorTask@odata.bind' : `msdyn_projecttasks(${record.fromEvent.id ? record.fromEvent.id : record.fromEvent})`,
'msdyn_SuccessorTask@odata.bind' : `msdyn_projecttasks(${record.toEvent.id ? record.toEvent.id : record.toEvent})`,
'msdyn_projecttaskdependencylinktype' : 1,
'@odata.type' : 'Microsoft.Dynamics.CRM.msdyn_projecttaskdependency',
'msdyn_Project@odata.bind' : `msdyn_projects(${projectId})`
},
'OperationSetId' : operationSetId
};
const response = await fetch(apiUrl, {
method : 'POST',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
body : JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
In this create function, we use the same D365 Project schedule msdyn_PssCreateV1
API that we used to create a task. The '@odata.type'
entity property indicates the type of entity the function creates, which in this case is a Project task dependency.
Updating dependencies
Project task dependencies can’t be updated directly. To update a dependency, you need to delete the dependency and then create a new dependency with the updated data.
Deleting dependencies
Add the following deleteProjectTaskDependency
function to the bottom of the crudFunctions.js
file:
export async function deleteProjectTaskDependency(recordId, operationSetId) {
const accessToken = await getToken();
ensureScope(`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/.default`);
if (!accessToken) {
throw new Error('Access token is missing');
}
const apiUrl = `https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/api/data/v9.1/msdyn_PssDeleteV1`;
const payload = {
'EntityLogicalName' : 'msdyn_projecttaskdependency',
'RecordId' : recordId,
'OperationSetId' : operationSetId
};
const response = await fetch(apiUrl, {
method : 'POST',
headers : {
'Authorization' : `Bearer ${accessToken}`,
'OData-MaxVersion' : '4.0',
'OData-Version' : '4.0',
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
body : JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
In this function, we use the same D365 Project schedule msdyn_PssDeleteV1
API that we used to delete a task. The 'EntityLogicalName'
entity property indicates the type of entity the function deletes, which in this case is a Project task dependency.
Using a data change event listener and CRUD methods to sync Bryntum Gantt with Microsoft Project
Next, we’ll add a data change event listener to our Bryntum Gantt. This will allow us to update the user’s Microsoft Project tasks and dependencies when they update their tasks and dependencies in the Bryntum Gantt.
Add the following listener
property to the gantt
config in the main.js
file:
listeners : {
dataChange : function(event) {
updateMicrosoftProject(event);
}
}
The event
argument in the dataChange
event listener callback contains the changed event’s data. We pass the event data to a function called updateMicrosoftProject
, which we’ll use to update the user’s Microsoft Project plan tasks and dependencies.
Add the following definition for the updateMicrosoftProject
function at the bottom of the main.js
file:
async function updateMicrosoftProject({ action, record, store, records }) {
const storeId = store.id;
if (storeId === 'tasks') {
if (action === 'update') {
if (`${record.id}`.startsWith('_generated')) {
if (!record.name) return;
let operationSetId = '';
const projectId = import.meta.env.VITE_MSDYN_PROJECT_ID;
const projectBucketId = import.meta.env.VITE_MSDYN_PROJECTBUCKET_VALUE;
const description = 'Create operation set for new project task';
try {
gantt.maskBody('Creating task...');
operationSetId = await createOperationSet(projectId, description);
let previousSibling = record.previousSibling?.msdyn_displaysequence;
let nextSibling = record.nextSibling?.msdyn_displaysequence;
// Check if task is a subtask
const isSubtask = record.parentId !== null;
if (isSubtask) {
const parentTask = gantt.taskStore.getById(record.parentId);
const { prevSeq, nextSeq } = getSubtaskBoundaries(parentTask, record);
previousSibling = prevSeq;
nextSibling = nextSeq;
}
// If previous sibling has children, get the last child's display sequence.
if (previousSibling && record.previousSibling?.children?.length > 0) {
previousSibling = record.previousSibling.children.map((child) => child.data.msdyn_displaysequence).sort((a, b) => a - b).at(-1);
}
// Prev and no next - check if previous sibling has children - if yes -> get its next sibling's display sequence.
if (previousSibling && record.previousSibling?.children?.length > 0 && !nextSibling) {
const newNextSibling = record.previousSibling.nextSibling;
if (newNextSibling) {
nextSibling = newNextSibling.data.msdyn_displaysequence;
}
}
const msdyn_displaysequence = calculateNewDisplaySequence(previousSibling, nextSibling);
const createProjectTaskResponse = await createProjectTask(projectId, projectBucketId, operationSetId, record, msdyn_displaysequence);
const newId = JSON.parse(createProjectTaskResponse.OperationSetResponse)['k__BackingField'][3].Value;
await executeOperationSet(operationSetId);
// Update id
gantt.project.taskStore.applyChangeset({
updated : [
// Will set proper id for added task
{
$PhantomId : record.id,
id : newId
}
]
});
// Check if task is available for CRUD operations
await waitForOperationSetCompletion(operationSetId, 'task');
return;
}
catch (error) {
await abandonOperationSet(operationSetId);
console.error('Error:', error);
}
finally {
gantt.unmaskBody();
}
}
else {
if (Object.keys(record.meta.modified).length === 0) return;
if (record.meta.modified.effort === 0 && Object.keys(record.meta.modified).length === 1) return;
if (record.meta.modified.id && Object.keys(record.meta.modified).length === 1) return;
if (record.meta.modified.name === null) return;
let operationSetId = '';
const projectId = import.meta.env.VITE_MSDYN_PROJECT_ID;
const description = 'Create operation set for updating a project task';
try {
operationSetId = await createOperationSet(projectId, description);
let previousSibling = record.previousSibling?.msdyn_displaysequence;
let nextSibling = record.nextSibling?.msdyn_displaysequence;
// Check if task is subtask
const isSubtask = record.parentId !== null;
if (isSubtask) {
const parentTask = gantt.taskStore.getById(record.parentId);
const { prevSeq, nextSeq } = getSubtaskBoundaries(parentTask, record);
previousSibling = prevSeq;
nextSibling = nextSeq;
}
// If previous sibling has children, get the last child's display sequence
if (previousSibling && record.previousSibling?.children?.length > 0) {
previousSibling = record.previousSibling.children.map((child) => child.data.msdyn_displaysequence).sort((a, b) => a - b).at(-1);
}
// Prev and no next - check if previous sibling has children - if yes -> get its next sibling's display sequence
if (previousSibling && record.previousSibling?.children?.length > 0 && !nextSibling) {
const newNextSibling = record.previousSibling.nextSibling;
if (newNextSibling) {
nextSibling = newNextSibling.data.msdyn_displaysequence;
}
}
const msdyn_displaysequence = calculateNewDisplaySequence(previousSibling, nextSibling);
const isReorder = record.meta.modified.orderedParentIndex !== undefined;
gantt.project.taskStore.commit();
const isParentTask = record.children?.length > 0;
await updateProjectTask(operationSetId, record, msdyn_displaysequence, isReorder, isParentTask);
await executeOperationSet(operationSetId);
return;
}
catch (error) {
await abandonOperationSet(operationSetId);
console.error('Error:', error);
}
}
}
if (action === 'remove') {
const recordsData = records.map((record) => record.data);
recordsData.forEach(async(record) => {
if (record.id.startsWith('_generated')) return;
let operationSetId = '';
const projectId = import.meta.env.VITE_MSDYN_PROJECT_ID;
const description = 'Create operation set for deleting project task';
try {
operationSetId = await createOperationSet(projectId, description);
await deleteProjectTask(operationSetId, record.id);
await executeOperationSet(operationSetId);
return;
}
catch (error) {
await abandonOperationSet(operationSetId);
console.error('Error:', error);
}
});
}
}
if (storeId === 'dependencies') {
const recordsData = records.map((record) => record.data);
recordsData.forEach(async(record) => {
if (action === 'update') {
if (`${record.id}`.startsWith('_generated')) {
// Create new dependency
let operationSetId = '';
const projectId = import.meta.env.VITE_MSDYN_PROJECT_ID;
const description = 'Create operation set for new project task dependency';
try {
gantt.maskBody('Creating dependency...');
operationSetId = await createOperationSet(projectId, description);
const createProjectTaskDependencyResponse = await createProjectTaskDependency(projectId, operationSetId, record);
await executeOperationSet(operationSetId);
// Update id
gantt.project.dependencyStore.applyChangeset({
updated : [
// Will set proper id for added dependency
{
$PhantomId : record.id,
id : JSON.parse(createProjectTaskDependencyResponse.OperationSetResponse)['k__BackingField'][3].Value
}
]
});
// temporarily disable the dependency update listener
disableUpdate = true;
setTimeout(() => {
disableUpdate = false;
}, 600);
await waitForOperationSetCompletion(operationSetId, 'dependency');
return;
}
catch (error) {
await abandonOperationSet(operationSetId);
console.error('Error:', error);
}
finally {
gantt.unmaskBody();
}
}
else {
// Delete old dependency
let operationSetId = '';
let description = '';
const projectId = import.meta.env.VITE_MSDYN_PROJECT_ID;
description = 'Operation set for updating a project task dependency: delete old and create new';
try {
gantt.maskBody('Updating dependency...');
operationSetId = await createOperationSet(projectId, description);
if (disableUpdate) return;
await deleteProjectTaskDependency(record.id, operationSetId);
const createProjectTaskDependencyResponse = await createProjectTaskDependency(projectId, operationSetId, record);
await executeOperationSet(operationSetId);
// Update id
gantt.project.dependencyStore.applyChangeset({
updated : [
// Will set proper id for added dependency
{
$PhantomId : record.id,
id : JSON.parse(createProjectTaskDependencyResponse.OperationSetResponse)['k__BackingField'][3].Value
}
]
});
await waitForOperationSetCompletion(operationSetId, 'dependency');
return;
}
catch (error) {
await abandonOperationSet(operationSetId);
await abandonOperationSet(operationSetId);
console.error('Error:', error);
}
finally {
gantt.unmaskBody();
}
}
}
if (action === 'remove') {
let operationSetId = '';
const projectId = import.meta.env.VITE_MSDYN_PROJECT_ID;
const description = 'Create operation set for deleting project task dependency';
try {
operationSetId = await createOperationSet(projectId, description);
await deleteProjectTaskDependency(record.id, operationSetId);
await executeOperationSet(operationSetId);
}
catch (error) {
await abandonOperationSet(operationSetId);
console.error('Error:', error);
}
}
});
}
}
We use this function to call the appropriate CRUD function in our crudFunction.js
file based on whether an 'update'
or 'remove'
action triggered the data change. We create a new task or dependency when an update
action occurs with an event id
that starts with '_generated'
. New records in Bryntum Gantt are assigned a temporary UUID beginning with '_generated'
. An 'update'
action then occurs right after the 'create'
action for a created task.
After creating a Microsoft Project task or dependency, we update the Bryntum Gantt task or dependency store with the id
value assigned to the task by Microsoft Project. We do this using the applyChangeset
method. The $PhantomId
field contains a unique, auto-generated client-side value used to identify a record. You can read more about phantom identifiers in our docs.
The display order of Microsoft Project tasks is determined by msdyn_displaysequence
, which we calculate using the previousSibling
and nextSibling
properties, as the msdyn_displaysequence
value of each Bryntum Gantt event depends on the msdyn_displaysequence
values of the events above and below it.
Add the following variable to the top of the main.js
file:
let disableUpdate = false;
Add the following getSubtaskBoundaries
function to the top of the main.js
file:
function getSubtaskBoundaries(parentTask, record) {
let prevSeq, nextSeq;
// 1. Figure out prev boundary
if (record.previousSibling) {
prevSeq = record.previousSibling.msdyn_displaysequence;
}
else {
// No previous sibling => use parent's sequence
prevSeq = parentTask.data.msdyn_displaysequence;
}
// 2. Figure out next boundary
if (record.nextSibling) {
nextSeq = record.nextSibling.msdyn_displaysequence;
}
else {
// If parent has a next sibling at top level, use that
if (parentTask.nextSibling) {
nextSeq = parentTask.nextSibling.data.msdyn_displaysequence;
}
else {
// Parent is last => fallback
nextSeq = prevSeq + 1;
}
}
return { prevSeq, nextSeq };
}
This function determines the display sequence boundaries used to calculate the display sequence of a created or updated subtask.
Add the following calculateNewDisplaySequence
function to the top of the main.js
file:
function calculateNewDisplaySequence(prevSeq, nextSeq) {
if (prevSeq === undefined) {
prevSeq = 1;
}
if (nextSeq === undefined) {
return prevSeq + 1;
}
let newSeq = (prevSeq + nextSeq) / 2;
// Round to max 9 decimal places
// so we don't exceed the msdyn_displaysequence column's precision limit
const seqParts = newSeq.toString().split('.');
const decimalPart = seqParts[1] || '';
const decimalCount = decimalPart.length;
if (decimalCount > 9) {
// Round to 9 decimals
newSeq = Math.round(newSeq * 1e9) / 1e9;
}
return newSeq;
}
This function calculates the display sequence of a created or updated subtask based on the display sequences of the tasks immediately preceding and following it in the Gantt chart.
Add the following waitForOperationSetCompletion
function to the crudFunctions.js
file:
export async function waitForOperationSetCompletion(operationSetId, operationType, maxRetries = 40, delay = 300) {
for (let i = 0; i < maxRetries; i++) {
const status = await getOperationSetStatus(operationSetId);
if (status === 192350003) {
console.log('Operation set completed');
return true;
}
if (status === 192350001) {
console.log(`Operation set processing. Waiting for ${operationType} creation in Dataverse...`);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
throw new Error('Operation set completion timed out');
}
We use this function to check that a newly created task or dependency has been added to Microsoft Project. We need to do this, as the API operations are slow, and as shown in the Microsoft article on Project schedule API performance, creating a task or dependency takes more than eight seconds.
As such, we need to make Bryntum Gantt unresponsive for a few seconds after a task or dependency is created, by masking the Gantt chart. This temporarily blocks updates, ensuring that the user cannot update a task before it is added to their Microsoft Project plan. If a Bryntum Gantt isn’t masked, any attempts to update a task immediately after creation will fail, as the task won’t have been created in Microsoft Project yet, and the user will encounter an error.
Note that even single-record operations need to be performed in an operation set.
Define the getOperationSetStatus
below the waitForOperationSetCompletion
function:
export async function getOperationSetStatus(operationSetId) {
const accessToken = await getToken();
ensureScope(`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/.default`);
const response = await fetch(
`https://${import.meta.env.VITE_MICROSOFT_DYNAMICS_ORG_ID}.api.crm4.dynamics.com/api/data/v9.1/msdyn_operationsets(${operationSetId})`, {
headers : {
'Authorization' : `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error('Failed to get operation set status');
}
const data = await response.json();
return data.msdyn_status;
}
This function gets the status of an operation set, which tells us whether it has been completed.
Import the CRUD functions from the crudFunctions.js
file in main.js
:
import { abandonOperationSet, createOperationSet, createProjectTask, createProjectTaskDependency, deleteProjectTask, deleteProjectTaskDependency, executeOperationSet, updateProjectTask, waitForOperationSetCompletion } from './crudFunctions.js';
Disabling create and delete operations when collapsing or expanding tasks
When you expand a parent task to show its child tasks, an add event is triggered. When you collapse a parent task to hide its child tasks, a remove event is triggered.
Let’s use flag variables and a cell click event listener to prevent tasks from being added or deleted when their parent tasks are expanded or collapsed.
Add the following variables to the top of the main.js
file:
let disableCreate = false;
let disableDelete = false;
Add the following line of code to the if
statement in the updateMicrosoftProject
function, where storeId === 'tasks'
, action === 'update'
, and `${record.id}`.startsWith('_generated')
:
if (disableCreate) return;
Add the following line of code to the if
statement in the updateMicrosoftProject
function, where storeId === 'tasks'
and action === 'remove'
:
if (disableDelete) return;
Add the following cellClick
event listener to the listeners
object of the gantt
:
cellClick : function({ target }) {
if (target.className === 'b-tree-expander b-icon b-icon-tree-collapse' || target.className === 'b-tree-expander b-icon b-icon-tree-expand') {
disableCreate = true;
disableDelete = true;
setTimeout(() => {
disableCreate = false;
disableDelete = false;
}, 50);
}
}
If the expand or collapse button of a parent task is clicked, we temporarily disable the ability to create or delete tasks.
Run your dev server using npm run dev
.
Create, update, delete, and edit tasks or dependencies in the Bryntum Gantt. The changes you make will be reflected in your Microsoft Project plan.
Note that updates aren’t always instantaneous; it may take a few seconds for changes to be reflected in Microsoft Project.
Next steps
This tutorial gives you a starting point for creating a vanilla JavaScript Bryntum Gantt app and syncing it with Microsoft Project. To improve your Bryntum Gantt, you can sync more of the Microsoft Project plan task fields, such as priority and buckets. You can also take a look at our Bryntum Gantt demo page to get an idea of all the available features you can add to your Gantt to meet your business needs.