UNPKG

@servicenow/sdk

Version:
233 lines (184 loc) 10.2 kB
--- tags: [business rule, server script, record trigger, sys_script, data validation, cascading updates, audit logging, before, after, async] --- # Business Rules Guide for creating ServiceNow Business Rules using the Fluent API. Business rules are server-side scripts that run automatically when records are queried, updated, inserted, or deleted. ## When to Use - Server-side logic that runs automatically on record operations - Auto-populating or transforming field values on insert or update - Validating data before a record is saved - Cascading changes to related records after an operation - Restricting or filtering queries server-side - Logging or auditing record changes **Note:** For pre-populating `assignment_group` or `assigned_to` on task-inherited tables on record operation, use an Assignment Rule instead. See `assignment-rule-guide`. ## Instructions 1. **Timing first:** Choose the correct `when` value before writing any logic. This is the most critical decision. 2. **Scope the action:** Only subscribe to the actions you need (`insert`, `update`, `delete`, `query`). Never use all four unless truly required. 3. **Use modules for scripts:** Write server-side logic in JavaScript module files with `import`/`export`, then import the function directly in your `.now.ts` file. This gives you typed Glide APIs, code reuse, and full IDE support. See the `module-guide` topic for details. 4. **Table scoping:** Always use the full scoped table name (e.g., `x_myapp_tablename`). 5. **Order matters:** Set an appropriate execution order for rules that may interact. Lower numbers run first. Default is 100. 6. **Conditions over scripts:** Prefer `filterCondition` to limit when the rule fires rather than putting guard clauses inside the script. The platform evaluates conditions before loading the script. ## API Reference See the `businessrule-api` topic for the full property reference. ## Key Concepts ### Choosing the Right Timing - **`before`** -- Runs before the record operation. Use for modifying the current record before save or validating/aborting. - **`after`** -- Runs after the record operation. Use when you need the final saved state or need to update related records. - **`async`** -- Runs asynchronously after the operation completes. Use for expensive operations that shouldn't block the user. - **`display`** -- Runs when the record is displayed. Use for calculated values shown on the form but not stored. ### Before vs After - `before` rules can modify `current` and the changes persist -- the record hasn't been written yet. - `after` rules cannot modify `current` effectively -- the record is already saved. To change fields after save, you must do a separate GlideRecord update. - `before` rules with `abortAction: true` prevent the record operation entirely. - `after` rules cannot abort -- the operation has already completed. ### Script File Pattern (Modules) Business rule scripts should be written as JavaScript modules and imported directly in the `.now.ts` file. Modules provide typed Glide API imports, code reuse, and full IDE support. See the `module-guide` topic for details. ```typescript fluent import { validateCategory } from '../../server/business-rules/validate-category' BusinessRule({ $id: Now.ID['validate-category'], name: 'Validate Category', table: 'x_myapp_item', when: 'before', action: ['insert', 'update'], script: validateCategory, }) ``` The module file exports a function that receives `current` and `previous` GlideRecords: ```typescript fluent // src/server/business-rules/validate-category.ts import { GlideRecord } from '@servicenow/glide' export function validateCategory(current: GlideRecord<'x_myapp_item'>, previous: GlideRecord<'x_myapp_item'>) { const category = current.getValue('category'); if (!category) { current.setValue('category', 'general'); } } ``` > **Note:** The BusinessRule API accepts both functions and strings for its `script` property, so modules work here. For existing non-modular scripts (IIFE-wrapped), you can also use `Now.include()`. Not all APIs support modules — if the compiler or build reports a type mismatch when you pass a module import to a `script` property, the API is string-only and you should use `Now.include()` instead. See the `now-include-guide` topic. ## Examples ### Before Insert -- Set Defaults and Validate Set default field values on new records and abort if validation fails. ```typescript fluent import { BusinessRule } from '@servicenow/sdk/core' import { setRequestDefaults } from '../../server/business-rules/set-request-defaults' BusinessRule({ $id: Now.ID['set-request-defaults'], name: 'Set Request Defaults', table: 'x_myapp_request', when: 'before', action: ['insert'], order: 100, script: setRequestDefaults, }) ``` ```typescript fluent // src/server/business-rules/set-request-defaults.ts import { gs, GlideRecord } from '@servicenow/glide' export function setRequestDefaults(current: GlideRecord<'x_myapp_request'>, previous: GlideRecord<'x_myapp_request'>){ current.setValue('state', 'new'); current.setValue('priority', '4'); const title = current.getValue('short_description'); if (!title) { gs.addErrorMessage('Short description is required'); current.setAbortAction(true); } } ``` ### After Update -- Cascade Changes to Related Records When a parent record's state changes, update all child task records. ```typescript fluent import { BusinessRule } from '@servicenow/sdk/core' import { cascadeProjectState } from '../../server/business-rules/cascade-project-state' BusinessRule({ $id: Now.ID['cascade-project-state'], name: 'Cascade Project State to Tasks', table: 'x_myapp_project', when: 'after', action: ['update'], filterCondition: 'stateVALCHANGES', script: cascadeProjectState, }) ``` ```typescript fluent // src/server/business-rules/cascade-project-state.ts import { GlideRecord } from '@servicenow/glide' export function cascadeProjectState(current: GlideRecord<'x_myapp_project'>, previous: GlideRecord<'x_myapp_project'>) { const newState = current.getValue('state'); if (newState === 'cancelled') { const tasks = new GlideRecord('x_myapp_task'); tasks.addQuery('project', current.getUniqueValue()); tasks.addQuery('state', '!=', 'closed'); tasks.query(); while (tasks.next()) { tasks.setValue('state', 'cancelled'); tasks.update(); } } } ``` ### Async -- Heavy Processing Without Blocking Send an external notification after record creation without making the user wait. ```typescript fluent import { BusinessRule } from '@servicenow/sdk/core' import { notifyExternalSystem } from '../../server/business-rules/notify-external' BusinessRule({ $id: Now.ID['async-external-notify'], name: 'Notify External System', table: 'x_myapp_order', when: 'async', action: ['insert'], priority: 100, script: notifyExternalSystem, }) ``` ```typescript fluent // src/server/business-rules/notify-external.ts import { gs, GlideRecord } from '@servicenow/glide' import { RESTMessageV2 } from '@servicenow/glide/sn_ws' export function notifyExternalSystem(current: GlideRecord<'x_myapp_order'>, previous: GlideRecord<'x_myapp_order'>): void { const request = new RESTMessageV2('x_myapp.OrderAPI', 'post'); request.setStringParameterNoEscape('order_id', current.getUniqueValue()); request.setStringParameterNoEscape('status', current.getValue('state')); const response = request.execute(); if (response.getStatusCode() !== 200) { gs.error('External notification failed: ' + response.getBody()); } } ``` ### Display -- Add Info Messages Show contextual information when a user views a record, without modifying stored data. ```typescript fluent import { BusinessRule } from '@servicenow/sdk/core' import { displayOverdueWarning } from '../../server/business-rules/display-overdue-warning' BusinessRule({ $id: Now.ID['display-overdue-warning'], name: 'Show Overdue Warning', table: 'x_myapp_request', when: 'display', addMessage: true, message: 'This request is past its due date.', filterCondition: 'due_date<javascript:gs.nowDateTime()', script: displayOverdueWarning, }) ``` ```typescript fluent // src/server/business-rules/display-overdue-warning.ts import { gs, GlideRecord } from '@servicenow/glide' export function displayOverdueWarning(current: GlideRecord<'x_myapp_request'>, previous: GlideRecord<'x_myapp_request'>): void { const daysPastDue = gs.dateDiff( current.getValue('due_date'), gs.nowDateTime(), true ); gs.addInfoMessage('This request is ' + daysPastDue + ' day(s) overdue.'); } ``` ## Avoidance - **Never call `current.update()` in a before rule** -- the record has not been saved yet. Modifying `current` fields directly is sufficient; calling `update()` causes a redundant save and can trigger infinite loops. - **Never modify `current` in an after rule** expecting it to persist -- the record is already saved. Use a separate GlideRecord update if you need to change the same record. - **Never use display rules for data changes** -- display rules run on form load and should only add messages or set scratchpad values, not modify stored fields. - **Never use `query` action without careful consideration** -- it runs on every single query against the table, including system queries, and can severely degrade performance. - **Never use `async` for logic that must complete before the user sees the result** -- async rules run in a separate transaction with no timing guarantee. - **Prefer `filterCondition` over script-based filtering** -- the platform evaluates conditions before loading the script, which is more efficient and easier to maintain than guard clauses inside `script`. - **Avoid using a Business Rule to pre-populate `assignment_group` on a task-inherited table** -- Use Assignment Rules (`sysrule_assignment`). They have built-in `order`, `condition`, `match_conditions`, and `group`/`user` fields better to maintain. See the `assignment-rule-guide` topic.