The Caller field on an incident shows “Alex Morgan”. What the database stores is 9ee1b13dc6112271007f9d0efdb69cd0, the sys_id of one specific row in sys_user. Almost every reference-field bug comes from forgetting that difference: matching people by name, comparing objects the wrong way, or assuming a filter on the picker is a rule. This is a beginner topic, but interviewers use it to check whether you think in terms of record identity.
What’s actually stored
A reference field points at exactly one record in one other table. The column holds that record’s sys_id, a 32-character hexadecimal identifier that’s unique across the instance and never changes for the life of the record. The form shows the referenced table’s display value, which comes from whichever field on that table is marked as the display field (name for users, number for tasks).
So the display value is borrowed, not stored. Rename a user and every incident they raised shows the new name immediately, because nothing on the incident changed. Two related field types are worth knowing: a Document ID field can point to a record in any table (it stores the table name and sys_id), and a reference key setting can make a reference store a different unique field instead of sys_id, which is rare and best avoided unless you have a strong reason.
Reading references in script
On the server, a reference field on a GlideRecord is a GlideElement, and you choose what you want from it (illustrative, not run on an instance):
var inc = new GlideRecord('incident');
inc.get('number', 'INC0010042');
inc.getValue('caller_id'); // "9ee1b13dc6112271007f9d0efdb69cd0" identity
inc.getDisplayValue('caller_id'); // "Alex Morgan" for people
inc.caller_id.department.getDisplayValue(); // "IT" dot-walking through the referenceUse getValue() when the value feeds logic (comparisons, queries, keys in an object) and getDisplayValue() when a human will read it. Dot-walking (caller_id.department) follows the reference to fields on the referenced record without writing a second query yourself.
Same name, different person
Large organisations have duplicate names. This Node.js model (plain JavaScript; ReferenceElement is a stand-in for a GlideElement, an object whose string form is the sys_id) shows two classic mistakes:
// A reference stores a sys_id; the name you see is looked up from the referenced record.
const sys_user = [
{ sys_id: '62826bf03710200044e0bfc8bcbe5df1', name: 'Alex Morgan', email: 'alex.morgan@acme.com', department: 'Finance' },
{ sys_id: '9ee1b13dc6112271007f9d0efdb69cd0', name: 'Alex Morgan', email: 'amorgan@acme.com', department: 'IT' },
];
// Stand-in for a reference GlideElement: an object whose toString() is the sys_id.
class ReferenceElement {
constructor(table, sysId) { this.table = table; this.sysId = sysId; }
toString() { return this.sysId; }
getDisplayValue() { return (this.table.find((r) => r.sys_id === this.sysId) || {}).name || ''; }
}
// Setting by display value: the first record with that name wins.
const byDisplay = (name) => sys_user.find((u) => u.name === name).sys_id;
const intended = sys_user[1]; // the IT Alex raised the incident
const caller = new ReferenceElement(sys_user, byDisplay('Alex Morgan'));
console.log('stored value :', String(caller));
console.log('display value:', caller.getDisplayValue());
console.log('department :', sys_user.find((u) => u.sys_id === String(caller)).department, '(intended:', intended.department + ')');
const currentUserId = '62826bf03710200044e0bfc8bcbe5df1';
console.log('caller == currentUserId ->', caller == currentUserId);
console.log('caller === currentUserId ->', caller === currentUserId);
console.log("caller.toString() === currentUserId ->", caller.toString() === currentUserId);stored value : 62826bf03710200044e0bfc8bcbe5df1
display value: Alex Morgan
department : Finance (intended: IT)
caller == currentUserId -> true
caller === currentUserId -> false
caller.toString() === currentUserId -> trueThe first mistake is setting a reference by its display value. byDisplay('Alex Morgan') returns the first matching user, which is the Finance Alex, so the incident now belongs to the wrong person. The display value looks perfect, which is exactly why nobody notices. On the platform, setDisplayValue('caller_id', 'Alex Morgan') and any import or API path that resolves names behave the same way: with duplicates, you get a match, not the match. Set references with a sys_id from a reliable lookup (email, employee number, user ID) instead.
The second is comparison. A reference element isn’t a string, so strict equality with a sys_id string is always false, while loose equality converts the element to its sys_id and works. In a business rule, current.assigned_to == gs.getUserID() works and current.assigned_to === gs.getUserID() never does, which makes a condition silently false. Be explicit and compare current.getValue('assigned_to') === gs.getUserID().
Display values at the edges
Imports, APIs and reports are where names sneak back in.
- Imports. A field map targeting a reference field matches incoming text against the display value unless you set a Referenced value field name (for example, match a manager by
user_nameor email). With duplicate names, display matching picks one record; with a choice action of create, a misspelling creates a new record. - The REST Table API. By default reference fields come back as an object with the sys_id
valueand alink. Addingsysparm_display_value=truereturns names instead, andsysparm_display_value=allreturns both. Integrations that store the display value and send it back later are building on sand; store thevalue. - Reports and grouping. Grouping by a reference groups by sys_id, so two departments with the same name appear as two rows with identical labels. That’s correct, and a hint that the data needs a clearer display value.
Reference qualifiers filter choices
A reference qualifier restricts which records appear when someone picks a value: a simple condition (“active users only”), a dynamic filter, or an advanced javascript: qualifier that calls a script include, such as “only members of the selected assignment group” for Assigned to. It’s a good usability feature.
It isn’t validation. The qualifier shapes the lookup list and autocomplete in the form. A script, a transform map or a Table API call can still set the field to any sys_id in the referenced table. If “assigned to must be a member of the assignment group” is a real rule, enforce it with a before business rule or the assignment logic itself, and keep the qualifier for the user experience.
When the referenced record goes away
Deleting a referenced record raises the question of what happens to everything pointing at it. The dictionary entry’s reference cascade rule decides: Clear (the default) empties the reference on referencing records; Delete also deletes the referencing records, which is rarely what you want for something like a user; Restrict blocks the delete while references exist; and None leaves the referencing records untouched, pointing at a sys_id that no longer exists. For people and CIs, deactivating instead of deleting avoids the question entirely and keeps history meaningful.
Interview practice
An integration creates incidents through the Table API and sends the caller as "caller_id": "Alex Morgan". Users report incidents assigned to the wrong Alex, and occasionally incidents with an empty caller. What’s happening, and how would you fix it?
Answer
The integration is identifying people by display name. When the API resolves that name to a user, duplicate names mean it lands on one of the matching records, not necessarily the intended one, which explains the wrong Alex. Empty callers happen when no record matches the text exactly (a nickname, a typo, a renamed user), so the reference can’t be set.
The fix is to send identity, not display text. The partner should send something stable that we can resolve unambiguously, such as email address or employee number, and either the integration resolves it to a sys_id before calling the Table API, or a Scripted REST endpoint or transform does the lookup on our side, rejecting the request when the lookup finds zero or more than one user instead of guessing. Existing incidents can be corrected by matching on whatever extra data the partner holds. And because the display name was the only thing anyone checked, I’d add a test with two users of the same name so the problem can’t return unnoticed.
Related reading
- ServiceNow interview questions
- ServiceNow multiple-choice questions
- ServiceNow Import Sets and Transform Maps, for matching references during loads
- ServiceNow Dictionary and Field Audit, where reference attributes and cascade rules are configured
- Reference field type (ServiceNow documentation)