We're reclaiming inactive PDIs to keep them available for active builders. Learn what's changing, who's affected, and how to protect your work. Read More

Jelly scripting in UI Macro

HameetK
Tera Contributor

Hi , I am working on a requirement to fetch the data from demand cost plans. Below is the code :

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
    <style>
        tr:nth-child(even) {
            background-color: #DCDCDC;
        }

        td,
        th {
            padding: 5px;
            border: 1px solid black;
            border-collapse: collapse;
        }

        table {
            margin-left: auto;
            margin-right: auto;
            border: 1px solid black;
            border-collapse: collapse;
        }

        p {
            /* margin-left: auto;
            margin-right: auto; */
            margin: 20px auto;
        }
    </style>

    <script>
        function saveCostPlanData() {
            var data = {};

            // Get all inputs for carry_over (this gives us all years)
            var carryInputs = document.querySelectorAll("input[id^='carry_over_']");

            carryInputs.forEach(function(input) {
                // Extract year from id → carry_over_FY25 → FY25
                var year = input.id.replace("carry_over_", "").toLowerCase();

                data[year] = {
                    carry_over: input.value || "",
                    supplemental: document.getElementById("supplemental_" + year)?.value || "",
                    release: document.getElementById("release_" + year)?.value || ""
                };
            });

            g_form.setValue('u_cost_plan_changes', JSON.stringify(data));

        }
    </script>

    <j:set var="jvar_project" value="${RP.getParameterValue('sys_id')}" />
    <g:evaluate var="jvar_savedData" jelly="true">
        var data = '';
        var gr = new GlideRecord('project_change_request');
        gr.addQuery('sys_id', jelly.jvar_project);
        gr.query();

        if (gr.next()) {
        data = gr.getValue('u_cost_plan_changes');
        }
        data;

    </g:evaluate>
    <span>${jvar_savedData}</span>



    <p style="width:80%"> <b>Baseline Cost Plan</b></p>
    <table style="width:80%">
        <tr>
            <th>Year</th>
            <th>Hardware</th>
            <th>Software</th>
            <th>Total</th>
        </tr>

        <tr>
            <td>2026</td>
            <td>Maria Anders</td>
            <td>Germany</td>
            <td>Germany</td>
        </tr>
    </table>

    <p style="width:80%"> <b>Draft Cost Plan</b></p>
    <table style="width:80%">
        <tr>
            <th>Year</th>
            <th>Hardware</th>
            <th>Software</th>
            <th>Total</th>
        </tr>

        <g:evaluate jelly="true">
            var gr = new GlideRecord('cost_plan_breakdown');
            gr.addQuery('task', jelly.sysparm_collectionID);
            gr.addNotNullQuery('cost_plan');
            gr.query();
            var totals = {};
            while (gr.next()) {
            var fp = gr.getDisplayValue('fiscal_period') || '';
            var fy = fp.split(':')[0].trim().toLowerCase();
            var expenseType = (gr.getDisplayValue('expense_type') || '').toLowerCase();
            var cost = parseFloat(gr.getValue('cost_default_currency')) || 0;
            if (!totals[fy]) {
            totals[fy]={total: 0};
            }
            if (!totals[fy][expenseType]) {
            totals[fy][expenseType] = 0;
            }
            totals[fy][expenseType] += cost;
            totals[fy].total += cost;
            }
        </g:evaluate>

        <j:forEach items="${Object.keys(totals)}" var="jvar_fy_key">
            <g:evaluate jelly="true">
                var fyData = totals[jelly.jvar_fy_key];
            </g:evaluate>
            <tr>
                <td>${jvar_fy_key}</td>
                <td>${fyData.capex}</td>
                <td>${fyData.opex}</td>
                <td>${fyData.total}</td>
            </tr>
        </j:forEach>
    </table>

    <p style="width:80%"> <b>Cost plan change classification</b></p>
    <table style="width:80%">
        <tr>
            <th>Year</th>
            <th>Baselined Cost plan</th>
            <th>Draft cost plan</th>
            <th>Carry over/Forward</th>
            <th>Supplemental</th>
            <th>Release</th>
            <th>New cost plan</th>
            <th>Change to be Claaified</th>
        </tr>

        <j:forEach items="${Object.keys(totals)}" var="jvar_fy_key1">

            <g:evaluate jelly="true">
                var year = jelly.jvar_fy_key1;
                var fyData1 = totals[year];
                var data = jelly.jvar_savedData;
                var data1 = JSON.parse(data);

                var carry = '';
                var supp = '';
                var rel = '';

                if (data1[year]) {
                carry = data1[year].carry_over || '';
                supp = data1[year].supplemental || '';
                rel = data1[year].release || '';
                }
            </g:evaluate>
            <tr>
                <td>${jvar_fy_key1}</td>
                <td>6789</td>
                <td>${fyData1.total}</td>

                <td>
                    <input type="number" id="carry_over_${jvar_fy_key1}" name="carry_over_${jvar_fy_key1}" value="${carry}" />
                </td>

                <td>
                    <input type="number" id="supplemental_${jvar_fy_key1}" name="supplemental_${jvar_fy_key1}" value="${supp}" />
                </td>

                <td>
                    <input type="number" id="release_${jvar_fy_key1}" name="release_${jvar_fy_key1}" value="${rel}" />
                </td>

                <td>Germany</td>
                <td>Germany</td>
            </tr>

        </j:forEach>
    </table>
</j:jelly>

However , the value for 'jvar_savedData' is not getting populated as per the 'u_cost_plan_changes' field data when i save the form. 'jvar_savedData' is reflecting the previous value even after i save the form and gets updated after some time with the current value .

 
1 REPLY 1

Naveen20
ServiceNow Employee

 The g:evaluate block runs server-side during page render, and your separate GlideRecord query inside it can return stale data due to server-side caching of the Jelly template.

There are a couple of fixes depending on your context:

Fix 1 — Use current instead of a fresh GlideRecord (if this is a UI Formatter/Macro)

If this Jelly runs as a UI Formatter on the project_change_request form, the current object is already available and holds the just-loaded record. Replace your g:evaluate block:

<g:evaluate var="jvar_savedData" jelly="true">
    var data = '';
    var cur = current;  // already loaded by the form
    if (cur && cur.isValidRecord()) {
        data = cur.getValue('u_cost_plan_changes') || '';
    }
    data;
</g:evaluate>

This avoids a second DB query that may hit a cached result.

Fix 2 — Read the value client-side instead of server-side

Since you already have g_form available (you use g_form.setValue in saveCostPlanData), read the persisted value client-side after the form loads. This is the most reliable approach because the form always has the current record values:

<script>
    // runs after form load, always has the latest saved value
    var savedDataRaw = g_form.getValue('u_cost_plan_changes');
    var savedData = {};
    try {
        if (savedDataRaw) savedData = JSON.parse(savedDataRaw);
    } catch(e) {}

    // now populate your input fields client-side
    document.querySelectorAll("input[id^='carry_over_']").forEach(function(input) {
        var year = input.id.replace("carry_over_", "").toLowerCase();
        if (savedData[year]) {
            input.value = savedData[year].carry_over || '';
            document.getElementById("supplemental_" + year).value = savedData[year].supplemental || '';
            document.getElementById("release_" + year).value = savedData[year].release || '';
        }
    });
</script>

With this approach, remove the server-side jvar_savedData GlideRecord lookup entirely and remove the value="${carry}" / value="${supp}" / value="${rel}" attributes from the inputs (let the client script populate them).

Why this happens: When the form saves and reloads, the Jelly template is re-rendered server-side. But g:evaluate results within UI Macros can be cached briefly by the platform. A separate GlideRecord query may also execute before the save transaction has fully committed. The current object and g_form values don't suffer from this because they're tied directly to the form's record load cycle.

One more thing — in your cost plan breakdown query, you're using jelly.sysparm_collectionID but for the saved data you're using RP.getParameterValue('sys_id'). Make sure these resolve to the same record. If this is a UI Formatter, sysparm_collectionID is the correct way to get the record's sys_id, and RP.getParameterValue('sys_id') may not return the expected value, which could also cause your GlideRecord to miss the record entirely.