Ch. 17 · ServiceNow

ServiceNow Business Rule Timing and Side Effects

When ServiceNow business rules run: before rules shape the record, after rules touch others, async rules move slow work off the save.

~6 min readbeginnerupdated Oct 12, 2026

A business rule is server-side script that runs when a record is displayed, inserted, updated, deleted or queried. The script itself is often the easy part. The decision that causes most bugs is the When field: before, after, async or display. Pick the wrong one and the code runs perfectly but its effect is lost, slows every save, or reads data that has already changed. This is a favourite beginner interview topic because the right answer depends on reasoning about the save, not on remembering an API.

Four timings, one save

Every insert or update passes through the same sequence. Rules are sorted by their Order field (lowest first, default 100) within each phase:

  1. Before rules run after the user submits but before anything is written. They see current (the record as it’s about to be saved) and previous (the record as it was).
  2. The database write happens.
  3. After rules run once the write is done, in the same transaction, while the user waits.
  4. Async rules don’t run in the transaction at all. Submitting creates a scheduled job, and the scheduler runs the rule shortly afterwards in the background.

Display rules are different: they run when a form loads, just after the record is read, and exist mainly to pass server data to client scripts.

There’s one more detail worth knowing. The platform’s own engines (approvals, assignment rules, data lookup, data policy, the workflow engine and others) run in the middle of the before phase, and notifications and other engines in the middle of the after phase. Rules with an order below 1000 run before those engines; rules at 1000 or above run after them. If your before rule needs to see the assignment group that an assignment rule picks, it needs an order of 1000 or more.

Tracing a save through a model

The quickest way to internalise the phases is to watch one save go through them. This Node.js script is a toy model of the pipeline (it doesn’t use any ServiceNow API), with five rules on an incident: two before, two after and one async.

const db = {
  incident: { INC001: { number: 'INC001', short_description: 'vpn down', priority: 3, problem_id: '' } },
  problem: { PRB01: { number: 'PRB01', u_incident_count: 4 } },
};
const asyncQueue = [];
const rules = [
  { name: 'Normalize description', when: 'before', order: 100,
    run: (cur) => { cur.short_description = cur.short_description.trim().replace(/^./, (c) => c.toUpperCase()); } },
  { name: 'Block P1 without CI', when: 'before', order: 200,
    run: (cur) => { if (cur.priority === 1 && !cur.cmdb_ci) cur.abort = 'Priority 1 needs a configuration item'; } },
  { name: 'Bump priority (wrong place)', when: 'after', order: 100,
    run: (cur) => { cur.priority = 2; } },
  { name: 'Count on parent problem', when: 'after', order: 200,
    run: (cur, prev) => { if (cur.problem_id && cur.problem_id !== prev.problem_id) db.problem[cur.problem_id].u_incident_count++; } },
  { name: 'Send to monitoring tool', when: 'async', order: 100,
    run: (cur, prev) => console.log(`  async sees "${cur.short_description}", priority ${cur.priority}, previous=${prev}`) },
];

function save(number, changes) {
  const prev = { ...db.incident[number] };
  const cur = { ...prev, ...changes };
  console.log(`save ${number} ${JSON.stringify(changes)}`);
  const byOrder = (w) => rules.filter((r) => r.when === w).sort((a, b) => a.order - b.order);
  for (const r of byOrder('before')) {
    r.run(cur, prev);
    console.log(`  before ${r.order} ${r.name}`);
    if (cur.abort) { console.log(`  ABORTED: ${cur.abort} (nothing written)`); return; }
  }
  db.incident[number] = { ...cur };                       // the database write
  console.log('  -- database write --');
  for (const r of byOrder('after')) { r.run(cur, prev); console.log(`  after  ${r.order} ${r.name}`); }
  for (const r of byOrder('async')) asyncQueue.push(() => r.run({ ...db.incident[number] }, null));
  console.log(`  stored: ${JSON.stringify(db.incident[number])}`);
}

save('INC001', { short_description: '  vpn down for sales  ', problem_id: 'PRB01' });
save('INC001', { priority: 1 });
console.log('transaction finished; the scheduler runs the queued async work:');
asyncQueue.forEach((job) => job());
console.log('PRB01 incident count:', db.problem.PRB01.u_incident_count);
JavaScript
save INC001 {"short_description":"  vpn down for sales  ","problem_id":"PRB01"}
  before 100 Normalize description
  before 200 Block P1 without CI
  -- database write --
  after  100 Bump priority (wrong place)
  after  200 Count on parent problem
  stored: {"number":"INC001","short_description":"Vpn down for sales","priority":3,"problem_id":"PRB01"}
save INC001 {"priority":1}
  before 100 Normalize description
  before 200 Block P1 without CI
  ABORTED: Priority 1 needs a configuration item (nothing written)
transaction finished; the scheduler runs the queued async work:
  async sees "Vpn down for sales", priority 3, previous=null
PRB01 incident count: 5
Text

Four lessons are in that output. The normalised description was stored even though no rule “saved” it, because before-rule changes are part of the write. The after rule’s priority change vanished: the write had already happened. The second save was stopped before the database was touched. And the async rule saw the record as stored, with no previous, after the user’s transaction had ended.

Before rules shape the record you’re saving

Use a before rule to derive or clean values on the same record and to reject invalid saves. On the platform, the incident rules above would look like this (illustrative; the template is the one ServiceNow generates):

// When: before, Insert + Update, Table: incident, Order: 100
(function executeRule(current, previous /*null when async*/) {
    current.short_description = current.short_description.trim();

    if (current.priority == 1 && current.cmdb_ci.nil()) {
        gs.addErrorMessage('Priority 1 incidents need a configuration item.');
        current.setAbortAction(true);
    }
})(current, previous);
JavaScript

Never call current.update() here. The platform saves the changes when all before rules finish, and an explicit update triggers the rules again. ServiceNow’s documentation is blunt about it: current.update() in a business rule is never necessary.

After rules touch other records

Once the record is written, it has a sys_id (on insert) and its final values, which makes the after phase the place for side effects on other records: updating a parent, creating a child task, queuing an event with gs.eventQueue() for notifications. A common improvement on the model’s counter is to recount rather than increment, so the value can’t drift:

// When: after, Update, Condition: current.problem_id.changes() && !current.problem_id.nil()
(function executeRule(current, previous) {
    var agg = new GlideAggregate('incident');
    agg.addQuery('problem_id', current.getValue('problem_id'));
    agg.addAggregate('COUNT');
    agg.query();
    var count = agg.next() ? parseInt(agg.getAggregate('COUNT'), 10) : 0;

    var prb = new GlideRecord('problem');
    if (prb.get(current.getValue('problem_id'))) {
        prb.setValue('u_incident_count', count);
        prb.update();
    }
})(current, previous);
JavaScript

The mistake interviewers listen for is setting a field on current in an after rule and wondering why it isn’t saved. Move that logic to a before rule.

Async rules move slow work off the save

Everything in before and after rules adds to the time the user stares at a spinner. Calls to external systems, heavy recalculations and anything that can fail for reasons outside the instance belong in an async rule. The trade-offs are real, though:

  • No previous. Async rules don’t get the previous version of the record, so changes(), changesTo() and changesFrom() don’t work in the script. They do work in the rule’s condition, which is evaluated at save time.
  • The record may have moved on. The rule reads the record when the scheduler runs it. If the user saved twice in quick succession, both jobs may see the second version.
  • No guaranteed order. If two async rules update the same record, their writes can interleave or overwrite each other. ServiceNow suggests an after rule or a system event in that situation.

Display rules and the scratchpad

A display rule runs as the form loads and can put values on g_scratchpad, which client scripts read without another server round trip:

// When: display, Table: incident
(function executeRule(current, previous) {
    g_scratchpad.callerIsVip = current.caller_id.vip == true;
})(current, previous);
JavaScript

It’s the cheap alternative to a GlideAjax call on load. It isn’t a security control: anything on the scratchpad is visible in the browser.

Interview practice

A colleague’s after-update business rule on incident sets current.u_last_touched_by = gs.getUserName() and ends with current.update(). It works, but saving an incident has become slow and the system log shows recursion warnings. What’s wrong, and what would you write instead?

Answer

The field belongs to the record being saved, so the rule is in the wrong phase. In an after rule the write is already done, which is why the colleague needed current.update(), and that update re-runs every insert and update rule on the table, including this one. The platform detects and stops the recursion, but each save now does at least two writes and runs the whole rule set twice.

The fix is a before-update rule with the same assignment and no update call: the value is saved with the user’s own write, once. If there were a genuine need to write to the same record after the fact (rare), setWorkflow(false) before update() would stop other rules running, at the cost of silently skipping every other rule and the notifications that depend on them, which is why it’s a last resort rather than a pattern.

More in ServiceNow

read ✓ServiceNow · easy

ServiceNow Business Rule Conditions and Scope

Narrow ServiceNow business rule conditions to the transition you mean: operations, filters, changes() and changesTo(), and rules on task tables.

~7 min readread →
esc