ServiceNow ACL Scripting and Access
Enforce access with ACLs, use scripted conditions carefully, and test as the actual user to verify.
ServiceNow admin and developer interviews: the platform and ITSM, tables and CMDB, business rules and client scripts, GlideRecord, Flow Designer, ACLs and integrations.
Official reference: ServiceNow Developer
Enforce access with ACLs, use scripted conditions carefully, and test as the actual user to verify.
Understand what the platform caches, when a change takes effect, and how to flush caches consistently across nodes.
Define fields in the dictionary, use per-table overrides, and enable audit on the fields that need a history.
Use the client form API for presentation, keep validation on the server, and avoid heavy client scripts.
Send notifications through events and business rules, gate them with conditions, and prevent the notification storm that floods users.
Collect input with a catalog form and create the right record, and know the limits when the target span multiple tables.
Expose data through Scripted REST or the Table API, version the contract, and avoid leaking internal tables.
Run recurring server-side work with scheduled jobs, avoid overlap and long runs, and log enough to debug failures.
Move configuration with update sets, resolve collisions in order, and avoid the manual edits that break promotion.
How ServiceNow decides record access: table ACLs before field ACLs, the most specific match wins, and why server scripts skip the check.
Model ServiceNow approvals so each decision is tied to the version reviewed: approval rules, rejection paths, timeouts and re-approval.
Design ATF tests that prove behaviour as real roles, know what rollback leaves behind, and keep suite results as release evidence.
When ServiceNow business rules run: before rules shape the record, after rules touch others, async rules move slow work off the save.
Client scripts and UI policies guide users on the form; ACLs, data policies and business rules enforce rules on every path into the data.
Keep one server as one CI: identification rules decide what's new, reconciliation rules decide which source owns each attribute.
Pick the right Flow Designer trigger and run option, avoid duplicate flows on one table, and build flows that a team can own and debug.
Count and summarise ServiceNow records in the database with GlideAggregate instead of looping over every row, and avoid its string-typed results.
Call server logic from a ServiceNow form with GlideAjax: a small JSON contract, validated parameters, and callbacks that ignore stale replies.
Write GlideRecord queries that fetch only what you need: catch invalid fields, stop encoded-query injection, avoid N+1 lookups and use setLimit.
Load external data into ServiceNow without duplicates: staging tables, coalesce on a stable key, reference choice actions and safe re-runs.
Incident restores service, problem finds the cause, change governs the fix: one outage traced through all three ServiceNow records.
Make ServiceNow outbound calls safe to retry: treat timeouts as unknown, send an idempotency key, back off, and keep calls out of the save.
A ServiceNow reference field stores a sys_id and shows a display value. Treating the name as the identity causes most reference bugs.
Narrow ServiceNow business rule conditions to the transition you mean: operations, filters, changes() and changesTo(), and rules on task tables.
How ServiceNow application scopes protect tables and scripts, how cross-scope privileges work, and how to expose a small API to other apps.
Design ServiceNow script includes with explicit inputs and plain results, so business rules, flows and GlideAjax can share one implementation.
Child tables in ServiceNow inherit fields, rules and access from their parents. See what extending task gives you and what it costs.
How ServiceNow UI policies decide visible, mandatory and read-only: evaluation order, Reverse if false, and why overlapping policies fight.
Know what a ServiceNow update set carries and what it leaves behind: configuration versus data, preview problems and environment values.
ServiceNow is a cloud platform for digital workflows. It started with IT Service Management, but today it runs HR, customer service, security operations, asset management and custom apps, all on one underlying platform, which ServiceNow markets as the Now Platform (recent docs call it the ServiceNow AI Platform).
Every customer gets their own instance: a dedicated application and database stack reached at a URL like company.service-now.com. Typically you have separate dev, test and prod instances.
The single data model means every app shares the same database, tables and services: users, groups, the CMDB, the Task table, workflows, notifications, reporting and security. So an incident, an HR case and a change can all point at the same user and the same CI, and one ACL model, one flow engine and one reporting engine work across all of them. That avoids integrating a dozen separate tools.
Likely follow-up: What instances would you expect in a typical landscape, and how do they get refreshed?
A business rule is a server-side script that runs when a record is displayed, inserted, updated, deleted or queried. The When field decides the timing:
current; changes save automatically, no update() needed. Can abort with current.setAbortAction(true).previous isn't available.g_scratchpad with server data for client scripts.There is also the Query operation, normally paired with before, which adds conditions to every query on the table, for example to hide records.
Likely follow-up: Why is current.update() in a business rule a bad idea? · Where do business rules sit in the order of execution relative to engines?
onChange script receive?easyClient scripts run JavaScript in the browser on forms. There are four types:
control, oldValue (the value when the record loaded), newValue, isLoading and isTemplate.false to cancel the submission, typically after validation fails.sysIDs, table, oldValues, newValue and a callback; calling callback(false) stops the change.The isLoading guard matters because onChange also fires while the form loads. And client scripts are for user experience, not security: anything enforced only in the browser can be bypassed through lists, imports or APIs.
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue === '') {
return; // don't run while the form is loading or when cleared
}
if (newValue == '1') {
g_form.setMandatory('work_notes', true);
g_form.showFieldMsg('priority', 'P1: add work notes', 'info');
}
}Likely follow-up: How would you stop the same logic from being bypassed through list editing?
On load, client scripts run before UI policies, and when they conflict on mandatory or read-only, the UI policy generally wins. The key interview point: UI policies and client scripts only protect the form. If a rule must hold for imports, APIs and list edits too, use a data policy (a data policy can also be applied as a UI policy on the client), a business rule, or an ACL.
Likely follow-up: A field is mandatory via UI policy, but records arrive via REST without it. Why, and how do you fix it?
An incident is an unplanned interruption to a service, or a reduction in its quality. The goal of Incident Management is to restore service as quickly as possible, even with a workaround. Example: email is down for the sales team, so the service desk restarts the mail service.
A problem is the cause, or potential cause, of one or more incidents. Problem Management looks for the root cause and a permanent fix, and it can be reactive (after repeated incidents) or proactive (spotting trends before users are hit).
A known error is a problem whose root cause is understood and that has a documented workaround, often published to the knowledge base so the service desk can resolve related incidents faster. The permanent fix usually goes through Change Management.
In ServiceNow, incidents link to a problem through the problem_id field, and a problem can raise a change request to deliver the fix.
Likely follow-up: How is incident priority calculated in ServiceNow? · What is a major incident and how is it handled differently?
addQuery, addEncodedQuery, setLimit and get.easyGlideRecord is the server-side API for database operations. You create it for a table, add conditions, call query(), then iterate with next().
addQuery(field, value) or addQuery(field, operator, value) adds a condition; multiple calls are ANDed.addEncodedQuery(string) takes the encoded query you can copy from a list filter, like active=true^priority=1. ^ is AND, ^OR is OR.setLimit(n) caps the number of rows fetched, which is important for performance.get(sys_id) or get(field, value) loads a single record and returns true if it was found, so always check the return value.Prefer getValue() for reading a field as a string and setValue() for writing. And test encoded queries carefully: an invalid field name is dropped from the query, which can silently return every record in the table.
var inc = new GlideRecord('incident');
inc.addQuery('active', true);
inc.addQuery('priority', '<=', 2);
inc.addEncodedQuery('assignment_groupISEMPTY');
inc.orderByDesc('sys_created_on');
inc.setLimit(10);
inc.query();
while (inc.next()) {
gs.info(inc.getValue('number') + ' ' + inc.getDisplayValue('caller_id'));
}
var one = new GlideRecord('incident');
if (one.get('number', 'INC0010001')) { gs.info(one.getUniqueValue()); }Likely follow-up: How would you express an OR condition without an encoded query? · What is the difference between getValue() and dot-walking gr.field?
The standard way is GlideAjax calling a client-callable script include.
On the server, the script include extends AbstractAjaxProcessor, its name matches the class name, and the Client callable (Glide AJAX enabled) box is checked. Methods read inputs with this.getParameter() and return a value.
On the client, you create new GlideAjax('ClassName'), set sysparm_name to the method name, add your own parameters (they must start with sysparm_), and send it asynchronously. getXMLAnswer() hands the callback the answer string directly; getXML() gives you the XML response to parse.
Avoid getXMLWait(): it's synchronous, freezes the browser and isn't available in scoped apps. Methods whose names start with an underscore aren't callable from the client, and recent releases protect client-callable script includes with ACLs, so check access when a call silently returns nothing.
// Client script
var ga = new GlideAjax('UserUtilsAjax');
ga.addParam('sysparm_name', 'getDepartment');
ga.addParam('sysparm_user', g_form.getValue('caller_id'));
ga.getXMLAnswer(function (answer) { g_form.setValue('u_department', answer); });
// Script include "UserUtilsAjax", Client callable checked
var UserUtilsAjax = Class.create();
UserUtilsAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getDepartment: function () {
var user = new GlideRecord('sys_user');
return user.get(this.getParameter('sysparm_user')) ? user.getValue('department') : '';
},
type: 'UserUtilsAjax'
});sysparm_name selects the method; params start with sysparm_Likely follow-up: When would you use a display business rule with g_scratchpad instead of GlideAjax?
current and previous in a business rule, and when is previous not available?easycurrent is a GlideRecord holding the record being processed, including the values the user just submitted. In a before rule you change it directly and the changes are saved automatically.
previous holds the record as it was before this update, so you can compare old and new values. The GlideElement helpers changes(), changesFrom() and changesTo() use it under the hood, e.g. current.state.changesTo(6).
previous is only meaningful on update and delete. It's not available in async business rules, because they run later as a scheduled job after the transaction has finished; that's why the default template says previous /*null when async*/, and why changes() doesn't work inside an async script. You can still use change conditions in the async rule's condition builder, because that's evaluated during the original transaction.
(function executeRule(current, previous /*null when async*/) {
if (current.state.changesTo(6)) { // 6 = Resolved on incident
current.resolved_by = gs.getUserID();
}
if (previous && previous.getValue('priority') != current.getValue('priority')) {
gs.info('Priority changed on ' + current.getValue('number'));
}
})(current, previous);An update set is a named group of configuration changes (customer updates in sys_update_xml) that you move between instances as a unit.
The flow:
Good habits: never build in the Default update set, keep sets small and focused, preview in test before prod, and use batching (parent/child sets) for large releases. Update sets are per application scope, so scoped app changes go into that app's update set.
Likely follow-up: What isn't captured in an update set? · How would you back out an update set?
To access a record, a user must pass both the table-level ACL and the field-level ACL for the operation (read, write, create, delete).
Table ACLs match from most specific to most general: the table (incident), then its parent (task), then *.
Field ACLs, per the docs: incident.number, task.number, *.number, then incident.*, task.*, *.*.
The first matching ACL the user passes grants access and stops processing at that level; if the user passes none of the matching ACLs, access is denied. If several ACLs match at the same point in the order, passing any one is enough. Within a single ACL, the user must meet all of its requirements: one of the required roles, the data condition and the script (answer = true).
Recent releases add Deny-Unless ACLs, which are evaluated before the normal Allow-If ones: fail a Deny-Unless and you're denied, whatever the Allow-If rules say. Admins can view and debug ACLs, but creating or editing them requires elevating to security_admin. The security debug output shows which rule passed or failed.
Likely follow-up: A user with the right role still can't see a field. How do you debug it?
A script include is a reusable server-side library: a class or function you call from business rules, flows, UI actions, other script includes or, if client callable, from the browser via GlideAjax.
The common shapes:
Class.create() with a prototype, an initialize constructor and a type property. Called with new IncidentUtils().countOpenForCaller(id).Object.extendsObject(Parent, {...}), most often extending AbstractAjaxProcessor for GlideAjax.The name must match the class or function name. Accessible from controls whether other scopes may call it, and callers from another scope use the scope prefix, like new global.IncidentUtils().
They beat global business rules because script includes load only when called, while global business rules load on every transaction. They're also easier to test and reuse.
var IncidentUtils = Class.create();
IncidentUtils.prototype = {
initialize: function () {},
countOpenForCaller: function (callerId) {
var ga = new GlideAggregate('incident');
ga.addQuery('caller_id', callerId);
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.query();
return ga.next() ? parseInt(ga.getAggregate('COUNT'), 10) : 0;
},
type: 'IncidentUtils'
};Likely follow-up: How do you call a scoped script include from another scope?
Client-side code runs in the user's browser, so it can only touch the page and must call the server for data.
g_form (GlideForm), g_user (GlideUser), GlideAjax, g_scratchpad (read on the client), spUtil in Service PortalServer-side code runs on the instance, with database access.
GlideRecord, GlideAggregate, GlideRecordSecure, gs (GlideSystem), GlideDateTime, current and previous, sn_ws.RESTMessageV2, $sp in widget server scriptsThe classic interview trap: calling gs or GlideRecord in a client script, or g_form in a business rule. Neither exists in the other context. The bridge between them is GlideAjax calling a client-callable script include, or a display business rule filling g_scratchpad.
ServiceNow follows the ITIL change types:
The Change Advisory Board is the group that reviews and authorizes changes, weighing risk, impact, schedule conflicts and blackout windows. ServiceNow supports it with CAB meetings, agendas and the CAB Workbench. Good practice is to push more low-risk work into standard changes so the CAB spends time on genuinely risky ones.
Likely follow-up: How would you decide that a change should become a standard change?
g_form and g_user? Name the methods you use most often.easyBoth are client-side globals available in form scripts.
g_form (GlideForm) controls the current form:
getValue and setValue; pass the display value as the third argument of setValue for reference fields to save a server round tripsetMandatory, setReadOnly, setDisplay (removes the space) versus setVisible (leaves a gap)showFieldMsg / hideFieldMsg, addInfoMessage / addErrorMessageaddOption, removeOption, clearOptions for choice listsisNewRecord, getUniqueValue, getTableName, save, submitg_user (GlideUser) describes the logged-in user: userID, userName, firstName, lastName, getFullName(), hasRole(), hasRoleExactly() and hasRoleFromList().
Watch out: g_user.hasRole('itil') also returns true for admins, while hasRoleExactly() doesn't. And checking roles in the browser is for UX only; real enforcement is ACLs.
function onLoad() {
if (!g_user.hasRole('itil')) {
g_form.setReadOnly('assignment_group', true);
g_form.setDisplay('work_notes', false);
}
if (g_form.isNewRecord()) {
g_form.setValue('caller_id', g_user.userID, g_user.getFullName());
g_form.addInfoMessage('Hi ' + g_user.firstName + ', describe the issue.');
}
}There are four ways to get server data into the browser:
g_scratchpad: the rule runs when the form is built and puts values into g_scratchpad, which ships with the form. No extra round trip, so it's the most efficient choice when you know what you need at load time.g_form.getReference(field, callback): fetches the whole referenced record. Easy, but it pulls every field; the docs no longer recommend it.A related anti-pattern is adding a field to the form only to hide it so a script can read it. It works, but it's slower than a scratchpad value. So: scratchpad for load-time data, async GlideAjax for dynamic data, and never a synchronous call.
// Display business rule on incident
(function executeRule(current, previous) {
g_scratchpad.isVip = current.caller_id.vip == true;
g_scratchpad.managerName = current.caller_id.manager.getDisplayValue();
})(current, previous);
// onLoad client script
function onLoad() {
if (g_scratchpad.isVip) {
g_form.addInfoMessage('VIP caller. Manager: ' + g_scratchpad.managerName);
}
}getRowCount() with GlideAggregate.midgetRowCount() is a GlideRecord method: you run a normal query, which retrieves the matching records, and then ask how many came back. That's fine if you're going to loop over the records anyway, but wasteful if all you need is the number.
GlideAggregate extends GlideRecord and pushes the aggregation to the database: COUNT, SUM, MIN, MAX, AVG and a few others. You add the aggregate before query(), then read it with getAggregate(). Pass a field name, or call groupBy(), to get one row per group, like a SQL GROUP BY. addHaving() filters on the aggregate, and orderByAggregate() sorts by it.
So the rule of thumb from the docs: if you only need a count, use GlideAggregate; use getRowCount() only when you also need the records. Note that getAggregate() returns a string, so convert it before doing arithmetic.
// Count active incidents per category
var ga = new GlideAggregate('incident');
ga.addActiveQuery();
ga.addAggregate('COUNT', 'category');
ga.orderByAggregate('COUNT', 'category');
ga.query();
while (ga.next()) {
gs.info(ga.getValue('category') + ': ' + ga.getAggregate('COUNT', 'category'));
}current.update() in a business rule, and what should you do instead?midcurrent.update() saves the record again, which triggers the insert/update business rules on that table again, which may call update() again. At best you get a double save with duplicate audit entries and notifications; at worst a recursion that the platform has to detect and stop. The docs say it's never necessary.
What to do instead:
current. Changes are written automatically once all before rules finish.In the rare case where you must update the current record from an after or async rule, setWorkflow(false) before update() suppresses business rules, but it also skips engines, notifications and audit-driven logic, so treat it as a last resort and document why.
// Before update rule: just set the field, no update() needed
(function executeRule(current, previous) {
if (current.priority.changesTo(1)) {
current.setValue('u_escalated', true);
}
})(current, previous);
// After update rule: update a RELATED record, not current
(function executeRule(current, previous) {
var prb = current.problem_id.getRefRecord();
if (prb.isValidRecord()) {
prb.work_notes = 'Linked incident ' + current.getValue('number') + ' was escalated';
prb.update();
}
})(current, previous);Tables are defined in sys_db_object and their columns in the dictionary, sys_dictionary, which stores each field's type, length, default value, reference target and attributes. Every record has system fields like sys_id (a 32-character GUID), sys_created_on and sys_updated_by.
A table can extend another, and only at creation time. The child inherits all the parent's fields and adds its own; sys_class_name tells you which class a record belongs to. Custom global tables and fields get a u_ prefix; scoped ones get the app's x_ prefix.
Task is the key base table. Incident, Problem, Change Request, Requested Item, Catalog Task and many others extend it, so they share fields like number, state, assignment group, assigned to, priority, work notes and comments, plus features built on Task: approvals, SLAs, assignment rules and the activity stream. That's why you extend Task for any new work-tracking app.
When a child needs different behaviour for an inherited field, such as a different default or reference qualifier, you use a dictionary override rather than changing the parent.
Likely follow-up: What would you gain by extending Task for a new custom app?
A reference field stores the sys_id of a record in another table, like a foreign key, and shows that record's display value. caller_id on Incident references sys_user.
Dot-walking follows references with dots to reach fields on related records: current.caller_id.manager.email. You can use it:
caller_id.vip=trueOn the client you can't dot-walk with g_form: g_form.getValue('caller_id') only gives you the sys_id. Use a display rule with g_scratchpad, GlideAjax, or, less ideally, g_form.getReference().
Two tips: dot-walked values are GlideElement objects, so call toString() or getDisplayValue() when you need a string; and each hop can mean another database lookup, so avoid long dot-walks inside big loops.
var inc = new GlideRecord('incident');
if (inc.get('number', 'INC0010001')) {
gs.info(inc.caller_id.getDisplayValue()); // caller's name
gs.info(inc.caller_id.manager.email.toString()); // dot-walk two levels
var caller = inc.caller_id.getRefRecord(); // full sys_user GlideRecord
gs.info(caller.getValue('department'));
}Submitting a cart creates a three-level structure:
sc_request, numbered REQ...): the overall order, like a shopping-cart receipt. It holds who requested it, for whom, and the overall approval and state.sc_req_item, RITM...): one per catalog item in the order. Each RITM carries the variable answers the user filled in and runs that item's fulfillment process, a flow or a legacy workflow.sc_task, SCTASK...): the actual work assigned to fulfillment groups, created by the RITM's flow. One RITM can have several tasks, in sequence or in parallel.All three extend Task, so they get assignment, SLAs, approvals and the activity stream. On the server, you read variables from the RITM with current.variables.<variable_name>. Interviewers like to ask where approvals sit: typically on the RITM for item-specific approval (for example the user's manager), sometimes on the REQ for the whole order.
A record producer is a special catalog item that creates a record in a target table, typically a task table like Incident or an HR case, instead of a Request and Requested Item. It gives end users a friendly catalog-style form while producing a normal record behind it.
Ways to populate the target record:
caller_id, and it maps automatically.current is the record being created and producer holds the user's answers, e.g. producer.issue_type.producer.redirect in the platform UI or producer.portal_redirect in Service Portal.Don't call current.update() or setAbortAction() in the script; the platform inserts the record for you. And the docs say not to use record producers to create Requested Items: if the user is ordering something that needs fulfillment, use a real catalog item so the request process runs.
// Record producer script (target table: incident)
current.short_description = 'Laptop issue: ' + producer.issue_type.getDisplayValue();
current.urgency = producer.is_blocking == 'true' ? 1 : 3;
current.assignment_group.setDisplayValue('Service Desk');
producer.portal_redirect = 'sp?id=ticket&table=incident&sys_id=' + current.sys_id;A catalog item is something users can order from the Service Catalog, like a laptop or software access. It has a name, description, category, pricing, a fulfillment flow and, most importantly, variables: the questions shown on the order form, such as select boxes, reference fields, checkboxes or dates.
A variable set is a reusable group of variables you attach to many items. The classic example is a "Requested for" block used by dozens of items; change it once and every item picks up the change.
Variable sets come in two kinds:
Behaviour on the order form comes from catalog client scripts and catalog UI policies, which work like their form counterparts but target variables. They can be defined on an item or on a variable set.
An import has three parts:
sys_user or cmdb_ci_server, through field maps and optional scripts.Coalesce decides insert versus update. Mark one or more field maps as coalesce: if a target record matches on all coalesce fields, it's updated; if not, a new record is inserted. With no coalesce, every row is inserted, which is the classic cause of duplicates. Coalesce on fields that are genuinely unique, like email or employee number; if several target records match, only the first is updated.
For custom logic, transform event scripts run at onStart, onBefore, onAfter and onComplete, among others, with source, target, map, log and action. Setting ignore = true in onBefore skips that row.
// onBefore transform script: update existing records only
(function runTransformScript(source, map, log, target) {
if (action == 'insert') {
ignore = true; // skip rows with no coalesce match
log.info('Skipped unmatched row: ' + source.u_email);
}
})(source, map, log, target);Likely follow-up: How would you coalesce on email OR username? · What does "Run business rules" on a transform map control?
Update sets track configuration on tables that have the update_synch attribute: forms, fields, business rules, client scripts, ACLs, catalog item definitions and so on. Things that commonly catch people out:
To move data, use Export XML / Import XML for a few records (keeps sys_ids), or import sets, including instance-to-instance imports, for larger volumes. Don't add the update_synch attribute yourself: the docs warn it can cause serious performance problems.
Flow Designer is ServiceNow's low-code automation tool; in recent releases it lives inside Workflow Studio. A flow has a trigger and a series of actions:
The legacy Workflow editor is the older drag-and-drop canvas of activities and transitions, attached to a table. It still runs in many instances, and you'll maintain existing workflows, but the docs now point to Workflow Studio for new process automation, and even suggest it over business rules. Flows are easier to read, reuse and upgrade, and the execution details page makes debugging much simpler.
Likely follow-up: When would you still write a business rule instead of a flow?
A UI action adds a button, link or menu item to forms and lists: form button, form context menu, form link, list button, list choice, list context menu and so on. It has a Condition (evaluated on the server) and optional required roles that control when it appears, and a script that runs when it's clicked.
By default the script runs on the server, with current available; action.setRedirectURL() controls where the user lands. With the Client box checked, the Onclick field names a function that runs in the browser with g_form available.
To do both, use the classic pattern from base-system actions: the client function validates, then calls gsftSubmit(null, g_form.getFormElement(), '<action name>') to submit the form back to the server with that action name. On the server there is no window object, so a typeof window == 'undefined' check calls the server function. The Action name must match the one passed to gsftSubmit. Workspaces use a separate Workspace client script.
// UI action: Client = true, Onclick = resolveClient(), Action name = resolve_inc
function resolveClient() {
if (!g_form.getValue('close_notes')) {
g_form.showFieldMsg('close_notes', 'Close notes are required', 'error');
return false;
}
gsftSubmit(null, g_form.getFormElement(), 'resolve_inc'); // re-submit to server
}
if (typeof window == 'undefined') resolveServer();
function resolveServer() {
current.state = 6;
current.update();
action.setRedirectURL(current);
}The Configuration Management Database stores the things you manage, configuration items (CIs), and how they relate, so you can understand the impact of an incident or change.
cmdb_ci: for example cmdb_ci_hardware extends to cmdb_ci_computer, which extends to cmdb_ci_server and then to cmdb_ci_linux_server. Application and service classes sit alongside them. The CI Class Manager is where you view and extend the hierarchy.cmdb_rel_ci, each with a parent, a child and a type such as Depends on::Used by or Runs on::Runs. They let you trace from a business service down to the servers it relies on.A healthy CMDB is what makes impact analysis, change risk and event correlation trustworthy, and CMDB Health dashboards track completeness, correctness and compliance.
Likely follow-up: What is CSDM and why does it matter?
ITIL is a widely used framework of best practices for IT service management: how to deliver, support and improve IT services. It isn't software; ServiceNow ITSM implements ITIL-aligned processes on the platform.
The core ones interviewers expect:
Useful vocabulary: a service request is a standard request for something (not a failure), while an incident is something broken. Keeping them separate keeps metrics honest and routes work correctly. In ITIL 4 these are called practices rather than processes.
By default, users don't set Priority directly. They set Impact (how much of the business is affected) and Urgency (how quickly it must be resolved), and priority lookup rules, a data lookup, calculate Priority. For example:
The Priority field is read-only through a UI policy; admins change the matrix in the priority lookup rules rather than letting people pick priority freely. Priority then drives SLAs, assignment and notifications.
The base incident states are New, In Progress, On Hold (with a reason such as awaiting caller), Resolved, Closed and Canceled. Resolving requires resolution information such as a close code and notes; resolved incidents typically auto-close after a configurable period, and a caller can reopen one while it's resolved. In scripts, remember that states are stored as numbers, for example Resolved is 6 and Closed is 7.
A scoped application gets its own namespace, like x_acme_book_rooms (x_, the instance's company code, then the app ID). Its tables, script includes and other files carry that prefix, and the platform restricts what other scopes can touch.
Protections you can configure:
gs.info() instead of gs.log(), and no getXMLWait().Global is the legacy shared space where most base-system ITSM configuration lives; global code can reach almost anything, which is exactly the risk. Scoped apps give isolation, clearer ownership, safer upgrades and clean packaging through source control or the app repository, and new scoped apps default to the ES2021 JavaScript mode.
Likely follow-up: How do you call a global script include from a scoped app?
An event is a record in the event queue (sysevent) saying "something happened". You register the name in the Event Registry, then queue it from a script with gs.eventQueue(name, record, parm1, parm2). Scheduled jobs process the queue and hand each event to whatever listens:
A notification can instead be sent when a record is inserted or updated (with conditions), or be triggered from a flow's notification step. Each defines who receives it, what it contains (subject, body, email scripts for dynamic content), and a weight: when duplicate notifications target the same record and recipients, only the highest weight is sent; weight 0 is always sent.
Why use events? They decouple when something happens from what reacts, and processing is asynchronous, so the user's transaction isn't slowed.
// After update business rule on incident
(function executeRule(current, previous) {
if (current.priority.changesTo(1)) {
// name, record, parm1, parm2
gs.eventQueue('x_acme_ops.incident.escalated', current,
current.getValue('assignment_group'), gs.getUserName());
}
})(current, previous);Likely follow-up: A user says they never got an email. How do you troubleshoot it?
gs, and which GlideSystem methods do you use most?easygs is the GlideSystem object, available in server-side scripts. The methods you reach for most:
getUserID(), getUserName(), getUserDisplayName(), getUser() (a GlideUser object), hasRole(), getSession(), isInteractive()gs.info(), gs.warn(), gs.error(), gs.debug(), which accept {0}-style parameters. gs.log() exists only in global scope.addInfoMessage(), addErrorMessage(); getMessage() for translated textgetProperty(name, default), setProperty()eventQueue() and eventQueueScheduled()beginningOfToday(), daysAgo() and friends, often used in queriesnil(), generateGUID(), tableExists()Like g_user.hasRole(), gs.hasRole() returns true for admin users. And session messages don't make sense in async code: addInfoMessage() isn't supported in async business rules.
var me = gs.getUserID();
if (gs.hasRole('itil')) {
gs.info('Agent {0} opened the record', gs.getUserName());
}
var limit = parseInt(gs.getProperty('x_acme_app.max_rows', '100'), 10);
gs.addInfoMessage(gs.getMessage('Record saved'));
gs.eventQueue('x_acme_app.record.saved', current, me, '');sys_user) are people or integration accounts.sys_user_group) are teams, such as a Service Desk or a Network group. They're used for assignment, approvals and notifications.sys_user_role) grant permissions, like itil for fulfillers or catalog_admin. ACLs, modules, UI actions and scripts check roles.Roles can contain other roles, so granting one role can bring several others. A user gets roles directly or, preferably, by inheriting them from group membership.
Best practice: assign roles to groups, not individual users. When someone joins the Network team they're added to the group and get the right access automatically; when they leave the group it's removed. That's easier to audit and matches how HR and identity systems provision people. Keep admin to a small set of people, and remember that some roles, like security_admin, are elevated: an admin must explicitly elevate to use them. Also, roles that grant fulfiller rights often have licensing implications, so check before handing them out.
The out-of-box option is the Table API: /api/now/table/{tableName}. GET lists or reads a record by sys_id, POST creates, PUT or PATCH updates, DELETE deletes. Useful parameters include sysparm_query (an encoded query), sysparm_fields, sysparm_limit, sysparm_offset and sysparm_display_value. The REST API Explorer lets you try calls and generates sample code. Requests run as the authenticated user, so ACLs and data policies apply.
For inbound data that needs mapping or validation, the Import Set API posts rows into a staging table so a transform map handles coalescing.
Build a Scripted REST API when you want a clean contract that doesn't expose your table structure: combining several tables, business validation, custom status codes, or versioning. It lives at /api/<namespace>/<version>/<api_id>/<relative_path>, each resource has a script that receives request and response, and you can require ACLs on the API or individual resources.
request and response give you?midEach resource script is an IIFE receiving a RESTAPIRequest and a RESTAPIResponse.
From request you get:
request.body.data: the parsed request body; dataString gives the raw textrequest.pathParams: values from the relative path, like {id} in /incident/{id}request.queryParams: query-string valuesrequest.getHeader(name), plus uri and urlWith response you call setStatus() and setBody(), which serializes an object to the requested format (JSON or XML), or simply return an object from the script. You can also set headers.
Good practice: validate input and return proper status codes (400 for bad input, 201 for created, 404 when a record isn't found), keep business logic in a script include so the REST script stays thin, version the API so you can change it without breaking consumers, and rely on authentication plus ACLs rather than hand-rolled checks.
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var body = request.body.data;
if (!body.short_description) {
response.setStatus(400);
return { error: 'short_description is required' };
}
var inc = new GlideRecord('incident');
inc.initialize();
inc.setValue('short_description', body.short_description);
inc.setValue('caller_id', gs.getUserID());
var id = inc.insert();
response.setStatus(201);
response.setBody({ sys_id: id, number: inc.getValue('number') });
})(request, response);An SLA definition (contract_sla) describes a target: the table (for example Incident), the duration (say 4 hours), the schedule (such as 8-5 weekdays, which decides business time) and a set of conditions. The type can be SLA (with a customer), OLA (between internal teams) or underpinning contract (with a vendor).
When a task matches, the SLA engine attaches a task SLA (task_sla) record that tracks timing: start time, breach time, actual and business elapsed time and percentage, time left, and whether it has breached.
Conditions:
Task SLA stages are In progress, Paused, Completed and Cancelled. Notifications and escalations at thresholds such as 50% or 75% are usually driven by the SLA's flow. A common gotcha: badly overlapping start and pause conditions leave the SLA paused forever or cancelled.
An order guide lets a user fill in one form and submit a single request that generates several requested items. The standard example is new-hire onboarding: enter the starter's department, location and role once, and the guide orders a laptop, phone, badge and software access.
How it works:
A script field can also add or remove items with guide.add() and guide.remove(). Order guides can even run automatically from a flow or server script, without the user submitting, which suits HR-driven onboarding. The trade-off versus one big catalog item is that each item keeps its own fulfillment process.
Per the docs, when a record is inserted or updated:
Async rules run later, from a scheduled job created during the transaction. Display rules run only when a form loads, and query rules run before queries.
So Order is more than tie-breaking. If you need a value that assignment rules or a data lookup will set, your before rule must be order 1000 or above. If you want to adjust something before the data policy engine checks mandatory fields, keep it below 1000. Client-side scripts always run before the form is submitted, so they come before all of this.
inc.sys_id isn't a string. Dot-walking a field on a GlideRecord returns a GlideElement object, and the GlideRecord reuses its element objects as next() moves from row to row. So you push the same object reference on every iteration, and when you read the array afterwards every entry reflects the last row the record pointed at.
The fix is to push a primitive string:
ids.push(inc.getValue('sys_id'))ids.push(inc.getUniqueValue())ids.push(inc.sys_id.toString()) or String(inc.sys_id)The same bug shows up with any field, for example building an object like { number: inc.number } inside a loop. That's one reason experienced developers use getValue() by default. Remember that getValue() returns null for empty fields and "0" or "1" for booleans, so compare accordingly.
var ids = [];
var inc = new GlideRecord('incident');
inc.addQuery('active', true);
inc.addQuery('priority', 1);
inc.query();
while (inc.next()) {
ids.push(inc.sys_id); // bug
}
gs.info(ids.join(','));Use RESTMessageV2 from the sn_ws namespace. Two styles:
${title}. Scripts call new sn_ws.RESTMessageV2('Ticketing API', 'Create ticket') and fill variables with setStringParameter() or setStringParameterNoEscape().new sn_ws.RESTMessageV2() with setEndpoint(), setHttpMethod(), setRequestHeader(), setRequestBody().execute() returns a RESTResponseV2 with getStatusCode(), getBody() and haveError(). Authentication goes through setBasicAuth() or, better, an authentication profile such as OAuth 2.0 via setAuthenticationProfile(). For systems inside the corporate network, setMIDServer() routes the call through a MID Server.
Where to run it: never in a before rule that blocks the user's save. Use an async business rule, an event with a script action, or a flow with an IntegrationHub REST step, and handle timeouts, errors and retries explicitly.
var rm = new sn_ws.RESTMessageV2('Ticketing API', 'Create ticket');
rm.setStringParameterNoEscape('title', current.getValue('short_description'));
rm.setRequestHeader('Content-Type', 'application/json');
rm.setHttpTimeout(10000);
try {
var resp = rm.execute();
if (resp.getStatusCode() == 201) {
gs.info('Created external ticket ' + JSON.parse(resp.getBody()).id);
} else {
gs.error('Ticketing API failed: ' + resp.getStatusCode() + ' ' + resp.getBody());
}
} catch (e) {
gs.error('Ticketing API error: ' + e.message);
}The classic tool is a Scheduled Script Execution (a scheduled job, sysauto_script), under System Definition > Scheduled Jobs. You pick when it runs (daily, weekly, monthly, periodically, once or on demand), a Run as user and optionally a condition script, then write the server-side script. Behind the scenes the scheduler creates sys_trigger entries.
Alternatives: a flow with a scheduled trigger for low-code logic, scheduled data imports for integrations, and scheduled reports.
Things to watch:
gs.addInfoMessage(), no form, no user session context, and the Script Debugger can't pause it. Log with gs.info().setLimit() and indexed queries, rather than one giant transaction.// Scheduled Script Execution, runs daily at 02:00 as a system user
(function () {
var stale = new GlideRecord('incident');
stale.addEncodedQuery('state=6^resolved_at<javascript:gs.daysAgoStart(7)');
stale.setLimit(500); // process in batches
stale.query();
while (stale.next()) {
stale.setValue('state', 7); // Closed
stale.update();
}
})();ATF lets you build automated tests on the instance itself, without external tools.
In a process: build tests for critical flows (incident creation, catalog ordering, approvals), and run the suite after every upgrade and before promoting update sets.
Design tips from the docs: create your own test users and records rather than relying on existing data, and don't run ATF in production; a system property disables execution by default.
A portal is made of pages, pages have containers, rows and columns, and columns hold widget instances. A widget has:
{{c.data.items}}var c = thisdata object, reads instance options and, on later calls, the input object sent from the clientCommunication: the server script runs when the widget loads and data arrives as c.data. The client calls c.server.update() to send c.data back as input and rerun the server script, or c.server.get({ action: ... }) to send a custom payload. $sp gives server helpers like $sp.getParameter(); spUtil gives client helpers like recordWatch() and addInfoMessage().
Best practices: clone base-system widgets instead of editing them, or embed them; always setLimit() queries; avoid unfiltered record watchers and auto-refresh loops.
(function () { // server script
data.items = [];
var inc = new GlideRecord('incident');
inc.addQuery('caller_id', gs.getUserID());
inc.addActiveQuery();
inc.setLimit(options.limit || 5);
inc.query();
while (inc.next()) data.items.push({ number: inc.getValue('number'), sys_id: inc.getUniqueValue() });
})();
// Client controller
api.controller = function () {
var c = this;
c.refresh = function () { c.server.update(); }; // reruns the server script with input
};Server side
script_debugger role and only pauses interactive transactions (a user's form submit, a UI action), not scheduled jobs or async work.gs.info(), gs.warn(), gs.error(), gs.debug() (gs.log() in global), viewed in System Logs (syslog).Client side
console.log or jslog(), the Debug UI Policies option and, in the classic UI, the Field Watcher, which shows what changed a field.My approach: reproduce, ideally while impersonating the affected user, narrow down with the right debug module, then confirm the fix in a sub-production instance.
A Management, Instrumentation and Discovery (MID) Server is a Java application you install on a Windows or Linux host inside your network. It lets a cloud instance reach systems it can't reach directly.
How it talks to the instance: the MID Server always initiates the connection outbound over HTTPS, so you don't open inbound firewall ports. Work is exchanged through the ECC Queue (ecc_queue): the instance writes output records for a MID Server, the MID Server picks them up (it's notified through the message bus and also polls periodically), does the work, and writes input records with the results.
Common uses:
setMIDServer(), IntegrationHub PowerShell and SSH stepsOperational points: validate a new MID Server before use, run several for capacity and failover (clusters), give each only the capabilities and IP ranges it needs, and note that MID Servers upgrade automatically to match the instance.
IntegrationHub extends Flow Designer (Workflow Studio) with integration capabilities, so integrations are built as flow actions rather than scripts.
Why interviewers care: it's the low-code, upgrade-friendly alternative to hand-written RESTMessageV2 scripts, and it gives you reusable, testable, observable integrations.
Server side
setLimit(), avoid heavy CONTAINS and long OR chains on big tables.getRowCount().addJoinQuery() or a single encoded query.current.update() in business rules.Client side
g_scratchpad from a display rule for load-time data, async GlideAjax otherwise; never getXMLWait(), client GlideRecord or unnecessary getReference().g_form.setValue() for reference fields.Diagnose, don't guess: Slow Queries (sys_query_pattern) with the Index Suggestion Engine, transaction logs, slow scripts and slow transactions logs, and session debugging for SQL and business rules.
Likely follow-up: A form takes eight seconds to load. How do you find out why?
A before query rule runs before the platform queries a table, and it can add conditions to current so that some rows are never returned. The base system's rule that limits which incidents users without the itil role can see is a well-known example.
Compared with read ACLs:
The docs also warn to use them sparingly, keep conditions on indexed fields and avoid lots of OR clauses. Security still belongs in ACLs; use query rules for data segregation that ACLs handle badly, not as a replacement for domain separation.
// Before, Query checked, table: x_acme_vendor_case
(function executeRule(current, previous) {
if (gs.hasRole('x_acme.case_admin') || !gs.isInteractive()) {
return;
}
// vendors only ever see their own company's cases
current.addQuery('company', gs.getUser().getCompanyID());
})(current, previous);No, not automatically. Plain GlideRecord in business rules, script includes and scheduled jobs runs with system-level access: it reads and writes regardless of the user's ACLs. That's intentional, so platform logic can work, but it's a security risk when a script returns data to a user, for example through a client-callable script include, a Scripted REST API or a portal widget.
Options:
next(), and fields the user can't write are set to null on save. Query ACLs are opt-in.canRead(), canWrite(), canCreate(), canDelete() on the record, or on a field element.addUserEncodedQuery() or addUserQuery() enforce query access for user-supplied filters.Also remember that client-callable script includes are protected by their own execute ACLs, and that Scripted REST API scripts run as the authenticated user: to be sure the data you return respects that user's access, query with GlideRecordSecure or explicit checks.
// Runs as the user and honours read/write ACLs
var inc = new GlideRecordSecure('incident');
inc.addActiveQuery();
inc.setLimit(20);
inc.query();
while (inc.next()) {
gs.info(inc.getValue('number')); // only records the user may read
}
// Plain GlideRecord: check explicitly
var gr = new GlideRecord('incident');
if (gr.get(sysId) && gr.canRead()) { /* ... */ }When you customize a base-system record, like a business rule or form layout, a customer update record is written to sys_update_xml. During an upgrade, if ServiceNow ships a new version of a record you've customized, the upgrade skips it to protect your change and logs it as a skipped record. Customized records the upgrade didn't touch need no action.
After the upgrade, work through Skipped Changes to Review in Upgrade History, compare versions with the diff tool and choose:
Then retest. To make upgrades cheap: avoid modifying base records (copy them, or use extension points), set Replace on upgrade on a customer update you want the next upgrade to overwrite, such as a temporary fix, and upgrade dev first, run ATF regression, then test, then prod.
I'd work from the user's actual context rather than guessing:
Then fix the root cause, typically a group membership or ACL condition, rather than granting a broader role.
I follow the email's path step by step:
sysevent) to see if the event was queued and processed. For insert/update notifications, check the conditions and advanced condition against the actual record values.The email record and its logs usually pinpoint the break quickly.
Preview compares each incoming customer update with the target instance. Typical problems:
You can't commit until every problem is resolved, and each accept or skip is a real decision about which version wins.
For big releases, use batched update sets: a parent with child sets, previewed and committed together, so dependencies across sets resolve correctly. If a commit goes wrong, you can back out an update set: it reverses its record and dictionary changes, but can itself cause data loss, so sometimes a corrective update set is safer.
I decide based on timing, cost and who maintains it:
current. Keep it fast.previous isn't available and ordering between async rules isn't guaranteed.eventQueueScheduled().Rule of thumb: data integrity checks in before rules, anything slow off the user's transaction, and business processes in flows.
I'd cover four areas:
Outbound (ServiceNow to external): trigger from an async business rule, event or flow when relevant fields change, never inside the user's save. Call via an IntegrationHub spoke or RESTMessageV2 with an OAuth authentication profile and a MID Server if the target is internal. Store the external ID on the incident, for example in correlation_id.
Inbound (external to ServiceNow): expose a Scripted REST API or use the Import Set API so a transform map coalesces on the correlation ID; that makes retries idempotent. Authenticate with a dedicated integration user holding least-privilege roles.
Avoiding loops: an update from the external tool shouldn't echo straight back. Skip outbound calls when the update came from the integration user, or compare a sync timestamp or hash.
Reliability: handle timeouts and 4xx/5xx responses, retry with backoff, log requests and responses (without secrets), alert on failures, and make field mappings and state/priority translations configurable, for example with system properties or a mapping table. Test end to end in sub-production with realistic volumes.
Next Experience is ServiceNow's modern UI framework and look: the unified navigation, themes, and workspaces such as Service Operations Workspace, built from Next Experience components (web components) rather than the classic forms and lists.
UI Builder is the low-code tool for building and customizing those pages. Key ideas:
How it fits with the rest: the classic UI (forms, lists, client scripts, UI policies) still runs much of the platform, and many form-level rules also apply in workspaces. Service Portal (AngularJS widgets) remains the tool for portals like Employee Center; the docs note UI Builder isn't yet for building base-system service portals. New agent-facing experiences are generally built as workspaces with UI Builder.
The IRE is the gatekeeper that Discovery, Service Mapping, connectors and import sets route CI data through.
In scripts, you send a JSON payload of items (and relations) to sn_cmdb.IdentificationEngine.createOrUpdateCI(source, input), where the source must be one of the discovery_source choice values; there's an equivalent REST API. Writing CIs with plain GlideRecord bypasses all of this and is the classic cause of duplicate CIs.
var payload = {
items: [{
className: 'cmdb_ci_linux_server',
values: { name: 'app-node-01', serial_number: 'SN-12345', ip_address: '10.0.4.21' }
}]
};
var result = sn_cmdb.IdentificationEngine.createOrUpdateCI(
'ServiceNow', JSON.stringify(payload));
gs.info(result); // JSON describing insert/update per itemA clone copies the database of a source instance, usually production, over a target sub-production instance such as dev or test, so developers and testers work with realistic configuration and data.
Because it overwrites the target, you configure:
The classic mistakes the clone is meant to guard against: a test instance emailing real customers, or calling production integrations. Also, commit or back up in-flight update sets on the target first; anything not preserved is gone. Some teams chain clones: prod to test once, then test to the other sub-prod instances.
let, const, arrow functions and classes in ServiceNow server scripts?hardIt depends on the JavaScript mode of the application or script. The server-side engine has three modes:
let/const, arrow functions, template literals, destructuring, default and rest parameters, for...of, classes, Map/Set and optional chaining. It's the default for new scoped applications.For scripts in ES5 or Compatibility applications, including global ones, you can turn on ES12 mode per script. Before switching an existing app's mode, review the documented behaviour differences, because legacy scripts can behave differently.
This applies to server-side scripts. Client-side code runs in the user's browser, so it's constrained by the browsers you support, and many teams still write ES5-style client scripts for consistency with the base system.
// Works when the script runs in ECMAScript 2021 (ES12) mode
const openCount = (callerId) => {
const ga = new GlideAggregate('incident');
ga.addQuery('caller_id', callerId);
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.query();
return ga.next() ? Number(ga.getAggregate('COUNT')) : 0;
};
gs.info(`Open incidents: ${openCount(gs.getUserID())}`);No questions match that filter.
Prefer multiple choice? All 20 ServiceNow MCQs with answers →