Arsalan Khattak
21 February 2025

How to connect and sync Bryntum Gantt to a ServiceNow Project Portfolio Management planning chart

Bryntum Gantt is a fully customizable, high-performance JavaScript Gantt chart that you can use with vanilla JavaScript or your framework […]

Bryntum Gantt is a fully customizable, high-performance JavaScript Gantt chart that you can use with vanilla JavaScript or your framework of choice: React, Vue, or Angular.

In this tutorial, we’ll show you how to connect and sync Bryntum Gantt to a ServiceNow Project Portfolio Management (PPM) project. We’ll do the following:

Getting started with ServiceNow

ServiceNow offers free Personal Developer Instances (PDI) to registered users.

First, create a ServiceNow account to get a ServiceNow ID.

Then, log in to ServiceNow and select the Developer Program tile. Follow the prompts and you will be taken to a welcome page that displays a Start Building button. Click the button to initialize your PDI.

The ServiceNow PDI used in this tutorial uses the ServiceNow Washington DC release.

Once your instance is initialized, a popup dialog will indicate that your instance is ready. Click the Open Instance button. Your PDI has a unique instance ID that you can see in the URL, for example, https://<instance ID>.service-now.com.

Now that you have a PDI, let’s add an example PPM project.

Accessing ServiceNow Project Portfolio Management features

The ServiceNow Project Portfolio Management (PPM) product provides tools to manage projects, portfolios, their resources, and financials.

Follow these steps to install the PPM plugins in your PDI:

The installation process may take a couple of minutes.

Accessing an example PPM project

In your PDI, open the Project Workspace by searching for it in the All menu. It will be under the Project group:

In the My Projects Space, search for the “MobiChord – Phase 3” project card and click on it:

We’ll use this example PPM project for this tutorial.

The project dashboard has tabs for managing and analyzing the project.

Click on the Planning tab to see a Gantt chart displaying the project data:

The Gantt chart is interactive. You can drag tasks, resize tasks, and add dependencies using your mouse. You can edit a task by double-clicking it or right-clicking on it and selecting Edit in the menu that opens.

Notice that opening the task editor may be delayed as a new HTTP request is made each time it’s opened. The task editor is server-side rendered.

Let’s create a dependency. Hover your cursor over the Initiation taskbar. You’ll see two grey circles added to the ends of the task. Click and hold on the circle at the end of the task and drag your cursor to the start of the Planning taskbar. When you release, a dependency between the two tasks is added.

Click the save changes icon at the top-right of the Gantt chart.

Getting project data using the ServiceNow REST API Explorer

We’ll use the ServiceNow REST API Explorer to test the ServiceNow REST APIs that perform CRUD operations on our project’s data. Note that a ServiceNow GraphQL API is also available.

Click the All button in your PDI, search for “REST API Explorer”, and select it.

A form will open that you can use to build and send an HTTP request. If prompted, first click the Explore button to get to the form.

On the left-hand side of the form, you can select the API name and the type of HTTP request to make.

The Table API allows you to perform CRUD operations on existing database tables.

Retrieving all tasks and dependencies in a project

Let’s get all the tasks for our example “MobiChord – Phase 3” project. In the REST API Explorer, set the following form values:

Set the following Query parameters:

PPM tasks and projects are organized in a tree. The sub_tree_root query parameter allows you to retrieve all records that are descendants of a root record in the tree of data.

The sys_id value is a globally unique identifier assigned to each record in your PDI.

To get the sys_id of your “MobiChord – Phase 3” project, open the project in the My Projects Space page and navigate to the Details tab.

Click on the additional actions menu at the top left of the page and select “Copy sys_id” from the menu to copy it to your clipboard.

Click the Send button in the REST API Explorer form to send the HTTP request. Scroll to the bottom of the page to see the response body. An object will be returned with a “result” property that contains an array of the project’s tasks:

{
  "result": [
    {
      "short_description": "Closing"
    },
    {
      "short_description": "Control & Monitoring"
    },
    {
      "short_description": "Initiation"
    },
    {
      "short_description": "Execution"
    },
    {
      "short_description": "Planning"
    }
  ]
}

When we create the Bryntum Gantt app, we’ll add additional task fields and associated dependencies to include in the response when retrieving data.

Below the Send button, you’ll see a Code samples section where you can find code samples to send this request using commonly used languages.

There’s also a Request section that contains the URI to make this request. We’ll use this URI in our Bryntum Gantt app backend API.

Creating a full-stack Bryntum Gantt application

Clone the Express server starter GitHub repository. The completed-app branch contains the code for the completed tutorial.

Install the dependencies:

npm install

A basic Express app defined in the server.js file serves static files in the public folder and provides a way to query the ServiceNow REST API from API routes. The public folder contains the client-side HTML, CSS, and JavaScript files we’ll use to create a Bryntum Gantt. The requests require an authorization header that contains the login details for your ServiceNow PDI.

The utils.js file contains utility functions for converting ServiceNow data to Bryntum Gantt-compatible data and vice versa. We’ll use these utility functions in the Express API routes we create.

We’ll append the Bryntum Gantt to the <div> defined in the public/index.html file. The CSS file contains styling to make the Bryntum Gantt take up the whole screen.

Let’s create a load API to get the tasks and dependencies from our ServiceNow project.

Creating an API endpoint to get the ServiceNow project’s tasks and dependencies

We’ll create two API endpoints in our Express server: /api/load and /api/sync.

We’ll use the /api/sync endpoint to save data changes in the Bryntum Gantt to the ServiceNow project. The Bryntum Gantt Crud Manager will fetch data and sync data changes to the backend. The Crud Manager combines all changes from the Bryntum Gantt data stores (tasks and dependencies) into a single HTTP request, preventing data inconsistencies that can happen if stores are managed separately.

While this tutorial only uses two types of Bryntum Gantt data stores, tasks and dependencies, other available data stores include calendars, assignments, and resources.

In the root folder of your Bryntum Gantt app, create a .env file and add the following environmental variables to it:

PORT=1337
SERVICENOW_PDI_ID=
SERVICENOW_USERNAME=admin
SERVICENOW_PASSWORD=
SERVICENOW_PROJECT_SYS_ID=

To get the sys_id of your project, open the project in the My Projects Space page and navigate to the Details tab. Click on the additional actions menu at the top left of the page and select “Copy sys_id” from the menu to copy it to your clipboard.

Add your PDI’s ServiceNow ID (which you can find in the PDI URL: https://<SERVICENOW_PDI_ID>.service-now.com/), username, and password.

In the server.js file, add the following GET request /api/load route below the app.use(bodyParser.json()); code line:

app.get('/api/load', async(req, res) => {
    try {
        const projectResponse = await fetch(
          `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/pm_project?sysparm_fields=sys_id,short_description,start_date,end_date,wbs_order,percent_complete,description,status,override_status&sys_id=${process.env.SERVICENOW_PROJECT_SYS_ID}`,
          {
              headers : {
                  Accept         : 'application/json',
                  'Content-Type' : 'application/json',
                  Authorization :
                'Basic ' +
                btoa(
                  `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
                )
              }
          }
        );
        const projectResult = await projectResponse.json();
        const tasksResponse = await fetch(
          `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/pm_project_task?sysparm_fields=relation_applied,relation_applied.parent,relation_applied.lag,relation_applied.sub_type,sys_id,short_description,parent,project,start_date,end_date,wbs_order,percent_complete,description,status,override_status&sub_tree_root=${process.env.SERVICENOW_PROJECT_SYS_ID}`,
          {
              headers : {
                  Accept         : 'application/json',
                  'Content-Type' : 'application/json',
                  Authorization :
                'Basic ' +
                btoa(            `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
                )
              }
          }
        );
        const tasksResult = await tasksResponse.json();
        const dependencies = [];
        const tasks = tasksResult?.result.map((task) => {
        // add dependencies
            if (task.relation_applied) {
                // calculate lag and lagUnit
                const { lag, lagUnit } = serviceNowDepLagToBryntumDepLag(
                    task['relation_applied.lag']
                );
                dependencies.push({
                    id   : task.relation_applied.value,
                    from : task['relation_applied.parent'].value,
                    to   : task.sys_id,
                    // map to Bryntum Gantt dependency type
                    type : serviceNowDepTypeToBryntumDepType(
                        task['relation_applied.sub_type']
                    ),
                    lag,
                    lagUnit
                });
            }
            return {
                id                : task.sys_id,
                name              : task.short_description,
                parentId          : task.parent.value,
                startDate         : task.start_date,
                endDate           : task.end_date,
                parentIndex       : parseInt(task.wbs_order),
                percentDone       : Math.round(parseInt(task.percent_complete)),
                note              : task.description,
                status            : task.status,
                override_status   : parseInt(task.override_status),
                manuallyScheduled : true,
                expanded          : true
            };
        });
        if (!projectResult?.result[0]) throw new Error('Project data not found');
        const {
            sys_id,
            short_description,
            start_date,
            end_date,
            wbs_order,
            percent_complete,
            description,
            status,
            override_status
        } = projectResult?.result[0];
        tasks.push({
            id                : sys_id,
            name              : short_description,
            parentId          : null,
            startDate         : start_date,
            endDate           : end_date,
            parentIndex       : parseInt(wbs_order),
            percentDone       : Math.round(parseInt(percent_complete)),
            note              : description,
            status            : status,
            override_status   : parseInt(override_status),
            manuallyScheduled : true,
            expanded          : true
        });
        res.send({
            success : true,
            tasksResult,
            tasks   : {
                rows : tasks
            },
            dependencies : {
                rows : dependencies
            }
        });
    }
    catch (error) {
        console.error(error);
        res.send({
            success : false,
            message : 'There was an error getting the tasks'
        });
    }
});

Import the following utility functions from the utils.js file:

import {
    serviceNowDepLagToBryntumDepLag,
    serviceNowDepTypeToBryntumDepType
} from './utils.js';

We make two fetch requests in the API route. The first request fetches the project data from the ServiceNow project management project table called “pm_project”. We find the specific “MobiChord – Phase 3” project by adding its sys_id to the query parameters. We limit the data returned by adding only the fields we need to the sysparm_fields query parameter. This fetch request is a modified version of the example JavaScript code provided for this GET request by the ServiceNow REST API Explorer.

The second fetch request gets the tasks and dependencies for the project. After fetching the data, we loop through the fetched tasksResult data and modify the data structure so that it’s formatted for a Bryntum Gantt.

We also populate the dependencies array by checking if any dependencies have been applied to a task. We do this by checking if the relation_applied property has been populated for each task. If so, we structure the dependency to match the structure of a Bryntum Gantt dependency. We use two helper functions from the utils.js file, serviceNowDepLagToBryntumDepLag and serviceNowDepTypeToBryntumDepType, to get the values for the lag, lagUnit, and type dependency fields.

We then add the data for the project to the tasks array and sort the array in descending order, based on its parentIndex value.
The Bryntum Gantt Crud Manager expects the data it receives from the /api/load API endpoint to have a specific structure.

Run the development server locally using the following command:

npm start

Open your browser to the load API URL at http://localhost:1337/api/load and you’ll see the task and dependency data from your ServiceNow project:

{
  "success": true,
  "tasksResult": {
    "result": [
      {
        "end_date": "2022-07-02 00:00:00",
        "short_description": "Closing"
        // ...
      }
    ]
  }
}

Creating a Bryntum Gantt

Install the Bryntum Gantt component using npm. First, follow the access to npm registry guide in the Bryntum docs to access the private Bryntum npm registry. Once you’ve logged in to the registry, install the Gantt component by following the guide here.

In the index.js file in the public folder, add the following lines of code to create a Bryntum Gantt instance:

import { Gantt } from './gantt.module.js';
const gantt = new Gantt({
    appendTo   : 'app',
    viewPreset : 'year',
    barMargin  : 10,
    project : {
        taskStore : {
            transformFlatData : true
        },
        loadUrl          : 'http://localhost:1337/api/load',
        autoLoad         : true,
        syncUrl          : 'http://localhost:1337/api/sync',
        autoSync         : true,
        // This config enables response validation and dumping of found errors to the browser console.
        // It's meant to be used as a development stage helper only so please set it to false for production.
        validateResponse : true
    },
    columns : [{ type : 'name', field : 'name', width : 250 }]
});

The Bryntum Gantt will be appended to the <div> with an id of app in the index.html file. The project holds and links the data stores, and we configure it to load data from the /api/load API endpoint on page load. The Bryntum Gantt project Crud Manager loads data from the loadUrl and saves changes to the syncUrl, using the Fetch API for requests and JSON as the encoding format.

As the task data we receive from the ServiceNow API has a flat data structure, the task store is configured to transform flat data into a tree data structure.

Add the following line of code to your server.js file, below the app.use(express.static(path.join(__dirname, 'public'))); line:

app.use(express.static(path.join(__dirname, '/node_modules/@bryntum/gantt')));

This code makes the Bryntum Gantt npm library files accessible from the public folder.

Add the following <link> HTML tag to the <head> of the index.html file:

<link rel="stylesheet" href="./gantt.stockholm.css" data-bryntum-theme>

This adds the Stockholm theme styling to the Bryntum Gantt. Bryntum Gantt can be styled using one of five Bryntum themes, using multiple themes, or by creating a custom theme.

Now run the local server and browse to http://localhost:1337/ to see the Bryntum Gantt populated with the ServiceNow project tasks and dependencies data:

Creating custom Bryntum Gantt fields

The Bryntum Gantt and ServiceNow project data share some overlap in their task and dependency fields, for example, the start-date and end-date fields. Let’s add custom fields to the Bryntum Gantt project to accommodate ServiceNow-specific data.

In the public folder, create a folder called lib. Create a file called CustomTaskModel.js in the lib folder and add the following lines of code to it:

import { TaskModel } from '../gantt.module.js';
// Custom event model
export default class CustomTaskModel extends TaskModel {
    static $name = 'CustomTaskModel';
    static fields = [
        { name : 'status', type : 'string', defaultValue : 'red' },
        { name : 'override_status', type : 'boolean', defaultValue : false }
    ];
}

We create a custom task model by extending the existing TaskModel and adding status and override_status fields to the static fields property. We return the data for these additional fields through the /api/load API endpoint.

To use this custom task model in the Bryntum Gantt, add the following property to the project configuration in the Gantt’s index.js file:

eventModelClass: CustomTaskModel,

Import the custom task model:

import CustomTaskModel from './lib/CustomTaskModel.js';

In the development server, right-click on a task and select Edit from the popup menu to open the task editor.

Notice that the task editor displays no inputs for the custom task fields. Let’s add them.

Copy the following features property and paste it into the Bryntum Gantt config:

    features : {
        taskEdit : {
            items : {
                generalTab : {
                    items : {
                        statusField : {
                            type     : 'combo',
                            label    : 'Status',
                            name     : 'status',
                            items    : ['red', 'yellow', 'green'],
                            required : 'true'
                        },
                        overrideStatusField : {
                            type          : 'checkbox',
                            label         : 'Override status',
                            labelPosition : 'before',
                            name          : 'override_status'
                        }
                    }
                }
            }
        }
    }

The taskEdit feature allows you to customize the task editor. We added the two custom fields as inputs in the General tab.

Now you’ll see the inputs for the custom fields in the task editor:

Next, we’ll create an API endpoint to save data changes in the Bryntum Gantt to the ServiceNow project.

Creating an API endpoint to sync Bryntum Gantt data changes to the ServiceNow project

We’ll use a single API endpoint to save data changes in the Bryntum Gantt to the ServiceNow project.

In the server.js file, add the following /api/sync route below the /api/load route:

app.post('/api/sync', async function(req, res) {
    const { requestId, tasks, dependencies } = req.body;
    try {
        const response = { requestId, success : true };
        // if task changes are passed
        if (tasks) {
            const rows = await applyTableChanges('tasks', tasks);
            // if got some new data to update client
            if (rows) {
                response.tasks = { rows };
            }
        }
        // if dependency changes are passed
        if (dependencies) {
            const rows = await applyTableChanges('dependencies', dependencies);
            // if got some new data to update client
            if (rows) {
                response.dependencies = { rows };
            }
        }
        res.send(response);
    }
    catch (error) {
        console.error(error);
        res.send({
            requestId,
            success : false,
            message : 'There was an error syncing the data changes'
        });
    }
});

Define the applyTableChanges function at the end of the server.js file:

async function applyTableChanges(table, changes) {
    let rows;
    if (changes.added) {
        rows = await createOperation(changes.added, table);
    }
    if (changes.updated) {
        await updateOperation(changes.updated, table);
    }
    if (changes.removed) {
        await deleteOperation(changes.removed, table);
    }
    // if got some new data to update client
    return rows;
}

The /api/sync route handler checks the request body of a sync POST request to determine which CRUD action should be performed on the project data. The applyTableChanges function is called to perform the CRUD operations using helper functions.

Next, we’ll define the CRUD operation helper functions.

Creating a task

Add the following createOperation function at the end of the server.js file:

function createOperation(added, table) {
    return Promise.all(
        added.map(async(record) => {
            const { $PhantomId, ...data } = record;
            let result;
            if (table === 'tasks') {
                const taskData = {
                    short_description : data.name,
                    parent            : data.parentId,
                    start_date        : formatDateServiceNow(data.startDate),
                    end_date          : formatDateServiceNow(data.endDate),
                    duration          : calculateDuration(data.startDate, data.endDate),
                    project           : process.env.SERVICENOW_PROJECT_SYS_ID,
                    wbs_order         : data.parentIndex+1,
                    description       : data.note,
                    status            : data.status,
                    override_status   : parseInt(data.override_status),
                    constraint_date   : formatDateServiceNow(data.startDate),
                    time_constraint   : "snet",
                };
                const createTaskResponse = await fetch(
                  `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/pm_project_task`,
                  {
                      method  : 'POST',
                      headers : {
                          Accept         : 'application/json',
                          'Content-Type' : 'application/json',
                          Authorization :
                        'Basic ' +
                        btoa(
                          `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
                        )
                      },
                      body : JSON.stringify(taskData)
                  }
                );
                const createTaskResJSON = await createTaskResponse.json();
                result = createTaskResJSON?.result;
            }
            if (table === 'dependencies') {
            }
            // Report to the client that the record identifier has been changed
            return { $PhantomId, id : result?.sys_id };
        })
    );
}

We loop through the added store records. If a task should be created, we create a taskData object. This object maps the Bryntum Gantt task fields to the equivalent ServiceNow project task fields. We use the formatDateServiceNow utility function from our utils.js function to format the start date and end date fields for ServiceNow. The calculateDuration utility method is used to calculate the task duration, which in ServiceNow is the number of days and time from January 1, 1970, 00:00:00.

The fetch request is a modified version of the example JavaScript code provided for this POST request by the ServiceNow REST API Explorer.

We return the added store item’s ID and phantom ID (a unique auto-generated client-side value used to identify a record) so that the Bryntum Gantt can set the correct ServiceNow-generated ID for the added item. The phantom ID should not be persisted in the database. The backend should assign new IDs for records.

Import the utility functions we used to create the taskData object:

import { formatDateServiceNow, calculateDuration } from './utils.js';

Creating a dependency

We’ll now add the code to create a new ServiceNow task dependency. Add the following code to the if statement in the createOperation function where table === 'dependencies' is:

const dependencyData = {
    parent   : data.from,
    child    : data.to,
    sub_type : bryntumDepTypeToServiceNowDepType(data.type),
    lag      : bryntumGanttDepLagToServiceNowDepLag(data.lag, data.lagUnit)
};
const createDependencyResponse = await fetch(
    `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/planned_task_rel_planned_task`,
    {
        method  : 'POST',
        headers : {
            Accept         : 'application/json',
            'Content-Type' : 'application/json',
            Authorization :
          'Basic ' +
          btoa(
            `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
          )
        },
        body : JSON.stringify(dependencyData)
    }
);
const createDependencyResJSON = await createDependencyResponse.json();
result = createDependencyResJSON?.result;

Import the utility functions that we used to create the dependencyData object:

import { bryntumDepTypeToServiceNowDepType, bryntumGanttDepLagToServiceNowDepLag } from './utils.js';

The code used to create a dependency is similar to the code used to create a task. The fetch request makes a POST request to the ServiceNow REST API to add the task dependency in the Planned Task Relationship table called planned_task_rel_planned_task.

Deleting a task

Add the following deleteOperation function at the end of the server.js file:

function deleteOperation(deleted, table) {
    return Promise.all(
        deleted.map(async(record) => {
            const { id } = record;
            if (table === 'tasks') {
                await fetch(
                  `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/pm_project_task/${id}`,
                  {
                      method  : 'DELETE',
                      headers : {
                          Accept         : 'application/json',
                          'Content-Type' : 'application/json',
                          Authorization :
                        'Basic ' +
                        btoa(
                          `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
                        )
                      }
                  }
                );
            }
            if (table === 'dependencies') {
            }
        })
    );
}

We loop through the items to delete. For each item, we get the ID of the record. If a task is deleted, we make a DELETE request to the ServiceNow API. The ID of the task is added to the URL as a route parameter.

Deleting a dependency

To delete a dependency, add the following code to the if statement in the deleteOperation function where table === 'dependencies' is:

await fetch(
    `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/planned_task_rel_planned_task/${id}`,
    {
        method  : 'DELETE',
        headers : {
            Accept         : 'application/json',
            'Content-Type' : 'application/json',
            Authorization :
          'Basic ' +
          btoa(
            `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
          )
        }
    }
);

We delete the task dependency from the planned_task_rel_planned_task ServiceNow table.

Updating a task

Add the following updateOperation function at the end of the server.js file:

function updateOperation(updated, table) {
    let result;
    return Promise.all(
        updated.map(async({ id, ...data }) => {
            const updateBody = {};
            if (table === 'tasks') {
                for (const [key, value] of Object.entries(data)) {
                    if (bryntumTaskFieldsToServiceNowFields[key]) {
                        updateBody[bryntumTaskFieldsToServiceNowFields[key]] = value;
                    }
                }

                // format dates and add required ServiceNow duration field
                // 2 possible cases for updating a task date:
                if (updateBody?.start_date && updateBody?.end_date) {
                    updateBody.start_date = formatDateServiceNow(data.startDate);
                    updateBody.end_date = formatDateServiceNow(data.endDate);
                    updateBody.duration = calculateDuration(data.startDate, data.endDate);
                    updateBody.percent_complete = `${data.percentDone}`;
                }
                else if (updateBody?.end_date && updateBody?.duration) {
                    const startDate = calcStartDate(
                        data.endDate,
                        data.duration,
                        data?.durationUnit
                    );
                    updateBody.end_date = formatDateServiceNow(data.endDate);
                    updateBody.start_date = startDate;
                    updateBody.duration = calculateDuration(startDate, data.endDate);
                }

                if (typeof updateBody.wbs_order === 'number'
                ) {
                    updateBody.wbs_order += 1;
                }

                const projectResponse = await fetch(
                    `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/pm_project?sysparm_fields=sys_id,short_description,start_date,end_date,wbs_order,percent_complete,description,status,override_status&sys_id=${process.env.SERVICENOW_PROJECT_SYS_ID}`,
                    {
                        headers : {
                            Accept         : 'application/json',
                            'Content-Type' : 'application/json',
                            Authorization :
                            'Basic ' +
                            btoa(
                            `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
                            )
                        }
                    }
                );
                let table = 'pm_project_task';
                const projectResult = await projectResponse.json();
                if (projectResult?.result[0]){
                    const { sys_id } = projectResult?.result[0];
                    if (sys_id === id){
                        table = 'pm_project';
                    }
                }

                await fetch(
                    `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/${table}/${id}`,
                    {
                        method  : 'PATCH',
                        headers : {
                            Accept         : 'application/json',
                            'Content-Type' : 'application/json',
                            Authorization :
                        'Basic ' +
                        btoa(
                            `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
                        )
                        },
                        body : JSON.stringify(updateBody)
                    }
                );
            }
            if (table === 'dependencies') {

            }
        })
    );
}

We loop through all the records to update. If tasks must be updated, we loop through the fields of the record and use the bryntumTaskFieldsToServiceNowFields helper method to map the Bryntum event fields to ServiceNow and save the result in the updateBody object. Import this method from the utils.js file:

import { calcStartDate, bryntumTaskFieldsToServiceNowFields } from './utils.js';

The passed-in updates only include the fields that have changed. We need the startDate, endDate, and duration values to update the ServiceNow project task. If the startDate has not changed, we calculate its value using the calcStartDate helper function. We pass in the endDate, duration, and durationUnit values as arguments for the helper function.

We then make a PATCH HTTP request to the ServiceNow REST API, passing in the updateBody object in the request’s body.

Updating a dependency

Add the following code to the if statement in the updateOperation function where table === 'dependencies' is:

for (const [key, value] of Object.entries(data)) {
    if (bryntumDependencyFieldsToServiceNowFields[key]) {
        updateBody[bryntumDependencyFieldsToServiceNowFields[key]] = value;
    }
}

updateBody.sub_type = bryntumDepTypeToServiceNowDepType(data.type);
updateBody.lag = bryntumGanttDepLagToServiceNowDepLag(
    data.lag,
    data.lagUnit
);

// 1. delete dependency then create new one. In ServiceNow it's a good practice to delete a relationship and create a new one between the correct tasks

await fetch(
    `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/planned_task_rel_planned_task/${id}`,
    {
        method  : 'DELETE',
        headers : {
            Accept         : 'application/json',
            'Content-Type' : 'application/json',
            Authorization :
          'Basic ' +
          btoa(
            `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
          )
        }
    }
);

const createDependencyResponse = await fetch(
    `https://${process.env.SERVICENOW_PDI_ID}.service-now.com/api/now/table/planned_task_rel_planned_task`,
    {
        method  : 'POST',
        headers : {
            Accept         : 'application/json',
            'Content-Type' : 'application/json',
            Authorization :
          'Basic ' +
          btoa(
            `${process.env.SERVICENOW_USERNAME}:${process.env.SERVICENOW_PASSWORD}`
          )
        },
        body : JSON.stringify(updateBody)
    }
);

const createDependencyResJSON = await createDependencyResponse.json();
result = createDependencyResJSON?.result;
// Report to the client that the record identifier has been changed
return { oldId: id, newId: result?.sys_id };

Import the bryntumDependencyFieldsToServiceNowFields utility function:

import { bryntumDependencyFieldsToServiceNowFields } from './utils.js';

Updating a dependency is different from updating a task. To update a dependency, we first delete the existing dependency and then create a new one. This is a recommended practice in ServiceNow. If you only update a dependency, the old dependency link to a task remains and the new dependency link to a task has a new sys_id.

If a dependency is created, we return the old ID and new ID to the client. To do this we need to modify the applyTableChanges function. Replace the applyTableChanges function with the following code:

async function applyTableChanges(table, changes) {
    let rows;
    let moreRows;
    if (changes.added) {
        rows = await createOperation(changes.added, table);
    }
    if (changes.updated) {
        moreRows = await updateOperation(changes.updated, table);
    }
    if (changes.removed) {
        await deleteOperation(changes.removed, table);
    }
    // if we got some new data to update client
    if (!moreRows) return rows;
    if (moreRows[0] == null) return rows;
    return moreRows;
}

We need to adjust our Bryntum Gantt to handle the custom modification to the response it will receive. We also need the Bryntum Gantt Crud Manager to send all the data when a dependency changes, not just the fields that have changed. To do this, add the following configuration property and event listener to the Bryntum Gantt project config in the public/index.js file:

dependencyStore : {
    // Setting to true will ensure this field is included in any update/insert request payload when a Store/Project/CrudManager performs a request
    writeAllFields : true
},
onSync({ response }) {
    if (
        response?.dependencies?.rows.length &&
      response.dependencies.rows[0]?.oldId
    ) {
        const { oldId, newId } = response.dependencies.rows[0];
        // temporarily disable autoSync to avoid sending the change back to the server
        gantt.project.autoSync = false;
        gantt.dependencyStore.getById(oldId).id = newId;
        gantt.project.autoSync = true;
    }
}

We modify the Crud Manager to send all dependency record fields when a dependency is changed by setting writeAllFields to true.

Disabling ServiceNow business rules to enable all task and dependency updates

We need to disable two ServiceNow Business Rules to enable all task and dependency updates: “Disable the Change of Planned Dates” and “Prevent relations to WIP/Closed tasks”. Business Rules are server-side logic executed when database records are queried, updated, inserted, or deleted.

In your ServiceNow PDI, do the following to disable these rules:

Repeat the above steps for the “Prevent relations to WIP/Closed tasks” business rule.

Now we can test our Bryntum Gantt. Run the local development server:

npm start

Open the development server URL: http://localhost:1337/.

Now try to create, update, delete, and edit items in the Bryntum Gantt chart. You’ll see the changes reflected in your ServiceNow project.

Next steps

This tutorial gives you a starting point for creating a Bryntum Gantt chart using vanilla JavaScript and syncing it with a ServiceNow project.
There are many ways you can improve your Bryntum Gantt chart, for example, you can add features like tree grouping. Take a look at our demos page to see demos of the available features.

You can also add more custom Bryntum Gantt fields for other ServiceNow task fields.

Arsalan Khattak

Bryntum Gantt ServiceNow