Making customization in Widget

rah dev
Tera Contributor

Hi Everyone,
I have created a custom widget that displays a list of Incidents. Now, I want to show the variables (coming from a Record Producer) inside this widget as part of the list (e.g., as a column).

rahdev_1-1776547319999.png

 



Currently:
The Incident record is also creating from Record Producer.
The variables submitted via the Record Producer are visible on the Incident form (at the bottom via Variable Editor).
However, these variables are not visible in the form layout and also not appearing in my custom widget.
What I need:
Display Record Producer variables (Variable Editor data) inside my custom widget (Incident list).
Ideally, show them as columns or part of each record output.
I have already:
Checked Form Layout but couldn’t find the Variable Editor field.
Tried searching for solutions but couldn’t figure out how to fetch/display variables in the widget.
This is my code for custom widget:

html:
<div class="incident-widget">
<h3>Incident List</h3>

<table class="incident-table">
<thead>
<tr>
<th>Number</th>
<th>Short Description</th>
<th>Priority</th>
<th>Assigned To</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="inc in data.incidents">
<td>{{inc.number}}</td>
<td>{{inc.short_description}}</td>
<td>
<span class="priority priority-{{inc.priority}}">
{{inc.priority}}
</span>
</td>
<td>{{inc.assigned_to}}</td>
</tr>
</tbody>
</table>


css:

.incident-widget {
padding: 15px;
background: #fff;
border-radius: 8px;
}

.incident-table {
width: 100%;
border-collapse: collapse;
}

.incident-table th {
background: #007bff;
color: white;
padding: 10px;
text-align: left;
}

.incident-table td {
padding: 10px;
border-bottom: 1px solid #ddd;
}

.incident-table tr:hover {
background-color: #f5f5f5;
}

/* Priority colors */
.priority-1 {
color: red;
font-weight: bold;
}

.priority-2 {
color: orange;
}

.priority-3 {
color: green;
}

.priority-4 {
color: blue;
}

 

client script:
function($scope) {

console.log("Incident widget loaded");
}


server script:
(function() {
data.incidents = [];
data.count = 0;

var gr = new GlideRecord('incident');
gr.addActiveQuery();
gr.orderByDesc('sys_created_on');
gr.setLimit(10);
gr.query();

while (gr.next()) {
data.incidents.push({
number: gr.getDisplayValue('number'),
short_description: gr.getDisplayValue('short_description'),
priority: gr.getDisplayValue('priority'),
assigned_to: gr.getDisplayValue('assigned_to') || 'Not Assigned'
});
}

data.count = data.incidents.length;

gs.info("Widget Incident Count: " + data.count);
})();

rahdev_2-1776547863549.png

need to show'Optional Software (backend name: optional_software) and Additional software requirements (u_asr) variables as column in widget.


Thanks in Advance.
 

1 ACCEPTED SOLUTION

Naveen20
ServiceNow Employee

When a Record Producer targets the Incident table, the submitted values are not stored as columns on incident. They're stored in the variable pool — specifically across these tables:

  • item_option_new — variable definition (name, label, type)
  • sc_item_option — the actual submitted value
  • sc_item_option_mtom — the link between the target record (your incident) and each sc_item_option

That's why Form Layout doesn't list them as fields and why your current GlideRecord query returns nothing for them — they aren't dot-walkable as normal incident fields.

Fix: use gr.variables.<name> in the server script

The cleanest approach is the variables API on GlideRecord. It transparently resolves the variable pool for record-producer-created tasks. Update your server script like this:

(function() {
    data.incidents = [];
    data.count = 0;

    var gr = new GlideRecord('incident');
    gr.addActiveQuery();
    gr.orderByDesc('sys_created_on');
    gr.setLimit(10);
    gr.query();

    while (gr.next()) {
        var optionalSoftware = '';
        var additionalSoftware = '';

        // Guarded access — only incidents created by a record producer will have these
        if (gr.variables && gr.variables.optional_software) {
            optionalSoftware = gr.variables.optional_software.getDisplayValue() || '';
        }
        if (gr.variables && gr.variables.u_asr) {
            additionalSoftware = gr.variables.u_asr.getDisplayValue() || '';
        }

        data.incidents.push({
            number: gr.getDisplayValue('number'),
            short_description: gr.getDisplayValue('short_description'),
            priority: gr.getDisplayValue('priority'),
            assigned_to: gr.getDisplayValue('assigned_to') || 'Not Assigned',
            optional_software: optionalSoftware,
            additional_software: additionalSoftware
        });
    }

    data.count = data.incidents.length;
})();

Why getDisplayValue() matters here: optional_software is a multi-checkbox/multi-row variable, so .value returns the raw sys_ids or internal values. getDisplayValue() returns the user-readable labels (comma-separated for multi-value).

Update the HTML to show the new columns

<div class="incident-widget">
  <h3>Incident List</h3>
  <table class="incident-table">
    <thead>
      <tr>
        <th>Number</th>
        <th>Short Description</th>
        <th>Priority</th>
        <th>Assigned To</th>
        <th>Optional Software</th>
        <th>Additional Requirements</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="inc in data.incidents">
        <td>{{inc.number}}</td>
        <td>{{inc.short_description}}</td>
        <td>
          <span class="priority priority-{{inc.priority}}">{{inc.priority}}</span>
        </td>
        <td>{{inc.assigned_to}}</td>
        <td>{{inc.optional_software || '--'}}</td>
        <td>{{inc.additional_software || '--'}}</td>
      </tr>
    </tbody>
  </table>
</div>

Alternative: query sc_item_option_mtom directly

If gr.variables.<name> ever returns empty (can happen if the variables were attached via custom code rather than through a standard Record Producer submission), fall back to an explicit join:

function getIncidentVariables(incidentSysId) {
    var out = {};
    var mtom = new GlideRecord('sc_item_option_mtom');
    mtom.addQuery('request_item', incidentSysId);
    mtom.query();
    while (mtom.next()) {
        var opt = mtom.sc_item_option.getRefRecord();
        if (!opt.isValidRecord()) continue;
        var varName = opt.item_option_new.name.toString();
        out[varName] = opt.getDisplayValue('value');
    }
    return out;
}

Then call var vars = getIncidentVariables(gr.getUniqueValue()); inside your loop and read vars.optional_software / vars.u_asr.

A couple of things to watch out for

  • ACLs on sc_item_option / sc_item_option_mtom — if portal users don't have read access to these tables, gr.variables will come back empty even though the data exists. Check with a user who only has the Service Portal role.
  • Performance — you're already limiting to 10 records, which is fine. If you scale this up, the MTOM fallback approach can get expensive; stick with gr.variables where possible.
  • Incidents not created via Record Producer — those won't have any variables, so the guarded if (gr.variables.optional_software) check prevents null errors.

View solution in original post

3 REPLIES 3

vaishali231
Kilo Sage

hey @rah dev 

Below script fetches

  • optional_software (checkbox)
  • u_asr (text) 

 

Server side script 

(function() {
    data.incidents = [];
    var gr = new GlideRecord('incident');
    gr.addActiveQuery();
    gr.orderByDesc('sys_created_on');
    gr.setLimit(10);
    gr.query();
    while (gr.next()) {
        var optionalSoftware = '';
        var additionalReq = '';
        // Fetch variables only if request_item exists
        if (gr.request_item) {
            var mtom = new GlideRecord('sc_item_option_mtom');
            mtom.addQuery('request_item', gr.request_item);
            mtom.query();
            while (mtom.next()) {
                var varName = mtom.sc_item_option.item_option_new.name.toString();
                var varValue = mtom.sc_item_option.value.toString();
                if (varName === 'optional_software') {
                    optionalSoftware += varValue + ', ';
                }
                if (varName === 'u_asr') {
                    additionalReq = varValue;
                }
            }
        }
        // Remove trailing comma
        optionalSoftware = optionalSoftware.replace(/, $/, '');
        data.incidents.push({
            number: gr.getDisplayValue('number'),
            short_description: gr.getDisplayValue('short_description'),
            priority: gr.getDisplayValue('priority'),
            assigned_to: gr.getDisplayValue('assigned_to') || 'Not Assigned',
            optional_software: optionalSoftware,
            u_asr: additionalReq
        });
    }
})();



 HTML Changes

Add new columns:

<th>Optional Software</th>
<th>Additional Requirements</th>

Update row:

<td>{{inc.optional_software}}</td>
<td>{{inc.u_asr}}</td>

 

*************************************************************************************************************************************

If this response helps, please mark it as Accept as Solution and Helpful.

Doing so helps others in the community and encourages me to keep contributing.

Regards

Vaishali Singh






Thanks for the response.

 

But the issue is that there is a Record Producer which is creating an Incident record. Its Variable Editor has already been added to the Incident table form. Now, I want the variables from that Variable Editor to also appear as columns in the widget.

 

In your code, it is written that the variables will show if there is an RITM, but a Record Producer does not create an RITM. That’s why this script is not running.

Naveen20
ServiceNow Employee

When a Record Producer targets the Incident table, the submitted values are not stored as columns on incident. They're stored in the variable pool — specifically across these tables:

  • item_option_new — variable definition (name, label, type)
  • sc_item_option — the actual submitted value
  • sc_item_option_mtom — the link between the target record (your incident) and each sc_item_option

That's why Form Layout doesn't list them as fields and why your current GlideRecord query returns nothing for them — they aren't dot-walkable as normal incident fields.

Fix: use gr.variables.<name> in the server script

The cleanest approach is the variables API on GlideRecord. It transparently resolves the variable pool for record-producer-created tasks. Update your server script like this:

(function() {
    data.incidents = [];
    data.count = 0;

    var gr = new GlideRecord('incident');
    gr.addActiveQuery();
    gr.orderByDesc('sys_created_on');
    gr.setLimit(10);
    gr.query();

    while (gr.next()) {
        var optionalSoftware = '';
        var additionalSoftware = '';

        // Guarded access — only incidents created by a record producer will have these
        if (gr.variables && gr.variables.optional_software) {
            optionalSoftware = gr.variables.optional_software.getDisplayValue() || '';
        }
        if (gr.variables && gr.variables.u_asr) {
            additionalSoftware = gr.variables.u_asr.getDisplayValue() || '';
        }

        data.incidents.push({
            number: gr.getDisplayValue('number'),
            short_description: gr.getDisplayValue('short_description'),
            priority: gr.getDisplayValue('priority'),
            assigned_to: gr.getDisplayValue('assigned_to') || 'Not Assigned',
            optional_software: optionalSoftware,
            additional_software: additionalSoftware
        });
    }

    data.count = data.incidents.length;
})();

Why getDisplayValue() matters here: optional_software is a multi-checkbox/multi-row variable, so .value returns the raw sys_ids or internal values. getDisplayValue() returns the user-readable labels (comma-separated for multi-value).

Update the HTML to show the new columns

<div class="incident-widget">
  <h3>Incident List</h3>
  <table class="incident-table">
    <thead>
      <tr>
        <th>Number</th>
        <th>Short Description</th>
        <th>Priority</th>
        <th>Assigned To</th>
        <th>Optional Software</th>
        <th>Additional Requirements</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="inc in data.incidents">
        <td>{{inc.number}}</td>
        <td>{{inc.short_description}}</td>
        <td>
          <span class="priority priority-{{inc.priority}}">{{inc.priority}}</span>
        </td>
        <td>{{inc.assigned_to}}</td>
        <td>{{inc.optional_software || '--'}}</td>
        <td>{{inc.additional_software || '--'}}</td>
      </tr>
    </tbody>
  </table>
</div>

Alternative: query sc_item_option_mtom directly

If gr.variables.<name> ever returns empty (can happen if the variables were attached via custom code rather than through a standard Record Producer submission), fall back to an explicit join:

function getIncidentVariables(incidentSysId) {
    var out = {};
    var mtom = new GlideRecord('sc_item_option_mtom');
    mtom.addQuery('request_item', incidentSysId);
    mtom.query();
    while (mtom.next()) {
        var opt = mtom.sc_item_option.getRefRecord();
        if (!opt.isValidRecord()) continue;
        var varName = opt.item_option_new.name.toString();
        out[varName] = opt.getDisplayValue('value');
    }
    return out;
}

Then call var vars = getIncidentVariables(gr.getUniqueValue()); inside your loop and read vars.optional_software / vars.u_asr.

A couple of things to watch out for

  • ACLs on sc_item_option / sc_item_option_mtom — if portal users don't have read access to these tables, gr.variables will come back empty even though the data exists. Check with a user who only has the Service Portal role.
  • Performance — you're already limiting to 10 records, which is fine. If you scale this up, the MTOM fallback approach can get expensive; stick with gr.variables where possible.
  • Incidents not created via Record Producer — those won't have any variables, so the guarded if (gr.variables.optional_software) check prevents null errors.