A business rule’s script says what happens. Its conditions say when, and that’s where most noisy or missing automation comes from. A notification rule that fires on every work note, a recalculation that runs on every save of a busy table, a check that integrations skip because of a role condition: all condition bugs. Narrow conditions make a rule cheaper (the script isn’t even loaded when the condition is false) and easier to explain, which is why interviewers ask you to write one for a concrete requirement.
Every filter a business rule has
A business rule only runs when all of these agree:
- Table. The rule’s table, and every table that extends it.
- When. Before, after, async or display.
- Operations. Any of insert, update, delete and query.
- Filter conditions. Conditions built in the condition builder, including the operators changes, changes to and changes from.
- Role conditions. The user performing the operation must have one of the listed roles.
- Advanced condition. A one-line script expression in the Condition field, evaluated before the script.
They’re combined with AND. If you fill in both the filter conditions and the advanced condition, both must be true. “Scope” in this sense means the set of records and operations a rule applies to; it’s unrelated to application scope, which governs what an app’s code may access.
State versus transition
The single most useful idea for writing conditions: decide whether the requirement is about a state or a transition.
“While an incident is In Progress, the assignee can’t be cleared” is about a state: every update to an In Progress incident should be checked. “Tell the assignee when they become responsible for an In Progress incident” is about a transition: it should fire once, at the moment responsibility moves, and not on every later save. Using a state condition for a transition is the classic source of notification spam. In script, transitions are expressed with changes(), changesTo() and changesFrom() on a field, which compare the value being saved with previous.
Four conditions against one incident’s history
This Node.js model (plain JavaScript; el() mimics the GlideElement methods) replays nine saves of one incident and reports when each candidate condition would let the rule run. The requirement is the transition above: notify the assignee when they become responsible for an In Progress incident, either because it’s assigned to them while In Progress or because their incident moves into In Progress.
// Which updates make each condition true? A field "changes" when its value differs from previous.
// (This model treats an insert as a change from empty values; check insert behaviour on your instance.)
const IN_PROGRESS = 2;
const history = [
{ step: 1, op: 'insert', set: { state: 1, assigned_to: '' } },
{ step: 2, op: 'update', set: { assigned_to: 'beth' } },
{ step: 3, op: 'update', set: { state: IN_PROGRESS } },
{ step: 4, op: 'update', set: { work_notes: 'checked logs' } },
{ step: 5, op: 'update', set: { assigned_to: 'carl' } },
{ step: 6, op: 'update', set: { priority: 2 } },
{ step: 7, op: 'update', set: { assigned_to: '' } },
{ step: 8, op: 'update', set: { state: 3 } },
{ step: 9, op: 'update', set: { state: IN_PROGRESS } },
];
const el = (cur, prev, f) => ({
nil: () => !cur[f],
changes: () => cur[f] !== prev[f],
changesTo: (v) => cur[f] !== prev[f] && cur[f] === v,
is: (v) => cur[f] === v,
});
const conditions = {
'A (none)': () => true,
'B state is In Progress': (c) => c('state').is(IN_PROGRESS),
'C assigned_to changes + state': (c) => c('assigned_to').changes() && c('state').is(IN_PROGRESS),
'D transition into ownership': (c) => !c('assigned_to').nil() && c('state').is(IN_PROGRESS)
&& (c('assigned_to').changes() || c('state').changesTo(IN_PROGRESS)),
};
for (const [name, cond] of Object.entries(conditions)) {
let prev = {}, cur = {};
const fired = [];
for (const h of history) {
cur = { ...prev, ...h.set };
const c = (f) => el(cur, prev, f);
if (cond(c)) fired.push(`${h.step}${cur.assigned_to ? '' : '(nobody)'}`);
prev = cur;
}
console.log(`${name.padEnd(31)} runs at steps: ${fired.join(', ')}`);
}A (none) runs at steps: 1(nobody), 2, 3, 4, 5, 6, 7(nobody), 8(nobody), 9(nobody)
B state is In Progress runs at steps: 3, 4, 5, 6, 7(nobody), 9(nobody)
C assigned_to changes + state runs at steps: 5, 7(nobody)
D transition into ownership runs at steps: 3, 5A, no condition, runs the script on every save, including the insert, and pushes all the filtering into code that has to be loaded and executed nine times. It might send the right notifications if the script is careful, but it’s the most expensive and hardest to read.
B uses the state. It fires at step 3 (correct), then again for a work note (4), a reassignment (5, correct by accident), a priority change (6), the assignee being cleared (7) and the return to In Progress with nobody assigned (9). That’s six notifications for two real events, two of them addressed to nobody.
C looks precise: assignment changes while In Progress. It catches Carl at step 5, but misses Beth at step 3 (her assignment happened before the state change), and fires at step 7 when the assignee is cleared, which is a change too.
D matches the requirement exactly: someone is assigned, the incident is In Progress, and either the assignee or the state has just changed into those values. Two runs, both meaningful. Writing out a short history like this one before building the rule is the fastest way to find the edge cases.
Writing it on the platform
Condition D translates directly into the rule’s advanced Condition field, with the script queuing an event that a notification listens for (illustrative, not run on an instance):
// Business rule: after update on incident
// Condition: !current.assigned_to.nil() && current.state == 2 && (current.assigned_to.changes() || current.state.changesTo(2))
(function executeRule(current, previous /*null when async*/) {
gs.eventQueue('incident.owner.inprogress', current,
current.getValue('assigned_to'), previous.getValue('assigned_to'));
})(current, previous);It’s an after rule because it reacts to a saved change rather than altering the record. changes() still returns true in after rules, because the platform only resets the previous values once all after rules have run. Some teams prefer the condition builder for readability (“Assigned to changes” OR “State changes to In Progress”, plus “Assigned to is not empty” and “State is In Progress”); the logic is the same, so pick whichever your team reviews more reliably.
Rules on parent tables run for children
Because incident, problem and change_request all extend task, a rule on task runs for all three, plus any custom table extending task. That’s useful for genuinely shared behaviour and a trap for everything else. A “set priority from impact and urgency” rule written on task will apply to change requests whose priority should be managed differently. If a rule is about incidents, put it on incident. If it must live on task but exclude some classes, say so in the condition (current.sys_class_name != 'change_request') so the exclusion is visible.
Role conditions describe the user, not the data
A role condition checks the user performing the operation. That’s appropriate for behaviour that should differ by audience, for example adding an information message only for agents. It’s wrong for anything that must always hold. A validation rule with a role condition of itil doesn’t run for integration users, imports, flows running as other users, or self-service users, so the rule is skipped on precisely the paths you didn’t test. If the rule is about the data, leave roles out of the conditions.
Conditions that look right but aren’t
A few patterns pass review and still misbehave:
- Journal fields. ServiceNow’s documentation says
changes()works for all field types except journal fields such aswork_notesandcomments, so don’t build a transition on them that way. - Async rules. Async scripts have no
previous, sochanges()doesn’t work inside the script. It does work in the condition, which is evaluated at save time; put the transition there. - Strict equality.
current.assigned_to === gs.getUserID()is always false, because the field is an element object, not a string. Use==or comparecurrent.getValue('assigned_to'). - Query rules. A before query rule runs on every query of the table, including lists, reports and other scripts. Its condition (often a role check such as “not admin”) must be fast, and its effect must be intended for every caller.
Interview practice
An after-update business rule on task sends a “your ticket was updated” email to the caller. Its only condition is current.active == true. Callers complain about receiving five emails for one change, and change managers ask why requesters get emails about change tasks. Rewrite the conditions and explain each choice.
Answer
The rule fires on every update to any active task, which explains both complaints: every save sends an email, and the rule runs for every class that extends task, including change tasks. I’d move it to the tables it’s meant for, probably incident (and a separate rule for requested items if needed), rather than task. Then I’d make the condition describe the events callers care about, for example “Additional comments added” or “State changes”, and leave out work notes and internal fields. Since changes() doesn’t support journal fields, I’d trigger the comment case from the event the platform raises for comments, or use the notification’s own conditions, rather than writing current.comments.changes() in a business rule.
Better still, the rule shouldn’t send email at all: it should queue an event, and a notification with its own conditions and the caller’s preferences should decide whether to send. Finally I’d replay a realistic history (comment, work note, state change, reassignment) to confirm one email per meaningful change.
Related reading
- ServiceNow interview questions
- ServiceNow multiple-choice questions
- ServiceNow Business Rule Timing and Side Effects, the companion question of when in the save a rule runs
- ServiceNow Notification Controls, for events and notifications triggered by rules like these
- GlideElement, scoped API (ServiceNow documentation)