Skip to content
ServiceNow 17 min read 12 sections

GlideRecord in ServiceNow, explained with examples

Almost every ServiceNow developer interview reaches the same point: the panel asks you to read or write a few records with script, and you have about two minutes to show you can do it without help. The questions come from the scripting and performance rounds, and they are asked to admins, developers

RL

RizeX Labs

RizeX Labs

Published

Last updated

Related programme

ServiceNow Training

ITSM, CMDB, scripting and Flow Designer taught on a live instance, with a build project and interview practice before profiles reach the hiring partners.

See the curriculum

3000+ learners · 270+ hiring partners

Almost every ServiceNow developer interview reaches the same point: the panel asks you to read or write a few records with script, and you have about two minutes to show you can do it without help. The questions come from the scripting and performance rounds, and they are asked to admins, developers and consultants alike. This tutorial walks through the GlideRecord API in the order a real script uses it, with code you can paste into a background script and run. It also covers the three query-cursor methods, ACL behaviour and the query patterns that get flagged in a code review.

What GlideRecord is and where it runs

GlideRecord is the ServiceNow API for working with records in a table. You give it a table name, add conditions, run the query, then step through the results one row at a time. It replaces writing SQL. The platform turns your conditions into a database query and hands you back an object that points at one record at a time.

The important part is where it runs. GlideRecord is a server-side API. It executes inside the application node, on the same server that holds the database connection, in business rules, script includes, scheduled jobs, UI actions with the client checkbox unticked, Flow Designer script steps, transform map scripts and background scripts.

There is an older client-side GlideRecord available in some client contexts, but it is discouraged. It sends a synchronous request from the browser and freezes the form while it waits. When a client script needs server data, use GlideAjax to call a client-callable script include, and let the query run on the server where it belongs.

Our trainer sees this gap every batch: candidates know the syntax but not the execution. When it runs, where it runs and what data it can reach is the part that gets tested, and client-side versus server-side is the ServiceNow version of that question. If you say "GlideRecord fetches records", the follow-up is "from where, and who is running it".

// Runs on the server. Log the number of every active P1 incident.
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.addQuery('priority', '1');
gr.query();
while (gr.next()) {
    gs.info(gr.getValue('number') + ' | ' + gr.getValue('short_description'));
}

query, next and the standard loop

Four steps, always in this order.

  1. new GlideRecord('table_name') creates the object. Nothing has touched the database yet.
  2. addQuery() or addEncodedQuery() builds the conditions. Still nothing has touched the database.
  3. query() sends the query and opens a cursor.
  4. next() moves the cursor forward one record and returns true while a record is available.

Forgetting query() is the most common beginner error. The loop simply never runs, and there is no error message to tell you why.

When you expect a single record, get() is cleaner than a loop. It runs the query and positions the cursor in one call, and it returns a boolean.

// By sys_id
var inc = new GlideRecord('incident');
if (inc.get('9d385017c611228701d22104cc95c371')) {
    gs.info(inc.getValue('number'));
}

// By any other field
var inc2 = new GlideRecord('incident');
if (inc2.get('number', 'INC0010023')) {
    gs.info(inc2.getValue('short_description'));
}

Always wrap get() in an if. If the record does not exist, the cursor sits on nothing and every read after that returns empty values, which produces a silent wrong result instead of an error.

addQuery, addEncodedQuery and the operators

addQuery() takes either two arguments or three. With two, the operator is assumed to be equals. With three, you name the operator.

var gr = new GlideRecord('incident');
gr.addQuery('active', true);                          // equals
gr.addQuery('priority', 'IN', '1,2');                 // in a list
gr.addQuery('short_description', 'CONTAINS', 'vpn');  // substring match
gr.addQuery('sys_created_on', '>', gs.beginningOfLastMonth());
gr.addNotNullQuery('assignment_group');
gr.query();

Conditions added this way are joined with AND. For OR, you attach an or-condition to a query you have already added.

var gr = new GlideRecord('incident');
var qc = gr.addQuery('priority', '1');
qc.addOrCondition('priority', '2');   // priority is 1 OR 2
gr.addQuery('active', true);          // AND active is true
gr.query();

The operators you are expected to name in an interview:

Operator Meaning
= equals (the default)
!= not equals
> < >= <= greater or less than, works on numbers and dates
IN value is in a comma separated list
NOT IN value is not in the list
STARTSWITH field starts with the value
ENDSWITH field ends with the value
CONTAINS field contains the value anywhere
DOES NOT CONTAIN field does not contain the value
INSTANCEOF record belongs to a class, used on extended tables such as the CMDB

Alongside these, addNullQuery('field') and addNotNullQuery('field') handle empty checks, and addActiveQuery() is shorthand for active being true.

addEncodedQuery

An encoded query is the whole filter written as one string. It is the same string ServiceNow puts in the URL after sysparm_query, and the same string you get from a list filter by right-clicking the breadcrumb and choosing to copy the query.

var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^priority=1^ORpriority=2^assignment_groupISNOTEMPTY^ORDERBYDESCsys_created_on');
gr.query();

The syntax:

  • ^ joins two conditions with AND
  • ^OR joins with OR
  • ^NQ starts a new query, giving you OR between whole groups of conditions
  • ISEMPTY and ISNOTEMPTY test for empty fields
  • ORDERBY and ORDERBYDESC sort the result

Build the filter on the list view first, copy the query, and paste it into your script. That is faster than assembling twenty addQuery lines and much harder to get wrong. You can mix both styles on the same object, and the conditions combine with AND.

getValue versus dot-walking, and why getValue is safer

You can read a field two ways.

var a = gr.short_description;              // a GlideElement object
var b = gr.getValue('short_description');  // a string, or null when empty

gr.short_description does not return text. It returns a GlideElement, an object that knows how to print itself as text. Most of the time it behaves as if it were a string, which is exactly why it catches people out.

Three failures to know:

Empty checks stop working. A GlideElement object is always truthy, even when the field is empty. So this is wrong:

if (gr.short_description) {          // always true, empty or not
    // ...
}
if (gr.getValue('short_description')) {   // null when empty, so this is correct
    // ...
}

Stored values change under you. A GlideElement stays bound to the cursor. Push it into an array inside a loop and every entry in the array ends up showing the last record's value.

var titles = [];
var gr = new GlideRecord('incident');
gr.setLimit(5);
gr.query();
while (gr.next()) {
    titles.push(gr.getValue('short_description'));   // a plain string, safe
    // titles.push(gr.short_description);            // wrong, all entries end up the same
}

Reference fields print the display value, not the key. gr.getValue('caller_id') returns the sys_id, which is what you pass to another query or to setValue. For the readable name, ask for it explicitly with gr.getDisplayValue('caller_id').

Dot-walking still has its place for reaching a field on a referenced record, for example gr.caller_id.email. Use it knowing it costs an extra read per record, and call .toString() before you store the result. For everything on the current record, getValue() is the habit to build.

insert, update, deleteRecord and deleteMultiple

insert

var inc = new GlideRecord('incident');
inc.initialize();
inc.setValue('short_description', 'Printer on 3rd floor not responding');
inc.setValue('caller_id', gs.getUserID());
inc.setValue('category', 'hardware');
inc.setValue('urgency', '2');
var sysId = inc.insert();
gs.info('Created ' + sysId);

initialize() prepares an empty record with the table's default values applied. insert() returns the sys_id of the new record, or null if the insert failed.

update

var gr = new GlideRecord('incident');
if (gr.get('number', 'INC0010023')) {
    gr.setValue('state', '6');
    gr.setValue('close_notes', 'Resolved after cable replacement');
    gr.update();
}

update() writes the record the cursor is currently on and returns its sys_id. Inside a while (gr.next()) loop, call update() once per iteration, before you move on.

deleteRecord and deleteMultiple

deleteRecord() deletes the single record the cursor is on and returns a boolean.

var gr = new GlideRecord('u_temp_import_log');
gr.addQuery('u_processed', true);
gr.query();
while (gr.next()) {
    gr.deleteRecord();
}

deleteMultiple() deletes everything matching the query in one call, with no loop.

var gr = new GlideRecord('u_temp_import_log');
gr.addQuery('u_processed', true);
gr.deleteMultiple();

Two cautions worth saying out loud in an interview. deleteMultiple() does not honour setLimit() or chooseWindow(), so a mistake in the query deletes far more than you intended, and there is no undo. And on tables carrying currency or price fields, delete records one at a time in a loop instead, because those field types have related data the bulk delete does not handle.

GlideAggregate for counts

If all you want is a number, do not pull the records. GlideAggregate asks the database to do the counting and returns the total, so one row comes back instead of fifty thousand.

var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
var total = 0;
if (ga.next()) {
    total = parseInt(ga.getAggregate('COUNT'), 10);
}
gs.info('Open incidents: ' + total);

Group by a field to get a count per value:

var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT', 'category');
ga.groupBy('category');
ga.query();
while (ga.next()) {
    gs.info(ga.getValue('category') + ': ' + ga.getAggregate('COUNT', 'category'));
}

GlideAggregate also supports SUM, AVG, MIN and MAX, which is how you total a cost or effort field without reading every row.

var ga = new GlideAggregate('u_asset_cost');
ga.addAggregate('SUM', 'u_amount');
ga.groupBy('u_cost_centre');
ga.query();
while (ga.next()) {
    gs.info(ga.getValue('u_cost_centre') + ' total: ' + ga.getAggregate('SUM', 'u_amount'));
}

The comparison the panel wants: GlideRecord returns records, one row per record, and gives you access to every field. GlideAggregate returns computed numbers, one row per group, and gives you no field access at all. GlideRecord has getRowCount(), but it works by fetching the rows before it counts them, which is the slow way to get a number you could have asked the database for directly.

setLimit, chooseWindow and orderBy

orderBy('field') sorts ascending, orderByDesc('field') sorts descending, and you can call them more than once to sort on several fields in the order you call them.

setLimit(n) caps the number of records returned. It must be called before query().

// The ten most recent P1 incidents
var gr = new GlideRecord('incident');
gr.addQuery('priority', '1');
gr.orderByDesc('sys_created_on');
gr.setLimit(10);
gr.query();
while (gr.next()) {
    gs.info(gr.getValue('number'));
}

chooseWindow(first, last) returns a slice of the result set instead of the whole thing, taking records from the first index up to but not including the last. It is how you process a large table in batches from a scheduled job, and it only makes sense with an order applied, because without a sort the row positions are not stable between runs.

// Records 100 through 199 of the ordered result
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.orderBy('sys_created_on');
gr.chooseWindow(100, 200);
gr.query();
while (gr.next()) {
    // process this batch
}

Use setLimit() when you want a top-N sample. Use chooseWindow() when you are paging through a set that is too big for one transaction.

next, hasNext and _next

These three come up as a single interview question, and the answer is short.

Method What it does Moves the cursor
next() Moves to the next record and returns true if one was found Yes
hasNext() Reports whether another record is waiting No
_next() Identical to next() Yes

next() is the one you use. It is both the test and the move, which is why the whole loop fits in while (gr.next()).

hasNext() only looks ahead. It tells you whether another record exists without advancing, which is useful when you need to know if the result set is empty before you start, or when you want to behave differently on the last record.

var gr = new GlideRecord('incident');
gr.addQuery('assigned_to', gs.getUserID());
gr.query();
if (!gr.hasNext()) {
    gs.info('No incidents assigned to you');
}
while (gr.next()) {
    gs.info(gr.getValue('number'));
}

_next() is the interesting one, and the reason it exists has nothing to do with behaviour. It is a name collision escape. Field values on a GlideRecord are reachable as properties, so on a table with a column named next, writing gr.next becomes ambiguous between the method and that column. ServiceNow provides _next() as an alternative name no column can shadow. Same behaviour, different spelling, used only when the table you are querying has a field called next.

The same rule applies to _query(), the escape for a table with a column named query. If a candidate tells you _next() is faster or that it skips records, that is wrong. It moves the cursor exactly as next() does.

GlideRecordSecure and when ACLs apply

This surprises people. Server-side GlideRecord does not check ACLs. A business rule or script include running GlideRecord reads and writes whatever the query matches, regardless of what the logged-in user is allowed to see. That is deliberate, because platform code often has to read records the user cannot.

GlideRecordSecure is the same API with access control applied. The method names are identical, so you swap the constructor and nothing else changes.

var gr = new GlideRecordSecure('incident');
gr.addQuery('active', true);
gr.query();
while (gr.next()) {
    // Only records this user can read are returned.
    // Fields the user cannot read come back empty.
    gs.info(gr.getValue('number'));
}

With GlideRecordSecure, records the user has no read access to are left out of the result, fields they cannot read return empty, and a write they are not permitted to perform fails instead of going through.

Use it whenever the script runs on behalf of an end user and returns data to them, which in practice means Service Portal widgets, client-callable script includes reached through GlideAjax, and scripted REST APIs. Use plain GlideRecord for platform logic that has to see everything, such as an SLA calculation or a scheduled job. The wrong choice here is a data leak, so an interviewer will press on it.

setWorkflow, autoSysFields and setForceUpdate for data fixes

A data fix script updates existing records in bulk. Run one carelessly on a live instance and you fire thousands of business rules, send out thousands of notifications, and stamp your own name on every record you touched. Three methods control that.

setWorkflow(false) stops business rules, workflows and the notification engine from running for operations on that GlideRecord object. autoSysFields(false) stops the system fields from being updated, so sys_updated_by, sys_updated_on and sys_mod_count keep their original values and the audit trail still shows the person who genuinely last worked the record. It is a global scope method, so a scoped application will not have it available.

// Data fix: correct a mis-set category without side effects
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^category=inquiry');
gr.setWorkflow(false);      // no business rules, no notifications
gr.autoSysFields(false);    // keep the original updated by and updated on
gr.query();
gs.info('Records to fix: ' + gr.getRowCount());
while (gr.next()) {
    gr.setValue('category', 'request');
    gr.update();
}

setForceUpdate(true) does the opposite job. Normally an update where no field value changed is skipped. Setting force update tells the platform to write the record anyway, which bumps the modified count and the updated timestamp and, if you have left workflow enabled, causes business rules on update to run. That is how you re-trigger logic across a set of records after fixing a broken business rule.

var gr = new GlideRecord('sc_req_item');
gr.addEncodedQuery('active=true^stage=fulfillment');
gr.query();
while (gr.next()) {
    gr.setForceUpdate(true);
    gr.update();   // business rules run even though no field changed
}

Two habits that keep you out of trouble. Run the script as a count and a log first, with the update() line commented out, and check the number against the list view. Then run it inside an update set on a sub-production instance before it goes anywhere near production.

This is also the answer to "give me a real project example" that actually lands. Candidates who only memorised definitions go quiet at that question. Candidates who have written a data fix, checked the count, and can say which table and which field they corrected do not.

The five mistakes that make a query slow

1. Querying inside a loop. A query inside a while (gr.next()) block runs once per record. Two hundred parent records become two hundred and one queries. Query the child table once with an IN condition, or build a lookup object first, then read from it inside the loop.

// Slow: one query per incident
while (gr.next()) {
    var task = new GlideRecord('sc_task');
    task.addQuery('request_item', gr.getValue('sys_id'));
    task.query();
}

2. Counting with getRowCount() or a full loop. Reading fifty thousand records to arrive at the number fifty thousand is wasted work. Use GlideAggregate with addAggregate('COUNT').

3. No limit when you only need a few records. If you want the latest record, apply orderByDesc() and setLimit(1). Without a limit the platform prepares the full result set and you throw almost all of it away.

4. Filtering on unindexed fields. CONTAINS cannot use an index, because the database has no way to jump to a match that might sit anywhere inside the text. On a large table it becomes a full table scan. Filter first on an indexed, selective field such as state, assignment group or a date range, and let the text condition narrow what is left. STARTSWITH is the better choice when it fits, and for a custom field you filter on often, add a database index on the dictionary entry.

5. Doing heavy work in a synchronous business rule. A before or after business rule holds the user's save open until it finishes. A loop over a few thousand records there means the user watches a spinner. Move it to an async business rule or a scheduled job.

Two supporting habits: use setLimit() while developing so a mistake costs seconds instead of minutes, and read the slow query log rather than guessing.

What the interviewer is actually checking

  • Whether you know where the code runs. Server-side versus client-side, which contexts GlideRecord is available in, and why a client script should call GlideAjax instead of querying directly.
  • Whether you write a complete query. The four steps in order, query() never skipped, get() wrapped in an if, and update() called on the right record.
  • Whether you think about volume. Reaching for GlideAggregate for a count, applying setLimit(), and not putting a query inside a loop. This separates someone who has worked on a real instance from someone who has only read about the API.
  • Whether you understand security. That GlideRecord ignores ACLs on the server and GlideRecordSecure does not, and that user-facing scripts need the secure one.
  • Whether you can debug. "Your script runs but updates nothing, what do you check" is the question that sorts the room. Missing query(), a filter that matches nothing, an ACL or business rule blocking the write, or a get() that returned false and was never tested.

Candidates lose these rounds on demonstration, not knowledge. They know what GlideRecord is and cannot write eight correct lines when a laptop is pushed across the table. Type these examples into a background script on a personal developer instance, break them on purpose, and read the errors. That practice is what turns an answer into a demonstration.

If you want structured practice on ServiceNow scripting with live instance work and mock interview rounds, our ServiceNow training in Pune covers GlideRecord, business rules, script includes and integrations with real project scenarios rather than slides.