GlideAjax is how a client script asks the server a question without reloading the form: “is this user eligible for that laptop?”, “what’s the manager of the selected person?”, “does this CI have an open change?”. The mechanics take five lines. The parts that make it reliable are the ones beginners skip: a defined response shape, a server method that doesn’t trust its input, and a callback that copes with replies arriving late or out of order.
The round trip
On the client, you create a GlideAjax object with the name of a script include, add a sysparm_name parameter naming the method, add your own parameters (their names must start with sysparm_), and send the request:
var ga = new GlideAjax('CatalogEligibilityAjax');
ga.addParam('sysparm_name', 'checkEligibility');
ga.addParam('sysparm_item', g_form.getValue('laptop_model'));
ga.getXMLAnswer(function(answer) {
// answer is the string the server method returned
});On the server, the script include must be marked callable from the client (the checkbox is labelled Client callable, or Glide AJAX enabled in newer releases) and extend AbstractAjaxProcessor, which gives you this.getParameter(). Whatever the method returns is sent back as a string.
There are three ways to send. getXMLAnswer(callback) is asynchronous and hands the callback just the answer string, which is what you want most of the time. getXML(callback) is also asynchronous but gives you the whole XML response to dig the answer out of. getXMLWait() is synchronous: it freezes the form until the server answers, ServiceNow’s API reference recommends against it, and it isn’t available to scoped applications. If you find yourself wanting getXMLWait() in an onSubmit script, that’s usually a sign the check belongs in a server-side rule instead.
Designing the contract
Because the reply is a string, it’s tempting to return "true", "false" or a bare name. That works until you need a second value, or need to tell “not eligible” apart from “something went wrong”. Return JSON with a fixed shape from the start:
{ "ok": true, "item": "4c1a9e2f1b2e30105b8f3c4d5e6f7081", "eligible": false, "reason": "Needs director approval above 2,500 USD" }A few rules keep the contract healthy. Every reply has ok; when it’s false, there’s an error string and the other fields aren’t relied on. Echo the input the answer is about (item above), which the client uses to detect stale replies. Keep payloads small: send the three fields the form needs, not a serialised record. And treat the method name and fields as an API other people’s client scripts will call, because once a script include is client-callable, they will.
The server treats parameters as hostile
A client-callable method is reachable by anyone who can make the HTTP request, not just by your form. Three habits follow from that.
Validate format. If the parameter should be a sys_id, check it’s 32 hex characters before using it. Reject anything else with ok: false.
Decide access from the session. Never accept “which user am I asking about” from the client when the answer should be “the current user”; use gs.getUserID(). When the method reads records on the user’s behalf, use GlideRecordSecure (or check canRead()) so ACLs apply, because plain GlideRecord ignores them.
Lock down the endpoint. Client-callable script includes are protected by ACLs on the execute operation; newer releases prompt for a role when you tick the box and create that ACL for you. Script includes can also be public or private, and a public one can be called by users who aren’t logged in. Keep yours private unless a public page genuinely needs it.
An illustrative server side (not run on an instance) that keeps the GlideAjax layer thin and delegates the business rule to an ordinary server-only script include:
var CatalogEligibilityAjax = Class.create();
CatalogEligibilityAjax.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
checkEligibility: function() {
var itemId = this.getParameter('sysparm_item');
if (!/^[0-9a-f]{32}$/.test(itemId || '')) {
return JSON.stringify({ ok: false, error: 'bad item' });
}
var item = new GlideRecordSecure('sc_cat_item');
if (!item.get(itemId)) {
return JSON.stringify({ ok: false, item: itemId, error: 'unknown item' });
}
// Domain logic lives in a reusable, server-only script include.
var verdict = new CatalogEligibility().check(gs.getUserID(), item);
return JSON.stringify({ ok: true, item: itemId, eligible: verdict.eligible, reason: verdict.reason });
},
type: 'CatalogEligibilityAjax'
});Splitting it this way means the same eligibility rule can run in a business rule or flow at submit time, where it’s actually enforced. The GlideAjax call is advice for the user; the submit-time check is the control.
Replies arrive out of order
onChange scripts fire on every change. If a user picks one item, then quickly picks another, two requests are in flight, and nothing guarantees the first reply arrives first. This Node.js simulation (plain JavaScript; fakeAjax stands in for getXMLAnswer with a different latency per item) shows the result:
const catalog = { 'mbp-16': { eligible: false, reason: 'Needs director approval above 2,500 USD' },
'thinkpad-x1': { eligible: true, reason: '' } };
const latencyMs = { 'mbp-16': 300, 'thinkpad-x1': 80 };
// Server side: validate input, answer with a small JSON contract, never throw.
function serverCheck(params) {
const item = params.sysparm_item;
if (!/^[a-z0-9-]{1,40}$/.test(item || '')) return JSON.stringify({ ok: false, error: 'bad item' });
const hit = catalog[item];
return JSON.stringify(hit ? { ok: true, item, ...hit } : { ok: false, error: 'unknown item' });
}
// Client side: a fake GlideAjax with an async getXMLAnswer
function fakeAjax(params, callback) {
setTimeout(() => callback(serverCheck(params)), latencyMs[params.sysparm_item] ?? 50);
}
function makeForm(guarded) {
const form = { item: '', message: '' };
form.onChange = (newValue) => {
form.item = newValue;
fakeAjax({ sysparm_name: 'checkEligibility', sysparm_item: newValue }, (answer) => {
let res;
try { res = JSON.parse(answer); } catch (e) { res = { ok: false, error: 'unreadable reply' }; }
if (guarded && res.item !== form.item) { console.log(` [guarded] ignored stale reply for ${res.item}`); return; }
form.message = res.ok ? (res.eligible ? `${res.item}: OK to order` : `${res.item}: ${res.reason}`) : `error: ${res.error}`;
console.log(` reply for ${res.item} -> message "${form.message}"`);
});
};
return form;
}
async function scenario(guarded) {
console.log(guarded ? 'with stale-reply guard:' : 'without guard:');
const form = makeForm(guarded);
form.onChange('mbp-16'); // user picks the expensive laptop...
setTimeout(() => form.onChange('thinkpad-x1'), 20); // ...and changes their mind 20 ms later
await new Promise((r) => setTimeout(r, 400));
console.log(` final: item=${form.item}, message="${form.message}"`);
}
(async () => { await scenario(false); await scenario(true); console.log(serverCheck({ sysparm_item: "x'; drop" })); })();without guard:
reply for thinkpad-x1 -> message "thinkpad-x1: OK to order"
reply for mbp-16 -> message "mbp-16: Needs director approval above 2,500 USD"
final: item=thinkpad-x1, message="mbp-16: Needs director approval above 2,500 USD"
with stale-reply guard:
reply for thinkpad-x1 -> message "thinkpad-x1: OK to order"
[guarded] ignored stale reply for mbp-16
final: item=thinkpad-x1, message="thinkpad-x1: OK to order"
{"ok":false,"error":"bad item"}Without the guard, the form ends up showing a warning about a laptop the user no longer selected, under a field that now holds a perfectly eligible one. The fix costs one line: when the reply arrives, compare the item it’s about with the field’s current value and drop it if they differ. The last line shows the server rejecting a malformed parameter with a structured error instead of throwing.
The client script on the platform
Putting the pieces together in a catalog client script (illustrative):
// Catalog client script: onChange, Variable name: laptop_model
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || !newValue) return;
var requested = newValue;
var ga = new GlideAjax('CatalogEligibilityAjax');
ga.addParam('sysparm_name', 'checkEligibility');
ga.addParam('sysparm_item', newValue);
ga.getXMLAnswer(function(answer) {
if (g_form.getValue('laptop_model') != requested) return; // stale reply
var res;
try { res = JSON.parse(answer); } catch (e) { res = { ok: false }; }
g_form.hideFieldMsg('laptop_model');
if (!res.ok) {
g_form.showFieldMsg('laptop_model', 'Eligibility could not be checked. You can still submit.', 'info');
} else if (!res.eligible) {
g_form.showFieldMsg('laptop_model', res.reason, 'error');
}
});
}Note the isLoading check (don’t call the server just because the form opened), the stale check, the defensive parse, and the failure message that doesn’t block the user, since the real check happens on submit.
When not to reach for GlideAjax
If the data is known when the form loads, a display business rule can put it on g_scratchpad with no extra request. If you only need a field from a referenced record, g_form.getReference(field, callback) does it without writing a script include. Show and hide logic belongs in UI policies. GlideAjax earns its place when the answer depends on what the user just did and needs server data to compute.
Interview practice
A client script uses getXMLWait() to call a script include that takes a sysparm_user_id parameter and returns "true" if that user may order an item. A security review flags it, and users complain the form freezes. What would you change?
Answer
Three things. First, the freeze: getXMLWait() blocks the browser until the server answers, and it isn’t available in scoped applications anyway. I’d switch to getXMLAnswer() with a callback, and make sure the callback ignores replies for a value the user has since changed.
Second, the security finding: the method trusts a user ID supplied by the client, so anyone can ask about any user, and a modified request could make the check pass for someone else. The server should use gs.getUserID() for the current user, validate the item parameter’s format, read records with GlideRecordSecure, and be protected by an execute ACL with an appropriate role (and be private, not public).
Third, the contract: a bare "true" can’t express errors, so a failure looks like “not allowed”. I’d return JSON with ok, eligible, reason and the echoed item. And because the client check is only advisory, the same eligibility logic, moved into a reusable script include, should run again at submit time in a business rule or flow so the rule is enforced.
Related reading
- ServiceNow interview questions
- ServiceNow multiple-choice questions
- ServiceNow Client Scripts Versus Server Enforcement, on why the submit-time check is the real control
- ServiceNow g_form and Client Script Design, for the form-side API used in the callback
- GlideAjax client API (ServiceNow documentation)