Ch. 17 · ServiceNow

ServiceNow Client Scripts Versus Server Enforcement

Client scripts and UI policies guide users on the form; ACLs, data policies and business rules enforce rules on every path into the data.

~6 min readbeginnerupdated Oct 12, 2026

ServiceNow gives you two places to put logic: the browser and the server. Client scripts and UI policies make a form pleasant: fields appear when they’re relevant, the user gets a hint before they make a mistake, and nothing needs a page reload. Server-side logic (ACLs, data policies, business rules) decides what’s actually allowed into the database. The interview question hiding behind “client script or business rule?” is whether you know which of those two is a control and which is a convenience.

What runs in the browser

Client-side logic runs only when someone is working in a form (or, for onCellEdit, a list cell) in the browser:

  • Client scripts of type onLoad, onChange, onSubmit and onCellEdit, using the g_form API to read values, show messages, and set fields mandatory, read-only or hidden.
  • UI policies, which do the common cases (show, hide, mandatory, read-only when a condition holds) without script.

Both are quick and friendly. Neither is in a position to enforce anything, because the browser belongs to the user. ServiceNow’s own client script documentation says to rely on server-side controls (ACLs, data policies, business rules) for security and validation beyond the client.

Every path that skips the form

It helps to list the ways a record changes without anyone opening its form, because each is a hole if your rule only lives on the client:

  • The REST Table API and other web services
  • Import sets and transform maps
  • Flows, business rules, scheduled jobs and background scripts doing GlideRecord updates
  • Inbound email actions
  • List editing, unless an onCellEdit script covers that specific field
  • Mobile apps, where client scripts don’t run

Even inside the form there are gaps. UI policy conditions are only rechecked when a user changes a field by hand; if a UI action or the list editor changes the value, the policy isn’t re-evaluated. And a user with browser developer tools can remove a mandatory flag or post a request directly. Making a field read-only on the form doesn’t make it read-only.

One rule, three channels

Take a real rule: an incident can’t be resolved without close notes, and the notes must say something useful (20 characters or more). This Node.js model (plain JavaScript, no ServiceNow APIs) shows which checks run for a form submission, a REST call and an import as you add server-side enforcement step by step:

const RESOLVED = 6;
const clientChecks = [
  { name: 'UI policy: close_notes mandatory when Resolved',
    fails: (r) => r.state === RESOLVED && !r.close_notes },
];
const serverChecks = [];

function submit(channel, record) {
  if (channel === 'form') {                      // only the browser form runs client logic
    const c = clientChecks.find((x) => x.fails(record));
    if (c) return `blocked in browser (${c.name})`;
  }
  const s = serverChecks.find((x) => x.applies(channel) && x.fails(record));
  return s ? `rejected by server (${s.name})` : 'saved';
}

const attempts = [
  ['form', { state: RESOLVED, close_notes: '' }],
  ['rest', { state: RESOLVED, close_notes: '' }],
  ['import', { state: RESOLVED, close_notes: '' }],
  ['rest', { state: RESOLVED, close_notes: 'fixed' }],
];
const run = (label) => {
  console.log(label);
  for (const [ch, rec] of attempts) console.log(`  ${ch.padEnd(6)} notes=${JSON.stringify(rec.close_notes).padEnd(8)} ${submit(ch, rec)}`);
};

run('1) UI policy only');
serverChecks.push({ name: 'data policy: close_notes mandatory', applies: () => true,
  fails: (r) => r.state === RESOLVED && !r.close_notes });
run('2) plus a data policy (applied to import sets)');
serverChecks.push({ name: 'before rule: notes need 20+ characters', applies: () => true,
  fails: (r) => r.state === RESOLVED && r.close_notes.trim().length < 20 });
run('3) plus a before business rule for length');
JavaScript
1) UI policy only
  form   notes=""       blocked in browser (UI policy: close_notes mandatory when Resolved)
  rest   notes=""       saved
  import notes=""       saved
  rest   notes="fixed"  saved
2) plus a data policy (applied to import sets)
  form   notes=""       blocked in browser (UI policy: close_notes mandatory when Resolved)
  rest   notes=""       rejected by server (data policy: close_notes mandatory)
  import notes=""       rejected by server (data policy: close_notes mandatory)
  rest   notes="fixed"  saved
3) plus a before business rule for length
  form   notes=""       blocked in browser (UI policy: close_notes mandatory when Resolved)
  rest   notes=""       rejected by server (data policy: close_notes mandatory)
  import notes=""       rejected by server (data policy: close_notes mandatory)
  rest   notes="fixed"  rejected by server (before rule: notes need 20+ characters)
Text

In stage 1 the rule looks enforced to anyone testing in the form, and two of the four attempts sail through. Stage 2 closes the empty-notes hole on every channel, but a data policy can only say “mandatory”, so “fixed” passes. Stage 3 adds the part that needs logic. Notice that the form row never changes: users of the form had a good experience all along, which is exactly why client-only rules survive testing.

Choosing the server-side tool

Each server-side mechanism answers a different question:

  • ACLs decide who can read, write, create or delete a table or field. “Only the assignment group can resolve” is an ACL (or ACL plus condition) problem.
  • Data policies make fields mandatory or read-only under a condition, on all GlideRecord operations by default, including the REST Table API and Scripted REST. You can opt them out of import sets, SOAP targets and the client. Ticking Use as UI Policy on client gives you the form behaviour from the same record, so the rule is defined once.
  • Before business rules handle anything that needs logic: lengths, formats, cross-field and cross-record checks. They abort the save with current.setAbortAction(true).

A UI policy can be converted to a data policy from its form, which is a quick way to fix a rule that was built client-only.

Writing the pair

The pattern that works is server enforcement plus client guidance. On the server, an illustrative before rule (written for this article, not run on an instance):

// Business rule: before insert/update on incident, Condition: current.state == 6
(function executeRule(current, previous /*null when async*/) {
    var notes = (current.getValue('close_notes') || '').trim();
    if (notes.length < 20) {
        gs.addErrorMessage('Close notes need at least 20 characters to resolve an incident.');
        current.setAbortAction(true);
    }
})(current, previous);
JavaScript

On the client, a script that explains the rule before the user hits it:

// Client script: onChange, Table: incident, Field name: state
function onChange(control, oldValue, newValue, isLoading) {
    if (isLoading) return;
    if (newValue == '6') {
        g_form.showFieldMsg('close_notes', 'Describe the fix in 20 or more characters.', 'info');
    } else {
        g_form.hideFieldMsg('close_notes');
    }
}
JavaScript

Then prove the server half without the form. A Table API call like this one, made as a non-admin integration user, should fail:

curl -s -u integration.user -X PATCH \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"state":"6","close_notes":"fixed"}' \
  'https://<instance>.service-now.com/api/now/table/incident/<sys_id>'
Terminal

When a before rule aborts a REST update, the API returns an error instead of the updated record, with a message saying the operation against the incident table was aborted by the named business rule. If it returns the record with "state":"6", the rule isn’t enforced.

Trust nothing that comes from the browser

The same reasoning applies to data flowing the other way. A client-callable script include called through GlideAjax receives parameters the user controls: check the user’s rights on the server rather than trusting a sysparm_ value that says which record they may touch. Values a display rule puts on g_scratchpad are readable in the browser, so they must not contain anything the user isn’t allowed to see. Hidden fields are still on the page. Client logic is for experience; every decision that matters gets made again on the server.

Interview practice

A team made the Business justification field mandatory on a custom request table using a UI policy. Requests created through the Service Portal and through a partner’s REST integration keep arriving without a justification. The team’s proposed fix is an onSubmit client script. What would you recommend?

Answer

The onSubmit script would fail for the same reason the UI policy did: the REST integration never runs client logic, and portal behaviour depends on the UI type the script is set to run in. The rule belongs on the server. Because it’s a straightforward “mandatory when” rule, a data policy is the right first tool: it applies to all GlideRecord operations including the Table API, can be applied to import sets, and can also act as the UI policy on the form via Use as UI Policy on client, which lets the team retire the separate UI policy. If the requirement grows (minimum length, different rules per request type), add a before business rule.

I’d verify by calling the Table API as the partner’s integration user without the field and confirming the insert is rejected, then check the portal and the form. Finally I’d tell the partner, since their integration will start receiving errors it never got before, and agree how they want those surfaced.

More in ServiceNow

read ✓ServiceNow · mid

ServiceNow g_form and Client Script Design

Use g_form for what the user sees, guard onChange scripts with isLoading, avoid synchronous lookups, and keep the real rules on the server.

~6 min readread →
esc