Install
$ agentstack add skill-happy-technologies-llc-happy-platform-skills-business-rules ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Business Rules Development
Overview
Business rules are server-side scripts that execute when records are displayed, inserted, updated, deleted, or queried. They are the backbone of ServiceNow automation.
- What problem does it solve? Automates record-level logic, enforces data integrity, and triggers workflows based on record changes
- Who should use this skill? ServiceNow developers building custom automation logic
- Expected outcomes: Well-structured, performant business rules that follow ServiceNow best practices
Prerequisites
- Roles:
adminor scoped app developer role - Knowledge: JavaScript fundamentals, GlideRecord API basics
- Access: sys_script table, target table for business rule
- Related skills:
admin/script-execution,admin/update-set-management
When to Use Business Rules
Timing Matrix
| When | Trigger | Use Case | current/previous | |------|---------|----------|------------------| | before | Insert/Update/Delete | Validate data, set field values, abort operations | Both available | | after | Insert/Update/Delete | Create related records, send notifications, external integrations | Both available | | async | Insert/Update/Delete | Long-running operations, external API calls | Only current | | display | Query/Display | Calculate runtime values, populate scratchpad | Only current |
Decision Guide
Use BEFORE when:
- Setting default values based on other fields
- Validating data before save
- Modifying field values before commit
- Aborting invalid operations with
current.setAbortAction(true)
Use AFTER when:
- Creating child/related records
- Sending notifications (after record is committed)
- Updating other tables
- Triggering workflows
Use ASYNC when:
- Making external REST/SOAP calls
- Processing large data sets
- Operations that can fail without blocking the user
- Long-running calculations
Use DISPLAY when:
- Calculating values for form display only
- Populating g_scratchpad for client scripts
- Runtime-only field values (not stored)
Procedure
Phase 1: Create a Business Rule
Step 1.1: Query Table Schema
First, understand the target table structure.
Using MCP:
Tool: SN-Get-Table-Schema
Parameters:
table_name: incident
Step 1.2: Create Basic Business Rule
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Set Priority Based on Impact and Urgency
collection: incident
active: true
when: before
order: 100
filter_condition: impactCHANGES^ORurgencyCHANGES
script: |
(function executeRule(current, previous /*null when async*/) {
// Calculate priority from impact and urgency matrix
var impact = parseInt(current.impact);
var urgency = parseInt(current.urgency);
// Priority matrix: 1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning
var matrix = {
'1-1': 1, '1-2': 2, '1-3': 3,
'2-1': 2, '2-2': 3, '2-3': 4,
'3-1': 3, '3-2': 4, '3-3': 5
};
var key = impact + '-' + urgency;
var newPriority = matrix[key] || 4;
if (current.priority != newPriority) {
current.priority = newPriority;
gs.info('Priority calculated: ' + newPriority + ' for ' + current.number);
}
})(current, previous);
Step 1.3: Create After Business Rule
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Create Related Task on P1 Incident
collection: incident
active: true
when: after
order: 200
filter_condition: priority=1^stateVALCHANGES1
script: |
(function executeRule(current, previous /*null when async*/) {
// Only on insert or when becoming P1
if (current.operation() == 'insert' ||
(previous && previous.priority != 1)) {
var task = new GlideRecord('sc_task');
task.initialize();
task.short_description = 'P1 Response: ' + current.short_description;
task.description = 'Critical incident requires immediate response.\n\nIncident: ' + current.number;
task.assignment_group = current.assignment_group;
task.assigned_to = current.assigned_to;
task.priority = 1;
task.parent = current.sys_id;
var taskId = task.insert();
gs.info('Created P1 response task: ' + taskId + ' for ' + current.number);
}
})(current, previous);
Step 1.4: Create Async Business Rule
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Notify External System on Incident Create
collection: incident
active: true
when: async
order: 500
action_insert: true
script: |
(function executeRule(current, previous /*null when async*/) {
try {
var request = new sn_ws.RESTMessageV2('External Notification', 'POST');
request.setStringParameterNoEscape('incident_number', current.number.toString());
request.setStringParameterNoEscape('short_description', current.short_description.toString());
request.setStringParameterNoEscape('priority', current.priority.toString());
var response = request.execute();
var httpStatus = response.getStatusCode();
if (httpStatus == 200 || httpStatus == 201) {
gs.info('External notification sent for: ' + current.number);
} else {
gs.error('External notification failed: ' + httpStatus + ' - ' + response.getBody());
}
} catch (e) {
gs.error('External notification error: ' + e.message);
}
})(current, previous);
Phase 2: Using current and previous Objects
Step 2.1: Understanding current vs previous
The current object represents the record being processed. The previous object contains field values before the current transaction.
Key Differences: | Aspect | current | previous | |--------|---------|----------| | Availability | All business rules | before/after only (null in async) | | Modifiable | Yes (before rules) | No (read-only) | | Insert operations | Has values | null | | Values | New/modified | Original before change |
Step 2.2: Detecting Field Changes
Using .changes() Method (Recommended):
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Log State Changes
collection: incident
active: true
when: after
order: 100
script: |
(function executeRule(current, previous /*null when async*/) {
// EFFICIENT: Exit early if field hasn't changed
if (!current.state.changes()) {
return; // No work to do
}
// Get old and new values
var oldState = previous ? previous.state.getDisplayValue() : '(new)';
var newState = current.state.getDisplayValue();
gs.info('Incident ' + current.number + ' state changed: ' + oldState + ' -> ' + newState);
// Add work note
current.work_notes = 'State changed from ' + oldState + ' to ' + newState;
})(current, previous);
Manual Change Detection (When .changes() Not Suitable):
// For complex comparisons or calculated changes
if (previous && current.priority != previous.priority) {
// Priority changed
}
// For reference fields, compare sys_id
if (previous && current.assigned_to.toString() != previous.assigned_to.toString()) {
// Assignment changed
}
// For multiple fields
var fieldsToCheck = ['state', 'priority', 'assigned_to'];
var changedFields = [];
for (var i = 0; i 0) {
gs.info('Changed fields: ' + changedFields.join(', '));
}
Step 2.3: Checking Operation Type
// Determine what triggered the business rule
var operation = current.operation();
switch (operation) {
case 'insert':
// Record is being created
gs.info('New record: ' + current.getTableName());
break;
case 'update':
// Record is being updated
gs.info('Update to: ' + current.getUniqueValue());
break;
case 'delete':
// Record is being deleted
gs.info('Deleting: ' + current.getUniqueValue());
break;
}
Phase 3: Condition Field vs Script Conditions
Step 3.1: Condition Field (Preferred for Simple Conditions)
The condition field uses encoded queries and is evaluated BEFORE the script runs. This is more efficient because:
- Evaluated at the database level
- Script never executes if condition fails
- No JavaScript overhead
Best Practices for Condition Field:
# Only run on active P1 incidents
active=true^priority=1
# Only when state changes to Resolved
stateVALCHANGES6
# Only when assigned_to changes
assigned_toCHANGES
# Multiple conditions (AND)
active=true^priority=1^stateVALCHANGES6
# Only on insert (no previous value for state)
stateISEMPTYfalse^ORstateISEMPTY
Common Condition Operators: | Operator | Meaning | Example | |----------|---------|---------| | CHANGES | Field value changed | stateCHANGES | | VALCHANGES | Changed TO specific value | stateVALCHANGES6 | | CHANGESFROM | Changed FROM specific value | stateCHANGESFROM1 | | = | Equals | priority=1 | | != | Not equals | state!=7 | | ISEMPTY | Field is empty | assigned_toISEMPTY | | ISNOTEMPTY | Field has value | assigned_toISNOTEMPTY |
Step 3.2: Script Conditions (For Complex Logic)
Use script conditions only when the condition field cannot express the logic.
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Complex Condition Example
collection: incident
active: true
when: before
order: 100
condition: |
// Script condition - returns true/false
// Available: current, previous
// Check if escalating (priority going from higher number to lower)
if (current.priority.changes()) {
var oldPri = previous ? parseInt(previous.priority) : 5;
var newPri = parseInt(current.priority);
return newPri 0) {
var errorMsg = errors.join('\n');
gs.addErrorMessage(errorMsg);
current.setAbortAction(true);
gs.info('[Validation BR] Aborted save: ' + errorMsg);
}
})(current, previous);
Phase 6: Performance Optimization
Step 6.1: Use .changes() for Efficiency
WRONG - Always executes full script:
// Inefficient - script runs on every update
(function executeRule(current, previous) {
if (previous && current.state != previous.state) {
// State changed - do work
}
})(current, previous);
RIGHT - Use condition field + .changes():
Filter Condition: stateCHANGES
Script:
(function executeRule(current, previous) {
// Script only runs when state changes
// Additional validation with .changes() for safety
if (current.state.changes()) {
// Do work
}
})(current, previous);
Step 6.2: Minimize GlideRecord Queries
WRONG - Query inside loop:
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
// BAD: Query for each incident
var user = new GlideRecord('sys_user');
user.get(incidents.assigned_to);
// ...
}
RIGHT - Use dot-walking or batch queries:
// Option 1: Dot-walking (single query)
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
var userName = incidents.assigned_to.name; // Dot-walk
var userEmail = incidents.assigned_to.email;
}
// Option 2: Batch query with lookup map
var userIds = [];
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
if (!gs.nil(incidents.assigned_to)) {
userIds.push(incidents.assigned_to.toString());
}
}
var userMap = {};
if (userIds.length > 0) {
var users = new GlideRecord('sys_user');
users.addQuery('sys_id', 'IN', userIds.join(','));
users.query();
while (users.next()) {
userMap[users.sys_id.toString()] = {
name: users.name.toString(),
email: users.email.toString()
};
}
}
Step 6.3: Order and Active Flag Best Practices
| Practice | Recommendation | |----------|----------------| | Order | 100-199 for validation, 200-399 for field setting, 400+ for external/async | | Active | Set to false during development, enable when tested | | Condition | Use filter condition field, not script conditions | | Inheritance | Set "Inherits" carefully - usually leave unchecked |
Step 6.4: Avoid Recursive Updates
DANGEROUS - Can cause infinite loops:
// After business rule on incident
(function executeRule(current, previous) {
current.work_notes = 'Updated at ' + gs.now();
current.update(); // DANGER: Triggers business rules again!
})(current, previous);
SAFE - Set workflow false or use before rules:
// Option 1: Disable workflow on update
var gr = new GlideRecord('incident');
if (gr.get(current.sys_id)) {
gr.setWorkflow(false); // Skip business rules
gr.autoSysFields(false); // Skip sys field updates
gr.work_notes = 'Updated at ' + gs.now();
gr.update();
}
// Option 2: Use before rule instead (preferred)
// Before business rule - modifies current directly
(function executeRule(current, previous) {
current.work_notes = 'Updated at ' + gs.now();
// No .update() needed - current is saved automatically
})(current, previous);
Phase 7: Testing Business Rules
Step 7.1: Query Existing Business Rules
Using MCP:
Tool: SN-Query-Table
Parameters:
table_name: sys_script
query: collection=incident^active=true
fields: name,when,order,filter_condition,active
limit: 50
Step 7.2: Test with Background Script
Using MCP:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Test business rule by simulating record operation
var testIncident = new GlideRecord('incident');
testIncident.initialize();
testIncident.short_description = 'Test incident for BR testing';
testIncident.caller_id = gs.getUserID();
testIncident.impact = 1;
testIncident.urgency = 1;
// Insert will trigger before/after insert rules
var sysId = testIncident.insert();
gs.info('Created test incident: ' + sysId);
// Verify priority was calculated
testIncident.get(sysId);
gs.info('Priority after BR: ' + testIncident.priority.getDisplayValue());
// Clean up (optional)
// testIncident.deleteRecord();
description: Test priority calculation business rule
Step 7.3: Check System Logs
Using MCP:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: messageLIKEbusiness rule^ORmessageLIKE[BR]^sys_created_on>javascript:gs.minutesAgo(10)
fields: message,level,sys_created_on,source
limit: 50
Step 7.4: Local Development with Script Sync
Using MCP:
Tool: SN-Sync-Script-To-Local
Parameters:
script_sys_id: [business_rule_sys_id]
local_path: /scripts/business_rules/validate_incident.js
instance: dev
Then edit locally with your IDE and sync back:
Tool: SN-Sync-Local-To-Script
Parameters:
local_path: /scripts/business_rules/validate_incident.js
script_sys_id: [business_rule_sys_id]
instance: dev
Tool Usage Summary
| Operation | MCP Tool | Purpose | |-----------|----------|---------| | Create BR | SN-Create-Record (sys_script) | Create new business rule | | Update BR | SN-Update-Record | Modify existing business rule | | Query BRs | SN-Query-Table | Find business rules on table | | Test BR | SN-Execute-Background-Script | Simulate record operations | | Debug | SN-Query-Table (syslog) | Check execution logs | | Local Dev | SN-Sync-Script-To-Local | Edit scripts in IDE | | Get Schema | SN-Get-Table-Schema | Understand table structure |
Best Practices
Code Quality
- Use IIFE Pattern: Wrap all scripts in
(function executeRule(current, previous) {...})(current, previous); - **Name Variables
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Happy-Technologies-LLC
- Source: Happy-Technologies-LLC/happy-platform-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.