Ch. 17 · ServiceNow

ServiceNow Update Sets and Deployment Scope

Know what a ServiceNow update set carries and what it leaves behind: configuration versus data, preview problems and environment values.

~7 min readadvancedupdated Oct 12, 2026

An update set is a named container that records configuration changes in one instance so they can be moved to another: business rules, script includes, catalog items, form layouts, flows. The mechanics (mark it current, make changes, complete it, retrieve it in the target, preview, commit) take an afternoon to learn. What separates a smooth release from a broken one is knowing the scope of what moves: which changes were captured, which weren’t, and which were captured but shouldn’t be applied as-is. That’s what interviewers mean by “deployment scope”.

What an update set actually records

While an update set is current, each change to a tracked record writes a customer update (sys_update_xml) containing the complete XML of that record as it now stands, plus a version history entry. Change the same business rule five times and the update set holds one customer update with the final version. It’s a snapshot of records, not a list of steps and not a diff.

Which tables count as configuration is decided by the platform: tables tracked for customisations carry an internal update_synch attribute that customers aren’t meant to add themselves. Some configuration spans several tables, such as form sections, lists, related lists, choice lists, dictionary entries, field labels and workflows, and is packaged by special handlers into a single update so it’s applied as a unit. ServiceNow warns that some of these handlers delete and reinsert records when applied.

Two working rules follow. Changes in a scoped application go to an update set for that application, so a single story touching global and an app produces more than one update set. And the Default update set is where changes land when nobody picked one; it isn’t meant to be moved, so work found there has to be re-homed deliberately.

Configuration travels, data doesn’t

ServiceNow’s documentation draws the line clearly: update sets capture configuration but not task or process data. A catalog item’s definition, variables and variable choices move; the requests people submitted against it don’t. In practice, these don’t travel either:

  • users, groups, group memberships and role assignments;
  • records created by processes (incidents, changes, requests) and most reference data loaded into tables;
  • home pages and content pages, which are excluded by default;
  • deleting a table (removal isn’t tracked, so it must be done in the target by hand), and dictionary changes that would lose data, such as changing a column’s type, which are blocked.

The trap runs the other way as well. System properties are configuration, so a property value changed in development is captured and will overwrite the production value on commit. That’s how a production integration ends up calling a development endpoint.

Inventorying a release

Before completing an update set, it pays to list everything the story changed and check how each item will reach production. This Node.js model (plain JavaScript; the tracked-table list is a small illustrative subset, not the platform’s) does that for a “Laptop refresh” release, then simulates the target’s preview checking references:

// Which of a release's changes travel in an update set, and what would preview flag in the target?
// The table lists are a small illustrative subset, not the platform's full update_synch list.
const TRACKED = new Set(['sys_script', 'sys_script_include', 'sc_cat_item', 'item_option_new',
  'sys_hub_flow', 'sys_properties', 'sys_ui_policy', 'sys_security_acl']);
const ENVIRONMENT_SPECIFIC = new Set(['sys_properties']);   // tracked, but values often differ per instance

const devChanges = [
  { table: 'sys_script_include', name: 'LaptopRefreshEligibility' },
  { table: 'sys_script',         name: 'Laptop refresh eligibility check' },
  { table: 'sc_cat_item',        name: 'Laptop refresh', refs: { group: '7a2f0c1e1b2e3010a5b8c3d4e5f60718' } },
  { table: 'item_option_new',    name: 'laptop_model (variable)' },
  { table: 'sys_hub_flow',       name: 'Laptop refresh fulfilment', refs: { group: '7a2f0c1e1b2e3010a5b8c3d4e5f60718' } },
  { table: 'sys_properties',     name: 'x_acme.laptop.vendor_endpoint (dev URL)' },
  { table: 'sys_user_group',     name: 'Hardware Fulfilment', sys_id: '7a2f0c1e1b2e3010a5b8c3d4e5f60718' },
  { table: 'sys_user_grmember',  name: '3 members added to Hardware Fulfilment' },
  { table: 'sc_req_item',        name: '12 test requests' },
];
const target = { sys_user_group: new Set(['287ebd7da9fe198100f92cc8d1d2154e']) };  // prod has other groups only

const captured = [];
for (const c of devChanges) {
  const tracked = TRACKED.has(c.table);
  const note = !tracked ? 'NOT captured (data)'
    : ENVIRONMENT_SPECIFIC.has(c.table) ? 'captured, but the value is environment-specific' : 'captured';
  console.log(`${c.table.padEnd(19)} ${c.name.slice(0, 44).padEnd(45)} ${note}`);
  if (tracked) captured.push(c);
}
console.log('preview in production:');
for (const c of captured)
  for (const [col, id] of Object.entries(c.refs || {}))
    if (!target.sys_user_group.has(id)) console.log(`  ${c.table} '${c.name}': ${col} references sys_user_group ${id}, not found`);
JavaScript
sys_script_include  LaptopRefreshEligibility                      captured
sys_script          Laptop refresh eligibility check              captured
sc_cat_item         Laptop refresh                                captured
item_option_new     laptop_model (variable)                       captured
sys_hub_flow        Laptop refresh fulfilment                     captured
sys_properties      x_acme.laptop.vendor_endpoint (dev URL)       captured, but the value is environment-specific
sys_user_group      Hardware Fulfilment                           NOT captured (data)
sys_user_grmember   3 members added to Hardware Fulfilment        NOT captured (data)
sc_req_item         12 test requests                              NOT captured (data)
preview in production:
  sc_cat_item 'Laptop refresh': group references sys_user_group 7a2f0c1e1b2e3010a5b8c3d4e5f60718, not found
  sys_hub_flow 'Laptop refresh fulfilment': group references sys_user_group 7a2f0c1e1b2e3010a5b8c3d4e5f60718, not found
Text

The script include, business rule, catalog item, variable and flow are captured, as expected. The vendor endpoint property is captured too, with a development URL in it. The new Hardware Fulfilment group, its members and the test requests aren’t captured, and because the catalog item and the flow both reference that group by sys_id, preview in production flags them: the update points at a record the target doesn’t have. On a real instance this appears as a preview problem on each affected update, saying the referenced record couldn’t be found.

Fixing what preview finds

Every preview problem needs a decision, and “accept remote update” without understanding the problem is how broken references reach production.

Missing data the configuration depends on. For the group, there are two clean options. Move the group record itself (an XML export and import keeps its sys_id, so references resolve), or create it in production with the same name before loading. ServiceNow documents that some tables, including sys_user_group and sys_user_role, coalesce by name in update sets, and that references to coalescing tables can be rewritten to the target’s sys_id when an update is loaded; whichever route you take, retrieve or re-preview afterwards and confirm the problem has cleared. In scripts, avoid hard-coding group sys_ids at all; look the group up by a property or by name so the code doesn’t depend on matching IDs.

Environment-specific values. Keep them out of release update sets. Create the property once, set its value per instance after deployment (or through the instance’s own configuration process), and protect it from being overwritten by clones with a clone data preserver. If a property value did get captured, skip that update in preview and set the value by hand.

Collisions. If the target has a newer version of the same record, preview reports a collision. That usually means someone changed production directly, which deserves a conversation before anyone picks a winner. The companion article on update set conflicts covers resolution order in detail.

Packaging releases

A few structural choices make update sets manageable:

  • One update set per story or fix, named with the story ID, so preview problems can be traced to an owner and a story can be held back without untangling it.
  • Batch update sets when several sets must ship together: child sets grouped under a parent are previewed and committed as one batch, in a defined order.
  • Scoped applications for substantial features. An app moves as a versioned unit through source control or the application repository, with its own tables, scripts and access settings, which avoids most of the “did we capture everything?” question for that feature. Update sets remain the tool for global configuration.

Before committing in production

Treat the commit as a change with evidence. Preview in an instance that mirrors production (a recent clone), resolve every problem, and record what was skipped and why. Commit in the agreed order. Then run a smoke test, ideally an automated test suite, covering the stories in the release.

Plan for failure realistically. Update sets can be backed out, but back-out reverses the configuration records the set changed; it doesn’t undo data created in the meantime or changes made outside the set, so a forward fix in a new update set is often the safer recovery.

Interview practice

A release moved a new catalog item and its fulfilment flow to production with a clean preview in the test instance. In production, preview showed two problems, which the deployer accepted. Since then, requests for the item sit unassigned, and the item’s vendor integration is calling a development URL. Explain what went wrong and how you’d prevent it next time.

Answer

Two scope gaps. The flow and the catalog item reference a fulfilment group created in development; groups are data and weren’t captured, so in production the references point at a sys_id that doesn’t exist, which is what the two preview problems were saying. The test instance previewed cleanly probably because the group had been created there earlier with the same sys_id (for example by a clone or an XML import), so it hid the gap. Second, the vendor endpoint system property was edited in development while the update set was current, so its development value was captured and overwrote production’s on commit.

To recover: set the production property back to the production endpoint immediately, create or import the group (with members) and confirm the flow and item resolve to it, then reassign the stuck requests. To prevent it: inventory data dependencies for each story and move them deliberately (XML import or a documented manual step) before previewing; never accept a preview problem without recording why it’s safe; keep environment-specific properties out of release update sets and protect them with clone data preservers; preview against a fresh clone of production; and run an automated smoke test after commit that would have caught both the unassigned request and the wrong endpoint.

More in ServiceNow

read ✓ServiceNow · hard

ServiceNow Update Sets and Conflicts

How preview detects update set collisions, why a hotfix made directly in production causes most of them, and how to resolve each deliberately.

~6 min readread →
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 →
esc