UI Builder Tips #4: How to Create a Custom Data Resource

UI Builder Tips #4: How to Create a Custom Data Resource

UI Builder Tips Series

Welcome to the UI Builder Tips series! I'll use this series to document what I've learned while working with ServiceNow's UI Builder. Since this is a learning process, these blog posts should be seen as a set of notes.

The content of these posts will likely change often to include new information as I continue to learn. Feel free to leave feedback or corrections in the comments, and I'll update the posts as needed. Let's learn together!

An Introduction to Data Resources

In this blog post, we will explore everything I have learned so far about data resources, including how to create your own.

Pages built in UI Builder can be divided into two main parts:

  1. The Components that form the page

  2. The Data that populates those components

The Lifecycle of a Data Resource

A data resource specifies a piece of dynamic data and determines when it should be retrieved. There can be many data resources on a UI Builder page, each with its own triggers, logic, and events. You can think of each one like a dog playing fetch: when you tell it to get something, it runs out to retrieve it and then returns with the item after some time. All data resources are triggered, execute, and then cause resulting events to be fired off.

All data resources have 3 events that a fired off throughout their lifecycles:

  1. Data Fetch Initiated: Fired when the data resource starts running

  2. Data Fetch Succeeded: Fired when the data resource completes successfully

  3. Data Fetch Failed: Fired when a data resource fails or throws an error

Non-Mutating vs Mutating

Some data resources only retrieve/read data (non-mutating) while others modify/update data (mutating). This is an important distinction and will have an impact on the behavior of the data resource.


Non-mutating data resources can be triggered in 3 different ways:

  1. Immediately (eager): Triggered as soon as the page loads

  2. Only when invoked (explicit): Triggered only when a specific refresh resource event is fired

  3. Property updated by client state: Triggered any time that a client state variable bound to a property on the data resource updates

Non-mutating data resources can return a result.


Mutating data resources can only be triggered in 2 ways:

  1. Only when invoked (explicit): Triggered only when a specific execute resource event is fired

  2. Property updated by client state: Triggered any time that a client state variable bound to a property on the data resource updates

Mutating data resources cannot return a result.

The Different Types of Data Resources

Data resources, also referred to as data brokers, come in a couple of different flavors, each with their different use-cases. Let’s take a brief look at each one:

GraphQL (sys_ux_data_broker_graphql)
Allows you to write a GraphQL query against a ServiceNow GraphQL API. A lot of the OOTB GlideRecord data resources are GraphQL-based.
Scriptlet (sys_ux_data_broker_scriptlet)
Executes client-side JavaScript in a sandboxed environment within the browser. These are typically faster than transform data resources because they run natively in the browser. Use this when you need to execute logic that doesn’t require any server-side APIs.
REST (sys_ux_data_broker_rest)
Executes a REST request against a ServiceNow REST endpoint. Requires you to specify an endpoint, query params, and a request body.
Composite (sys_ux_data_broker_composite)
Composite data resources are a combination of multiple data resources. For instance, a composite data resource can integrate both a GraphQL and a Scriptlet data resource into a single entity. This approach can streamline data resource chaining and enhance performance when multiple operations are required.
Transform (sys_ux_data_broker_transform)
Transform data resources allow you to write a script that runs server-side, this means that you have full access to existing server-side APIs and script includes. These tend to be my go-to in most cases.

📢
I primarily use transform data resources, so I will focus on them for the remainder of this article. In the future, I may write another blog post to explore the other types as I gain more experience with them.

How to Create a Custom Transform Data Resource

You can create a custom data resource by going to the appropriate table and clicking the new button in the classic UI:

Or through the data resource menu in UI Builder:

Either way will take you to the classic UI form view of a new data resource record:

Once you have a new data resource record open you can fill in each of the fields:

  • Name

  • Mutates server data - distinguishes a mutating data resource from a non-mutating data resource

  • Private - hides the data resource from selection menus

  • Description

  • Properties - accepts a list of property JSON objects

  • Script - defines the logic that the data resource will execute

  • Output schema - accepts a JSON schema that defines the shape of the output

  • ACL Failure Result - accepts a JSON object that will be returned when an ACL failure occurs

Properties

The properties field defines the input properties that your data resource accepts. This field will ultimately contain a list of property JSON objects. Each property object should have the following structure:

{
    "name": "",
    "label": "",
    "description": "",
    "readOnly": true | false,
    "fieldType": "",
    "mandatory": true | false,
    "defaultValue": "",
}

Each property defines an input field that can have values passed in through UI Builder.

💡
There are a bunch of different field types that you can pick from, some even require special attributes that have to be added to the property JSON in order to function. Here is an excellent blog post from Brad Tilton that goes through all of the supported field types and their caveats: Component and Data Resource Properties in UI Builder.

Here is an example of a complete property list from a custom transform data resource that I created recently for my DBML Generator application:

[
  {
    "name": "generationType",
    "label": "Generation Type",
    "description": "Select generation type",
    "readOnly": false,
    "fieldType": "choice",
    "mandatory": true,
    "defaultValue": "table",
    "typeMetadata": {
        "variant": "generation-type-choices",
        "choices": [
            {"label": "Table", "value": "table"},
            {"label": "App","value": "app"}
        ]
    }
  },
  {
    "name": "table",
    "label": "Table",
    "description": "",
    "readOnly": false,
    "mandatory": false,
    "fieldType": "string",
    "defaultValue": ""
  },
  {
    "name": "app",
    "label": "App",
    "description": "",
    "readOnly": false,
    "mandatory": false,
    "fieldType": "string",
    "defaultValue": ""
  },
  {
    "name": "includeSysColumns",
    "label": "Include System Columns",
    "description": "Include system columns like sys_update_on, sys_created_on, etc. from the DBML",
    "readOnly": false,
    "mandatory": false,
    "fieldType": "Boolean",
    "defaultValue": false
  },
  {
    "name": "includeInheritedColumns",
    "label": "Include Inherited Columns",
    "description": "Include columns in child tables that are inherited from a parent table",
    "readOnly": false,
    "mandatory": false,
    "fieldType": "Boolean",
    "defaultValue": false
  }
]

Here’s what the data resource configuration looks like in UI Builder after it has been added:

Script

The script field defines the logic that the data resource will execute when it is triggered. The field accepts a function that receives an inputs object as a parameter.

💡
The inputs object can be destructured using ES6 destructuring assignment

This script runs server-side which means that it has access to APIs like GlideRecord and script includes. It can take any shape that you want but here is an example template that I submitted as part of the ServiceNow 2024 Hacktoberfest Code Snippets challenge.

/**
 * This is a template for a transform data resource
 * @param {{param1: string, param2: number, param3?: boolean}} inputs 
 * Inputs from the properties field above; param1 and param2 are mandatory.
 * @returns {string} The value returned after transformation.
 */
function transform({ param1, param2, param3 }) {
  const lib = "Data Broker";
  const func = "<insert data broker name here>";
  let res;

  try {
    if (!param1) throw new Error("Missing required param 'param1'");
    if (!param2) throw new Error("Missing required param 'param2'");

    // Add transformation logic here

    return res;
  } catch (e) {
    gs.error(`${lib} ${func} - ${e}`);
    throw new Error(e);
  }
}

Here is another example from DBML Generator:

function transform({generationType, table, app, includeSysColumns, includeInheritedColumns}) {
    const lib = "Data Resource";
    const func = "Generate DBML";

    try {
        const dbmlGen = new x_1128327_dbml_gen.DBMLGenerator(includeSysColumns, includeInheritedColumns);

        let dbml;
        if (generationType == "table") {
            dbml = dbmlGen.generateTableDBML(table);
        } else if (generationType == "app") {
            dbml = dbmlGen.generateAppDBML(app);
        } else {
            throw new Error(`Unknown generation type ${generationType}`);
        }

        return dbml;
    } catch (e) {
        gs.error(`${lib} ${func} - ${e}`);
        throw new Error(e.message);
    }
}
Why catch an error just to throw it again? I do it so that the error can be logged in the system logs but still cause the data resource to fail. You don’t have to take this approach, its just something that I like to do.

Create Data Resource ACL

After creating your data resource it is essential to create an ACL for the data resource. Your data resource will not be allowed to execute without an accompanying ACL!

  1. Elevate to security admin

  2. Go to the ACL table (sys_security_acl)

  3. Create a new ACL with the following details

    1. Type = ux_data_broker

    2. Operation = execute

    3. Name = the sys_id of your data resource record. You may need to click the blue arrow next to the name field to enter a sys_id

    4. Role = A role appropriate to execute your data resource

    5. All of the other fields can stay as their defaults

Conclusion

Now you should have a basic understanding of UI Builder data resources and the tools necessary to create your own custom transform data broker to use in UI Builder!

References: