g_form is the object client scripts use to talk to the form in front of the user. It reads and sets field values, shows and hides fields, marks them mandatory or read-only, adds messages next to fields, and manages choice lists. It’s the most used client API on the platform, and because it’s easy to use, it’s also where instances accumulate slow, fragile forms: thirty onChange scripts, each making its own server call, half of them firing on load for no reason.
When interviewers ask about client script design, they’re checking two things. Do you know the mechanics well enough to avoid the common performance traps? And do you understand that nothing in the browser can be trusted to enforce a rule?
The parts of g_form you’ll actually use
A handful of methods cover most real scripts:
g_form.getValue('field')returns the stored value as a string. For a reference field that’s the sys_id, not the name.g_form.setValue('field', value, displayValue)sets a value. For a reference field, passing the display value as the third argument saves the form from fetching it.g_form.setMandatory,setReadOnlyandsetDisplay(orsetVisible) change how a field behaves on screen.g_form.showFieldMsg('field', 'text', 'error')andhideFieldMsgput a message under a field;addInfoMessageandaddErrorMessageuse the banner at the top.g_form.clearOptions,addOptionandremoveOptionreshape a choice list.
Client scripts come in four types. onLoad runs once when the form opens. onChange runs when a specific field changes. onSubmit runs when the user saves, and returning false stops the submission. onCellEdit runs when someone edits a value directly in a list. The onChange signature is function onChange(control, oldValue, newValue, isLoading, isTemplate), and the fourth argument is the one people forget.
The isLoading trap
An onChange script runs when its field changes, and it also runs once while the form loads, with isLoading set to true. That’s useful when you want the same logic to set up the form initially. Most of the time it isn’t what you want, which is why the default template ServiceNow generates starts with if (isLoading || newValue === '') { return; }. Developers delete that line while editing and the script starts doing its work on every form open.
Consider an incident form that fills in the caller’s location when the caller changes. The first version below is the one you find on many instances:
// onChange on caller_id, version 1
function onChange(control, oldValue, newValue, isLoading) {
var caller = g_form.getReference('caller_id'); // synchronous: the browser waits for the server
g_form.setValue('location', caller.location);
}It has three problems. It runs on every load. getReference without a callback is synchronous, so the form freezes until the server answers. And when the agent clears the caller, it looks up an empty value and writes whatever comes back. Synchronous calls are also not supported in the Service Portal, so this script fails there entirely.
Here’s the shape I’d want instead. A display business rule runs on the server just before the form is sent and can put values into g_scratchpad, which the client reads without another request:
// Display business rule on incident (server)
(function executeRule(current, previous) {
g_scratchpad.callerLocation = current.caller_id.location.toString();
g_scratchpad.callerLocationName = current.caller_id.location.getDisplayValue();
})(current, previous);// onChange on caller_id, version 2 (client)
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) {
if (!g_form.getValue('location') && g_scratchpad.callerLocation) {
g_form.setValue('location', g_scratchpad.callerLocation, g_scratchpad.callerLocationName);
}
return;
}
if (!newValue) {
g_form.clearValue('location');
return;
}
var ga = new GlideAjax('IncidentFormUtils'); // client-callable script include
ga.addParam('sysparm_name', 'getUserLocation');
ga.addParam('sysparm_user', newValue);
ga.getXMLAnswer(function (answer) {
var loc = JSON.parse(answer);
if (loc) g_form.setValue('location', loc.sysId, loc.name);
});
}These are illustrative; no instance was available to run them. To check the control flow I modelled both versions in Node with a fake form and a counter on server calls. The scenario is one form load, one caller change, then the agent clearing the caller:
naive: round trips=3, location="Pune"
isLoading + scratchpad: round trips=1, location=""The first version made a server call for each event, including the load and the clear, and ended with a location for a caller who no longer exists. The second made exactly one call (for the real change), used the preloaded value on load, and cleared the field when the caller was cleared. Notice too that version 2 passes the display value to setValue, so the form doesn’t need another call just to show the location’s name.
Script less: UI policies first
A good share of client scripts on any mature instance do things a UI policy does better: show a field when category is Hardware, make close notes mandatory when state is Resolved, lock the priority when it was set by an SLA. UI policies are declarative, show up in one list per table, run in a predictable order, and can reverse themselves automatically when the condition stops being true. A client script that calls setMandatory(true) has to remember to call setMandatory(false) on every other path, and often doesn’t.
So the order of preference for form behaviour is: dictionary settings for anything that’s always true, UI policies for conditional presentation, and client scripts for the rest, such as calculations, fetching data, or messages that depend on several fields. If a reviewer finds a client script that only toggles visibility, it should usually become a UI policy.
The browser isn’t a security boundary
Everything above shapes the experience. None of it enforces anything. A user can open the browser console and call g_form.setReadOnly('priority', false). An import set, a REST call, an email reply or a flow updates the record without loading the form at all, so your onSubmit validation never runs.
That’s the interview point most candidates miss when asked “how do I make sure close notes are filled in before resolving?” The full answer is a UI policy so the agent sees the field become mandatory, and a data policy or before business rule so the server rejects a resolution without notes from any channel. Data policies can be set to also apply as UI policies, which keeps the two in step. Access to the field itself is an ACL question, not a setReadOnly one.
Interview tip
When you write an onSubmit check, ask “what happens if this record arrives by REST?” If the answer is “the bad data gets saved”, the check needs a server-side twin.
Keeping forms fast
A few habits keep client scripts cheap. Return early with isLoading unless you mean to run at load. Batch server lookups into one GlideAjax call that returns a JSON object, instead of three calls for three fields. Preload anything the form needs at open through g_scratchpad. Avoid getReference without a callback and synchronous GlideAjax (getXMLWait). Don’t use DOM manipulation or document.getElementById; it breaks on upgrades and in the portal. And set “UI Type” deliberately (desktop, mobile or all) so the script runs only where it’s meant to.
Practice question: the form that takes four seconds to open
Agents complain the incident form takes about four seconds to open. Looking at the browser’s network panel, you see nine requests to xmlhttp.do before the form becomes usable. What’s your approach?
Answer
Nine xmlhttp.do requests at load points to client scripts making server calls during load, most likely onChange scripts without an isLoading guard or onLoad scripts using GlideAjax or getReference. I’d list the client scripts on incident, check each onLoad and onChange script for server calls, and use the browser’s network panel with the JavaScript log to match requests to scripts. For each one, decide whether it needs to run at load at all; most don’t, so an isLoading early return fixes them. Data that the form genuinely needs at open goes into a single display business rule that fills g_scratchpad. Any remaining calls become asynchronous and combined where possible. Scripts that only show, hide or make fields mandatory become UI policies. I’d then retest with an agent account, not admin, since admin forms can load different scripts and fields, and measure again in the network panel.