UNPKG

@servicenow/sdk

Version:
287 lines (215 loc) 11.8 kB
--- tags: [script include, sys_script_include, server-side, reusable logic, GlideAjax, client callable, Class.create, AbstractAjaxProcessor, module bridge, require, facade, legacy, sandboxCallable, sandbox callable, sandbox] --- # Script Includes Guide for creating ServiceNow Script Includes using the Fluent API. Script includes bundle reusable server-side logic into classes or utilities that can be called from business rules, scheduled scripts, other script includes, and client-side code via GlideAjax. ## When to Use - Creating reusable server-side logic (utility classes, helper functions) - Building server-side APIs callable from client code via GlideAjax - Sharing business logic across multiple business rules, scheduled scripts, or other server scripts - Creating AJAX processors for UI Pages or client scripts ## Instructions 1. **Use `Now.include` for scripts:** The ScriptInclude API only accepts strings, so keep JavaScript in a standalone file and reference it with `Now.include('../../server/script-includes/file.js')`. This enables syntax highlighting and two-way sync. Note: Script Include class files (`Class.create()`) should NOT import Glide APIs — they are auto-available in the Script Include execution context. 2. **Use ES5 syntax:** Use the classic `Class.create()` pattern or plain functions. ES6 `class` syntax is not supported on the ServiceNow platform for script includes. 3. **Match `type` to class name:** In the `Class.create()` prototype, the `type` property must exactly match the class name and the `name` property in the Fluent definition. Mismatches cause runtime failures. 4. **Set `clientCallable` for GlideAjax:** If client-side code needs to call the script include, set `clientCallable: true`. Without this, GlideAjax calls will be rejected. 5. **Set `sandboxCallable` only when needed:** Set `sandboxCallable: true` only when the script include will be called from sandbox-evaluated expressions such as `javascript:` query conditions, filter expressions, or column default values. Do not set it by default — it widens the security surface. 6. **Extend `AbstractAjaxProcessor` for AJAX:** When the script include is called via GlideAjax, extend `global.AbstractAjaxProcessor` and use `this.getParameter()` to read client parameters. 7. **Scope accessibility:** Set `accessibleFrom: 'public'` only when other scoped apps need access. Default to `'package_private'` for internal use. ## Key Concepts ### Class-Based vs Classless - **Class-based** (most common): Use `Class.create()` with a prototype. Best for grouping related methods. The `type` property in the prototype must match the class name. - **Classless (on-demand)**: A single function that runs when called by name. Best for one-off utility functions. The function name must match the `name` property. ### GlideAjax Pattern To make a script include callable from client-side code: 1. Set `clientCallable: true` in the Fluent definition 2. Extend `global.AbstractAjaxProcessor` in the JavaScript 3. Use `this.getParameter('sysparm_name')` to read parameters passed from the client 4. Return data as JSON strings for structured responses ### Sandbox Callable Pattern When a sandbox-evaluated expression (e.g., a scheduled script `condition`, `javascript:` filter, or column default value) needs complex logic that can't be written as a single expression, create a `sandboxCallable` script include to hold that logic. The sandbox expression then calls it as a single expression: - Condition field: `new global.MyConditionChecker().shouldRun()` - Filter query: `javascript:new global.MyFilterHelper().getFilterValue()` The script include body runs in the normal server-side context where `var`, `if`, loops, and GlideRecord are fully supported. Only the calling expression is restricted to a single expression. Fluent definition (`src/fluent/script-includes/sync-condition-checker.now.ts`): ```typescript fluent import '@servicenow/sdk/global' import { ScriptInclude } from '@servicenow/sdk/core' import { ScriptInclude } from '@servicenow/sdk/core' ScriptInclude({ $id: Now.ID['SyncConditionChecker'], name: 'SyncConditionChecker', script: Now.include('../../server/script-includes/sync-condition-checker.js'), description: 'Checks whether daily sync should run', sandboxCallable: true, }) ``` JavaScript logic (`src/server/script-includes/sync-condition-checker.js`): ```javascript var SyncConditionChecker = Class.create() SyncConditionChecker.prototype = { initialize: function () {}, shouldRun: function () { if (gs.getProperty('x_myapp.sync.enabled', 'false') !== 'true') { return false } var dayOfWeek = new GlideDateTime().getDayOfWeekLocalTime() if (dayOfWeek < 2 || dayOfWeek > 6) { return false } var gr = new GlideRecord('x_myapp_queue') gr.addQuery('processed', false) gr.setLimit(1) gr.query() return gr.hasNext() }, type: 'SyncConditionChecker', } ``` ### Project Structure ``` src/ server/ script-includes/ my-utils.js <-- JavaScript business logic fluent/ script-includes/ my-utils.now.ts <-- Fluent record definition ``` ### Bridging Modules Through Script Includes Script includes have been the standard way to write reusable server-side code in ServiceNow for years. JavaScript modules are the modern replacement, but many platform features still rely on script includes — GlideAjax, cross-scope APIs, script include-based extension points, and integrations that call script includes by name. When your logic lives in a module but needs to be accessible through one of these legacy mechanisms, create a script include that acts as a thin bridge. **When to use this pattern:** - You have module code that needs to be callable via GlideAjax from client scripts - Another scoped app needs to call your logic by script include name - A platform feature (e.g., dynamic reference qualifier, condition script) expects a script include - You want to keep your business logic in a module (with typed imports, testability, and code reuse) while still being accessible to legacy callers **How it works:** 1. Write your business logic in a module file with ES module syntax (`import`/`export`) 2. Create a thin Script Include wrapper that uses `require()` to load the module and delegates to it 3. Define the Fluent record pointing at the wrapper via `Now.include()` The wrapper `.js` file uses `Class.create` and calls `require()` to pull in the bundled module from `./dist/modules/`. At build time, the SDK bundles your module into the `dist/modules/` directory using repack. At runtime on the platform, `require()` resolves to that bundled output. **Example — exposing a module through a script include:** Module file (`src/modules/server/string-utils.js`): ```javascript export function capitalize(text) { if (!text) return '' return text.charAt(0).toUpperCase() + text.substring(1) } export function truncate(text, maxLength) { if (!text || text.length <= maxLength) return text return text.substring(0, maxLength) + '...' } ``` Wrapper script (`src/server/script-includes/string-utils.js`): ```javascript var StringUtils = Class.create() StringUtils.prototype = { initialize: function () { this._mod = require('./dist/modules/server/string-utils.js') }, capitalize: function (text) { return this._mod.capitalize(text) }, truncate: function (text, maxLength) { return this._mod.truncate(text, maxLength) }, type: 'StringUtils', } ``` Fluent definition (`src/fluent/script-includes/string-utils.now.ts`): ```typescript fluent import '@servicenow/sdk/global' import { ScriptInclude } from '@servicenow/sdk/core' ScriptInclude({ $id: Now.ID['StringUtils'], name: 'StringUtils', script: Now.include('../../server/script-includes/string-utils.js'), description: 'Bridge to string utility module', }) ``` **Key rules for the wrapper:** - Keep the wrapper as thin as possible — it should only `require()` the module and delegate. All business logic belongs in the module. - The wrapper uses `Class.create` and must NOT import Glide APIs (they are auto-available in Script Include context). - The module file MUST import Glide APIs from `@servicenow/glide` (they are NOT auto-available in module context). - The `require()` path points to `./dist/modules/...` — this is the bundled output location. Match the path structure of your module source file under `src/modules/`. - The `type` property, class name, and Fluent `name` must all match exactly. ## Avoidance - **Never use ES6 `class` syntax** -- the ServiceNow server runtime does not support it for script includes - **Never mismatch `type` and class name** -- the `type` property in the prototype, the `Class.create()` variable name, and the `name` in the Fluent definition must all match exactly - **Never forget `clientCallable: true` for GlideAjax** -- client calls will silently fail without it - **Avoid inline scripts** for anything beyond a one-liner -- use `Now.include()` for maintainability and syntax highlighting ## API Reference For the full property reference, see the `scriptinclude-api` topic. ## Examples ### Basic Script Include Fluent definition (`src/fluent/script-includes/math-utils.now.ts`): ```typescript fluent import '@servicenow/sdk/global' import { ScriptInclude } from '@servicenow/sdk/core' export const MathUtils = ScriptInclude({ $id: Now.ID['MathUtils'], name: 'MathUtils', script: Now.include('../../server/script-includes/math-utils.js'), description: 'Basic math utility functions', accessibleFrom: 'package_private', }) ``` JavaScript logic (`src/server/script-includes/math-utils.js`): > **Important:** Script Include class files (`Class.create` pattern) must NOT import Glide APIs — they are auto-available in the Script Include execution context. See the `module-guide` topic. ```javascript var MathUtils = Class.create() MathUtils.prototype = { initialize: function () {}, multiply: function (a, b) { return a * b }, type: 'MathUtils', // IMPORTANT: must match the class name } ``` ### GlideAjax Script Include (Client-Callable) Fluent definition (`src/fluent/script-includes/todo-ajax.now.ts`): ```typescript fluent import '@servicenow/sdk/global' import { ScriptInclude } from '@servicenow/sdk/core' export const TodoAjax = ScriptInclude({ $id: Now.ID['TodoAjax'], name: 'TodoAjax', script: Now.include('../../server/script-includes/todo-ajax.js'), description: 'AJAX processor for to-do operations', clientCallable: true, accessibleFrom: 'public', }) ``` JavaScript logic (`src/server/script-includes/todo-ajax.js`): ```javascript var TodoAjax = Class.create() TodoAjax.prototype = Object.extendsObject(global.AbstractAjaxProcessor, { getTasks: function () { var tasks = [] var gr = new GlideRecord('x_snc_todo_item') gr.query() while (gr.next()) { tasks.push({ sys_id: gr.getUniqueValue(), task: gr.getValue('task'), state: gr.getValue('state'), }) } return JSON.stringify(tasks) }, addTask: function () { var task = this.getParameter('sysparm_task') var state = this.getParameter('sysparm_state') || 'ready' var gr = new GlideRecord('x_snc_todo_item') gr.initialize() gr.setValue('task', task) gr.setValue('state', state) var sysId = gr.insert() return JSON.stringify({ success: true, task: { sys_id: sysId, task: task, state: state }, }) }, type: 'TodoAjax', }) ```