- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
yesterday
Reducing Duplicate Tickets with AI Search
Duplicate tickets are a quiet productivity tax. This article walks through a lightweight pattern that catches duplicates before they are created — as the user types a short description (or any other field), an AI Search query runs in the background and any matching tickets surface in a modal where the user can either open the existing record or follow it for updates. The pattern works on any custom ticket-style table.
1. What the user sees
The flow is intentionally minimal — it sits inside the standard new-record form and never blocks the user. As the user types into the short_description field, a Client Script fires a GlideAjax call that runs an AI Search across existing tickets.
If matches come back, a modal opens listing the candidate tickets — number, title, and per-row View / Follow actions. The user can open a match in a new workspace tab, or click Follow to be added to the ticket's watch_list without leaving the new-ticket form. If no matches exist, the modal does not appear.
2. The dialog in action
Here is what the user actually sees the moment AI Search returns matches. The new-ticket form stays in the background, dimmed but not dismissed — the modal appears as a contextual layer rather than a page navigation.
The interaction is designed to feel optional and non-blocking — the AI banner frames it as an assistive suggestion, the Follow button shows three visual states (idle, following, hover), and the close button is always one click away with no confirmation prompt.
3. Architecture overview
Four moving parts make the pattern work — each maps to a single artefact in the platform:
[ New Ticket Form ]
|
| onChange(short_description)
v
[ Client Script ] --GlideAjax--> [ Script Include: doAISearch() ]
|
| AISASearchUtil.search()
v
[ AI Search Profile + Source ]
|
v
Returns matching sysIds
+-----------------------------+
v
[ g_modal.showFrame() ]
|
v
[ UI Page: STL_Similar_Tickets ]
|
+--> View (opens record in workspace)
+--> Follow (GlideAjax -> updates watch_list)
4. Keyword search vs. AI Search
short_description only fire on literal substring matches. Synonyms, paraphrases and translations are missed. Maintenance grows with every new phrasing.
5. AI Search configuration
AI Search needs three records configured against your ticket table — x_yourapp_ticket in the steps below. Navigate to AI Search > Sources > Indexed Sources and create the source, then create a matching Search Profile and an aisa_rp_config record:
| Field | Value |
|---|---|
| Source table | x_yourapp_ticket |
| Embedding model | E5FT (default) |
| Field mapping — short_description | map_to: title |
| Field mapping — description | map_to: text |
| aisa_rp_config sys_ids | Store both in x_yourapp.ai_search.configuration — avoids hard-coded sys_ids |
6. The Script Include
A small client-callable Script Include wraps AISASearchUtil and reads the configuration from system properties. Mark it client_callable = true and grant cross-scope execute access to AISASearchUtil:
var Ticket_AI_Utils = Class.create();
Ticket_AI_Utils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
doAISearch: function() {
var searchTerm = this.getParameter('sysparm_search_term');
var cfg = gs.getProperty('x_yourapp.ai_search.configuration', '').split(',');
var data = {
rpSysId: cfg[0],
searchContextConfigId: cfg[1],
searchTerm: searchTerm
};
return JSON.stringify(new global.AISASearchUtil().search(data));
},
followTicket: function() {
return this._toggleWatchList(this.getParameter('sysparm_sys_id'), true);
},
unFollowTicket: function() {
return this._toggleWatchList(this.getParameter('sysparm_sys_id'), false);
},
_toggleWatchList: function(recordId, addUser) {
var userId = gs.getUserID();
var gr = new GlideRecord('x_yourapp_ticket');
if (!gr.get(recordId)) return false;
var list = (gr.getValue('watch_list') || '').split(',').filter(Boolean);
var idx = list.indexOf(userId);
if (addUser && idx === -1) list.push(userId);
if (!addUser && idx > -1) list.splice(idx, 1);
gr.setValue('watch_list', list.join(','));
gr.autoSysFields(false);
gr.setWorkflow(false);
gr.update();
return true;
},
type: 'Ticket_AI_Utils'
});
7. The Client Script
An onChange Client Script on the new-ticket form's short_description field — only fires for new records, not on edits:
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue === '' || oldValue === newValue || !g_form.isNewRecord())
return;
var ticketId = g_form.getUniqueValue();
var ticketNum = g_form.getValue('number');
var shortDesc = g_form.getValue('short_description');
var ga = new GlideAjax('x_yourapp.Ticket_AI_Utils');
ga.addParam('sysparm_name', 'doAISearch');
ga.addParam('sysparm_search_term', shortDesc);
ga.getXMLAnswer(handleResponse);
function handleResponse(response) {
if (!response) return;
var parsed = JSON.parse(response);
if (parsed.status !== 200) return;
var results = parsed.data.search.searchResults;
if (!results || !results.length) return;
var sysIds = results.map(function(r) { return r.sysId; }).join(',');
g_modal.showFrame({
title: 'Similar tickets found',
url: 'x_yourapp_STL_Similar_Tickets.do' +
'?sysparm_similar=' + sysIds +
'&sysparm_ticket_id=' + ticketId +
'&sysparm_ticket_number=' + ticketNum,
size: 'lg',
height: '550px',
width: '900px'
});
}
}
8. The UI Page
The Follow toggle handler is short and reads naturally:
function toggleFollow(sysId) {
var btn = document.getElementById('follow_' + sysId);
var isFollowing = btn.classList.contains('following');
// Update UI first - feels instant
btn.classList.toggle('following');
btn.innerHTML = isFollowing
? '<span class="icon-star-empty"></span> Follow'
: '<span class="icon-star"></span> Following';
// Then persist
var ga = new GlideAjax('x_yourapp.Ticket_AI_Utils');
ga.addParam('sysparm_name', isFollowing ? 'unFollowTicket' : 'followTicket');
ga.addParam('sysparm_sys_id', sysId);
ga.getXML();
}
9. Implementation Checklist
If your custom app cannot call
global.AISASearchUtil, the GlideAjax response will be empty. Add a Cross Scope Privilege with operation execute.After creating the source, AI Search needs minutes to build the index on a populated table. Empty results immediately after configuration usually mean indexing is incomplete.
Always check
isLoading and g_form.isNewRecord() — otherwise opening any existing ticket triggers the modal.Use
autoSysFields(false) and setWorkflow(false) on the follow update to avoid notification noise.Move the Record Producer Configuration sys_id and Search Application Configuration sys_id into a system property — portable across environments and self-documenting.
10. Typical Errors to Watch For
When AI Search Assist or the surrounding plumbing is misconfigured, the failure modes are quiet — the modal simply never appears, or it appears empty. These are the patterns to recognise in browser console and server logs.
doAISearch result: null
TypeError: Cannot read properties of null (reading 'data')What it means: the Script Include executed but the call to global.AISASearchUtil was blocked at the cross-scope boundary. The script silently returned undefined, which serialises to null over GlideAjax. Fix by adding a Cross Scope Privilege record granting execute on AISASearchUtil from your application scope.
{ "status": 200, "data": { "search": { "searchResults": [] } } }What it means: the API responded successfully but no documents matched. Two common causes — first, the indexed source has not finished building yet (check AI Search > Indexed Sources for index status). Second, the field mappings on the source do not include the field your users are searching against. Verify short_description is mapped to title with the correct map_to attribute.
11. Adapting the pattern
The same architecture extends well beyond ticket deflection. The shape is always type → search → modal → action — once the wiring is in place, swapping the source and the action is a small change:
kb_knowledge#ai-search-assist #ticket-deflection #custom-apps #duplicate-prevention #client-script #ui-page
#scoped-applications
If this article was useful, please consider marking it as helpful. Feedback is always welcome.
