Ch. 17 · ServiceNow

ServiceNow Dictionary and Field Audit

How dictionary entries and per-table overrides define a field, how auditing writes to sys_audit, and which fields you should keep out of history.

~6 min readintermediateupdated Oct 19, 2026

Every field on a ServiceNow form is a row in the dictionary (sys_dictionary). That row says what type the field is, how long it can be, whether it’s mandatory, what its default value is, which table a reference points to and a set of attributes that tweak its behaviour. Because tables inherit from each other (incident extends task, for example), a single dictionary entry can govern a field on dozens of tables at once. Auditing sits on top of this: when it’s on for a table, each change to a field becomes a row in the audit table, and that’s what the history and activity views are built from.

Interviewers ask about both together because the decisions overlap. “Where should this change go?” is a dictionary question. “How do we prove who changed it?” is an audit question. Getting either wrong is expensive to undo once a table has a few million rows.

What the dictionary entry controls

Open any field’s dictionary entry and the main settings are easy to read: type, column label, column name, max length, mandatory, read only, default value, reference and reference qualifier, plus a free-form attributes field. A table also has its own dictionary entry, the collection record with an empty column name, and that’s where table-wide settings such as auditing live.

The entry is defined on the table that introduces the field. assigned_to, short_description, state and assignment_group are all defined on task. When you look at the assigned_to field on an incident form, you’re looking at the task dictionary entry. So if you edit that entry to make the field mandatory, you’ve made it mandatory on incident, problem, change request, catalog task, HR case and every other table that extends task on the instance. That surprise is the classic mistake in this area, and it usually shows up as a broken integration that creates catalog tasks without an assignee.

Some changes are also hard to reverse. Shortening a max length on a table with data can truncate values, and only certain type changes are allowed once a column holds data (ServiceNow documents which conversions are supported). Treat edits on parent tables as platform changes, not form tweaks.

Dictionary overrides: change one child only

A dictionary override (sys_dictionary_override) changes selected properties of an inherited field for one child table. You pick the base table and field, the child table, and which properties to override. Typical ones are the default value, mandatory, read only, the reference qualifier, and attributes.

Here’s a realistic case. The service desk wants assigned_to mandatory on incidents once they leave the New state, and they want new incidents to default to the Service Desk assignment group. Nobody wants either rule on change requests.

task.assignment_group     dictionary entry      default: (none)
incident.assignment_group dictionary override   override default: javascript:gs.getProperty('acme.incident.default_group')

task.assigned_to          dictionary entry      mandatory: false
incident.assigned_to      dictionary override   (not used: mandatory depends on state)
Text

The default value goes in an override, because it’s a static fact about incidents. The mandatory rule doesn’t, because it depends on state: a dictionary-level mandatory flag applies from the moment the record is created. That rule belongs in a UI policy for the form and a data policy (or before business rule) so the server enforces it on imports and API calls too. Knowing when not to use an override is as useful in an interview as knowing what one is.

Note

The default group above reads a property so the sys_id isn’t hard-coded in the dictionary. The value is set once per instance, which is the kind of rarely changing configuration properties are meant for.

Overrides also show up when you upgrade. Because the base entry stays untouched, ServiceNow can update task without overwriting your incident-specific behaviour, and your change is visible as its own record in the update set.

How auditing records history

Auditing is enabled on the table’s collection dictionary entry with the Audit check box. Once it’s on, inserts and updates to that table write rows into sys_audit, one row per changed field. ServiceNow’s documentation lists the main columns: table name, field name, document key (the sys_id of the changed record), the user who made the change, old value and new value. Two details catch people out. Reference fields store sys_ids in the old and new value columns, not display values. Date and time fields are stored in UTC as they are in the database, not in the viewer’s time zone. Also, to avoid loops and performance problems, business rules and workflows don’t fire on inserts into sys_audit, so you can’t hang automation off it.

The history list and activity stream you see on a form aren’t read straight from sys_audit every time. The platform builds history sets (sys_history_set and sys_history_line) for the record when someone views its history, using the audit data and journal entries. That’s why the first history view of an old record can be slower than later ones.

Choosing what not to audit

Auditing a whole table sounds safe until you count rows. Take a custom table u_vendor_contract with 40,000 records and an integration that writes u_last_synced on every record once an hour, whether or not anything else changed. With auditing on and that field included, the sync alone adds:

40,000 records × 24 updates/day × 365 days = 350,400,000 sys_audit rows per year
Text

None of those rows answer a question anyone will ask. Exclude the field by adding the no_audit=true attribute to its dictionary entry, and the sync stops generating history while changes to the contract value, owner and end date are still recorded. The same applies to counters, “last viewed” timestamps and fields that a scheduled job recalculates nightly.

The opposite mistake is just as real: turning off auditing on a table that compliance depends on, because “the table is slow”. If auditors need to see who changed approval amounts, that field stays audited, and the fix for performance is elsewhere (usually the queries hitting sys_audit).

Reading audit data from a script

Sometimes you do need history in code, for example to find who last moved a contract to Terminated. Always filter by table and document key so the query uses the audit table’s index, and add the field name:

// Illustrative server script: who set state to terminated on this contract?
function lastStateChangeTo(tableName, sysId, value) {
  var audit = new GlideRecord('sys_audit');
  audit.addQuery('tablename', tableName);
  audit.addQuery('documentkey', sysId);
  audit.addQuery('fieldname', 'u_state');
  audit.addQuery('newvalue', value);
  audit.orderByDesc('sys_created_on');
  audit.setLimit(1);
  audit.query();
  return audit.next() ? { user: audit.getValue('user'), when: audit.getValue('sys_created_on') } : null;
}
JavaScript

An unfiltered sys_audit query, or one that filters only on fieldname, scans one of the largest tables on the instance. If reporting needs lots of history, consider a dedicated table populated by a business rule on the source table at change time, rather than mining sys_audit afterwards.

Interview practice: the mandatory field that broke an integration

A developer needed assigned_to required on HR cases and set Mandatory on the field’s dictionary entry. The next morning, the procurement integration that creates catalog tasks started failing. What happened, and how should it have been done?

Answer

assigned_to is defined on task, so the developer’s edit changed the base entry and made the field mandatory on every task-derived table, including sc_task. The integration creates catalog tasks before anyone is assigned, so its inserts were rejected. First, revert the base entry, and check the update set so the change doesn’t get promoted. Then decide how strict the HR rule really is. If an HR case must always have an assignee from creation, a dictionary override on the HR case table with mandatory set is fine, and it touches no other table. If it’s only required after triage, use a UI policy for the form plus a data policy or before business rule so imports and APIs follow the same rule. Either way, audit assigned_to on HR cases so you can show who changed assignment, and test the integration path, not just the form.

More in ServiceNow

read ✓ServiceNow · hard

ServiceNow ACL Scripting and Access

When an ACL really needs a script, how to write one that is cheap and null-safe on every row, and how to prove it works by testing as the user.

~7 min readread →
read ✓ServiceNow · hard

ServiceNow Caching and Invalidation

What a ServiceNow instance caches on each node, why a system property change triggers a costly flush, and where changing settings belong.

~7 min readread →
read ✓ServiceNow · mid

ServiceNow g_form and Client Script Design

Use g_form for what the user sees, guard onChange scripts with isLoading, avoid synchronous lookups, and keep the real rules on the server.

~6 min readread →
esc