David House
Tera Guru

TL;DR

The on-before business rule script is at the bottom of this article, but it is worth reading through to understand what it is achieving and how it affects you.


Introduction

I have been asked (and pondered the same myself) whether we could automatically swap out record (ticket) numbers with a hyperlink to the record itself, when entered into the work notes of an incident (or any ticket with work notes, e.g., requested item, etc.).

First of all, in order to insert a hyperlink into a journal field, you need to use a code block:

[code]<a href="___URL_HERE___">___LABEL_HERE___</a>[/code]

NOTE: This method requires the ability to render HTML in a journal field using code blocks. If you have security hardening enabled to prevent this, it will not work, see more info here:

 

However, simply pasting in a ticket number is the easiest method to refer to another ticket in a work note, and having to convert that to a link in a code block each time can get tedious very quickly.


An on-before business rule

I've seen several questions around the community forum attempting to cover this topic, and I've researched the same myself, but I haven't yet found a suitable solution for this specific query, but after some digging and tinkering, I've come up with a solution.

Targeting the [task] table and running when Work notes changes:

find_real_file.png


The early stages of the script

The script for this is fairly straightforward in terms of the overall process:

  • Get the list of ticket number prefixes and the count of digits that make up the number (eg. the [incident] table has a prefix of "INC" and uses 7 digits)
  • Build a regex string to find and replace these in the work_notes string
  • Add this formatted string to the work_notes of the ticket

There was also the issue of potentially having more than one table using the same number prefix. This isn't an OOB practice, but can happen in some environments. This was an easy fix by ensuring the link uses "text_search_exact_match.do?sysparm_search=TICKET_NUMBER".

// Grab raw input for processing
var currentNote = current.work_notes.toString();

// Get all tables with a number prefix
var table = new GlideRecord("sys_db_object");
table.addEncodedQuery("sys_update_nameISNOTEMPTY^number_refISNOTEMPTY");
table.query();

// Build regex string
var regex = "";
while (table.next()) {
	if (table.number_ref.maximum_digits > 0) {
		if (regex != "") {
			regex += "|";
		}
		regex += table.number_ref.getDisplayValue() + "\\d{" + table.number_ref.maximum_digits.getDisplayValue() + "}";
	}
}

if (regex != "(") {
	// Finalise regex string (add negative look ahead)
	var regX = new RegExp("(" + regex + ")(?!\\w)", "gi");

	// Replace ticket numbers with exact match search hyperlink
	var formattedNote = currentNote.replace(regX, "[code]<a href=\"text_search_exact_match.do?sysparm_search=$&\">$&</a>[/code]");

	// Apply formatted string
	current.work_notes = formattedNote;
}

The problem

This does the job, but has several issues that need to be addressed.

For example, if I was to add a work note of "INC0009009 is similar to RITM0000001", the following would be the result:

find_real_file.png

The reason for this is the existing string has already been added to a Journal Entry, and assigning a new string to the work_notes property of the ticket simply adds a new journal entry, so you get the original raw input with the formatted string above it.

I looked at working around this by aborting the update, and then setting the work_notes and triggering an update afterwards, but this simply cancels out the abort, and gives the same result as above.


The workaround

There is a work around, however, and it's pretty easy to use: 

// Disable processing and flush raw input
current.setWorkflow(false);
current.update();

// Enable processing again and apply formatted string
current.setWorkflow(true);
current.work_notes = formattedNote;

// NOTE: no need to run current.update() here

Basically, this disables further processing if an update was to occur on this ticket, followed by triggering an update. This causes the journal entry of the raw input to be saved in the [sys_journal_field] table, but not applied to the ticket. Effectively, this "flushes" the raw input.

If you combine this with your replace, then make sure to re-enable the processing before you apply your own string, you will get something like this:

find_real_file.png

Perfect!


Code block conflicts

Except there is one more issue I was experiencing, and that is any manually entered code blocks that contained a ticket number were broken by this business rule.

This was a tricky one to decipher, but I was able to find that the replace() function for strings can take in a function as the second parameter. This allowed me to make use of capture groups in regex, not only allowing me to ignore code blocks, but also capitalise ticket numbers for consistency. All leading me to...


The final script

Bear in mind this is aimed at remaining generic and usable in most environment setups, but you can tailor it to your own environment and needs.

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

	// Grab raw input for processing
	var currentNote = current.work_notes.toString();

	// Get all tables with a number prefix
	var table = new GlideRecord("sys_db_object");
	table.addEncodedQuery("sys_update_nameISNOTEMPTY^number_refISNOTEMPTY");
	table.query();

	// Build regex string
	var regex = "";
	while (table.next()) {
		if (table.number_ref.maximum_digits > 0) {
			if (regex != "") {
				regex += "|";
			}
			regex += table.number_ref.getDisplayValue() + "\\d{" + table.number_ref.maximum_digits.getDisplayValue() + "}";
		}
	}

	// Define replacer function to only replace matches that are caught in capture a group (non-code blocks)
	function replacer(match, p1, offset, string) {
		if (p1 != null) {
			return "[code]<a href=\"text_search_exact_match.do?sysparm_search=" + p1.toUpperCase() + "\">" + p1.toUpperCase() + "</a>[/code]";
		} else {
			return match;
		}
	}

	if (regex != "(") {
		// Finalise regex string (add negative look ahead)
		// This ensures any code blocks are not matched inside a group
		var regX = new RegExp("\\[code][^\\[]+\\[\\/code]|(" + regex + ")(?!\\w)", "gi");

		// Replace ticket numbers with exact match search hyperlink
		currentNote = currentNote.replace(regX, replacer);

		// Disable processing and flush raw input
		current.setWorkflow(false);
		current.update();

		// Enable processing again and apply formatted string
		current.setWorkflow(true);
		current.work_notes = currentNote;
	}
})(current, previous);

 


NOTE: This was built in a PDI running San Diego, and another PDI running Tokyo. It will likely work in other versions, but I have not tested this in those versions.


Hope this helps! Any questions please let me know.

Kind regards,
David

Comments
Mark Manders
Mega Patron

Be aware that instances with security hardening can't use this option and I've also seen that working with email scripts can mess up notifications when using code blocks in journal fields.

But nice find! And good to see the entire process instead of just the end product 🙂

If my answer helped you in any way, please then mark it as helpful.

Mark

David House
Tera Guru

Thanks for the kind words, and that's a very good point. I'll add a reference to the article pointing to:

Michael Farine1
Tera Explorer

Hello! This is an interesting script, I'm wondering if there's a way to fix an issue I'm having:

 

1. Only digits are turned into a hyperlink, not the preceeding text (IE: INC, ESC, CS, REQ). This breaks the ability to click into the ticket. (see below)

2. Is there a way to filter this to only action against text that begins with INC, ESC, CS, REQ? For instance, right now if I add a phone number to the notes, it will turn it into a URL. 

 

MichaelFarine1_0-1674064180122.png

 

David House
Tera Guru

Hi @Michael Farine1 

 

For the first issue, are you able to provide a snippet (or all) of your business rule script? I just did a test with this on a fresh PDI and it is working as intended, so I'd like to see what may be different in your script so I can troubleshoot with you.

 

For the second question, you can absolutely filter this down to specific prefixes.  There are a couple of ways of achieving this:

1. You could potentially add additional queries to the initial GlideRecord query on the "sys_db_object" table. However, this would require you tracking down the sys_ids on the "sys_number" table for each prefix you want to filter by.

Example:

 

// Get all tables with a number prefix
var table = new GlideRecord("sys_db_object");
table.addEncodedQuery("sys_update_nameISNOTEMPTY^number_refISNOTEMPTY");
// ADD THESE QUERY CONDITIONS --------------------------------------------------
var numRefQ = table.addQuery("number_ref", '3'); // INC
numRefQ.addOrCondition("number_ref", 'req2'); // REQ
// -----------------------------------------------------------------------------
table.query();

 

 

2. Create an array containing your prefixes, and add a simple ".indexOf()" check when building the regex string.

Example:

// ADD THIS --------------------------------------------------
// Filter by prefix
var prefixFilter = ['INC', 'ESC', 'CS', 'REQ'];
// -----------------------------------------------------------

// Build regex string
var regex = "";
while (table.next()) {
	// MODIFY THIS --------------------------------------------------
	if (table.number_ref.maximum_digits > 0 && prefixFilter.indexOf(table.number_ref.getDisplayValue()) > -1) {
	// --------------------------------------------------------------
		if (regex != "") {
			regex += "|";
        }
        regex += table.number_ref.getDisplayValue() + "\\d{" + table.number_ref.maximum_digits.getDisplayValue() + "}";
    }
}

 

Personally, I would find the second option to be the easiest to use. However, technically the first option is the most performant option, but requires the extra step of tracking down sys_ids. Pick whichever suits you best.

Michael Farine1
Tera Explorer

Hello David!

 

First of all - Thank you for the quick reply, and thank you for putting this work together. It has already been immensely helpful. 

 

Here's the script I'm running - I was unclear in my previous response, but right now it's only ESC references that aren't turning into a hyperlink. I can confirm that CS, INC and REQ are working. 

 

I have not tried your additional code, I will try it out this morning. In the meantime, here's the version I'm currently using:

 

 

 

 

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

// Grab raw input for processing
var currentNote = current.work_notes.toString();

// Get all tables with a number prefix
var table = new GlideRecord("sys_db_object");
table.addEncodedQuery("sys_update_nameISNOTEMPTY^number_refISNOTEMPTY");
table.query();

// Build regex string
var regex = "";
while (table.next()) {
if (table.number_ref.maximum_digits > 0) {
if (regex != "") {
regex += "|";
}
regex += table.number_ref.getDisplayValue() + "\\d{" + table.number_ref.maximum_digits.getDisplayValue() + "}";
}
}

// Add table for sn_customerservice_case
regex += "|CS\\d{7}";

// Define replacer function to only replace matches that are caught in capture a group (non-code blocks)

function replacer(match, p1, offset, string) {
if (p1 != null) {
return "[code]<a href=\"text_search_exact_match.do?sysparm_search=" + p1.toUpperCase() + "\">" + p1.toUpperCase() + "</a>[/code]";
} else {
return match;
}
}


if (regex != "(") {
// Finalise regex string (add negative look ahead)
// This ensures any code blocks are not matched inside a group
var regX = new RegExp("\\[code][^\\[]+\\[\\/code]|(" + regex + ")(?!\\w)", "gi");

// Replace ticket numbers with exact match search hyperlink
currentNote = currentNote.replace(regX, replacer);

// Disable processing and flush raw input
current.setWorkflow(false);
current.update();

// Enable processing again and apply formatted string
current.setWorkflow(true);
current.work_notes = currentNote;
}
})(current, previous);

Michael Farine1
Tera Explorer

Here's an example of random numbers being turned into a hyperlink without any preceding text:

 

MichaelFarine1_0-1674143473688.png

 

 

David House
Tera Guru

Hi @Michael Farine1,

 

Apologies for the delay on this, I was having some difficulties getting a reply to actually post on here, and I have also been unable to update my original article since the Community revamp.

 

I have found a solution to your particular issue, and this has also fixed a wider issue. The cause looks to be related to a few OOB tables that somehow simultaneously don't have auto-number enabled, but still have an auto-number turned on (I'm unsure how this is the case). The tables in question are:

  • Indicator [pa_indicators]
  • Breakdown [pa_breakdowns]
  • Application License Definition [sys_app_license_defn

The related info is as follows:

TABLE: sys_app_license_defn  | PREFIX: ''  | DIGITS: 7
TABLE: pa_indicators         | PREFIX: ''  | DIGITS: 9
TABLE: pa_breakdowns         | PREFIX: ''  | DIGITS: 9

As you can see, there are two tables using a number of digits matching 9, and one matching 7. All of which have no prefix.

 

Since the regex conditions we are using utilised the format "PREFIX\d{NUMBER_OF_DIGITS}", we end up having any string that contains at least 7 or 9 digits in a row matching one of these tables and being converted into a hyperlink (more than likely not pointing to any real record). Example:

DavidHouse_0-1682312900415.png

 

 

The solution to this was actually rather simple, and just required a small addition to the query to filter out any tables without a prefix (^number_ref.prefixISNOTEMPTY):

// Get all tables with a number prefix
var table = new GlideRecord("sys_db_object");
table.addEncodedQuery("sys_update_nameISNOTEMPTY^number_refISNOTEMPTY^number_ref.prefixISNOTEMPTY");
table.query();

 

The end result is now the expected behaviour. Same example as above, but with the updated query:

DavidHouse_1-1682313047355.png

 

I hope this helps if you are still looking to utilise this business rule.

 

Sadly, I cannot update my article, so I will leave this in the comments for now.

David House
Tera Guru

Further testing has brought to light an issue related to inserting records.

 

Since we are triggering an update() call within the before business rule, this throws the following error when the new record is inserted (Unique Key violation detected by database):

DavidHouse_0-1682317902210.png

 

There are three possible solutions I have found:

Solution 1:

Simply uncheck "Insert" in the business rule under the "When to run" tab.

This will prevent the issue as it will simply not attempt to insert hyperlinks when creating a new record.

 

Solution 2:

Adjust the script to not attempt an update() call when the operation is "insert":

 

// Only clear current input on update, if done on insert, duplicate entry errors will be met
if (current.operation() == "insert") {
  currentNote = "Initial work note with hyperlinks:\n" + currentNote;
} else {
  // Disable processing and flush raw input
  current.setWorkflow(false);
  current.update();
}

 

 This will allow the initial work note to be saved, and will add a second work note with the hyperlinks inserted, along with a prefix describing its nature. Example:

DavidHouse_1-1682318184955.png

 

NOTE: As with the behaviour of this business rule normally, this extra note will only be added if there are strings found that match the prefix and number of digits of valid tables. Any new record that does not contain such strings in the work note will be unaffected.

 

There may be other options for how to handle this issue. However, I have been unable to find a solution to achieve the same outcome as an "update" operation.

DominykasI
Tera Explorer

Hello,  I know this is an old post, but I'd like to ask if anyone else have had this problem, where the Activity stream shows duplicate entries - one formatted and the other is original:

DominykasI_0-1742224091801.png

If anyone has had this issue and solved it somehow, please do tell, because I am out of ideas

We are on Xanadu patch 4a btw

David House
Tera Guru

Hi @DominykasI,

 

I do not currently use this solution as I have a found a more suitable solution for my use case, using DOM manipulation instead of modifying the note itself. This means I cannot confirm if your issue is related to the latest updates.

 

That being said, for your particular issue, I can say the important line is as follows:

// Disable processing and flush raw input
current.setWorkflow(false); // <--- this line here
current.update();

 

Using the call to setWorkflow(false) ensures the existing work note is not converted to a journal entry, thereby preventing the duplicate work note. However, you will need to consider if the work note is part of the record creation. In that case I would recommend reading through the comments on this page.

 

Sadly, I cannot update the original article ever since the article authorship changes a year or so ago, so I haven't been able to incorporate these comments into the body of the article.

 

I hope this helps you.

DominykasI
Tera Explorer

Hi, @David House,

 

Thanks for your response. But unfortunately it doesn't help as I am already using it. What I did find, is that it still creates a journal entry in the journal_entries_field table even with 

 

setWorkflow(false);

 

It is very weird, but I guess I will try looking further

Version history
Last update:
‎08-03-2022 09:14 PM
Updated by: