ServiceNow GlideRecord CRUD Operations: A Complete Developer Guide
GlideRecord is how server-side scripts read and change data in ServiceNow. This guide covers creating, reading, updating and deleting records, then encoded queries, OR conditions, performance, access control, and the mistakes that cause most GlideRecord bugs.
01 What is GlideRecord?
Every table in ServiceNow, from incident to a custom u_ table, can be reached through GlideRecord. The official ServiceNow documentation describes it as the class that handles database operations for server scripts. It hides the database underneath, so you work with records and fields instead of SQL.
If you know SQL, this query:
-- SQL (not used in ServiceNow)
SELECT * FROM incident WHERE priority = 1 AND state = 2
is written like this with GlideRecord:
var grIncident = new GlideRecord('incident');
grIncident.addQuery('priority', 1);
grIncident.addQuery('state', 2);
grIncident.query();
while (grIncident.next()) {
gs.info('Incident: ' + grIncident.getValue('number'));
}
new GlideRecord()Pick the table.addQuery()Add conditions. Nothing runs yet.query()Send the query to the database.next()Move to each matching record in turn.Where you use GlideRecord
GlideRecord belongs in server-side scripts. The ServiceNow Community developer articles list the usual places:
- Business Rules: run logic when a record is inserted, updated, deleted or queried.
- Script Includes: reusable server-side functions and classes.
- UI Actions (server side): work done when a user clicks a form button or link.
- Scheduled jobs and fix scripts: batch processing and one-off data changes.
- Flow Designer script steps and workflow scripts: custom logic inside automation.
02 Where the Glide name comes from
Fred Luddy started the company as Glidesoft in 2003. According to Wikipedia's ServiceNow history, it was incorporated in California in 2004 and renamed Service-now.com in 2006. The company name changed, but "Glide" stayed as the name of the platform's scripting APIs. The ServiceNow Glide APIs guide gives an overview of the family.
The Glide APIs you will meet first
| API | Runs on | Used for |
|---|---|---|
GlideRecord | Server | Creating, reading, updating and deleting records |
GlideRecordSecure | Server | The same, with access controls enforced |
GlideAggregate | Server | COUNT, SUM, AVG, MIN and MAX in the database |
GlideSystem (gs) | Server | Logging, the current user, dates, properties |
GlideQuery | Server | A stricter, chainable query API built on GlideRecord |
GlideAjax | Client | Asynchronous calls from the browser to a Script Include |
GlideForm (g_form) | Client | Reading and changing fields on the open form |
GlideUser (g_user) | Client | The signed-in user's name, roles and ID |
let/const and template literals fail there. Scoped applications can switch on ECMAScript 2021 mode (available since the Tokyo release) in the application's settings. The examples in this guide use ES5 so they work in both places.
03 CRUD operations
CRUD stands for create, read, update and delete. The ServiceNow real-time scenario guide walks through all four. Here is the short version, method by method.
| Operation | One record | Many records | Returns |
|---|---|---|---|
| Create | initialize() then insert() | Call insert() in a loop | The new sys_id, or null if the insert failed |
| Read | get() | query() then next() | get() and next() return true or false |
| Update | update() | updateMultiple() | update() returns the sys_id, or null |
| Delete | deleteRecord() | deleteMultiple() | deleteRecord() returns true or false |
Create
Make a GlideRecord for the table, call initialize(), set the fields and call insert():
var grIncident = new GlideRecord('incident');
grIncident.initialize();
grIncident.setValue('short_description', 'Network connectivity issue');
grIncident.setValue('description', 'Users cannot reach internal applications');
grIncident.setValue('priority', 2);
grIncident.setValue('category', 'network'); // choice value, not the label
grIncident.setValue('caller_id', gs.getUserID());
var newSysId = grIncident.insert();
if (newSysId) {
gs.info('Created incident ' + grIncident.getValue('number'));
} else {
gs.error('Insert failed: ' + grIncident.getLastErrorMessage());
}
Choice fields store a value, which is often different from the label you see on the form. For the incident Category field the value is network and the label is "Network". Check the value in the field's choice list before you script it.
Read
Use get() for one record and query() with next() for several:
// One record by sys_id
var grIncident = new GlideRecord('incident');
if (grIncident.get('46d44a5dc0a8010e0006b48d9b0e4e6d')) {
gs.info('Found: ' + grIncident.getValue('short_description'));
}
// One record by another field
var grByNumber = new GlideRecord('incident');
if (grByNumber.get('number', 'INC0001234')) {
gs.info('Priority: ' + grByNumber.getValue('priority'));
}
// Several records
var grNew = new GlideRecord('incident');
grNew.addQuery('state', 1); // New
grNew.addQuery('priority', '<=', 2); // Priority 1 or 2
grNew.orderByDesc('sys_created_on');
grNew.query();
while (grNew.next()) {
gs.info(grNew.getValue('number') + ': ' + grNew.getValue('short_description'));
}
Update
Read the record, change the fields, then call update(). Reference fields such as Assigned to hold a sys_id, not a user name:
var grIncident = new GlideRecord('incident');
if (grIncident.get('number', 'INC0001234')) {
var grUser = new GlideRecord('sys_user');
if (grUser.get('user_name', 'john.doe')) {
grIncident.setValue('assigned_to', grUser.getUniqueValue()); // a sys_id
}
grIncident.setValue('state', 2); // In Progress
grIncident.work_notes = 'Assigned to the network team'; // journal field
grIncident.update();
}
// Several records, one at a time
var grUnassigned = new GlideRecord('incident');
grUnassigned.addQuery('state', 1);
grUnassigned.addNullQuery('assignment_group'); // assignment group is empty
grUnassigned.query();
while (grUnassigned.next()) {
grUnassigned.setValue('assignment_group', 'DEFAULT_GROUP_SYS_ID');
grUnassigned.update();
}
To test for an empty field, use addNullQuery(), or addNotNullQuery() for the opposite. addQuery('assignment_group', 'empty') looks for the literal text "empty" and finds nothing.
Update many records with updateMultiple()
updateMultiple() applies the same change to every record the query matches, without a loop:
var grResolved = new GlideRecord('incident');
grResolved.addQuery('state', 6); // Resolved
grResolved.addQuery('resolved_at', '<', gs.daysAgoStart(30)); // 30+ days ago
grResolved.setValue('state', 7); // Closed
grResolved.updateMultiple();
updateMultiple() runs the table's Business Rules for the records it changes. To skip them, call setWorkflow(false) first, and only when that is what you intend. Two more rules: set values with setValue(), because direct assignment such as gr.state = 7 can update every record in the table instead of only the ones matched, and do not combine it with setLimit() or chooseWindow().
Delete
Use deleteRecord() for one record and deleteMultiple() for every record a query matches:
// One record
var grTest = new GlideRecord('u_test_table');
if (grTest.get('RECORD_SYS_ID')) {
grTest.deleteRecord();
}
// Many records: check how many will go before you delete them
var grOld = new GlideRecord('u_temp_data');
grOld.addQuery('sys_created_on', '<', gs.daysAgoStart(90)); // older than 90 days
grOld.query();
gs.info('Records to delete: ' + grOld.getRowCount()); // fine for a one-off check
// grOld.deleteMultiple(); // uncomment once the count looks right
deleteMultiple() works on the whole query, and developers report that it ignores setLimit() and chooseWindow(). Test the conditions with a read first, as above, and run the delete in a sub-production instance. Do not count on being able to restore bulk-deleted records.
04 Building queries
Most of your time with GlideRecord goes into building queries. A query is built from conditions, and conditions are joined with AND unless you add an OR.
addQuery()
addQuery() takes a field and a value, or a field, an operator and a value:
gr.addQuery('priority', 1); // priority = 1
gr.addQuery('priority', '<=', 2); // priority <= 2
gr.addQuery('short_description', 'CONTAINS', 'network'); // LIKE '%network%'
gr.addQuery('opened_at', '>=', gs.daysAgoStart(7)); // opened in the last 7 days
gr.addNullQuery('assigned_to'); // assigned_to is empty
gr.addNotNullQuery('resolved_at'); // resolved_at has a value
Operators
| Operator | Meaning | Example |
|---|---|---|
= | Equals (the default) | addQuery('state', '=', 1) |
!= | Not equal | addQuery('state', '!=', 7) |
> >= | Greater than, greater than or equal | addQuery('priority', '>=', 2) |
< <= | Less than, less than or equal | addQuery('priority', '<=', 2) |
CONTAINS | Contains the text | addQuery('short_description', 'CONTAINS', 'error') |
STARTSWITH | Starts with | addQuery('number', 'STARTSWITH', 'INC') |
ENDSWITH | Ends with | addQuery('email', 'ENDSWITH', '@company.com') |
IN | In a list | addQuery('state', 'IN', '1,2,3') |
NOT IN | Not in a list | addQuery('state', 'NOT IN', '6,7,8') |
Dot-walking
You can follow a reference field to a field on the referenced record, which the community's GlideRecord hints and tips recommend over nested queries:
var grUser = new GlideRecord('sys_user');
if (grUser.get('name', 'John Doe')) {
var grIncident = new GlideRecord('incident');
grIncident.addQuery('caller_id', grUser.getUniqueValue());
grIncident.query();
}
var grIncident = new GlideRecord('incident');
grIncident.addQuery('caller_id.name', 'John Doe');
grIncident.query();
while (grIncident.next()) {
gs.info(grIncident.getValue('number'));
}
Sorting
var grIncident = new GlideRecord('incident');
grIncident.orderBy('priority'); // first sort, ascending
grIncident.orderByDesc('opened_at'); // then newest first
grIncident.query();
05 Encoded queries
An encoded query is a whole set of conditions written as one string, such as active=true^priority=1. It is the same format a list filter uses in the URL, which makes it easy to build a filter in the list view and paste it into a script. The ServiceNow developer training covers the format.
Syntax
| Part | Meaning | Example |
|---|---|---|
^ | AND | priority=1^state=2 |
^OR | OR with the previous condition | priority=1^ORpriority=2 |
^NQ | Start a new query; the two groups are joined with OR | priority=1^NQstate=2 |
ORDERBY | Sort ascending | ^ORDERBYopened_at |
ORDERBYDESC | Sort descending | ^ORDERBYDESCpriority |
addEncodedQuery()
var grIncident = new GlideRecord('incident');
grIncident.addEncodedQuery('active=true^priorityIN1,2^ORDERBYDESCopened_at');
grIncident.query();
while (grIncident.next()) {
gs.info(grIncident.getValue('number'));
}
Copy an encoded query from a list
The ServiceNow Scripting 101 guide describes the quickest way to get one:
Go to the table's list view, for example incident.list.
Add every condition in the condition builder.
Click Run and check that the list shows the records you expect.
Right-click the last part of the breadcrumb and choose Copy query.
Dynamic values with javascript:
The encoded query documentation allows a javascript: prefix for values worked out when the query runs:
// Opened in the last 7 days
grIncident.addEncodedQuery('opened_at>=javascript:gs.daysAgoStart(7)');
// Assigned to the current user
grIncident.addEncodedQuery('assigned_to=javascript:gs.getUserID()');
// Opened this month
grIncident.addEncodedQuery('opened_at>=javascript:gs.beginningOfThisMonth()');
06 OR conditions
OR is where most GlideRecord queries go wrong, because the way conditions group together is not obvious from the code.
addOrCondition()
addQuery() returns a condition object. Call addOrCondition() on that object, not on the GlideRecord:
// priority = 1 OR priority = 2
var grIncident = new GlideRecord('incident');
var condition = grIncident.addQuery('priority', 1);
condition.addOrCondition('priority', 2);
grIncident.query();
AND with OR
// (priority = 1 OR priority = 2) AND state = 1 AND active = true
var grIncident = new GlideRecord('incident');
var condition = grIncident.addQuery('priority', 1);
condition.addOrCondition('priority', 2);
grIncident.addQuery('state', 1);
grIncident.addQuery('active', true);
grIncident.query();
^NQ for two separate groups
When you need two whole groups of conditions joined with OR, use ^NQ in an encoded query:
// (active AND first name Ben or Simon) OR (inactive AND first name Emma or Brenda)
var grUser = new GlideRecord('sys_user');
grUser.addEncodedQuery('active=true^first_name=Ben^ORfirst_name=Simon' +
'^NQactive=false^first_name=Emma^ORfirst_name=Brenda');
grUser.query();
The community's query performance practices note that ^NQ queries can be slow on large tables. Two simpler queries are sometimes faster.
IN instead of many ORs on one field
var grIncident = new GlideRecord('incident');
grIncident.addQuery('state', 'IN', '1,2,3');
grIncident.query();
// The same as an encoded query
grIncident.addEncodedQuery('stateIN1,2,3');
07 Performance
A careless GlideRecord query can slow down the whole instance. The ServiceNow Support knowledge base and the community guides agree on the main points.
setLimit()
var grIncident = new GlideRecord('incident');
grIncident.addQuery('active', true);
grIncident.setLimit(10); // at most 10 records
grIncident.query();
If you only need to know whether any record matches, setLimit(1) lets the database stop at the first match.
Filter on indexed fields
sys_id is always indexed. On task tables, number, active, sys_created_on and sys_updated_on usually are too, but check the table's indexes on your instance. A condition on an unindexed custom field can mean a full table scan on a large table; add an indexed condition alongside it, or ask for an index.
Count with GlideAggregate
var count = 0;
var grIncident = new GlideRecord('incident');
grIncident.addQuery('priority', 1);
grIncident.query();
while (grIncident.next()) {
count++;
}
var gaIncident = new GlideAggregate('incident');
gaIncident.addQuery('priority', 1);
gaIncident.addAggregate('COUNT');
gaIncident.query();
var total = gaIncident.next() ? gaIncident.getAggregate('COUNT') : 0;
gs.info('Count: ' + total);
getRowCount() looks like a shortcut, but it still runs the full query, so use GlideAggregate for counts in production code too.
Paging with chooseWindow()
The pagination guide uses chooseWindow(first, last), where first is included, last is not, and rows count from 0:
// Page 2 at 50 records a page: rows 50 to 99
var grIncident = new GlideRecord('incident');
grIncident.addQuery('active', true);
grIncident.orderByDesc('opened_at');
grIncident.chooseWindow(50, 100);
grIncident.query();
Large offsets such as chooseWindow(10000, 10050) get slow. For deep paging, filter on a sorted field instead, for example "opened before the last record of the previous page".
Common performance mistakes
| Mistake | Effect | Fix |
|---|---|---|
| Query without conditions | Reads the whole table | Always add conditions, and setLimit() where you can |
update() in a loop over many records | One database write and one set of Business Rules per record | updateMultiple() with setValue(); add setWorkflow(false) only if skipping the rules is intended |
Counting with a loop or getRowCount() | Reads every matching record | GlideAggregate with COUNT |
CONTAINS on large text fields | Full table scan | Narrow with indexed conditions first |
| Misspelt field name | The condition is dropped and the query can return every record | Check field names; see section 08 |
08 Common mistakes
These come up again and again in the community's GlideRecord hints and tips.
1. Storing gr.field instead of getValue()
var sysIds = [];
var grIncident = new GlideRecord('incident');
grIncident.addQuery('priority', 1);
grIncident.query();
while (grIncident.next()) {
sysIds.push(grIncident.sys_id); // a GlideElement, not a string
}
// Every entry now shows the last record's sys_id
while (grIncident.next()) {
sysIds.push(grIncident.getValue('sys_id')); // or grIncident.getUniqueValue()
}
grIncident.sys_id is an object that belongs to the GlideRecord, and its value changes each time next() runs. getValue() returns a plain string you can keep.
2. Forgetting query()
var grIncident = new GlideRecord('incident');
grIncident.addQuery('priority', 1);
// grIncident.query(); // missing: next() returns false and the loop never runs
while (grIncident.next()) {
gs.info(grIncident.getValue('number'));
}
3. Not checking what get() returned
As the developer forum explains, get() returns false when no record matches, and your code carries on unless you check:
var grIncident = new GlideRecord('incident');
grIncident.get('SYS_ID_THAT_DOES_NOT_EXIST'); // returns false
grIncident.setValue('short_description', 'Updated');
grIncident.update(); // no record is loaded, so this inserts a new incident
var grIncident = new GlideRecord('incident');
if (grIncident.get(sysId)) {
grIncident.setValue('short_description', 'Updated');
grIncident.update();
} else {
gs.warn('Incident not found: ' + sysId);
}
The new record gets a new sys_id, not the one you asked for, so the mistake is easy to miss until duplicates appear.
4. A misspelt field name
var grIncident = new GlideRecord('incident');
grIncident.addQuery('prioritty', 1); // typo
grIncident.query(); // no error, and the condition is dropped
By default an invalid condition is ignored, so the query can return every record in the table, which is dangerous before an update or delete. Check names with isValidField() when they come from input. The system property glide.invalid_query.returns_no_rows makes invalid queries return nothing instead; check whether it is set on your instance. GlideQuery fails with an error instead, which is one reason to use it in new code.
if (grIncident.isValidField(fieldName)) {
grIncident.addQuery(fieldName, value);
}
5. Cross-scope access
A scoped application can only reach another scope's table if that table allows it. Many global tables, sys_user among them, can be read from any scope by default. For a table that does not allow it, the call is blocked and a cross-scope access message appears in the system log. Check the table's Application Access settings, or ask for a cross-scope privilege record.
Debugging helpers
| Method | Use it to |
|---|---|
gr.getEncodedQuery() | See the query GlideRecord actually built |
gr.getRowCount() | Check how many records matched (debugging only) |
gr.getTableName() | Confirm which table you are on |
gr.getLastErrorMessage() | Read why an insert or update failed |
gs.info() | Write to System Logs > All |
09 Good habits
These habits, collected from the practical quick guide and community experience, keep GlideRecord code readable and safe.
Name variables after the table
// Hard to follow
var gr = new GlideRecord('incident');
var gr2 = new GlideRecord('sys_user');
// Clear
var grIncident = new GlideRecord('incident');
var grUser = new GlideRecord('sys_user');
A plain gr also collides easily with another script's gr in the same scope, which causes some very confusing bugs in Business Rules.
Use getValue() and getDisplayValue()
var incidentData = {
number: grIncident.getValue('number'),
callerSysId: grIncident.getValue('caller_id'),
callerName: grIncident.getDisplayValue('caller_id'), // the user's name
group: grIncident.getDisplayValue('assignment_group')
};
Handle the failure path
function updateIncidentState(sysId, newState) {
var grIncident = new GlideRecord('incident');
if (!grIncident.get(sysId)) {
return { success: false, error: 'Record not found' };
}
grIncident.setValue('state', newState);
if (grIncident.update()) {
return { success: true, number: grIncident.getValue('number') };
}
return { success: false, error: grIncident.getLastErrorMessage() };
}
Build long encoded queries from labelled pieces
// Active P1/P2 incidents opened in the last 7 days for the Network group,
// not resolved or closed, by priority then newest first
var QUERY = 'active=true' +
'^priorityIN1,2' +
'^opened_at>=javascript:gs.daysAgoStart(7)' +
'^assignment_group.name=Network' +
'^stateNOT IN6,7,8' +
'^ORDERBYpriority' +
'^ORDERBYDESCopened_at';
var grIncident = new GlideRecord('incident');
grIncident.addEncodedQuery(QUERY);
grIncident.query();
Call the server from the client with GlideAjax
var ga = new GlideAjax('IncidentAjax');
ga.addParam('sysparm_name', 'getShortDescription');
ga.addParam('sysparm_incident_id', g_form.getUniqueValue());
ga.getXMLAnswer(function(answer) {
g_form.addInfoMessage('Short description: ' + answer);
});
var IncidentAjax = Class.create();
IncidentAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getShortDescription: function() {
var grIncident = new GlideRecordSecure('incident'); // respects the caller's access
if (grIncident.get(this.getParameter('sysparm_incident_id'))) {
return grIncident.getValue('short_description');
}
return '';
},
type: 'IncidentAjax'
});
10 Security and access control
GlideRecord does not check access controls (ACLs). Server-side code sees every record and field, whoever triggered it. That is fine for system jobs and a data leak waiting to happen in code that acts for a user. The ServiceNow Developer Blog and the ServiceNow Community both explain the difference.
GlideRecord and GlideRecordSecure
| GlideRecord | GlideRecordSecure | |
|---|---|---|
| ACLs | Not checked; call canRead(), canWrite() yourself | Checked on every read and write |
| Records the user cannot read | Returned | Skipped |
| Fields the user cannot read | Returned | Come back empty |
| Fields the user cannot write | Written | Not written |
| Speed | Slightly faster | Slightly slower (ACL checks) |
| Use for | System jobs, migrations, admin scripts | Anything that acts for a user: GlideAjax, REST, portal widgets |
var grsIncident = new GlideRecordSecure('incident');
grsIncident.addQuery('active', true);
grsIncident.query();
while (grsIncident.next()) {
// Only records this user can read; unreadable fields are empty
gs.info(grsIncident.getValue('number'));
}
Checking ACLs yourself with GlideRecord
var grIncident = new GlideRecord('incident');
grIncident.addQuery('active', true);
grIncident.query();
while (grIncident.next()) {
if (!grIncident.canRead()) {
continue;
}
if (grIncident.caller_id.canRead()) {
gs.info('Caller: ' + grIncident.getDisplayValue('caller_id'));
}
}
Security methods
// Record level
gr.canRead(); gr.canWrite(); gr.canDelete(); gr.canCreate();
// Field level
gr.short_description.canRead();
gr.short_description.canWrite();
// Roles and groups
gs.hasRole('itil');
gs.getUser().isMemberOf('Network'); // group name or sys_id
Business Rules, system fields and ACLs are separate switches
| Call | What it changes | What it does not change |
|---|---|---|
setWorkflow(false) | Business Rules and other engines (such as notifications) do not run for this record's next write | ACLs |
autoSysFields(false) | sys_updated_on, sys_updated_by, sys_mod_count and the other system fields are not set | Business Rules, ACLs |
current.setAbortAction(true) | Cancels the insert or update that triggered a before Business Rule | ACLs |
| GlideRecord instead of GlideRecordSecure | No ACL checks, in any script context | Business Rules |
Security checklist
- Default to GlideRecordSecure in code that runs for a user: GlideAjax, Scripted REST APIs, portal widgets.
- Treat sys_ids and query text from the client as untrusted.
addQuery(field, value)keeps a value as a value, but a string passed intoaddEncodedQuery()can add its own^ORconditions. Never build an encoded query by joining user input. - Check
canDelete()before deleting on a user's behalf. - Log with
gs.info(),gs.warn()andgs.error().gs.log()is not available in scoped applications. - Ask for the least access a scoped app needs: read rather than write, one table rather than all.
More ServiceNow guides are in the ServiceNow category on this blog.
11 Frequently Asked Questions
updateMultiple() runs the table's Business Rules unless you call setWorkflow(false) first. Set field values with setValue() before calling it, not by direct assignment.addQuery() adds one condition at a time: a field, an optional operator and a value. addEncodedQuery() takes a whole condition string such as priority=1^state=2, usually copied from a list filter. They run the same way; encoded queries are easier for long filters with OR groups.gr.field_name is a GlideElement object that belongs to the GlideRecord, and its value changes every time next() moves to the next record. getValue('field_name') returns the value as a plain string, so it is safe to store in an array or object.setLimit() when you need only a few records, count with GlideAggregate instead of looping or getRowCount(), avoid large chooseWindow() offsets and check field names, because an invalid field can make a query return the whole table.g_form.getReference() with a callback for a single reference lookup.sys_updated_on, sys_updated_by, sys_mod_count and the other system fields on the next insert or update. It is useful for data migrations that must keep the original audit values.12 Abbreviations & Glossary
caller_id.name.^.gr.priority.x_myapp.
Caution: The
updateMultiple()method updates ALL matching records without triggering Business Rules. Use it carefully and only when you intentionally want to bypass Business Rules for bulk operations.this is not accurate, updateMultiple DOES run business rules