Ch. 17 · ServiceNow

ServiceNow Table Inheritance and Shared Behavior

Child tables in ServiceNow inherit fields, rules and access from their parents. See what extending task gives you and what it costs.

~7 min readbeginnerupdated Oct 12, 2026

Open the dictionary for incident and you’ll find only a couple of dozen fields defined there. The rest, including Number, Short description, Assignment group, State and Work notes, are defined on task, which incident extends. That’s table inheritance: a child table shares its parent’s fields, its records, and a lot of its behaviour. It’s one of the first data-model ideas interviewers check, because it explains both why building on task is so productive and why a rule written for one process can suddenly affect another.

What extending a table gives you

When a table extends another, the child is a specialised kind of the parent. In the base system, task is extended by incident, problem, change_request and many more, and cmdb_ci (configuration item) is the root of a deep hierarchy such as cmdb_ci_computer, then cmdb_ci_server, then cmdb_ci_linux_server. A table that isn’t an extension of anything is a base class.

The child gets every field of its parent and adds its own. Every row also carries sys_class_name, which says which class it really is, so an incident row and a problem row can sit in the same parent structure and still be told apart. And because a record is one record across the hierarchy, it has a single sys_id whether you look it up through task or incident.

ServiceNow only lets you choose a parent when a table is created, so the decision to extend (or not) is effectively permanent.

How the rows are stored

The platform offers three extension models, which decide how many physical database tables back a hierarchy:

  • Table per class: each class has its own physical table, and the parent replicates its children’s records with the same sys_id.
  • Table per hierarchy: one physical table holds the parent and all its children’s records, with sys_class_name distinguishing them. ServiceNow’s documentation uses task as the example: the Task table contains all records from its child classes.
  • Table per partition: like table per hierarchy, but storage is split into partitions that are added as needed.

You rarely choose the model yourself, but it explains behaviour you’ll see: a query on task returns incidents, problems and changes because they genuinely live in the same structure.

Behaviour flows down too

Fields are the obvious inheritance. Behaviour is the part that surprises people:

  • Business rules on a parent run for records of every child table. A before rule on task runs when an incident, a problem or a change request is saved, interleaved by order with the child table’s own rules.
  • ACLs follow the matching order table, then parent, then *. A task ACL governs incidents until someone creates an incident ACL for the same operation, at which point the more specific rule takes over.
  • UI policies with Inherit ticked apply to child tables, and the documentation notes that the child table’s own policies run first regardless of order.
  • Client scripts have an Inherited flag, data policies an Inherit option, and record-triggered flows can be set to run on the current table only or on extended tables too.

To change a parent field’s behaviour for one child without touching the parent, use a dictionary override: the child can have its own default value, mandatory setting, read-only setting or reference qualifier for an inherited field. Labels can also differ per table.

Watching it happen

This Node.js model (plain JavaScript, not the platform’s dictionary) attaches fields, a dictionary override and three business rules at different levels, then shows what an incident and a change request each inherit, and what a query on the parent returns:

// A table hierarchy with fields, dictionary overrides and business rules attached at different levels.
const tables = {
  task:           { parent: null,   fields: { number: '', short_description: '', impact: 3, assignment_group: '' } },
  incident:       { parent: 'task', fields: { caller_id: '', category: 'inquiry' }, overrides: { impact: 2 } },
  problem:        { parent: 'task', fields: { known_error: false } },
  change_request: { parent: 'task', fields: { type: 'normal', risk: '' } },
};
const rules = [
  { table: 'task',     order: 100, name: 'Default assignment group from category' },
  { table: 'task',     order: 200, name: 'Stamp first response time' },
  { table: 'incident', order: 150, name: 'Calculate priority from impact and urgency' },
];

const lineage = (t) => (t ? [t, ...lineage(tables[t].parent)] : []);

function fieldsOf(t) {                       // parent fields first, child fields added, overrides win
  const chain = lineage(t).reverse();
  const fields = Object.assign({}, ...chain.map((c) => tables[c].fields));
  for (const c of chain) Object.assign(fields, tables[c].overrides || {});
  return fields;
}
function rulesFor(t) {
  const chain = lineage(t);
  return rules.filter((r) => chain.includes(r.table)).sort((a, b) => a.order - b.order)
    .map((r) => `${r.order} ${r.name} [${r.table}]`);
}

for (const t of ['incident', 'change_request']) {
  console.log(`${t}: lineage ${lineage(t).join(' -> ')}`);
  console.log(`  fields/defaults: ${JSON.stringify(fieldsOf(t))}`);
  console.log(`  insert runs: ${rulesFor(t).join('; ')}`);
}

// Table per hierarchy: one physical table holds every class, told apart by sys_class_name
const taskRows = [
  { number: 'INC0010001', sys_class_name: 'incident' },
  { number: 'PRB0040001', sys_class_name: 'problem' },
  { number: 'CHG0030001', sys_class_name: 'change_request' },
  { number: 'INC0010002', sys_class_name: 'incident' },
];
const show = (rs) => rs.map((r) => `${r.number} (${r.sys_class_name})`).join(', ');
console.log('query on task:          ', show(taskRows));
console.log('task, class is incident:', show(taskRows.filter((r) => r.sys_class_name === 'incident')));
JavaScript
incident: lineage incident -> task
  fields/defaults: {"number":"","short_description":"","impact":2,"assignment_group":"","caller_id":"","category":"inquiry"}
  insert runs: 100 Default assignment group from category [task]; 150 Calculate priority from impact and urgency [incident]; 200 Stamp first response time [task]
change_request: lineage change_request -> task
  fields/defaults: {"number":"","short_description":"","impact":3,"assignment_group":"","type":"normal","risk":""}
  insert runs: 100 Default assignment group from category [task]; 200 Stamp first response time [task]
query on task:           INC0010001 (incident), PRB0040001 (problem), CHG0030001 (change_request), INC0010002 (incident)
task, class is incident: INC0010001 (incident), INC0010002 (incident)
Text

The incident has its own fields plus all of task’s, and its impact default is 2 because the override on incident replaces the inherited default of 3. The change request keeps task’s default. On insert, the incident runs both task rules and its own, sorted together by order. The change request runs the two task rules as well, including “Default assignment group from category”, even though change requests use categories differently, which is exactly how a rule written with incidents in mind starts misrouting changes. The last two lines show why queries on task need a class filter when you only want one kind of record.

Querying parents and children

The same behaviour in script (illustrative, not run on an instance). Querying the parent is useful for cross-process views, such as “everything assigned to my group”:

var t = new GlideRecord('task');
t.addActiveQuery();
t.addQuery('assignment_group', groupId);
t.query();
while (t.next()) {
    gs.info(t.getValue('number') + ' is a ' + t.getRecordClassName());
}
JavaScript

But a task GlideRecord only knows task fields. To read caller_id, which exists only on incident, look the record up on the child table using the same sys_id:

var inc = new GlideRecord('incident');
if (inc.get(t.getUniqueValue())) {
    gs.info(inc.caller_id.getDisplayValue());
}
JavaScript

For the reverse direction, filter the parent with sys_class_name (or query the child table directly, which is clearer when you only want one class).

Deciding whether to extend task

Extending task gives a new table a great deal for free: numbering, assignment, state, work notes and comments, SLAs, approvals, the activity stream, inclusion in “my work” views and reporting across processes. If the records represent work that someone is assigned to and completes, that’s usually the right call.

The cost is everything that already runs on task. Every task business rule, ACL, inherited policy and flow configured for extended tables now applies to your records, and other teams will keep adding more. Before extending, list the task-level automation in your instance and ask which of it should apply. For data that isn’t work (a list of meeting rooms, a lookup of cost centres), a standalone table is simpler and safer. And because the parent can’t be changed later, make the choice deliberately, ideally in a design review rather than in the table creation form.

Interview practice

After a developer added a before-insert business rule on task that sets the assignment group from the category, the change management team reports that new change requests are being assigned to the service desk. Incidents behave correctly. Explain what happened and how you’d fix it.

Answer

Because change_request extends task, a business rule on task runs for change requests too. Change requests have categories, but the rule’s mapping was designed for incident categories, so changes fall through to the default, the service desk. Incidents look correct because the rule was tested only with incidents.

The fix is to put the rule where it belongs: move it to incident (or to the specific classes it was designed for). If there’s a genuine reason to keep it on task, add an explicit class condition such as current.sys_class_name == 'incident' so the scope is visible to the next reader. Then correct the misassigned change requests, and add a review step for new rules on task and other shared parents: list the child tables the rule will affect and test at least one record of each.

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 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 readread →
esc