Stop Using toString() in Your GlideRecord Loops

One small habit change that makes every ServiceNow script you write faster and more correct. Most developers use toString() without thinking about what it actually triggers — and in loops over hundreds or thousands of records, the performance difference is real and measurable.

The pattern almost every developer learns first

When you start writing ServiceNow scripts, you discover that accessing gr.state directly returns a GlideElement object — not a plain string. You learn quickly to add .toString() to get something usable in comparisons and logic. The habit gets established over your first few scripts and baked in across dozens more. Nobody flags it in code review because the code works.

var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.query();
while (gr.next()) {
  var state = gr.state.toString();
  var priority = gr.priority.toString();
  var assignedTo = gr.assigned_to.toString();
  // logic based on these values
}

The problem is not that this is broken — it is that it is doing far more work than it needs to, inside the loop where performance matters most.

What toString() actually triggers under the hood

When you call gr.state.toString(), ServiceNow does not simply read the stored value from memory. It invokes the field's display value resolution process — passing the stored value through the field type handler to produce a human-readable result. This has different costs depending on field type.

For a Choice field like state or priority, toString() resolves the choice label from the instance's choice list table. The stored value is an integer like "1"; toString() returns "New." That label lookup is not free.

For a Reference field like assigned_to or assignment_group, toString() queries the referenced table to retrieve the display value. Calling gr.assigned_to.toString() issues a SELECT against the sys_user table to fetch that user's name. In a loop over 500 records, this is 500 extra database queries — even though you never asked for them.

For Date/Time fields, toString() applies timezone conversion and locale-specific formatting. For a simple String field the overhead is smaller but still present. The type handler always runs.

In a loop over 10 records: negligible. In a Scheduled Job processing 2,000 incidents nightly with toString() on four fields per record: 8,000 unnecessary resolution operations per run, including database hits for every reference field. Switch to getValue() and those operations disappear entirely.

getValue() — the correct default for all script logic

getValue('fieldname') returns the raw database value directly — no type handler, no choice list lookup, no reference table query. Just the stored string from the database column:

The common objection: "but I need to compare against 'Resolved', not '6'." The answer is to compare against the stored value — which you look up once, or memorise. The incident Resolved state is always "6." Comparing gr.getValue('state') == '6' is faster, more portable, and more locale-independent than comparing against a display value that could change if someone edits the choice list label.

getDisplayValue() — when you actually need the human-readable version

There are legitimate situations where you need the display value. Use getDisplayValue('fieldname') explicitly, and only when you genuinely need it:

// Building an email notification body — human-readable values needed
var body = 'Incident ' + gr.getValue('number') + ' has been updated.
'
         + 'Priority: '   + gr.getDisplayValue('priority')        // "2 - High"
         + '
State: '    + gr.getDisplayValue('state')            // "In Progress"
         + '
Assigned to: ' + gr.getDisplayValue('assigned_to'); // "John Smith"

// Writing to a work note a human will read
gr.work_notes = 'Escalated to ' + gr.getDisplayValue('assignment_group');

// Populating a display-only UI field
current.setValue('u_priority_label', current.getDisplayValue('priority'));

The rule: if a human will read the value — use getDisplayValue(). If a script will process it — use getValue(). The distinction is that simple.

Field reading methods — complete reference

MethodReturnsPerformance costWhen to use
getValue('field')Raw stored DB valueMinimal — direct readScript logic, comparisons, building queries
getDisplayValue('field')Human-readable labelHigher — type resolutionNotifications, work notes, anything humans read
gr.field.toString()Human-readable labelHigher — same as getDisplayValueAvoid — use getDisplayValue() explicitly
getUniqueValue()sys_id of current recordMinimalWhen you need the sys_id of the current record
gr.field (GlideElement)GlideElement objectNone until you call somethingOnly when you need the full GlideElement API

Reference fields — where this matters most

The performance impact of toString() is proportionally largest on Reference fields because resolving the display value requires querying another table. Consider a Business Rule that checks assignment group on every incident update:

// SLOW — a database query on sys_user_group for every record
if (current.assignment_group.toString() == 'Network Operations') {
    triggerNetworkAlert(current);
}

// FAST — compare sys_ids directly
var networkOpsId = gs.getProperty('my.network.ops.group.sysid', '');
if (current.getValue('assignment_group') == networkOpsId) {
    triggerNetworkAlert(current);
}

// ALSO GOOD — use addQuery for the filter instead
gr.addQuery('assignment_group.name', 'Network Operations');

Storing the target sys_id in a system property and comparing against it is both faster and more maintainable — if the group gets renamed, you update one property value, not every script that checked the name string.

Dot-walking with getValue

You can pass dot-walk paths to getValue() to traverse reference fields in a single call, without loading separate GlideRecord instances:

// Get the department name of the assigned_to user
var deptName = gr.getValue('assigned_to.department.name');

// Get the group manager's email
var managerEmail = gr.getValue('assignment_group.manager.email');

// Get the caller's company name
var company = gr.getValue('caller_id.company.name');

This is significantly more efficient than calling toString() on a Reference field and then creating a second GlideRecord to look up related data. The dot-walk is resolved as a SQL join at the database level rather than two separate application-layer queries.

Practical before/after: a nightly batch job

// BEFORE — toString() on every field, every iteration
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^state=1');
gr.setLimit(500);
gr.query();
while (gr.next()) {
  var priority = gr.priority.toString();         // choice list lookup x500
  var group = gr.assignment_group.toString();    // reference query x500
  var caller = gr.caller_id.toString();          // reference query x500
  process(priority, group, caller);
}

// AFTER — getValue() everywhere, getDisplayValue() only for output
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^state=1');
gr.setLimit(500);
gr.query();
while (gr.next()) {
  var priority = gr.getValue('priority');         // raw "1"/"2"/"3"
  var groupId  = gr.getValue('assignment_group'); // sys_id
  var callerId = gr.getValue('caller_id');        // sys_id
  processWithIds(priority, groupId, callerId);
  // only call getDisplayValue when building human-readable output
}

The setValue counterpart

The same principle applies to writing values. Always use setValue() rather than direct property assignment:

// WRONG — direct assignment bypasses proper type handling
current.state = 6;

// RIGHT — setValue processes through field type handling correctly
current.setValue('state', '6');
current.setValue('assigned_to', userSysId);   // pass sys_id for reference fields
current.setValue('assignment_group', groupSysId);

Catching this in code review

Add .toString() on GlideElement properties to your code review mental checklist, especially inside loops. It is one of the most common and least visible performance anti-patterns in ServiceNow codebases — alongside unconstrained GlideRecord queries (covered in the GlideRecord performance tips guide) and counting with loops instead of GlideAggregate. The Instance Scan tool catches the latter two automatically but does not flag toString() patterns — making this a developer discipline rather than an automated check.

Related scripting guides: GlideRecord performance tips · GlideAggregate guide · Encoded queries · gs object reference · Debugging scripts · Business Rule types

Why this does not show up as a performance problem immediately

The toString() habit is insidious because the cost is invisible at small scale. A developer writes a Business Rule that calls toString() on five fields — tests it on a PDI with a few hundred records — and it runs in milliseconds. The same Business Rule deployed to a production instance with 2 million incidents, firing on every priority change, accumulates 5 × reference-resolution costs per execution across high-volume operations. The degradation is gradual and distributed, not a single obvious spike.

This is why the Instance Scan findings around GlideRecord patterns matter — they surface accumulated technical debt that individual scripts are too small to reveal. When you run an Instance Scan and see a Business Rule flagged for performance, one of the first things to check alongside the query conditions is the field-reading pattern inside the script body.

Interview implications

The getValue() vs toString() distinction appears in most mid-level and senior ServiceNow technical interviews. The question is usually phrased as: "What is the difference between getValue() and toString() on a GlideElement?" The expected answer covers: getValue() returns the raw stored value, toString() triggers display resolution, and getDisplayValue() is the explicit API to use when you actually want the display value. Candidates who can explain why this matters for performance in loops — not just what the difference is — stand out as developers who think about execution, not just syntax.

A follow-up question that reveals deeper understanding: "What happens when you call toString() on a Reference field inside a loop over 1,000 records?" The answer — a database query per iteration for each reference field, totalling potentially thousands of hidden queries — demonstrates you understand the ServiceNow server architecture, not just the API surface.

The one-line fix that compounds over time

Of all the ServiceNow scripting habits worth establishing early in your career, the getValue() habit is among the most impactful per unit of effort. It takes no more characters to type gr.getValue('state') than gr.state.toString(). The change costs nothing during development and pays compounding returns as your scripts run across larger datasets in production. Make it the default. Reserve getDisplayValue() for the cases where human-readable output is genuinely needed — notifications, work notes, audit messages — and let getValue() handle everything else.

Common toString() Patterns and Their Replacements

Beyond the basic field reading scenario, toString() appears in several other contexts that each have better alternatives. When checking whether a reference field is populated, if (gr.assigned_to) evaluates the GlideElement as a boolean — truthy if populated, falsy if empty — without needing toString() at all. When building an encoded query string to pass to another GlideRecord, use gr.getEncodedQuery() on the source query rather than converting field values to strings and concatenating them manually. When comparing two GlideElement values from different records, use gr1.assigned_to.toString() == gr2.assigned_to.toString() only as a last resort — if both are reference fields pointing to the same table, comparing gr1.getValue('assigned_to') == gr2.getValue('assigned_to') is clearer and avoids any potential display value vs sys_id confusion. The encoded query guide covers query construction without string concatenation.

50 scripting patterns in one guide

The NowSpectrum Pro Tips and Tricks guide covers 50 battle-tested ServiceNow scripting patterns — getValue vs toString, GlideRecord performance, Business Rules, and Script Includes.

Get the Pro Tips Guide →
← Back to all posts