MohammedSAli
ServiceNow Employee

 

πŸ”Ž Troubleshooting & Preventing Duplicate Task Numbers in ServiceNow

A complete guide to understanding, detecting, and enforcing unique numbering on Task and Task child tables

Duplicate numbering on Task or its child tables (Incident, Problem, Change, etc.) is rare, but when it occurs, it can impact reporting, integrations, and user trust in the platform.

This article explains:

  • Why duplicates occur
  • How to detect duplicates (GlideRecord scripts included)
  • How to fix numbering issues
  • How to enforce unique numbering going forward

1. Why Duplicate Numbers Occur

Out of the box, ServiceNow does not enforce uniqueness on the task.number field. This means the database will allow two or more records with identical numbers unless you enforce constraints.

Common Causes

A) Double inserts caused by UI actions

  • Multiple rapid clicks on Submit or Save
  • Browser automation tools triggering actions twice
  • Latency causing resubmission when a user thinks submitting failed

B) Business Rule creating an unintended second insert

Example:

current.insert();  // inside a before/after insert BR β†’ creates duplicates

C) Scripted inserts using workflows, script includes, or integrations

Any logic that inserts a Task record without proper guards can generate duplicates.

D) Clone or data import inconsistencies

If a cloned target already contains a number sequence ahead of the source, duplicates may arise after clone/import.

E) Autonumbering collisions under heavy load

The default sequential numbering is not atomic. Two inserts occurring simultaneously may generate the same number before either commit completes.


2. How to Detect Duplicate Numbers

You can run the following scripts in Background Scripts to identify duplicate numbers on Task or any child table.

A) Find duplicate numbers on Task

var dup = new GlideAggregate('task');
dup.addAggregate('COUNT', 'number');
dup.groupBy('number');
dup.addHaving('COUNT', '>', 1);
dup.query();

while (dup.next()) {
  gs.print('Number: ' + dup.number + ' | Count: ' +
           dup.getAggregate('COUNT', 'number'));
}

B) Retrieve the actual duplicate records

Replace TASKNUMBER with the number you found above:

var gr = new GlideRecord('task');
gr.addQuery('number', 'TASKNUMBER');
gr.query();

while (gr.next()) {
  gs.print(gr.sys_id + ' | ' + gr.number + ' | ' +
           gr.sys_created_on);
}

C) Check child tables only (Example: Incident)

var dup = new GlideAggregate('incident');
dup.addAggregate('COUNT', 'number');
dup.groupBy('number');
dup.addHaving('COUNT', '>', 1);
dup.query();

while (dup.next()) {
  gs.print('Duplicate Incident: ' + dup.number);
}

3. Fixing Existing Duplicate Numbers

If duplicates already exist, you must either renumber or delete/archive invalid records before enforcing uniqueness.

Options

Option A: Reassign new numbers to duplicates

You can modify the number field using:

  • Fix scripts
  • A one-time update script
  • A Business Rule that triggers only on existing duplicates

Option B: Delete invalid duplicates

Only if the instance owner agrees and the data is not required.


4. Preventing Future Duplicates (Enforcing Unique Numbering)

There are two fully supported approaches:

A) Preferred Method: Before Insert Business Rule

This is the method recommended in official documentation.

Create a Before Insert Business Rule on Task (set Apply to extended tables = true so child tables inherit it):

(function executeRule(current, previous /* null when async */) {

  var num = current.number + '';
  if (!num)
    return;

  var rec = new GlideRecord(current.getRecordClassName());
  rec.addQuery('number', num);
  rec.setLimit(1);
  rec.query();

  if (rec.next()) {
    // Number already exists – assign next available
    var newNum = getNextObjNumberPadded(); // system function
    gs.addInfoMessage(
      "Duplicate number detected. Changing '" + num +
      "' to '" + newNum + "'."
    );
    current.number = newNum;
  }

})(current, previous);

Why this helps

  • Prevents duplicates before the record is saved
  • Requires no database schema changes
  • Automatically applies to Task and all child tables

When to choose this

βœ” Mixed workloads βœ” High insert volume βœ” When graceful recovery is preferred over hard errors

B) Alternative Method: Unique Index on task.number

You can enforce uniqueness at the database level:

  • System Definition β†’ Tables β†’ Task β†’ Columns β†’ number β†’ Unique = true
  • Or create a custom index under Database Indexes

Pros

  • Hard guarantee of uniqueness
  • Enforced at database level

Cons

  • Insert attempts fail with an error
  • All existing duplicates must be resolved first
  • May break integrations or scripts with incorrect numbering logic

C) Prevent numbering gaps (related, but a different problem)

By default, ServiceNow assigns a number as soon as a record is created. If that insert is then aborted, the number is still consumed and leaves a gap in the sequence. Gaps don't create duplicates, but if gap-free numbering matters to you, you can have numbers assigned only when a record is saved:

  1. Navigate to All > System Properties > System.
  2. Set glide.itil.assign.number.on.insert to true.
  3. Click Save.

This requires the admin role. For details, see Prevent numbering gaps in the Zurich ServiceNow AI Platform Administration documentation.

Note that this property only addresses gaps. It does not make numbers unique, so duplicate prevention still requires the Before Insert Business Rule or the unique index described above.


5. Additional Recommendations

A) Review all custom Business Rules and Flows

Search for:

  • current.insert()
  • current.update() inside insert Business Rules
  • Flows or ACLs referencing the number field

B) Check for automation tools causing double submits

Bots and browser automations often behave differently than humans.

C) Review logs

If the issue is intermittent:

  • syslog_transaction
  • Node logs for simultaneous inserts
  • Slow Business Rules preceding failures

6. Summary

  • Duplicate numbers occur because OOTB numbering does not enforce uniqueness
  • Duplicates can be detected using GlideRecord / GlideAggregate scripts
  • Fix existing duplicates before enforcing constraints
  • Use a Before Insert Business Rule or a Unique Index to prevent recurrence
  • Audit automations, scripts, and integrations for double inserts
  • To avoid gaps in numbering (separate from duplicates), set glide.itil.assign.number.on.insert to true
7 Comments
Antony_Alldis
ServiceNow Employee

Thank you Mohammed.

 

This is a really great article, explaining how (and why) this occurs OOB, and how to prevent it from happening! 

linhnhatbui
Tera Explorer

Hi @MohammedSAli ,

Can the before insert business rule solely prevent the duplicated values? Is there any chance where two records can be inserted concurrently, in which both recognize the same value when calling getNextObjNumberPadded()?

brianlan25
Kilo Patron

What would the fix script look like. It looks like we have some duplicates for scrum tasks that were created by a business rule.

Pavel4
Tera Expert

edit: following statement is not valid, it is not required to instantiate  new global.NumberManager if you have specified the current variable

I think that in the script in 4. A) Preferred Method: Before Insert Business Rule
you are missing new global.NumberManager('u_my_table') in part 

var newNum = getNextObjNumberPadded(); // system function

better and working is 

newNum = new global.NumberManager("u_my_table").getNextObjNumberPadded();

 

MohammedSAli
ServiceNow Employee

No, the before insert business rule cannot solely prevent duplicate values. Two concurrent inserts can both end up with the same number.

The business rule checks whether a record with the current number exists, and if so, it calls getNextObjNumberPadded() to get a new one. That’s a check-then-act sequence, and nothing locks it.

Here’s how a race can happen:

  1. Transaction A runs the business rule, queries for the number, and finds no match.
  2. Transaction B runs at the same moment, queries, and also finds no match.
  3. Both transactions proceed to insert with the same number, because neither has committed yet, so neither sees the other’s record.

The number generator itself is also not guaranteed to be unique under concurrency. getNextObjNumberPadded() reads the current sequence value and increments it, and two sessions can read the same value before either writes the increment back. The article itself notes that default numbering is not atomic, which is the underlying cause. So even if the business rule runs cleanly, two transactions can receive the same value from the generator, and the business rule’s query may not catch it because the other record isn’t committed yet.


The only mechanism that prevents this at the database level is a unique index on task.number. The database serializes the two inserts, so one succeeds and the other fails with a constraint violation. That’s why the article’s business rule should be treated as reducing the likelihood of duplicates rather than eliminating them. If the customer needs a guarantee, the unique index is the option that provides one, with the cleanup prerequisite and error handling considerations noted earlier.

@linhnhatbui 

MohammedSAli
ServiceNow Employee

The renumbering script depends on how the duplicates were created, so I’d first check the business rule. If it calls current.insert() or inserts a related record without a guard, it will keep creating duplicates after any cleanup. Once the rule is fixed, you can renumber the existing duplicates with a script like the one below. Run it as a dry run on a sub-production instance first, and keep the oldest record in each pair so its number stays stable.”

Dry-run script:

 
javascript
// Renumbers duplicate scrum task numbers. Keeps the oldest record in each set.
// Run as a background script on a sub-production instance first.
var TABLE = 'rm_scrum_task';
var DRY_RUN = true;

var agg = new GlideAggregate(TABLE);
agg.addAggregate('COUNT', 'number');
agg.groupBy('number');
agg.addHaving('COUNT', '>', 1);
agg.query();

while (agg.next()) {
  var num = agg.number.toString();
  var recs = new GlideRecord(TABLE);
  recs.addQuery('number', num);
  recs.orderBy('sys_created_on');
  recs.query();

  var isFirst = true;
  while (recs.next()) {
    if (isFirst) { // keep the oldest record unchanged
      isFirst = false;
      continue;
    }
    var newNum = new global.NumberManager(TABLE).getNextObjNumberPadded();
    gs.print((DRY_RUN ? '[DRY RUN] ' : '') + recs.sys_id + ' ' + num + ' -> ' + newNum);

    if (!DRY_RUN) {
      recs.setWorkflow(false);
      recs.autoSysFields(false);
      recs.number = newNum;
      recs.update();
    }
  }
}

Things to check before running it:

  • Read-only number field: The number field may be protected in your release. If the update is rejected, you may need to use a different approach or a fix script with elevated rights.
  • NumberManager syntax: The comment thread disagrees on whether new global.NumberManager(TABLE) is needed. Confirm the call works on your release in the dry run.
  • External references: Integrations, emails, or reports that use the old scrum task numbers will break for the renumbered records. Check for these before running it.
  • Backup: Take a backup or export of the affected records first.


    @brianlan25 
MohammedSAli
ServiceNow Employee

Thanks for the follow-up. You’re right that the sample uses the bare getNextObjNumberPadded() call, which depends on the context of the business rule.

Using new global.NumberManager('table_name').getNextObjNumberPadded() is more explicit and works in a server-side script, so it’s a safe choice for the table in question. I’d recommend testing either form in a sub-production instance before deploying, since the exact behaviour can vary by release. The main point of the article still holds: the business rule reduces duplicates, but a unique index on the number field is the only way to guarantee uniqueness.

@Pavel4