Week 5: Business Rules: Order, Types & Best Practices

This is where most of ServiceNow's server-side automation actually lives. Business Rules run automatically around database operations — no explicit call site anywhere in your code triggers them, which makes understanding exactly when they fire the single most important thing to get right this week.

Module 5 of 25 Week 5 of 26 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Choose the correct Business Rule type (before/after/async/display) for a given requirement
  • Reason about execution order among multiple rules on the same table
  • Detect field changes safely with changes()/changesFrom() instead of a bare previous object

1. Before, After, Async & Display Business Rules

A Business Rule is scoped to a table and an operation (insert, update, delete, query), and its type determines exactly when it fires relative to the database write:

  • Before — runs before the record is saved, in the same transaction. Can modify current's fields and those changes get saved too. Use for validation and field defaulting.
  • After — runs after the record is saved. Use for logic that acts on other records once this one is committed (e.g. updating a related record's count).
  • Async — runs after the save, but off the user's transaction entirely, in a scheduled job. Use for anything slow (notifications, external calls) that shouldn't make the user wait.
  • Display — runs before the form renders, pushing data to the client for a Client Script to use via g_scratchpad. Rare; used when a Client Script needs server-only data at load time.
Default to "before" or "after," reach for async deliberately

Async rules are tempting for anything that feels slow, but they run outside the user's transaction — if it fails, the user who triggered it never finds out. Use async for genuinely fire-and-forget work (an email notification), not for anything the user needs confirmation actually happened.

2. Execution Order

Multiple Business Rules of the same type on the same table run in the order set by their numeric Order field (lower runs first; default is 100). When debugging "why did my rule not see the value I expected," checking the order of every before-rule on that table is usually the first move — a rule with a higher order number running after yours may have already changed the field you're reading.

3. Detecting Field Changes Safely

It's tempting to reach for a raw "previous value" comparison, but the reliable, well-documented idiom is the changes() family of methods available on every GlideElement (i.e. every field on current):

Business Rule — after, update — incident
if (current.state.changesTo(6)) { // 6 = Resolved
  current.resolved_at = new GlideDateTime();
  // (would need .update() here if this were a "before" rule instead;
  //  an "after" rule's direct field writes require an explicit update())
}

if (current.priority.changes()) {
  gs.info('Priority changed from ' + current.priority.getJournalEntry(1));
}

changes() — did the field change at all this transaction. changesTo(value) — did it change to this specific value. changesFrom(value) — did it change from this specific value. All three only make sense inside an update-triggered Business Rule; on insert, every field "changes from nothing," which is rarely what you want to branch on.

4. Avoiding Recursive Triggers

An after or before rule that updates the same record it's running on (via current.update() in an after rule, for instance) can re-trigger Business Rules on that table again — including itself, if its own condition still matches. The platform has some built-in recursion guards, but the safe habit is to make any such condition check changesTo()/changesFrom() narrowly enough that it can't match twice in a row for the same transition.

5. Hands-on Exercise

Hands-on

Auto-populate a resolution timestamp and notify on reopen

Requirements:

  1. Add a custom Date/Time field u_resolved_at to the incident table.
  2. Write a before Business Rule on update: when state.changesTo(6) (Resolved), set current.u_resolved_at to the current GlideDateTime.
  3. Write a separate after Business Rule on update: when state.changesFrom(6) back to In Progress (a "reopen"), log an info message (stand-in for a notification you'd build in a real system).
  4. Give your reopen rule an Order value lower than 100, and explain in a comment why order matters here even though there's only one rule reacting to this transition.
Hint

u_resolved_at should only get set on the transition into Resolved, not every time the record is saved while already Resolved — changesTo() naturally handles this since it's only true on the transaction where the value actually changes.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What's the practical difference between a before and an after Business Rule?

A before rule runs prior to the database write, in the same transaction, and can modify current's own fields with those changes saved automatically as part of that same write. An after rule runs once the record is already committed, so any field changes it makes to current require an explicit update() call, and it's the right place for logic that touches other records.

Q2

Why are async Business Rules risky for anything the user needs confirmation about?

Async rules run after the save but outside the user's own transaction, in a scheduled job. If the async logic fails, the user who triggered the save has no visibility into that failure — they already got their success response. Async is appropriate for genuinely fire-and-forget work, not anything requiring guaranteed completion the user is relying on.

Q3

Why is current.field.changesTo(value) generally safer than trying to compare against a raw "previous" value yourself?

changesTo() is a well-documented, built-in method on every GlideElement specifically designed for this comparison, and it correctly handles the update-only semantics (it's not meaningful on insert, where everything "changes from nothing"). Hand-rolling a comparison risks getting insert-vs-update edge cases wrong.

Q4

How can a Business Rule accidentally trigger itself, and what's the practical guard against it?

If a Business Rule updates the same record it's running on (e.g. current.update() in an after rule) and its own condition still matches the new state, it can re-trigger — including itself. Writing the condition as a narrow changesTo()/changesFrom() transition check, rather than a broad state-based condition, keeps it from matching again on the rule's own resulting update.