Built something you're proud of? Tell the story. A quick G2 review of App Engine or Build Agent helps other developers see what's possible on ServiceNow. Share your experience.

Copy email history from original HR case on the transferred HR case

ArpitaVK
Tera Guru

The requirement is that - full email history to transfer with an HR case when it is reassigned or recategorized (transferred), so that all communications related to the case remain visible, accurate, and centralized in one record. I am trying to consolidate the email logs from the original case and print them in work notes using a business rule.

Table: HR Case [sn_hr_core_case]

When to run: After insert, Transferred from is not empty AND Transferred from changes.

 

(function executeRule(current, previous /*null when async*/ ) {

    var oldCaseId = current.getValue('transferred_from');

    gs.info('AK07 oldCaseSysID = ' + oldCaseSysID);

    var consolidatedLog = "--- CONSOLIDATED EMAIL HISTORY FROM " + current.transferred_from.getDisplayValue() + " ---\n\n";

    var emailGr = new GlideRecord('sys_email');
    //emailGr.addQuery('instance', oldCaseId);
    emailGr.addQuery('instance', current.sys_id);
    // emailGr.addQuery('target_table', 'sn_hr_core_case');
    emailGr.addEncodedQuery('target_tableSTARTSWITHsn_hr_core_case');
    emailGr.orderBy('sys_created_on');
    emailGr.query();

    gs.info('AK07 Queried sys_email table');

    if (!emailGr.hasNext()) {
        return;
    }

    while (emailGr.next()) {
        consolidatedLog += "Date: " + emailGr.sys_created_on + "\n";
        if (emailGr.user_id) {
            consolidatedLog += "From: " + emailGr.user_user + " (" + emailGr.user_id.getDisplayValue() + ")\n";
        } else {
            consolidatedLog += "From: " + emailGr.user_user + " (" + emailGr.user + ")\n";
        }
        consolidatedLog += "Subject: " + emailGr.subject + "\n";
        var bodyText = emailGr.body.replace(/<[^>]*>?/gm, '');
        consolidatedLog += "Body:\n" + bodyText + "\n";
        consolidatedLog += "--------------------------------------------------\n\n";
    }
   
    gs.info('AK07 Consolidated email log  = ' + consolidatedLog);
    current.work_notes = consolidatedLog;
    current.update();
    gs.info('AK07 HR case ' + current.number + ' updated with email logs');

})(current, previous);
 
This business rule is not working as expected. Could you please help me with that?

I am open to any suggestions on how it could be done in any other/better way as well.

 

Thanks much in advance!

1 ACCEPTED SOLUTION

AhsanM
Tera Expert

Hi Arpita,

 

Great progress! The BR is firing and finding emails correctly now. The two remaining issues are straightforward to fix.

 

Issue 1: From showing null (null)

The user_name field on sys_email is not always populated, especially for system generated emails. Replace the From logic with this:

 
var fromName = emailGr.getValue('user_name') || emailGr.getValue('user') || 'System';
var fromEmail = '';

if (!emailGr.user_id.nil()) {
    fromEmail = emailGr.user_id.getDisplayValue();
} else {
    fromEmail = emailGr.getValue('user') || 'unknown';
}

consolidatedLog += "From: " + fromName + " (" + fromEmail + ")\n";

This adds proper null guards and falls back to "System" when no user is found, which is common for automated notification emails.

 

Issue 2: Body containing HTML, CSS and special characters

You are reading body_text but the email was likely stored as HTML in the body field. The body_text field is sometimes empty for template based emails. Try this improved body extraction:

 
var rawBody = emailGr.getValue('body_text') || emailGr.getValue('body') || '';

// Remove style and script blocks entirely first
rawBody = rawBody.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
rawBody = rawBody.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');

// Remove all remaining HTML tags
rawBody = rawBody.replace(/<[^>]*>/gm, '');

// Clean up HTML entities
rawBody = rawBody.replace(/&nbsp;/g, ' ');
rawBody = rawBody.replace(/&amp;/g, '&');
rawBody = rawBody.replace(/&lt;/g, '<');
rawBody = rawBody.replace(/&gt;/g, '>');
rawBody = rawBody.replace(/&quot;/g, '"');

// Remove excessive whitespace and blank lines
rawBody = rawBody.replace(/\n\s*\n\s*\n/g, '\n\n');
rawBody = rawBody.trim();

consolidatedLog += "Body:\n" + rawBody + "\n";

 

This handles template based emails much more cleanly by stripping style blocks before removing tags, then converting common HTML entities to readable characters.

 

Give these two fixes a try and let me know what the output looks like after. The body should now be human readable plain text.

Ahsan
ServiceNow Developer & Admin
Builder of NowFixer | Free AI debugging tool for ServiceNow scripts

View solution in original post

8 REPLIES 8

AhsanM
Tera Expert

Hi Arpita,

 

If both solutions are not working, the issue is likely deeper than the script logic itself. Let us troubleshoot systematically.

 

Step 1: Verify the Business Rule is actually firing

Add this as the very first line inside the function and check your system logs:

 
gs.info('AK07 BR fired - current sys_id: ' + current.sys_id + ' transferred_from: ' + current.getValue('transferred_from'));

If you do not see this in the logs, the Business Rule is not firing at all, which means the condition is the problem not the script.

 

Step 2: Verify emails actually exist in sys_email for the old case

Run this in Scripts Background replacing the sys_id with your actual old case sys_id:

 
var emailGr = new GlideRecord('sys_email');
emailGr.addQuery('instance', 'YOUR_OLD_CASE_SYS_ID');
emailGr.query();
gs.info('Email count: ' + emailGr.getRowCount());
while (emailGr.next()) {
    gs.info('Email found: ' + emailGr.subject + ' | target_table: ' + emailGr.target_table);
}

This will tell you two things: whether emails are actually linked to the old case via the instance field, and what the target_table value looks like so you can match it exactly in your query.

 

Step 3: Check the Business Rule trigger timing

The requirement says After Insert with transferred_from changes. But when a case is transferred in HR Service Delivery, the new case is created as a fresh insert. At the time of insert, ServiceNow may not recognise transferred_from as "changed" since there is no previous value to compare against on an insert operation.

 

Try changing your condition to simply:

 
!gs.nil(current.getValue('transferred_from'))

And set the Business Rule to run on Insert only without the changes() condition.

 

Can you share what the system logs show after adding the Step 1 logging line? That will tell us exactly where the process is breaking down.

Ahsan
ServiceNow Developer & Admin
Builder of NowFixer | Free AI debugging tool for ServiceNow scripts

Hello @AhsanM , Thank you for the response!

 

I added the first line of the system log. Verified that the emails actually exist in sys_email for the old case using the background script. Modified the BR trigger to change the condition as you suggested.

Currently, my script looks like this:

 

 

(function executeRule(current, previous) {

    gs.info('AK07 BR fired - current sys_id: ' + current.sys_id + ' transferred_from: ' + current.getValue('transferred_from'));

    var oldCaseId = current.getValue('transferred_from');
   
    if (!oldCaseId) {
        gs.info('AK07 No transferred_from value found, exiting.');
        return;
    }

    gs.info('AK07 oldCaseId = ' + oldCaseId);

    var consolidatedLog = "--- CONSOLIDATED EMAIL HISTORY FROM "
        + current.transferred_from.getDisplayValue() + " ---\n\n";

    var emailGr = new GlideRecord('sys_email');
    emailGr.addQuery('instance', oldCaseId);                          // old case
    emailGr.addEncodedQuery('target_tableSTARTSWITHsn_hr_core_case'); // HR tables only
    emailGr.orderBy('sys_created_on');
    emailGr.query();

    gs.info('AK07 Queried sys_email table');

    if (!emailGr.hasNext()) {
        gs.info('AK07 No emails found for old case: ' + oldCaseId);
        return;
    }

    while (emailGr.next()) {
        consolidatedLog += "Date: " + emailGr.getValue('sys_created_on') + "\n";

        if (emailGr.user_id) {
            consolidatedLog += "From: " + emailGr.getValue('user_name')
                + " (" + emailGr.user_id.getDisplayValue() + ")\n";
        } else {
            consolidatedLog += "From: " + emailGr.getValue('user_name')
                + " (" + emailGr.getValue('user') + ")\n";
        }

        consolidatedLog += "Subject: " + emailGr.getValue('subject') + "\n";

        var rawBody = emailGr.getValue('body_text') || '';               // null guard
        var bodyText = rawBody.replace(/<[^>]*>?/gm, '');
        consolidatedLog += "Body:\n" + bodyText + "\n";
        consolidatedLog += "--------------------------------------------------\n\n";
    }

    gs.info('AK07 Consolidated log built, length = ' + consolidatedLog.length);

    // Safe update without recursion
    var updateGr = new GlideRecord('sn_hr_core_case');
    if (updateGr.get(current.sys_id)) {
        updateGr.work_notes = consolidatedLog;
        //updateGr.autoSysFields(false);
        //updateGr.setWorkflow(false);
        updateGr.update();
        gs.info('AK07 HR case ' + current.number + ' updated with email logs');
    }

})(current, previous);


The output printed the date and Subject correctly. But from shows 'From: null (null)' and Body is not human-readable. It includes special characters like nbsp and contains CSS code. (Maybe because the notifications triggered have email templates used on them?) 

AhsanM
Tera Expert

Hi Arpita,

 

Great progress! The BR is firing and finding emails correctly now. The two remaining issues are straightforward to fix.

 

Issue 1: From showing null (null)

The user_name field on sys_email is not always populated, especially for system generated emails. Replace the From logic with this:

 
var fromName = emailGr.getValue('user_name') || emailGr.getValue('user') || 'System';
var fromEmail = '';

if (!emailGr.user_id.nil()) {
    fromEmail = emailGr.user_id.getDisplayValue();
} else {
    fromEmail = emailGr.getValue('user') || 'unknown';
}

consolidatedLog += "From: " + fromName + " (" + fromEmail + ")\n";

This adds proper null guards and falls back to "System" when no user is found, which is common for automated notification emails.

 

Issue 2: Body containing HTML, CSS and special characters

You are reading body_text but the email was likely stored as HTML in the body field. The body_text field is sometimes empty for template based emails. Try this improved body extraction:

 
var rawBody = emailGr.getValue('body_text') || emailGr.getValue('body') || '';

// Remove style and script blocks entirely first
rawBody = rawBody.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
rawBody = rawBody.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');

// Remove all remaining HTML tags
rawBody = rawBody.replace(/<[^>]*>/gm, '');

// Clean up HTML entities
rawBody = rawBody.replace(/&nbsp;/g, ' ');
rawBody = rawBody.replace(/&amp;/g, '&');
rawBody = rawBody.replace(/&lt;/g, '<');
rawBody = rawBody.replace(/&gt;/g, '>');
rawBody = rawBody.replace(/&quot;/g, '"');

// Remove excessive whitespace and blank lines
rawBody = rawBody.replace(/\n\s*\n\s*\n/g, '\n\n');
rawBody = rawBody.trim();

consolidatedLog += "Body:\n" + rawBody + "\n";

 

This handles template based emails much more cleanly by stripping style blocks before removing tags, then converting common HTML entities to readable characters.

 

Give these two fixes a try and let me know what the output looks like after. The body should now be human readable plain text.

Ahsan
ServiceNow Developer & Admin
Builder of NowFixer | Free AI debugging tool for ServiceNow scripts

@AhsanM Thank you! This works for me!