This commit is contained in:
Jamiu Awoke 2026-08-14 08:55:38 -07:00 committed by GitHub
commit 46c59ade78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 10638 additions and 0 deletions

230
skills/servicenow/SKILL.md Normal file
View File

@ -0,0 +1,230 @@
---
name: servicenow
description: "ServiceNow platform engineering assistant. Use when the task is ServiceNow or SNOW development and architecture involving GlideRecord, Business Rules, Flow Designer, Service Portal, UI Builder, CMDB, MID Server, update sets, ServiceNow APIs, ITSM, ITOM, ITAM/SAM, FSM, SPM, SecOps, IntegrationHub, vulnerabilities, security incidents, assets, work orders, or service modeling. Do not use for non-ServiceNow JavaScript."
license: MIT
compatibility: "Works in Claude.ai, Claude Code, and API. Bundled CLI helper requires Python 3.8+."
metadata:
author: "Jamiu Awoke"
version: "1.0.0"
category: "enterprise-development"
tags: "servicenow, itsm, itom, itam, fsm, spm, secops, gliderecord, flow-designer, service-portal, now-experience"
---
# ServiceNow Developer Skill
ServiceNow development and architecture guidance across classic scripting, platform configuration, workflow automation, workspace UX, domain apps, and enterprise delivery practices.
## Instructions
### Step 1: Triage the request before writing code
Always identify:
1. The product family or workflow domain.
2. Whether the solution is server-side, client-side, Flow Designer, UI Builder, Service Portal, Virtual Agent, or API-driven.
3. Whether the user is working in global scope or a scoped app.
4. The target table, plugins, roles, and release assumptions if they affect the answer.
5. Whether the task is read-only, transactional, or security-sensitive.
Load the minimum set of reference files that matches the task. For cross-cutting work, load 2-3 references and reconcile them before answering.
| Need | Reference File |
|------|---------------|
| Incident, Problem, Change, Knowledge, assignment, SLA, reporting | `references/itsm.md` |
| GlideRecord, GlideAggregate, GlideSystem, GlideAjax, GlideForm, dates | `references/glide-api.md` |
| Business Rules, Script Includes, Client Scripts, UI Policies, UI Actions, jobs | `references/scripting-patterns.md` |
| REST API, Scripted REST, Table API, outbound REST, Import Sets | `references/rest-api.md` |
| Flow Designer, subflows, spokes, script actions, PAD | `references/flow-designer.md` |
| Playbooks and workspace automation patterns | `references/playbooks.md` |
| UI Builder, Now Experience, Workspace components, mobile workspace | `references/now-experience.md` |
| ACLs, roles, scoped security, SecOps, audit/compliance | `references/security.md` |
| Performance, indexing, caching, query tuning, client perf | `references/performance.md` |
| ATF, instance scan, debugging, regression coverage | `references/testing.md` |
| Catalog Items, Record Producers, Order Guides, fulfillment | `references/service-catalog.md` |
| Notifications, mail scripts, inbound email, SMS, push | `references/notifications.md` |
| Update Sets, source control, pipelines, CI/CD APIs | `references/update-sets-cicd.md` |
| Discovery, MID Server, Event Management, Service Mapping, HLA | `references/itom.md` |
| HRSD, CSM, lifecycle events, entitlements, portals | `references/hrsd-csm.md` |
| Service Portal widgets, `$sp`, Angular providers, theming | `references/service-portal.md` |
| App Engine Studio, scoped apps, low-code architecture | `references/app-engine.md` |
| Virtual Agent, NLU, topic design, handoff patterns | `references/virtual-agent.md` |
| CMDB, Predictive Intelligence, Agent Workspace, Employee Center, upgrades | `references/advanced.md` |
| ITAM, HAM, SAM, stockrooms, contracts, reclamation | `references/itam.md` |
| FSM, work orders, dispatch, scheduling, mobile technician flows | `references/fsm.md` |
| IRM, GRC, policy/compliance, vendor risk, SecOps workflows | `references/irm-secops.md` |
| SPM/PPM, demand, portfolios, projects, resource plans, Agile 2.0 | `references/spm.md` |
| Vulnerable items, vulnerability groups, remediation and exception workflows | `references/vulnerability-response.md` |
| Security incidents, containment, eradication, response tasks, forensic workflows | `references/security-incident-response.md` |
| SAM Pro, entitlements, reclamation, normalization, publishers, software models | `references/sam-pro.md` |
| CSDM, service taxonomy, ownership, CMDB governance, health models | `references/csdm-cmdb-governance.md` |
| IntegrationHub, spokes, Connection & Credential Aliases, custom actions, Data Stream Actions | `references/integrationhub.md` |
### Step 2: Apply ServiceNow platform standards
All generated code and recommendations should follow these rules:
1. Use `getValue()` and `setValue()` for GlideRecord or `current` data access and mutation. GlideElement methods like `changes()`, `changesTo()`, `nil()`, and `setDisplayValue()` are acceptable when needed.
2. Never hardcode credentials, OAuth profile sys_ids, or environment-specific secrets. Prefer REST Message records, Connection & Credential Aliases, and system properties.
3. In scoped apps, treat `GlideRecord.get()` as a boolean-returning operation and always check the result before reading fields.
4. Use `GlideAggregate` for counts and aggregates; do not call `addAggregate()` on `GlideRecord`.
5. Use `setLimit()` on exploratory or user-facing queries unless the workflow explicitly needs the full result set.
6. Prefer encoded queries only for static, known-safe complex filters. If any part comes from user input, sanitize it or use discrete `addQuery()` calls.
7. Use async Business Rules, events, or scheduled jobs for heavy work. Do not block the transaction with slow integrations.
8. Avoid `current.update()` in Before Business Rules. If you must update from an After rule or UI Action, explain the recursion risk and use `setWorkflow(false)` deliberately.
9. Use async `GlideAjax` patterns only; never use `getXMLWait()`.
10. Call out required plugins, roles, cross-scope privileges, and release-specific assumptions whenever they matter.
11. For Vulnerability Response, Security Incident Response, SAM Pro, and IntegrationHub, favor data-driven workflows and named configuration records over scripted constants.
### Step 3: Validate before you return anything
Before responding:
- Confirm the code matches the correct execution context.
- Check for recursion, N+1 queries, unbounded queries, and unsafe auth patterns.
- Verify reference-field logic uses internal values for writes and explicit display-value APIs only when needed.
- Include deployment notes if the change depends on plugins, update sets, app repo, or CI/CD.
- Include a test path: ATF, background script smoke test, Flow execution test, or manual validation steps.
### Common gotchas
- `gr.field` returns a GlideElement, not a plain string. Use `getValue()` unless you specifically need GlideElement behavior.
- `g_form`, `g_user`, and `g_list` are client APIs only.
- `getRowCount()` is not a cheap count.
- `addEncodedQuery(userInput)` is an injection risk.
- Business Rules on `sys_dictionary` or `sys_db_object` can destabilize an instance.
- Querying by display value such as `assignment_group.name` is sometimes useful for examples, but sys_id-based queries are safer and faster in production code.
- Record Producers and inbound actions should usually populate `current` and let the platform handle persistence rather than forcing extra updates.
- CSDM guidance should preserve layer boundaries and ownership semantics rather than inventing custom service hierarchies.
- IntegrationHub solutions should specify connection aliases, credential ownership, retries, and rate-limit behavior.
### Fast patterns
Use these without loading a reference file when the request is simple and clearly matches the pattern.
#### Safe GlideRecord query
```javascript
var gr = new GlideRecord('incident');
gr.addActiveQuery();
gr.addQuery('priority', '1');
gr.setLimit(50);
gr.query();
while (gr.next()) {
gs.info(gr.getValue('number') + ' ' + gr.getValue('short_description'));
}
```
#### Scoped-safe single-record lookup
```javascript
var user = new GlideRecord('sys_user');
if (!user.get(userSysId)) {
gs.addErrorMessage('User not found');
return;
}
gs.info(user.getValue('email'));
```
## CLI Helper
```bash
# Generate queries
python3 skills/servicenow/scripts/snow_helper.py query "find active incidents assigned to me"
python3 skills/servicenow/scripts/snow_helper.py query "find all work orders in state Dispatch Pending"
# Generate templates
python3 skills/servicenow/scripts/snow_helper.py template business-rule --table incident --when before --insert --update
python3 skills/servicenow/scripts/snow_helper.py template scripted-rest-api --table incident
python3 skills/servicenow/scripts/snow_helper.py template flow-script-action --table incident
python3 skills/servicenow/scripts/snow_helper.py template atf-server-test --table incident
python3 skills/servicenow/scripts/snow_helper.py template integrationhub-action --table incident
# Lint a script
python3 skills/servicenow/scripts/snow_helper.py lint < myscript.js
# Validate the skill package and bundled references
python3 skills/servicenow/scripts/snow_helper.py validate-skill skills/servicenow
```
## Examples
### Example 1: ITSM query
**User says:** "Write a query to find all P1 incidents opened this week"
**Actions:** Load `references/glide-api.md` and `references/itsm.md`, generate a safe query, and check for limits and boolean `get()` usage.
### Example 2: Scripted REST
**User says:** "Create a REST endpoint that returns user details by employee number"
**Actions:** Load `references/rest-api.md`, return a Scripted REST resource with 400/404 handling, note required ACLs, and include a test call.
### Example 3: ITAM lifecycle
**User says:** "How do I reclaim laptops from terminated employees and update stockroom state?"
**Actions:** Load `references/itam.md` and `references/flow-designer.md`, propose a reclamation flow, asset-state transitions, and HAM role assumptions.
### Example 4: FSM dispatch
**User says:** "Build a flow to auto-assign field work orders by territory and skills"
**Actions:** Load `references/fsm.md` and `references/flow-designer.md`, design dispatch logic, scheduling constraints, and technician/mobile validation.
### Example 5: SecOps / IRM
**User says:** "Create a Vulnerability Response integration that opens exceptions only for approved compensating controls"
**Actions:** Load `references/irm-secops.md`, `references/security.md`, and `references/rest-api.md`, then return data model, approval logic, and audit requirements.
### Example 6: SPM
**User says:** "Model demand intake and project creation with resource plans"
**Actions:** Load `references/spm.md` plus `references/app-engine.md` or `references/flow-designer.md`, then return table/process guidance and portfolio governance notes.
### Example 7: Vulnerability Response
**User says:** "Build triage logic for vulnerable items that routes exceptions to security governance"
**Actions:** Load `references/vulnerability-response.md`, `references/irm-secops.md`, and `references/integrationhub.md`, then return routing logic, exception controls, and audit requirements.
### Example 8: Security Incident Response
**User says:** "Create a security incident workflow for containment and forensic handoff"
**Actions:** Load `references/security-incident-response.md`, `references/flow-designer.md`, and `references/security.md`, then return lifecycle design, task model, and escalation rules.
### Example 9: SAM Pro
**User says:** "Reconcile software entitlements against installs and flag reclaim candidates"
**Actions:** Load `references/sam-pro.md` and `references/itam.md`, then return normalization assumptions, compliance logic, and reclamation workflow.
### Example 10: CSDM / CMDB governance
**User says:** "Model business applications and services using CSDM and clean up ownership gaps"
**Actions:** Load `references/csdm-cmdb-governance.md` and `references/advanced.md`, then return class mapping, governance steps, and validation queries.
### Example 11: IntegrationHub
**User says:** "Create a custom spoke action that streams paginated records from an external API"
**Actions:** Load `references/integrationhub.md` and `references/rest-api.md`, then return the action contract, credential strategy, pagination handling, and retry notes.
## Troubleshooting
| Error | Cause | Solution |
|-------|-------|----------|
| "Record not found" in Scripted REST | `gr.get()` result not checked | Use `if (!gr.get(id)) { ... }` before reading fields |
| Flow script count logic fails | `addAggregate()` called on `GlideRecord` | Replace with `GlideAggregate` |
| REST call works in dev only | Auth or endpoint details hardcoded in script | Move auth and endpoint configuration into REST Message records or properties |
| Record Producer saves duplicate changes | Script mutates `current` and then calls `current.update()` | Set values on `current` and let the producer persist the record |
| Workspace component fails in scoped app | Missing cross-scope access or wrong data resource | Verify scope privileges and UI Builder data-resource config |
| Query is slow in production | Missing index or querying by display value | Move to sys_id/internal values, add indexes where justified, and limit record scans |
| Vulnerability automation keeps misrouting records | Grouping logic ignores state, source, or exception posture | Use vulnerability-grouping rules, explicit exception states, and table-driven routing |
| Security incident process stalls | No clear transition between containment, eradication, and recovery | Model phase ownership explicitly and create response tasks per phase |
| IntegrationHub action works manually but fails in flow | Missing connection alias or action outputs are underspecified | Validate alias binding, outputs, and error contracts in the action definition |
## Release Notes
Treat release behavior as instance-specific. If a feature depends on family release or plugin state, say so explicitly and ask which release the customer is on.
Recent family names often referenced in examples:
- Zurich
- Xanadu
- Washington DC

View File

@ -0,0 +1,417 @@
# Advanced ServiceNow Topics
## Predictive Intelligence
### Classification Model
```
Navigate: Predictive Intelligence > Prediction Definitions
Use case: Auto-classify incident category/assignment group
Configuration:
Table: incident
Training filter: active=false^resolved_atISNOTEMPTY (closed incidents)
Input fields (features):
- short_description (NLP)
- description (NLP)
- category
- caller_id.department
Prediction field: assignment_group
Training schedule: Weekly
Minimum records: 5,000+ for good accuracy
Confidence threshold: 0.7 (70%)
```
### Similarity Model
```
Navigate: Predictive Intelligence > Similarity Definitions
Use case: Find similar incidents for faster resolution
Configuration:
Table: incident
Active filter: active=true
Similarity fields:
- short_description (weight: 5)
- description (weight: 3)
- category (weight: 2)
- cmdb_ci (weight: 4)
Max similar results: 5
Minimum score: 0.6
```
### Using Predictions in Scripts
```javascript
// Check prediction result
var gr = new GlideRecord('incident');
if (!gr.get(incidentSysId)) {
gs.warn('Incident not found: ' + incidentSysId);
return;
}
// Prediction fills ml_prediction field
var prediction = gr.getValue('ml_prediction');
var confidence = gr.getValue('ml_confidence');
if (parseFloat(confidence) > 0.8) {
// Auto-assign if high confidence
gr.setValue('assignment_group', prediction);
gr.update();
}
```
### Now Assist (GenAI) — Xanadu+
```
Features:
- Summarize incident/case
- Generate resolution notes
- Suggest knowledge articles
- Code assist for developers
- Virtual Agent conversation improvement
Configuration:
Navigate: Now Assist > Setup
Enable: per-table, per-use-case
Model: ServiceNow LLM or bring-your-own (Azure OpenAI, etc.)
```
## Virtual Agent
### NLU Model Setup
```
Navigate: Virtual Agent > NLU Workbench
Model: My IT Support NLU
Intents:
- Reset Password
Utterances:
"I need to reset my password"
"forgot my password"
"can't log in to my account"
"password expired"
"change my password"
- Report Outage
Utterances:
"service is down"
"system is not working"
"email is broken"
"can't access the VPN"
Entities:
- Application (software, email, VPN, SAP, etc.)
- Priority (urgent, critical, normal, low)
- Location (office, remote, building names)
Training: Requires 15+ utterances per intent for good accuracy
```
### Topic Design
```
Topic: Reset Password
Trigger: NLU Intent = "Reset Password"
Flow:
1. Bot: "I can help you reset your password. Which system?"
→ Collect: Application entity
2. Bot: "Let me verify your identity. What's your employee ID?"
→ Collect: employee_id (text input)
3. Automation: Call Subflow "Verify Identity"
→ Input: employee_id
→ Output: verified (boolean)
4. Decision: verified?
→ Yes:
Automation: Call Subflow "Reset Password"
Bot: "Your password has been reset. Check your email for instructions."
→ No:
Bot: "I couldn't verify your identity. Let me connect you with an agent."
→ Transfer to Live Agent (queue: Password Reset)
```
### Virtual Agent Script Block
```javascript
// Server-side script in VA topic
(function execute(inputs, outputs) {
var userId = inputs.employee_id;
var gr = new GlideRecord('sys_user');
gr.addQuery('employee_number', userId);
gr.setLimit(1);
gr.query();
if (gr.next()) {
outputs.user_found = true;
outputs.user_name = gr.getValue('name');
outputs.user_sys_id = gr.getUniqueValue();
} else {
outputs.user_found = false;
}
})(inputs, outputs);
```
## CMDB Best Practices
### CI Lifecycle
```
States: Ordered → Received → In Stock → In Use → In Maintenance → Retired → Absent
Key tables:
cmdb_ci — Base CI class
cmdb_ci_server — Servers
cmdb_ci_app_server — Application servers
cmdb_ci_service — Business services
cmdb_ci_db_instance — Database instances
cmdb_ci_network_adapter — Network devices
cmdb_rel_ci — CI relationships
```
### CMDB Query Patterns
```javascript
// Find all CIs in a service map
var gr = new GlideRecord('cmdb_rel_ci');
gr.addQuery('parent', businessServiceSysId);
gr.addQuery('type.name', 'Depends on::Used by');
gr.query();
while (gr.next()) {
var childCI = gr.getDisplayValue('child');
var relType = gr.getDisplayValue('type');
gs.info(childCI + ' (' + relType + ')');
}
// Find all incidents related to a CI (including children)
var cis = new SNC.CMDBUtil().getAllChildren(ciSysId); // includes self
var gr = new GlideRecord('incident');
gr.addQuery('cmdb_ci', 'IN', cis);
gr.addActiveQuery();
gr.query();
```
### CMDB Health Dashboard
```
Navigate: CMDB > CMDB Health
Metrics:
- Completeness: % of required fields populated
- Compliance: CIs following naming standards
- Relationships: orphaned CIs, missing dependencies
- Staleness: CIs not updated by discovery in X days
- Duplicates: potential duplicate CIs
Health Checks:
Navigate: CMDB Health > CMDB Health Checks
- Missing relationships
- Orphan CIs (no service mapping)
- Stale CIs (no recent discovery)
- Duplicate detection rules
```
### Discovery & Service Mapping
```
Key components:
- Discovery: Auto-populate CMDB from network scans
- Service Mapping: Map CI relationships (application dependencies)
- MID Server: On-premise proxy for discovery
// Trigger discovery programmatically
var discovery = new SNC.DiscoveryAPI();
discovery.discoverIP('10.0.0.1');
// Check last discovery
var gr = new GlideRecord('cmdb_ci_server');
if (gr.get(serverSysId)) {
gs.info('Last discovered: ' + gr.getValue('last_discovered'));
}
```
## Agent Workspace Configuration
### Custom Record Page
```
Navigate: Workspace Experience > Workspace Configuration > [workspace]
Record Page layout:
Primary tab:
- Form fields (configurable sections)
- Related lists
Contextual sidebar:
- Activity Stream (default)
- Similar Records (Predictive Intelligence)
- AI-generated summary (Now Assist)
- Related CIs
- Playbook panel
Configurable per table:
incident, problem, change_request, sc_request, etc.
```
### Agent Assist
```
Features (Xanadu+):
- Recommended knowledge articles
- Similar incident resolutions
- Auto-generated summaries
- Suggested next actions
Configuration:
Navigate: Agent Workspace > Agent Assist Configuration
Enable per table/workspace
Configure recommendation sources
```
## Employee Center
### Content Management
```
Navigate: Employee Center > Content Management
Content types:
- Announcements
- Knowledge articles
- Quick links
- Catalog items
- Video content
Content blocks:
- Rich text
- Image carousel
- Catalog category browser
- Knowledge search
- My requests/approvals widget
```
### Custom Catalog Items
```
Navigate: Service Catalog > Catalog Items
Item: Request New Laptop
Variables:
- Laptop model (Select Box: Standard, Performance, Executive)
- Business justification (Multi-line text, mandatory)
- Preferred delivery date (Date)
- Manager approval required: true
Workflow/Flow:
1. Manager approval
2. IT approval (if Executive model)
3. Create procurement task
4. Delivery scheduling
5. Asset assignment
```
## Domain Separation
### Key Concepts
```
- Domain: logical partition of data (e.g., Company A, Company B)
- Domain visibility: which domains a user can see
- Domain context: current working domain
- Global domain: visible to all domains
Configuration:
Navigate: System Properties > Domain Support
Enable: glide.sys.domain.enable = true (instance-level setting)
Tables:
domain — Domain definitions
sys_user.sys_domain — User's primary domain
[any table].sys_domain — Record's domain
```
### Scripting with Domains
```javascript
// Get current domain
var currentDomain = gs.getSession().getCurrentDomainID();
// Query within domain (automatic if domain separation is on)
var gr = new GlideRecord('incident');
gr.addQuery('sys_domain', domainSysId);
gr.query();
// Create record in specific domain
var gr = new GlideRecord('incident');
gr.initialize();
gr.setValue('sys_domain', targetDomainSysId);
gr.setValue('short_description', 'Domain-specific incident');
gr.insert();
```
## Upgrade Planning
### Pre-Upgrade Checklist
```
1. Instance Scan — identify customizations on OOB records
2. Skipped records report — review sys_update_xml where remote_update_name is populated
3. Custom table review — check for deprecated API usage
4. Plugin compatibility — verify installed plugins are supported
5. Integration testing — test all external integrations
6. Performance baseline — document current metrics for comparison
```
### Upgrade-Safe Development Patterns
```javascript
// DON'T modify OOB Business Rules — create new ones
// DON'T modify OOB Script Includes — extend them
// DON'T modify OOB ACLs — add new ACLs
// DO: Extend OOB Script Includes
var MyIncidentUtils = Class.create();
MyIncidentUtils.prototype = Object.extendsObject(IncidentUtils, {
// Add your custom methods here
myCustomMethod: function() {
// Custom logic that won't be overwritten on upgrade
},
type: 'MyIncidentUtils'
});
// DO: Use sys_properties instead of hardcoding values
var threshold = gs.getProperty('x_myapp.escalation_threshold', '4');
// DO: Use update sets for ALL changes
// DO: Test upgrades in sub-prod first
// DO: Document all customizations
```
## Multi-Instance Architecture
```
Patterns:
- Dev → Test → Staging → Prod (standard pipeline)
- Hub-and-spoke (central instance + departmental instances)
- Geo-distributed (regional instances with data sync)
Data replication:
- Update sets: configuration deployment
- Data replication (replication_queue): runtime data sync
- MID Server: on-premise connectivity for each instance
- IntegrationHub: cross-instance API calls
```
## Delegated Development
```
App Engine Studio:
Navigate: App Engine Studio
Low-code development for:
- Tables and fields
- Forms and lists
- Flows and automation
- Workspaces
- Dashboards
Guard rails:
- Template-based development
- Admin review before publish
- Scoped application only
- Cannot modify OOB tables
- Limited scripting capability
```

View File

@ -0,0 +1,223 @@
# App Engine Studio & Low-Code Development
## App Engine Studio (AES)
### Overview
App Engine Studio is the low-code/no-code environment for building custom applications on the ServiceNow platform. It provides a guided experience for creating tables, forms, flows, and portals without deep platform knowledge.
### Getting Started
```
Navigate: App Engine Studio (separate URL: /now/app-engine-studio)
Creating an App:
1. Click "Create app"
2. Enter name, description, scope prefix
3. Choose template (blank, or pre-built starter)
4. AES creates: scoped app, tables, roles automatically
App structure (auto-generated):
- Scoped application record
- App-specific roles (user, admin)
- App menu and modules
- Default table with standard fields
```
### Table Builder
```
Visual table creation:
1. Navigate to Data tab in AES
2. Click "Add a table"
3. Options:
- Create from scratch
- Extend an existing table (e.g., extend task)
- Upload spreadsheet (auto-create from CSV/Excel)
Field types available:
- String, Integer, Decimal, Boolean
- Date, Date/Time, Duration
- Reference (to other tables)
- Choice (dropdown)
- HTML, URL, Email
- Attachment
- Journal (work notes, comments)
- Document ID, Conditions
Extending task table benefits:
- Inherits: assignment, state, SLA, approval
- Works with Flow Designer triggers
- Works with Playbooks
- Shows in Agent Workspace
```
### Form Builder
```
Drag-and-drop form designer:
- Add/remove fields
- Create sections and tabs
- Set field properties (mandatory, read-only)
- Configure related lists
- Add annotations (instructions, separators)
Form layout:
- 1-column or 2-column sections
- Section visibility conditions
- Field-level conditions (show/hide)
- Formatters (activity stream, process flow)
```
### Experience Builder (Portals)
```
Create user-facing portals:
1. Navigate to Experience tab in AES
2. Choose template (Record List, Record Detail, Landing)
3. Configure pages and navigation
4. Apply branding (logo, colors, fonts)
Page types:
- Landing page (dashboard/overview)
- List page (filterable record list)
- Record page (form view)
- Custom page (free-form layout)
Components:
- Data tables
- Charts and reports
- Rich text blocks
- Image and video
- Embedded flows
```
### Logic & Automation in AES
```
Built-in automation:
- Flow Designer integration (visual flow builder)
- Business Rules (guided creation)
- Notifications (template-based)
- Approval flows (configurable)
AES-created flows:
- Trigger: record insert/update on app table
- Actions: assignments, notifications, approvals
- No scripting required for basic workflows
```
## Guided App Creator (GAC)
### Templates
```
Pre-built app templates:
- Request Management (intake, approval, fulfillment)
- Case Management (customer cases, tracking)
- Project Tracking (tasks, milestones, assignments)
- Approval Workflow (multi-level approval chain)
- Knowledge Management (articles, categories)
Each template includes:
- Pre-configured tables and fields
- Standard workflows
- Basic portal pages
- Default roles and ACLs
```
## Collaboration & Governance
### Delegated Development
```
App Engine provides development guardrails:
Developer roles:
- sn_app_eng_studio.user — can build apps
- sn_app_eng_studio.admin — can manage/approve apps
Governance:
- App intake process (request → approve → develop → deploy)
- Template restrictions (limit what devs can create)
- Instance scan integration (check quality before deploy)
- Pipeline deployment (Dev → Test → Prod via CI/CD)
```
### Collaboration Features
```
AES supports team development:
- Multiple developers per app
- Source control integration (Git)
- Version management
- Branching support
- Change tracking (who modified what)
App Deployment:
- Publish to App Repository
- Install via instance CI/CD pipeline
- Or deploy via Update Sets (legacy)
```
## Scripting in Scoped Apps
### Scoped App Restrictions
```javascript
// Scoped apps have limited API access compared to global scope
// ALLOWED in scoped apps:
var gr = new GlideRecord('my_scoped_table'); // Own tables
gr.addQuery('active', true);
gr.query();
var sys = new GlideSystem(); // Limited GlideSystem methods
gs.info('Scoped log message');
gs.getProperty('x_myco_myapp.my_property'); // Own properties only
// RESTRICTED in scoped apps (need cross-scope access):
// - Direct access to tables in other scopes
// - System properties not in your scope
// - GlideRecord on system tables (need explicit access)
// Cross-scope access pattern:
// 1. Create a Script Include in global scope (or target scope)
// 2. Mark it as "Accessible from: All application scopes"
// 3. Call it from your scoped app
```
### Application Properties
```javascript
// Scoped app properties (editable per-environment)
// Navigate: sys_properties_list.do?sysparm_query=name STARTSWITH x_myco_myapp
// Set property
gs.setProperty('x_myco_myapp.max_retries', '3');
// Get property
var maxRetries = parseInt(gs.getProperty('x_myco_myapp.max_retries', '5'));
// App-specific system property categories
// Navigate: System Properties > Categories
// Create category for your app's properties
```
### Application Menus & Modules
```
Auto-created by AES, but can customize:
Table: sys_app_application (Menu)
Table: sys_app_module (Module)
Module types:
- List: show table records
- URL: link to any page
- Filter: pre-filtered list view
- Report: link to a report
- Separator: visual divider
- Content Page: static content
```
## App Engine Best Practices
1. **Extend task** for any work-tracking table — get SLA, assignment, state machine for free
2. **Use AES for citizen developers** — guided experience prevents platform damage
3. **Governance first** — set up intake/approval before opening AES to developers
4. **Instance scan** every app before deployment — catch issues early
5. **Scoped over global** — always build in a scoped app, never global
6. **Version your apps** — use semantic versioning via source control
7. **Template common patterns** — create custom AES templates for your org
8. **Limit direct table access** — use ACLs even in scoped apps
9. **Test data strategy** — plan how test data moves between instances
10. **Document APIs** — if your app exposes Script Includes for other apps, document them

View File

@ -0,0 +1,159 @@
# CSDM and CMDB Governance
## Scope
Use this reference for Common Service Data Model (CSDM), CMDB governance, service taxonomy, ownership, relationships, health, and lifecycle validation.
## Plugin and Role Assumptions
Clarify whether the environment uses:
- Discovery, Service Mapping, or manual CMDB stewardship only
- greenfield CSDM adoption or remediation of a legacy CMDB
- class manager or attestation processes
- service portfolio governance outside or inside ServiceNow
- custom CI classes that must be rationalized over time
Typical stakeholders are CMDB managers, service owners, application owners, discovery teams, and architecture governance boards.
## Core Concepts
CSDM separates concepts that organizations often blur together:
- business application
- application service
- business service
- service offering
- technical service
- supporting CI classes
Preserve those distinctions when designing data models or migrations.
## Key Tables
| Concept | Table |
|---------|-------|
| Base CI | `cmdb_ci` |
| Business Application | `cmdb_ci_business_app` |
| Business Service | `cmdb_ci_service` |
| Application Service | `cmdb_ci_service_auto` |
| Service Offering | `service_offering` |
| Relationship | `cmdb_rel_ci` |
## Ownership and Accountability
Minimum governance fields usually include:
- owned_by
- support_group
- assignment_group
- business_owner
- technical_owner
- operational status or install status
## Relationship Query Pattern
```javascript
var rel = new GlideRecord('cmdb_rel_ci');
rel.addQuery('parent', parentSysId);
rel.query();
while (rel.next()) {
gs.info(rel.getValue('parent') + ' -> ' + rel.getValue('child') + ' [' + rel.getValue('type') + ']');
}
```
## Missing Ownership Check
```javascript
var gr = new GlideRecord('cmdb_ci_service');
gr.addQuery('owned_by', '');
gr.setLimit(100);
gr.query();
while (gr.next()) {
gs.info('Missing owner: ' + gr.getValue('name'));
}
```
## Service Offering Integrity Check
Service offerings should not exist without the service context they represent.
```javascript
var offering = new GlideRecord('service_offering');
offering.addNullQuery('parent');
offering.setLimit(100);
offering.query();
while (offering.next()) {
gs.info('Service offering missing parent service: ' + offering.getDisplayValue());
}
```
## Governance Patterns
Strong CMDB governance usually includes:
1. class-specific required fields
2. ownership controls
3. relationship minimums
4. stale CI review
5. duplicate detection
6. intake rules for new services
## Guided Intake Workflow
For new business services or application services:
1. requestor submits purpose, owner, support group, and dependency context
2. validation checks confirm the right CI class and required fields
3. approval confirms the record fits the service taxonomy
4. follow-up tasks enforce relationship creation and data stewardship
5. ongoing health checks flag missing owners, stale services, and broken relationships
## CSDM Mapping Guidance
When helping users map data:
- map portfolio or product concepts carefully to business applications, not directly to every service layer
- use business services for customer-facing or business-consumable capabilities
- use application services for deployable or runtime service endpoints
- use service offerings when a service has distinct consumer-facing variants
## Health Metrics
- completeness
- compliance
- correctness
- relationship health
- staleness
- orphaned services without owners or supporting CIs
## Troubleshooting
Common failure modes:
- services are modeled at the wrong layer because application services and business services are treated as synonyms
- ownership fields exist but are not populated consistently across classes
- service offerings are created without a parent service or clear consumer meaning
- discovery populates infrastructure CIs but service relationships remain manual and incomplete
- duplicate services survive because intake and remediation rules are advisory instead of enforced
## Testing Guidance
Validate at least these scenarios:
- create a service record through guided intake and confirm required fields are enforced
- query for missing owners and confirm the exception list is actionable
- create a service offering with and without a parent service and verify the health check catches the bad case
- test relationship validation for a business service, application service, and supporting CI chain
- run stewardship checks against stale services and duplicate candidates
## Best Practices
1. Do not create custom CI classes or relationships when an out-of-box CSDM pattern already fits.
2. Keep ownership explicit and queryable.
3. Treat service taxonomy as governance, not just naming.
4. Call out whether the customer is doing greenfield modeling or remediation of an existing CMDB.
5. For automation, prefer validations and guided intake over free-form CI creation.

View File

@ -0,0 +1,256 @@
# Flow Designer & Process Automation
## Flow Triggers
### Record-Based Triggers
- **Created** — fires when record is inserted
- **Updated** — fires when record is updated (with optional conditions)
- **Created or Updated** — combination
- **Deleted** — fires on record deletion
```
Trigger: Record → Created or Updated
Table: incident
Condition: Priority is Critical AND State changes to In Progress
Run: For each unique change (deduplicate)
```
### Scheduled Triggers
- **Daily / Weekly / Monthly** — recurring schedule
- **Once** — one-time execution
- **Repeat** — custom interval
### Application Triggers
- **Inbound email** — fire on email receipt
- **REST** — fire from external REST call
- **Service Catalog** — catalog item submission
- **MetricBase** — threshold alerts
## Actions
### Built-in Actions (most common)
| Action | Use |
|--------|-----|
| Create Record | Insert a new record |
| Update Record | Modify existing record |
| Delete Record | Remove a record |
| Look Up Records | Query and return records |
| Ask for Approval | Create approval request |
| Send Email | Send notification |
| Send Notification | Use notification record |
| Create Task | Create a task record |
| Run Script | Execute server-side JavaScript |
| Log | Add to flow execution log |
| Wait for Condition | Pause until condition met |
### Look Up Records Action
```
Table: sys_user_grmember
Conditions: group = [Data Pill: Trigger.assignment_group]
Return: Multiple records (First 100)
Fields to return: user, user.email, user.name
```
### Run Script Action
```javascript
(function execute(inputs, outputs) {
var ga = new GlideAggregate('incident');
ga.addQuery('assignment_group', inputs.group_id);
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.query();
outputs.count = 0;
if (ga.next()) {
outputs.count = parseInt(ga.getAggregate('COUNT'), 10);
}
})(inputs, outputs);
// Inputs: group_id (Reference: sys_user_group)
// Outputs: count (Integer)
```
## Subflows (Reusable)
### Creating a Reusable Approval Subflow
```
Name: Standard Approval Process
Inputs:
- record (Reference: task)
- approval_type (String: "manager" | "group")
- approval_group (Reference: sys_user_group, optional)
Steps:
1. IF approval_type == "manager"
→ Look Up Record: Get manager of record.opened_by
→ Ask for Approval: manager, Due in 3 days
2. ELSE
→ Ask for Approval: approval_group, Due in 5 days
3. IF Approval.state == "approved"
→ Update Record: record.state = "Work in Progress"
→ Output: approved = true
4. ELSE
→ Update Record: record.state = "On Hold"
→ Output: approved = false
Outputs:
- approved (Boolean)
- approver (Reference: sys_user)
```
### Calling a Subflow from a Flow
```
Step: Subflow → Standard Approval Process
record: [Trigger Record]
approval_type: "manager"
IF [Subflow Output: approved] == true
→ Continue processing
ELSE
→ Send Email to requester: "Request denied"
```
## Error Handling
### Try/Catch in Flows
```
1. Try
└── Step 1: Call REST API
└── Step 2: Process Response
2. Catch
└── Step 3: Log Error
└── Step 4: Create Incident for integration failure
└── Step 5: Send notification to integration team
```
### Error Handling in Script Action
```javascript
(function execute(inputs, outputs) {
try {
var rm = new sn_ws.RESTMessageV2('ExternalAPI', 'POST');
rm.setStringParameterNoEscape('data', JSON.stringify(inputs.payload));
var response = rm.execute();
outputs.status_code = response.getStatusCode();
outputs.response_body = response.getBody();
outputs.success = (outputs.status_code >= 200 && outputs.status_code < 300);
} catch(e) {
outputs.success = false;
outputs.error_message = e.message;
}
})(inputs, outputs);
```
## Parallel Execution
```
Parallel Block:
Branch 1: Look up CI details (CMDB)
Branch 2: Get user's recent incidents
Branch 3: Check SLA status
After Parallel:
→ Use all three results in assignment logic
```
## Flow Variables & Data Pills
### Variable Types
- **String, Integer, Boolean** — primitives
- **Reference** — link to a record
- **Record** — full GlideRecord
- **Date/Time** — temporal values
- **Array** — list of values (limited support)
### Using Data Pills
```
Trigger Record → caller_id.email (dot-walking through reference)
Step 1 Output → records[0].number (first record from lookup)
Flow Variable → approval_count (custom variable)
```
### Transform Data Pills
```
String: [Trigger.number] + " - " + [Trigger.short_description]
Condition: [Step1.count] > 10
Reference: [Lookup.record.sys_id]
```
## Custom Spoke Actions
### Creating a Custom Action
```
Name: Send Slack Message
Category: Custom Integrations
Application: My Scoped App
Inputs:
- channel (String, mandatory)
- message (String, mandatory)
- webhook_url (String, mandatory)
Script Step:
(function execute(inputs, outputs) {
var rm = new sn_ws.RESTMessageV2();
rm.setEndpoint(inputs.webhook_url);
rm.setHttpMethod('POST');
rm.setRequestHeader('Content-Type', 'application/json');
rm.setRequestBody(JSON.stringify({
channel: inputs.channel,
text: inputs.message
}));
var response = rm.execute();
outputs.status = response.getStatusCode();
outputs.success = (outputs.status == 200);
})(inputs, outputs);
Outputs:
- status (Integer)
- success (Boolean)
```
## Decision Tables
```
Decision Table: Incident Auto-Assignment
Inputs: category, subcategory, location
Rules:
| Category | Subcategory | Location | → Assignment Group |
|----------|-------------|-------------|------------------------|
| network | * | Building A | Network Team - East |
| network | * | Building B | Network Team - West |
| software | email | * | Email Support |
| software | * | * | General Software Team |
| hardware | * | * | Hardware Team |
| * | * | * | Service Desk |
Usage in Flow:
Step: Decision Table → Incident Auto-Assignment
category: [Trigger.category]
subcategory: [Trigger.subcategory]
location: [Trigger.caller_id.location]
Output: assignment_group → use in Update Record step
```
## Flow Best Practices
1. **Name flows descriptively** — "Auto-assign Critical Incidents" not "Flow 1"
2. **Use subflows** for any logic reused in 2+ flows
3. **Add error handling** (Try/Catch) for all external calls
4. **Set run-as** to a service account, not admin
5. **Use conditions on triggers** to filter early — don't query in flow
6. **Limit lookups** — get only fields you need
7. **Test with Flow Execution** details view for debugging
8. **Avoid infinite loops** — watch for flows that update records which trigger the same flow
9. **Use Flow Variables** to reduce repeated lookups
10. **Document complex flows** with Log actions explaining each section
## Process Automation Designer (PAD) Connection
PAD / Playbooks use Flow Designer actions under the hood:
- Playbook activities can call Subflows
- Custom actions are reusable in both Flows and Playbooks
- See `playbooks.md` for Playbook-specific patterns

View File

@ -0,0 +1,196 @@
# Field Service Management (FSM)
## Scope
Use this reference for work orders, work order tasks, dispatching, territories, schedules, mobile technician workflows, skills, and parts logistics.
## Plugin and Role Assumptions
Clarify whether the environment uses:
- core FSM work orders only
- schedule optimization or external scheduling tools
- mobile inventory and stockroom extensions
- appointment booking and customer notifications
- offline mobile technician workflows
Typical stakeholders are dispatchers, field technicians, stockroom managers, service coordinators, and platform admins.
## Core Tables
| Area | Table |
|------|-------|
| Work Order | `wm_order` |
| Work Order Task | `wm_task` |
| Dispatch Group | `wm_dispatch_group` |
| Territory | `wm_territory` |
| Agent Schedule | `cmn_schedule` |
| Agent | `sys_user` |
| Skills | `cmn_skill` |
| Required Skill Mapping | `wm_task_required_skill` |
## Work Order Lifecycle
Typical stages:
1. New
2. Ready for dispatch
3. Dispatched
4. Accepted
5. On site / work in progress
6. Completed
7. Closed / cancelled
## Auto-Dispatch Pattern
Inputs commonly used:
- territory
- work type
- required skills
- SLA or appointment window
- technician availability
```javascript
// Before insert/update on wm_task
(function executeRule(current, previous) {
if (gs.nil(current.getValue('assignment_group')) && !gs.nil(current.getValue('territory'))) {
var dispatchGroup = new GlideRecord('wm_dispatch_group');
dispatchGroup.addQuery('territory', current.getValue('territory'));
dispatchGroup.addActiveQuery();
dispatchGroup.setLimit(1);
dispatchGroup.query();
if (dispatchGroup.next()) {
current.setValue('assignment_group', dispatchGroup.getUniqueValue());
}
}
})(current, previous);
```
## Technician Skill Match
```javascript
var skillGr = new GlideRecord('wm_task_required_skill');
skillGr.addQuery('task', taskSysId);
skillGr.query();
var requiredSkills = [];
while (skillGr.next()) {
requiredSkills.push(skillGr.getValue('skill'));
}
```
Use those skills together with schedule availability rather than assigning purely by group membership.
## Candidate Dispatch Pattern
When the instance needs a simple fallback selector, start with territory and availability before adding route optimization.
```javascript
var member = new GlideRecord('sys_user_grmember');
member.addQuery('group', dispatchGroupSysId);
member.setLimit(25);
member.query();
while (member.next()) {
gs.info('Dispatch candidate: ' + member.user.getDisplayValue());
}
```
Use this as a starting point only. Mature dispatch logic usually adds schedule, skill, appointment window, and part readiness checks outside a single Business Rule.
## Scheduling Guidance
When recommending dispatch logic, consider:
- travel time
- customer SLA window
- territory boundaries
- technician schedule and shifts
- required certifications
- parts availability
## Parts and Inventory
Patterns:
1. Reserve parts before dispatch if the job is part-dependent.
2. Link parts consumption to work order tasks, not just the parent order.
3. Keep stockroom and mobile inventory movements auditable.
## Mobile Technician Workflows
Common mobile requirements:
- offline-friendly forms
- required photo capture
- required signatures
- geolocation check-in
- completion checklist
```javascript
// Example completion guard on wm_task
(function executeRule(current, previous) {
if (current.state.changesTo('3') && gs.nil(current.getValue('close_notes'))) {
gs.addErrorMessage('Technicians must add completion notes before closing the task.');
current.setAbortAction(true);
}
})(current, previous);
```
## Flow Pattern: Dispatch and Notify
1. Trigger on `wm_task` insert or readiness state.
2. Resolve territory and required skills.
3. Look up available technicians.
4. Assign best candidate.
5. Notify technician and customer.
6. Update appointment / ETA.
## Workflow Example: Emergency Dispatch Escalation
For urgent work:
1. Detect a priority work order task with a near-term SLA breach.
2. Verify required parts are available locally or at a nearby stockroom.
3. Check whether any technician in the territory holds the required skills.
4. Escalate to a broader dispatch group if no qualified local technician exists.
5. Notify dispatch and customer when ETA changes.
6. Keep a clear audit trail of manual overrides.
## KPIs
- first-time fix rate
- mean time to dispatch
- mean travel time
- technician utilization
- appointment adherence
- parts-driven reschedules
## Troubleshooting
Common failure modes:
- tasks stay unassigned because territory data is incomplete or dispatch groups are inactive
- technicians receive work they cannot complete because required skills are not maintained
- mobile completion fails due to required notes, photos, or signatures not being captured
- parts-dependent work is dispatched before reservation or transfer is confirmed
- schedule integrations overwrite local assignment changes without dispatcher visibility
## Testing Guidance
Validate at least these scenarios:
- create a task with a territory and confirm the assignment group resolves correctly
- create a task without a territory and verify the fallback path is intentional
- test completion with and without close notes on mobile and desktop
- simulate a missing part and verify dispatch is delayed or rerouted
- test after-hours or overloaded technician schedules to confirm escalation logic
## Best Practices
1. Keep dispatch logic data-driven: territory, skills, and schedule should all be explicit records.
2. Separate parent work order orchestration from task-level execution.
3. Validate mobile completion data before allowing closure.
4. Do not overfit assignment to a single field; use multi-factor selection.
5. When integrating with external scheduling or GPS tools, keep outbound auth in REST Message records and properties.

View File

@ -0,0 +1,424 @@
# Glide API Quick Reference
## GlideRecord
### Basic Query
```javascript
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.addQuery('priority', '1');
gr.orderByDesc('sys_created_on');
gr.setLimit(50);
gr.query();
while (gr.next()) {
gs.info(gr.getValue('number') + ': ' + gr.getValue('short_description'));
}
```
### Encoded Query (preferred for complex filters)
```javascript
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^priority=1^assigned_toISNOTEMPTY^sys_created_on>=javascript:gs.beginningOfLastMonth()');
gr.query();
```
### Get Single Record
```javascript
var gr = new GlideRecord('incident');
if (gr.get('number', 'INC0010001')) { // query by field
gs.info(gr.getValue('short_description'));
}
// By sys_id (most common)
if (gr.get('a1b2c3d4...')) { // returns boolean in scoped apps!
gs.info(gr.getValue('number'));
}
```
### Insert
```javascript
var gr = new GlideRecord('incident');
gr.initialize();
gr.setValue('short_description', 'New incident from script');
gr.setValue('caller_id', gs.getUserID());
gr.setValue('category', 'software');
gr.setValue('impact', '2');
gr.setValue('urgency', '2');
var sys_id = gr.insert(); // returns sys_id or null
```
### Update
```javascript
var gr = new GlideRecord('incident');
gr.addQuery('number', 'INC0010001');
gr.query();
if (gr.next()) {
gr.setValue('state', '6'); // Resolved
gr.setValue('close_code', 'Solved (Permanently)');
gr.setValue('close_notes', 'Fixed by applying patch.');
gr.update();
}
```
### Delete
```javascript
var gr = new GlideRecord('incident');
gr.addQuery('active', false);
gr.addQuery('sys_created_on', '<', gs.daysAgoStart(365));
gr.setLimit(1000); // batch delete
gr.query();
while (gr.next()) {
gr.deleteRecord();
}
```
### Update Multiple (no Business Rules fired)
```javascript
var gr = new GlideRecord('incident');
gr.addQuery('assignment_group', oldGroupId);
gr.setValue('assignment_group', newGroupId);
gr.updateMultiple(); // WARNING: skips Business Rules
```
### Delete Multiple
```javascript
var gr = new GlideRecord('incident');
gr.addQuery('active', false);
gr.addQuery('sys_created_on', '<', gs.daysAgoStart(365));
gr.deleteMultiple(); // WARNING: skips Business Rules
```
### addJoinQuery (find records with related data)
```javascript
// Find incidents that have attachments
var gr = new GlideRecord('incident');
gr.addJoinQuery('sys_attachment', 'sys_id', 'table_sys_id');
gr.query();
// Find users in a specific group
var gr = new GlideRecord('sys_user');
var joinQuery = gr.addJoinQuery('sys_user_grmember', 'sys_id', 'user');
joinQuery.addCondition('group.name', 'Network');
gr.query();
```
### addNotNullQuery / addNullQuery
```javascript
gr.addNotNullQuery('assigned_to');
gr.addNullQuery('resolved_at');
```
### OR Queries
```javascript
var gr = new GlideRecord('incident');
var qc = gr.addQuery('priority', '1');
qc.addOrCondition('priority', '2');
gr.query();
// Better: encoded query
gr.addEncodedQuery('priority=1^ORpriority=2');
```
### Dot-walking (reference fields)
```javascript
var gr = new GlideRecord('incident');
if (gr.get('some_sys_id')) {
// Dot-walk only for reads
var email = gr.getValue('caller_id.email'); // not for writes
var callerName = gr.getDisplayValue('caller_id');
}
// For setting references, use sys_id
gr.setValue('assigned_to', userSysId);
```
### chooseWindow (pagination)
```javascript
var gr = new GlideRecord('incident');
gr.addActiveQuery();
gr.orderByDesc('sys_created_on');
gr.chooseWindow(0, 20); // first 20 records (offset, limit)
gr.query();
```
### setWorkflow / autoSysFields
```javascript
var gr = new GlideRecord('incident');
if (gr.get(sys_id)) {
gr.setWorkflow(false); // skip Business Rules
gr.autoSysFields(false); // don't update sys_updated_on/by
gr.setValue('state', '7');
gr.update();
}
```
## GlideAggregate
```javascript
// Count active incidents by priority
var ga = new GlideAggregate('incident');
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.groupBy('priority');
ga.query();
while (ga.next()) {
gs.info('Priority ' + ga.getValue('priority') + ': ' + ga.getAggregate('COUNT'));
}
// Sum, AVG, MIN, MAX
var ga = new GlideAggregate('incident');
ga.addAggregate('AVG', 'reassignment_count');
ga.addAggregate('MAX', 'reassignment_count');
ga.query();
if (ga.next()) {
gs.info('Avg reassignments: ' + ga.getAggregate('AVG', 'reassignment_count'));
gs.info('Max reassignments: ' + ga.getAggregate('MAX', 'reassignment_count'));
}
// Count with having clause
var ga = new GlideAggregate('incident');
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.groupBy('assigned_to');
ga.addHaving('COUNT', '>', 10);
ga.query();
```
## GlideSystem (gs)
```javascript
// User info
gs.getUserID(); // sys_id of current user
gs.getUserName(); // username
gs.getUserDisplayName(); // full name
gs.getUser().getRecord(); // GlideRecord of sys_user
// Roles
gs.hasRole('admin');
gs.hasRole('itil');
// Properties
gs.getProperty('glide.servlet.uri'); // instance URL
gs.getProperty('my_app.custom_prop', 'default_value');
// Logging
gs.info('Info message: {0}', paramValue);
gs.warn('Warning: {0}', paramValue);
gs.error('Error: {0}', paramValue);
gs.debug('Debug: {0}', paramValue); // only shows if debug enabled
// Events
gs.eventQueue('my_app.event_name', current, param1, param2);
// Date/Time helpers
gs.now(); // current date/time
gs.nowDateTime(); // current date/time string
gs.daysAgo(7); // date/time 7 days ago
gs.daysAgoStart(7); // start of day 7 days ago
gs.daysAgoEnd(7); // end of day 7 days ago
gs.beginningOfLastMonth();
gs.endOfLastMonth();
gs.beginningOfThisQuarter();
// Misc
gs.nil(value); // true if null, undefined, or empty string
gs.tableExists('my_table');
gs.sleep(1000); // pause 1 second (server-side only, use sparingly!)
gs.getSession(); // current session object
gs.setRedirect('incident.do?sys_id=' + sys_id);
gs.addInfoMessage('Record saved successfully');
gs.addErrorMessage('Something went wrong');
```
## GlideAjax (Client → Server)
### Client Script (caller)
```javascript
// ASYNC (preferred) — uses callback
var ga = new GlideAjax('MyAjaxUtil');
ga.addParam('sysparm_name', 'getIncidentInfo');
ga.addParam('sysparm_incident_id', g_form.getValue('sys_id'));
ga.getXMLAnswer(function(answer) {
var result = JSON.parse(answer);
g_form.setValue('description', result.description);
});
// NEVER use getXMLWait() — blocks the UI thread
```
### Script Include (server-side handler)
```javascript
var MyAjaxUtil = Class.create();
MyAjaxUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getIncidentInfo: function() {
var incId = this.getParameter('sysparm_incident_id');
var gr = new GlideRecord('incident');
if (gr.get(incId)) {
var result = {
description: gr.getValue('short_description'),
state: gr.getValue('state'),
priority: gr.getValue('priority')
};
return JSON.stringify(result);
}
return JSON.stringify({ error: 'Not found' });
},
// Return multiple values using XML (legacy pattern)
getUserDetails: function() {
var userId = this.getParameter('sysparm_user_id');
var gr = new GlideRecord('sys_user');
if (gr.get(userId)) {
this.setAnswer(gr.getValue('name'));
// Additional values via newItem
var item = this.newItem('email');
item.setAttribute('value', gr.getValue('email'));
}
},
type: 'MyAjaxUtil'
});
```
## GlideForm (g_form) — Client-Side Only
```javascript
// Get/Set values
g_form.getValue('field_name'); // internal value
g_form.getDisplayValue('field_name'); // display value
g_form.setValue('field_name', 'value'); // set value
g_form.setValue('field_name', sys_id, displayValue); // reference field with display
g_form.clearValue('field_name');
// Visibility & Read-only
g_form.setVisible('field_name', true);
g_form.setReadOnly('field_name', true);
g_form.setMandatory('field_name', true);
g_form.setDisabled('field_name', true);
// Labels & Styles
g_form.setLabelOf('field_name', 'New Label');
g_form.showFieldMsg('field_name', 'Warning message', 'error'); // 'info', 'error'
g_form.hideFieldMsg('field_name');
// Options (choice fields)
g_form.addOption('priority', '6', 'Planning');
g_form.removeOption('priority', '5');
g_form.clearOptions('priority');
// Sections
g_form.setSectionDisplay('section_name', false); // hide section
// Form-level
g_form.save(); // save without redirect
g_form.submit(); // save and redirect
g_form.isNewRecord();
g_form.addInfoMessage('Saved!');
g_form.addErrorMessage('Failed!');
g_form.flash('field_name', '#FFFACD', 0); // flash field
// Related lists
g_form.showRelatedList('incident.problem_id');
g_form.hideRelatedList('incident.problem_id');
```
## GlideUser (server-side)
```javascript
var user = gs.getUser();
user.getID(); // sys_id
user.getName(); // username
user.getDisplayName(); // full name
user.getEmail(); // email
user.getDomainID(); // domain sys_id
user.hasRole('itil');
user.isMemberOf('Database'); // group name
user.getMyGroups(); // comma-separated sys_ids
// From GlideRecord
var gr = new GlideRecord('sys_user');
gr.get(gs.getUserID());
gr.getValue('department');
```
## GlideDateTime & GlideDuration
```javascript
// GlideDateTime
var gdt = new GlideDateTime(); // now
gdt.getValue(); // internal format: 2024-01-15 14:30:00
gdt.getDisplayValue(); // user's timezone format
gdt.getNumericValue(); // epoch milliseconds
var gdt = new GlideDateTime('2024-01-15 14:30:00');
gdt.addDaysUTC(7);
gdt.addSeconds(3600);
// Compare
var gdt1 = new GlideDateTime('2024-01-15');
var gdt2 = new GlideDateTime('2024-01-20');
gs.info(gdt1.before(gdt2)); // true
gs.info(gdt1.after(gdt2)); // false
// Calculate duration between dates
var start = new GlideDateTime(gr.getValue('opened_at'));
var end = new GlideDateTime(gr.getValue('resolved_at'));
var dur = GlideDateTime.subtract(start, end);
gs.info(dur.getDurationValue()); // "3 00:15:30" (days HH:MM:SS)
// GlideDuration
var dur = new GlideDuration('3 00:00:00'); // 3 days
dur.add(new GlideDuration('0 12:00:00')); // add 12 hours
gs.info(dur.getDurationValue());
```
## GlideSPScriptable (Service Portal)
```javascript
// In a Service Portal widget server script
(function() {
var sp = new GlideSPScriptable();
// Get portal record
var portalGR = sp.getPortalRecord();
// Get current user
data.user = sp.getUserRecord().getDisplayValue('name');
// Get KB article
var kb = sp.getKBRecord('KB0010001');
// Check catalog item visibility
var canView = sp.canReadRecord('sc_cat_item', itemSysId);
// Get widget parameters
data.title = options.title || 'Default Title';
})();
```
## Scoped vs Global Differences
| Feature | Global | Scoped |
|---------|--------|--------|
| `gr.field_name` | Returns value (string coercion) | Returns GlideElement |
| `gr.get()` return | GlideRecord or null | boolean |
| `gs.include()` | Available | NOT available — use Script Includes |
| `GlideSecureRandomUtil` | Global namespace | Scoped namespace |
| `current.update()` | Works (but avoid in BR) | Works (but avoid in BR) |
| Cross-scope access | Unrestricted | Requires access rules |
| `GlideEvaluator` | Available | Available (own scope) |
| `gs.log()` | Available | Use `gs.info()` instead |
### Scoped App Best Practice
```javascript
// Always use getValue/setValue in scoped apps
var value = gr.getValue('field'); // ✅ returns string
var value = gr.field; // ❌ returns GlideElement object
// Check get() result
var gr = new GlideRecord('incident');
if (gr.get(sysId)) { // boolean in scoped
// record found
}
```

View File

@ -0,0 +1,341 @@
# HRSD & CSM
## HRSD (HR Service Delivery)
### Core Tables
```
sn_hr_core_case — HR Case (main record)
sn_hr_core_case_operation — Case operations/tasks
sn_hr_le_lifecycle_event — Lifecycle Events
sn_hr_core_template — Document templates
sn_hr_core_service — HR Services
sn_hr_core_topic — Topics/Categories
```
### HR Case Management
```
HR Case lifecycle:
Draft → Open → Work in Progress → Pending → Resolved → Closed
Case types:
- General Inquiry
- Benefits
- Payroll
- Leave of Absence
- Employee Relations
- Onboarding / Offboarding
- Document Requests
Creating cases:
- Employee Center portal
- Virtual Agent
- Email inbound action
- Walk-up / phone (agent creates)
- Lifecycle Event auto-generation
```
### HR Case Scripting
```javascript
// Business Rule: Auto-assign HR case based on topic
(function executeRule(current, previous) {
var topic = current.getValue('hr_topic');
var topicGr = new GlideRecord('sn_hr_core_topic');
if (topicGr.get(topic)) {
var group = topicGr.getValue('assignment_group');
if (group) {
current.setValue('assignment_group', group);
}
}
// Set SLA based on case type
var caseType = current.getValue('hr_service');
if (caseType) {
var svcGr = new GlideRecord('sn_hr_core_service');
if (svcGr.get(caseType) && svcGr.getValue('sla')) {
current.setValue('sla', svcGr.getValue('sla'));
}
}
})(current, previous);
```
### Lifecycle Events
```
Lifecycle Events automate multi-step HR processes triggered
by employee milestones.
Examples:
- New Hire Onboarding → generates 15-20 tasks across departments
- Promotion → update role, salary, badge access
- Transfer → notify both managers, update location
- Leave of Absence → suspend access, notify benefits
- Offboarding → collect equipment, revoke access, final pay
Structure:
Lifecycle Event
├── Activity Set 1: IT Tasks
│ ├── Provision laptop
│ ├── Create email account
│ └── Set up VPN access
├── Activity Set 2: Facilities
│ ├── Assign desk/badge
│ └── Parking pass
├── Activity Set 3: HR
│ ├── Benefits enrollment
│ ├── Policy acknowledgment
│ └── Emergency contacts
└── Activity Set 4: Manager
├── Assign buddy/mentor
└── Schedule orientation
Triggering lifecycle events:
```
```javascript
// Script to trigger a lifecycle event
var le = new sn_hr_le.LifecycleEventAPI();
var params = {
subject_person: userSysId, // Employee
lifecycle_event: leSysId, // Lifecycle Event definition
opened_for: userSysId,
source_record_table: 'sys_user',
source_record_id: userSysId
};
var caseId = le.createLifecycleEventCase(params);
```
### Document Templates
```
Navigate: HR Service Delivery > Document Templates
Use case: Auto-generate offer letters, tax forms, policy docs
Template variables:
${employee.name}, ${employee.title}, ${employee.department}
${employee.manager.name}, ${employee.location}
${effective_date}, ${salary}, ${start_date}
Generate document via script:
```
```javascript
// Generate document from template
var docGen = new sn_hr_core.HRDocumentGeneration();
var templateSysId = 'abc123...';
var caseSysId = current.sys_id.toString();
var result = docGen.generateDocument(templateSysId, caseSysId);
```
### Employee Center
```
Employee Center is the self-service portal for HR:
- Topic categories (browse by life event)
- Knowledge articles
- Request HR services (catalog items)
- View my HR cases
- Virtual Agent integration
Customization:
- Branding, layout via Employee Center Designer
- Content Blocks for announcements
- Audience targeting (by department, location, role)
```
### HRSD Best Practices
1. **Lifecycle Events** for repeatable processes — don't manually create tasks
2. **Topic taxonomy** should mirror how employees think, not HR org structure
3. **Virtual Agent** as first contact — deflect common queries (PTO balance, pay dates)
4. **Document templates** for anything requiring employee signatures
5. **Sensitive fields** — use ACLs to restrict PII visibility (SSN, salary)
6. **COE (Center of Excellence)** model — shared services with specialized tiers
---
## CSM (Customer Service Management)
### Core Tables
```
sn_customerservice_case — Customer Case
csm_consumer — Consumer (B2C)
customer_account — Account (B2B)
customer_contact — Contact (person at an account)
csm_product — Product
csm_asset — Installed Product/Asset
sn_customerservice_community — Community forums
```
### Customer Case Management
```
Case lifecycle:
New → Open → In Progress → Pending (customer/vendor) → Resolved → Closed
Case channels:
- Customer Portal (self-service)
- Email inbound
- Phone (agent creates)
- Chat (Agent Chat / Virtual Agent)
- Social media (Twitter, Facebook integration)
- Community forums (case from post)
- API (partner systems)
Matching:
- Email → Contact → Account (auto-match)
- Phone → CTI integration → Contact lookup
- Portal → Authenticated user → Contact record
```
### CSM Scripting Patterns
```javascript
// Business Rule: Auto-set account and entitlement on case creation
(function executeRule(current, previous) {
// Look up contact's account
if (!gs.nil(current.getValue('contact')) && gs.nil(current.getValue('account'))) {
var contact = new GlideRecord('customer_contact');
if (contact.get(current.getValue('contact'))) {
current.setValue('account', contact.getValue('account'));
}
}
// Check entitlement (SLA eligibility)
if (!gs.nil(current.getValue('account'))) {
var entGr = new GlideRecord('service_entitlement');
entGr.addQuery('account', current.getValue('account'));
entGr.addActiveQuery();
entGr.addQuery('end_date', '>', new GlideDateTime());
entGr.orderByDesc('priority');
entGr.setLimit(1);
entGr.query();
if (entGr.next()) {
current.setValue('entitlement', entGr.getUniqueValue());
}
}
})(current, previous);
```
### Account & Contact Management
```javascript
// Script Include: Customer lookup utility
var CustomerUtils = Class.create();
CustomerUtils.prototype = {
initialize: function() {},
getAccountHealth: function(accountSysId) {
var result = {
open_cases: 0,
escalated_cases: 0,
avg_resolution_hours: 0,
csat_score: 0
};
// Open cases
var ga = new GlideAggregate('sn_customerservice_case');
ga.addQuery('account', accountSysId);
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
result.open_cases = parseInt(ga.getAggregate('COUNT'));
}
// Escalated cases
ga = new GlideAggregate('sn_customerservice_case');
ga.addQuery('account', accountSysId);
ga.addQuery('escalation', '>', 0);
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
result.escalated_cases = parseInt(ga.getAggregate('COUNT'));
}
return result;
},
type: 'CustomerUtils'
};
```
### Entitlements & SLAs
```
Entitlements define what support a customer receives:
- Support level (Gold, Silver, Bronze)
- Response time SLAs
- Number of cases per month
- Available channels (phone, email, chat)
Tables:
- service_entitlement: entitlement definitions
- sla: SLA definitions
- task_sla: SLA instances on records
SLA setup:
Navigate: Service Level Management > SLA > SLA Definitions
Example SLA:
Name: Gold - P1 Response
Table: sn_customerservice_case
Conditions: entitlement.name = Gold AND priority = 1
Duration: 1 hour
Type: Response (time to first contact)
```
### Customer Portal
```
CSM Portal (Service Portal-based):
- Knowledge base browsing
- Case submission and tracking
- Community forums
- Product documentation
- Chat with agent / Virtual Agent
- Asset/product registration
Customization:
- Branded per account (multi-tenant)
- Role-based content visibility
- Custom widgets for product-specific features
```
### Community Forums
```
Navigate: Customer Service > Community
Features:
- Forum categories
- Q&A format (mark best answer)
- Moderation tools
- Auto-create case from unresolved posts
- Knowledge article linking
- Gamification (points, badges)
Scripting — create case from forum post:
```
```javascript
// Scheduled job or Business Rule on community post
var post = new GlideRecord('sn_customerservice_community_post');
post.addQuery('resolved', false);
post.addQuery('sys_created_on', '<', gs.daysAgoStart(7)); // Unresolved 7+ days
post.query();
while (post.next()) {
var caseGr = new GlideRecord('sn_customerservice_case');
caseGr.initialize();
caseGr.setValue('short_description', 'Community post: ' + post.getValue('title'));
caseGr.setValue('description', post.getValue('body'));
caseGr.setValue('contact', post.getValue('author'));
caseGr.setValue('category', 'community_escalation');
caseGr.insert();
// Link case to post
post.setValue('case', caseGr.getUniqueValue());
post.update();
}
```
### CSM Best Practices
1. **Account hierarchy** — parent/child accounts for enterprise customers
2. **Entitlements before SLAs** — define what each customer gets first
3. **Omni-channel** — same case regardless of how customer contacts you
4. **Auto-match contacts** — email domain matching to accounts
5. **Major Issue Management** — group related cases under a major case
6. **Proactive support** — monitor products, create cases before customer reports
7. **Community deflection** — encourage self-service before case creation
8. **CSAT surveys** — automate after case closure
9. **Asset-based support** — link cases to specific products/serials
10. **Knowledge from cases** — every resolved case is a potential KB article

View File

@ -0,0 +1,178 @@
# IntegrationHub
## Scope
Use this reference for IntegrationHub spokes, custom actions, Connection & Credential Aliases, Data Stream Actions, subflows, retry strategy, and external API integration patterns on the ServiceNow platform.
## Plugin and Role Assumptions
Clarify whether the environment uses:
- IntegrationHub only or packaged spokes
- REST, SOAP, JDBC, or Data Stream Actions
- Connection & Credential Aliases managed centrally or by each app team
- MID Servers for private network access
- external rate limits, webhook callbacks, or long-running polling patterns
Typical stakeholders are integration developers, platform admins, credential owners, and flow designers.
## Core Building Blocks
| Building Block | Use |
|----------------|-----|
| Connection & Credential Alias | decouple auth and config from script |
| Action | reusable integration step |
| Subflow | orchestration and reuse |
| Data Stream Action | paginated or streamed API ingestion |
| Spoke | packaged set of related actions |
## Recommended Design Order
1. Define external system contract
2. Configure connection alias and credentials
3. Build reusable actions
4. Wrap business orchestration in subflows or flows
5. Add retries, logging, and idempotency
6. Test with representative payloads
## Custom Action Script Step
```javascript
(function execute(inputs, outputs) {
var baseUrl = gs.getProperty('x_scope.integration.base_url');
if (gs.nil(baseUrl)) {
throw new Error('Missing x_scope.integration.base_url property');
}
var rm = new sn_ws.RESTMessageV2();
rm.setEndpoint(baseUrl + inputs.endpoint_path);
rm.setHttpMethod('GET');
rm.setRequestHeader('Accept', 'application/json');
rm.setHttpTimeout(30000);
var response = rm.execute();
outputs.status_code = response.getStatusCode();
outputs.body = response.getBody();
if (outputs.status_code < 200 || outputs.status_code >= 300) {
throw new Error('IntegrationHub action failed: ' + outputs.status_code);
}
})(inputs, outputs);
```
If the customer already has a REST Message record with alias-backed authentication, prefer `new sn_ws.RESTMessageV2('message_name', 'method_name')` instead of rebuilding the transport layer in script.
## Idempotent Create Pattern
For create or sync actions, send a stable external key whenever the upstream API supports it.
```javascript
(function execute(inputs, outputs) {
var rm = new sn_ws.RESTMessageV2('x_scope.vendor_api', 'create_record');
rm.setRequestHeader('Idempotency-Key', inputs.idempotency_key);
rm.setRequestBody(JSON.stringify({
external_id: inputs.external_id,
short_description: inputs.short_description
}));
var response = rm.execute();
outputs.status_code = response.getStatusCode();
outputs.body = response.getBody();
})(inputs, outputs);
```
## Data Stream Action Pattern
Use Data Stream Actions when:
- the source is paginated
- records arrive in large batches
- transformation must be incremental
- memory pressure matters
Document:
- page size
- cursor or offset strategy
- retry conditions
- terminal conditions
## Data Stream Cursor Guidance
For paginated sources, the action contract should expose:
- current cursor or offset
- next cursor returned by the upstream system
- whether the page is terminal
- retryable error conditions
Do not hide pagination state inside ad hoc script globals.
## Connection Strategy
Preferred patterns:
- Connection & Credential Aliases
- REST Message methods with configured auth
- property-backed non-secret config
Avoid:
- hardcoded usernames or passwords
- hardcoded OAuth profile sys_ids
- endpoint URLs duplicated across many scripts
## Error Handling
For resilient actions:
1. return clear outputs for status, body, and cursor
2. distinguish retryable vs non-retryable errors
3. log correlation IDs or upstream request IDs if available
4. make create or update calls idempotent where possible
## Retry and Rate Limits
Call out:
- 429 handling
- exponential backoff expectations
- upstream timeout behavior
- duplicate suppression
## Workflow Example: Action Plus Subflow
1. a reusable action handles transport, response parsing, and normalized outputs
2. a subflow decides whether to retry, create a follow-up task, or continue orchestration
3. business logic stays outside the transport action unless it is truly reusable
4. rate-limit and auth failures become explicit flow branches instead of generic script errors
## Troubleshooting
Common failure modes:
- the action works in test but fails in production because aliases or MID Server routing differ
- retries create duplicate records because no external key or idempotency strategy exists
- pagination loops indefinitely because the terminal condition is not explicit
- 429 or timeout behavior is treated like a hard failure instead of a retryable one
- downstream flows break because action outputs are renamed without versioning or documentation
## Testing Guidance
Good validation includes:
- happy path
- auth failure
- pagination
- malformed payload
- partial failure or retry
- duplicate create or update request
- MID Server or network path assumptions when applicable
## Best Practices
1. Keep credential ownership out of source control and inside aliases or configured connection records.
2. Separate transport concerns from business orchestration.
3. Prefer reusable actions over embedding full integration logic in many flows.
4. Write outputs explicitly so downstream flow steps are stable.
5. Explain the idempotency and retry story whenever proposing IntegrationHub automation.

View File

@ -0,0 +1,183 @@
# IRM, GRC, and SecOps
## Scope
Use this reference for Integrated Risk Management (IRM/GRC), policy and compliance workflows, vendor risk, Security Incident Response, Vulnerability Response, and exception handling.
## Plugin and Role Assumptions
Clarify which product families are active:
- IRM / Policy and Compliance
- Vendor Risk Management
- Vulnerability Response
- Security Incident Response
- exception workflows tied to risks or custom waiver tables
Also call out whether the instance routes governance through Flow Designer, classic workflow, decision tables, or product-native lifecycles.
## Core Tables
| Area | Table |
|------|-------|
| Risk | `sn_risk_risk` |
| Control | `sn_compliance_control` |
| Policy | `sn_compliance_policy` |
| Compliance Task | `sn_compliance_task` |
| Vendor Risk Assessment | `sn_vdr_risk_assessment` |
| Security Incident | `sn_si_incident` |
| Vulnerable Item | `sn_vul_vulnerable_item` |
| Vulnerability Group | `sn_vul_group` |
## Risk and Compliance Pattern
```javascript
// Example: create follow-up compliance task when control fails
var task = new GlideRecord('sn_compliance_task');
task.initialize();
task.setValue('short_description', 'Control remediation required');
task.setValue('assigned_to', ownerSysId);
task.setValue('state', '1');
task.insert();
```
Use controls, risks, and issues as distinct records. Do not collapse them into one custom table unless there is a very strong product reason.
## Control Failure Routing Pattern
Use a transparent owner lookup and due-date rule rather than hardcoded branching.
```javascript
var control = new GlideRecord('sn_compliance_control');
if (control.get(controlSysId)) {
var dueDate = new GlideDateTime();
dueDate.addDaysLocalTime(14);
var task = new GlideRecord('sn_compliance_task');
task.initialize();
task.setValue('short_description', 'Remediate failed control: ' + control.getDisplayValue());
task.setValue('assigned_to', control.getValue('owner'));
task.setValue('due_date', dueDate);
task.insert();
}
```
## Policy Exception Workflow
Suggested flow:
1. intake request for exception
2. link the affected policy, control, or risk
3. require compensating control evidence
4. route to risk or security approvers
5. set expiry date and review cadence
6. track renewal or closure
## Vulnerability Response Pattern
```javascript
var gr = new GlideRecord('sn_vul_vulnerable_item');
gr.addQuery('state', 'open');
gr.addQuery('assignment_group', assignmentGroupSysId);
gr.setLimit(50);
gr.query();
while (gr.next()) {
gs.info(gr.getValue('number') + ' ' + gr.getValue('risk_rating'));
}
```
When automating vulnerability triage, call out:
- source scanner
- CI mapping confidence
- exception process
- remediation SLA
- ownership model
## Security Incident Response
Typical lifecycle:
1. Detection
2. Triage
3. Containment
4. Eradication
5. Recovery
6. Lessons learned / closure
```javascript
// Before update on sn_si_incident
(function executeRule(current, previous) {
if (current.state.changesTo('containment') && gs.nil(current.getValue('assignment_group'))) {
gs.addErrorMessage('Security incidents entering containment must have an assignment group.');
current.setAbortAction(true);
}
})(current, previous);
```
## Vendor Risk
Key patterns:
- assess vendors periodically and on major changes
- track inheritable controls and evidence separately
- keep scoring logic explicit and reviewable
- store approval history for audit
## Workflow Example: Exception Review Board
For mature governance programs:
1. a request references a policy, vulnerable item, or risk record
2. the requester provides rationale, business impact, compensating controls, and expiry date
3. Flow Designer routes to the right risk, compliance, or security approver
4. approved exceptions create review reminders before expiry
5. rejected exceptions return to the requester with required remediation actions
6. all approvals and renewals stay linked to the original record for audit
## Exception and Waiver Guidance
Never hardcode approval routes or risk thresholds in source code when they vary by environment. Prefer:
- properties
- decision tables
- flow inputs
- reference-data tables
## Reporting KPIs
- open high risks by domain
- failed controls by framework
- overdue remediation tasks
- vulnerabilities past SLA
- security incidents by severity and mean time to contain
- exception inventory with expiry dates
## Troubleshooting
Common failure modes:
- remediation tasks route to nobody because control ownership is incomplete
- exceptions appear approved but have no expiry or review cadence
- vulnerability items cannot be actioned because CI ownership is missing or scanner connectors are stale
- vendor risk scores drift because scoring logic lives in undocumented scripts instead of controlled rules
- security incidents move phases without notes, evidence, or clear actor attribution
## Testing Guidance
Validate at least these scenarios:
- fail a control and confirm the right remediation task is created with an owner and due date
- submit an exception request with and without compensating controls
- route a vulnerable item with missing CI ownership and confirm the fallback path
- test security incident phase transitions that should block on missing assignment or notes
- verify auditability for approvals, renewals, closures, and rejected requests
## Best Practices
1. Keep auditability first-class: approvals, expiries, rationale, and evidence should all be retrievable.
2. Prefer table relationships over duplicating policy or control data into task records.
3. Use Flow Designer or decision tables for governance-heavy routing where admins need visibility.
4. For Vulnerability Response, distinguish exceptions, accepted risk, and false positives clearly.
5. Security automations should always document plugin assumptions and required roles.

View File

@ -0,0 +1,207 @@
# ITAM (IT Asset Management)
## Scope
Use this reference for Hardware Asset Management (HAM), Software Asset Management (SAM), stockrooms, consumables, models, contracts, reclaim workflows, and lifecycle automation.
## Plugin and Role Assumptions
Call out whether the customer is using:
- core Asset Management only
- HAM Professional workflows
- SAM Professional reconciliation and optimization
- procurement / contract integrations
- HR-driven offboarding triggers
Common implementation roles include asset admins, stockroom managers, procurement analysts, and SAM managers. Do not assume every environment exposes the same lifecycle choices or entitlement tables.
## Core Tables
| Area | Table |
|------|-------|
| Asset | `alm_asset` |
| Hardware Asset | `alm_hardware` |
| Consumable | `alm_consumable` |
| Software Entitlement | `alm_license` |
| Software Installation | `cmdb_sam_sw_install` |
| Stockroom | `alm_stockroom` |
| Model | `cmdb_model` |
| Contract | `ast_contract` |
## Asset Lifecycle Pattern
Typical hardware lifecycle:
1. Ordered
2. Received
3. In stock
4. Reserved
5. In use
6. In maintenance
7. Retired
8. Disposed
Recommended fields to keep aligned:
- `install_status`
- `substatus`
- `assigned_to`
- `location`
- `stockroom`
- `department`
## Hardware Asset Automation
```javascript
// Business Rule: before update on alm_hardware
(function executeRule(current, previous) {
if (current.install_status.changesTo('6')) { // In use
current.setValue('substatus', 'in_service');
}
if (current.assigned_to.changes() && !gs.nil(current.getValue('assigned_to'))) {
current.setValue('stockroom', '');
}
})(current, previous);
```
## Reclamation Flow Pattern
Use when employees terminate or transfer.
1. Trigger from HR lifecycle event or user deactivation.
2. Look up active `alm_hardware` records for the user.
3. Create reclaim tasks or work orders.
4. Set asset to reserved or pending return.
5. When received, move asset to stockroom and clear assignment.
6. Run post-return checks and retirement logic if needed.
```javascript
// Reclaim assets for a departing employee
var gr = new GlideRecord('alm_hardware');
gr.addQuery('assigned_to', userSysId);
gr.addQuery('install_status', '6'); // In use
gr.query();
while (gr.next()) {
gr.setValue('substatus', 'pending_return');
gr.update();
}
```
## Workflow Example: Employee Offboarding
Strong reclaim designs usually include:
1. HR event identifies a transfer or termination date.
2. Flow Designer looks up assigned `alm_hardware`, `alm_consumable`, and recoverable software allocations.
3. Hardware tasks route to stockroom, workplace, or FSM fulfillment.
4. Software reclamation requests route to SAM or app owners.
5. Asset state changes are delayed until equipment is actually returned or verified unavailable.
6. Exceptions are documented when equipment is lost, damaged, or executive-approved for retention.
## Stockroom Transfer Pattern
```javascript
var asset = new GlideRecord('alm_hardware');
if (asset.get(assetSysId)) {
asset.setValue('assigned_to', '');
asset.setValue('location', targetLocation);
asset.setValue('stockroom', targetStockroom);
asset.setValue('install_status', '2'); // In stock
asset.update();
}
```
## CMDB and Asset Alignment Pattern
When the asset is linked to a CI, keep the ownership story explicit.
```javascript
var asset = new GlideRecord('alm_hardware');
if (asset.get(assetSysId) && !gs.nil(asset.getValue('ci'))) {
var ci = new GlideRecord('cmdb_ci');
if (ci.get(asset.getValue('ci'))) {
if (!gs.nil(asset.getValue('assigned_to'))) {
ci.setValue('assigned_to', asset.getValue('assigned_to'));
}
if (!gs.nil(asset.getValue('location'))) {
ci.setValue('location', asset.getValue('location'));
}
ci.update();
}
}
```
Use this only when the customer has agreed that the asset record is an ownership source for the related CI.
## SAM / License Reconciliation
Use SAM data to compare:
- entitlements purchased
- installations discovered
- normalized software models
- publisher/product/version mappings
```javascript
var ga = new GlideAggregate('cmdb_sam_sw_install');
ga.addQuery('discovery_source', 'ServiceNow Discovery');
ga.groupBy('display_name');
ga.addAggregate('COUNT');
ga.query();
while (ga.next()) {
gs.info(ga.getValue('display_name') + ': ' + ga.getAggregate('COUNT'));
}
```
## Contract and Warranty Checks
```javascript
var asset = new GlideRecord('alm_hardware');
if (asset.get(assetSysId)) {
var contract = asset.getValue('contract');
if (!gs.nil(contract)) {
gs.info('Asset has contract: ' + contract);
}
}
```
## Reporting Patterns
Useful KPIs:
- assets in use by department
- assets pending return
- warranty expirations in next 30/60/90 days
- unlicensed installs vs. entitlements
- stockroom aging and dead stock
## Troubleshooting
Common failure modes:
- assets move to retired without a corresponding reclaim or disposal step
- stockroom remains populated even after assignment, which usually indicates incomplete lifecycle automation
- the linked CI still shows an old owner because asset and CMDB updates are not synchronized
- software compliance numbers are misleading because discovery is not normalized or installations are stale
- contract or warranty reporting is incomplete because asset records are missing vendor or model relationships
## Testing Guidance
Validate at least these scenarios:
- assign a hardware asset to a user and verify stockroom is cleared
- reclaim an in-use asset and confirm substatus changes without skipping return verification
- transfer an asset to a stockroom and verify assignment, location, and install status remain aligned
- run a SAM aggregation against representative install data and confirm normalized model coverage
- test edge cases for shared devices, lost devices, and executive exceptions
## Best Practices
1. Keep `alm_*` asset records aligned with related CMDB records, but do not assume they are interchangeable.
2. Use lifecycle state transitions consistently; avoid free-form status changes.
3. Drive reclaim and refresh processes from HR, procurement, or contract events.
4. Prefer sys_id-based lookups for stockrooms, models, and users.
5. For SAM, rely on normalization and entitlement models before building custom compliance logic.

View File

@ -0,0 +1,308 @@
# ITOM (IT Operations Management)
## Discovery
### What Discovery Does
Finds devices, applications, and services on your network and populates the CMDB automatically.
### Discovery Schedules
```
Navigate: Discovery > Discovery Schedules
Key fields:
- Name, Discover: CI type (Servers, Network, Cloud, etc.)
- IP ranges / CIDR blocks
- MID Server: which MID runs the discovery
- Schedule: frequency (daily, weekly)
- Max run time: prevent runaway scans
Types:
- IP-based: scan IP ranges for devices
- Cloud-based: AWS, Azure, GCP discovery
- Kubernetes: container/cluster discovery
```
### MID Server
```
The MID (Management, Instrumentation, and Discovery) Server is a
Java application installed on-premise that:
- Runs Discovery probes against your network
- Executes Orchestration activities
- Handles IntegrationHub connections to internal systems
Setup:
1. Download MID Server package from instance
2. Install on a server with network access to targets
3. Configure config.xml (instance URL, credentials)
4. Validate from instance: MID Server > Servers
Key tables:
- ecc_agent: MID Server records
- ecc_queue: Input/output queue for MID communication
```
### Discovery Patterns (Horizontal)
```
Navigate: Pattern Designer
Patterns define HOW to discover specific application types.
Structure:
- Trigger: entry point (e.g., listening port 3306 = MySQL)
- Steps: commands to run (SSH, WMI, SNMP)
- Parsing: extract data from command output
- CI Creation: map parsed data to CMDB tables
Example — MySQL Pattern:
1. Trigger: Port 3306 open
2. SSH: `mysql --version`
3. SSH: `mysql -e "SHOW DATABASES"`
4. Parse: version, database names
5. Create: cmdb_ci_db_mysql_instance + relationships
```
### Credentials
```
Navigate: Discovery > Credentials
Types:
- SSH (Linux/Unix)
- Windows (WMI/PowerShell)
- SNMP (v1/v2c/v3)
- VMware (vCenter)
- Cloud (AWS/Azure/GCP API keys)
Security:
- Credentials stored encrypted
- Tag-based: assign credentials to IP ranges
- Least privilege: discovery accounts need read-only access
```
## Event Management
### Event Collection
```
Sources:
- SNMP traps → MID Server → Event table
- Syslog → MID Server → Event table
- REST API push → /api/global/em/jsonv2
- Email parsing
- Cloud monitoring (CloudWatch, Azure Monitor)
- APM tools (Dynatrace, AppDynamics, Datadog)
Event table: em_event
Key fields:
- Source, Node, Type, Resource
- Severity (1-5: Critical to Clear)
- Message key (deduplication identifier)
- Additional info (JSON payload)
```
### Event Rules
```
Navigate: Event Management > Rules
Processing pipeline:
1. Filtering rules — drop noise events
2. Transform rules — normalize fields
3. Deduplication — merge duplicate events
4. Alert rules — create alerts from events
Deduplication:
- Message key = unique identifier
- Same message key = same event, update count
- Prevents alert storms from flapping devices
```
### Alerts
```
Table: em_alert
Created from events after processing pipeline
Alert lifecycle:
Open → Acknowledged → Reopen → Closed
Alert actions:
- Create incident automatically
- Run remediation flow
- Notify on-call team
- Execute Orchestration runbook
Alert Management Rules:
Navigate: Event Management > Alert Management Rules
- Secondary alerts: group related alerts
- Alert correlation: link alerts to CIs
- Maintenance windows: suppress during planned outages
```
### Operator Workspace
```
Event Management Operator Workspace:
- Real-time alert dashboard
- Service health map
- Alert timeline
- Contextual CMDB data
Configuration:
Navigate: Event Management > Operator Workspace
```
## Service Mapping
### What Service Mapping Does
Discovers and maps **application services** — the relationships between servers, databases, load balancers, and other CIs that make up a business service.
### Service Map Creation
```
Navigate: Service Mapping > Home
Approaches:
1. Top-Down Discovery
- Start from entry point (URL, IP:port)
- Discovery follows connections automatically
- Maps entire service topology
2. Traffic-Based Discovery
- Analyze network traffic patterns
- Build maps from actual communication flows
3. Tag-Based (Cloud)
- Use cloud resource tags to group CIs
- AWS/Azure/GCP tagging conventions
```
### Service Mapping Patterns
```
Navigate: Service Mapping > Patterns
Connection patterns define how to follow relationships:
- TCP connections between hosts
- Load balancer → pool members
- Application → database connections
- Process → filesystem dependencies
Custom patterns for proprietary applications:
- SSH: check config files for connection strings
- API: query app health endpoints
- File: parse config for upstream/downstream
```
### Business Service Maps
```
Table: cmdb_ci_service (Business Service)
Map displays:
- Entry point (URL or IP)
- Web servers
- Application servers
- Databases
- Load balancers
- Cloud resources
- Relationships between all CIs
Used for:
- Impact analysis (which services affected by this CI?)
- Change risk assessment
- Incident correlation
- Capacity planning
```
## Orchestration
### Orchestration Activities
```
Navigate: Orchestration > Activities
Built-in activities:
- SSH Command (Linux)
- PowerShell (Windows)
- REST Message
- JDBC Query
- SNMP Set
- Cloud management (AWS EC2, Azure VM)
Custom activities:
- Create via Activity Designer
- Package as reusable components
- Can be used in Flows and Playbooks
```
### Runbook Automation
```
Use case: Automated remediation
Example — Disk Space Alert:
1. Alert fires: disk > 90% on server
2. Alert rule triggers Flow
3. Flow → SSH to server → Run cleanup script
4. Check result → disk < 80%?
5. Yes → Auto-close alert
6. No → Create incident, page on-call
Implementation:
- Flow trigger: Alert created, severity = Critical
- Conditions: type = "disk_space"
- Actions: SSH command via MID Server
- Error handling: create incident if remediation fails
```
## Cloud Management
### Cloud Discovery
```
Supported clouds:
- AWS (EC2, RDS, S3, Lambda, ELB, VPC)
- Azure (VMs, SQL, Storage, Functions, App Service)
- GCP (Compute, Cloud SQL, Storage, Functions)
Configuration:
1. Cloud credentials (access keys or service principal)
2. Discovery schedule targeting cloud provider
3. Cloud regions to scan
4. CI mapping rules (cloud resource → CMDB class)
```
### Cloud Provisioning
```
Navigate: Cloud Management > Cloud Services Catalog
Catalog items for cloud resources:
- Provision VM (size, image, network)
- Create database instance
- Deploy container cluster
Lifecycle management:
- Start/Stop/Restart
- Resize/Scale
- Snapshot/Backup
- Decommission
```
## Health Log Analytics (HLA)
### Log Collection
```
Sources:
- Application logs via MID Server
- Cloud logs (CloudWatch Logs, Azure Log Analytics)
- Container logs (stdout/stderr)
Processing:
- Log sources → MID Server → HLA pipeline
- Pattern detection: identify anomalies
- Alert creation: unusual log patterns → alerts
- Correlation: link log anomalies to CIs
```
## ITOM Best Practices
1. **Start with Discovery** — clean CMDB is foundation for everything
2. **Service Mapping after Discovery** — need CIs before mapping relationships
3. **Deduplicate aggressively** — event storms kill alert value
4. **Automate remediation** — common issues should self-heal
5. **Maintenance windows** — suppress during planned changes
6. **MID Server sizing** — dedicated MID per function (discovery, orchestration, events)
7. **Credential management** — rotate regularly, audit access
8. **Tag your cloud resources** — enables clean discovery and cost tracking
9. **Monitor MID Server health** — it's a SPOF for ITOM operations
10. **Start small** — discover one data center, then expand

View File

@ -0,0 +1,726 @@
# ITSM (IT Service Management)
## Core ITSM Tables
```
incident — Incident Management
problem — Problem Management
change_request — Change Management
change_task — Change Tasks
sc_request — Service Request (RITM parent)
sc_req_item — Requested Item (RITM)
sc_task — Catalog Tasks
kb_knowledge — Knowledge Management
task_sla — SLA instances on records
cmdb_ci — Configuration Items
cmdb_rel_ci — CI Relationships
```
## Incident Management
### Incident Lifecycle
```
State flow:
1 - New
2 - In Progress
3 - On Hold
6 - Resolved
7 - Closed
8 - Canceled
Priority matrix (auto-calculated):
Impact × Urgency = Priority
Impact: 1-High 2-Medium 3-Low
Urgency: 1-High 2-Medium 3-Low
1×1=P1(Critical) 1×2=P2(High) 1×3=P3(Moderate)
2×1=P2(High) 2×2=P3(Moderate) 2×3=P4(Low)
3×1=P3(Moderate) 3×2=P4(Low) 3×3=P5(Planning)
```
### Incident Scripting Patterns
```javascript
// Auto-assign incident based on category + location
// Business Rule: Before Insert on incident
(function executeRule(current, previous) {
if (!current.assignment_group) {
var category = current.getValue('category');
var location = current.getValue('location');
var rules = new GlideRecord('x_assignment_rule');
rules.addQuery('category', category);
rules.addQuery('location', location);
rules.addActiveQuery();
rules.orderBy('order');
rules.setLimit(1);
rules.query();
if (rules.next()) {
current.setValue('assignment_group', rules.getValue('assignment_group'));
}
}
})(current, previous);
```
```javascript
// Reopen incident if customer responds after resolution
// Business Rule: Before Update on incident
(function executeRule(current, previous) {
if (current.getValue('state') == '6' && current.comments.changes()) {
// Check if comment is from caller (not agent)
var lastComment = current.comments.getJournalEntry(1);
if (lastComment.indexOf(current.caller_id.getDisplayValue()) > -1) {
current.setValue('state', '2'); // In Progress
current.setValue('work_notes', 'Reopened: customer added comments after resolution');
}
}
})(current, previous);
```
```javascript
// Escalation script — auto-escalate overdue P1 incidents
// Scheduled Job: runs every 15 minutes
(function() {
var gr = new GlideRecord('incident');
gr.addQuery('priority', 1);
gr.addQuery('state', 'IN', '1,2'); // New or In Progress
gr.addQuery('escalation', 0); // Not yet escalated
// SLA breached or about to breach
var slaGr = new GlideRecord('task_sla');
slaGr.addQuery('task.priority', 1);
slaGr.addQuery('stage', 'IN', 'breached,warning');
slaGr.addQuery('active', true);
slaGr.query();
var incidentIds = [];
while (slaGr.next()) {
incidentIds.push(slaGr.getValue('task'));
}
if (incidentIds.length > 0) {
gr.addQuery('sys_id', 'IN', incidentIds.join(','));
gr.query();
while (gr.next()) {
gr.setValue('escalation', '1');
gr.setValue('work_notes', 'Auto-escalated: SLA breached or in warning state');
gr.update();
// Notify management
gs.eventQueue('incident.escalated', gr,
gr.assignment_group.manager.toString(), '');
}
}
})();
```
### Major Incident Management
```
Major Incident Process:
1. Declare major incident (set major_incident_state = accepted)
2. Create communication channels (bridge call, Slack/Teams)
3. Assign Incident Commander
4. Track child incidents (parent_incident field)
5. Regular stakeholder updates (communication plan)
6. Resolve → Post-Incident Review → Problem record
Key fields on incident:
- major_incident_state: none, proposed, accepted, rejected, closed
- parent_incident: links child incidents to major
- business_impact: free text for executive communication
Communication plan script:
```
```javascript
// Script Include: Major Incident Communications
var MajorIncidentComms = Class.create();
MajorIncidentComms.prototype = {
initialize: function(incidentGr) {
this.incident = incidentGr;
},
sendStakeholderUpdate: function(updateText) {
// Get all affected CIs' business service owners
var stakeholders = this._getStakeholders();
// Create communication task
var commTask = new GlideRecord('task');
commTask.initialize();
commTask.setValue('short_description', 'Major Incident Update: ' +
this.incident.getValue('number'));
commTask.setValue('description', updateText);
commTask.setValue('parent', this.incident.getUniqueValue());
commTask.insert();
// Fire notification event
gs.eventQueue('major.incident.update', this.incident,
stakeholders.join(','), updateText);
},
_getStakeholders: function() {
var users = [];
// Get business service owners for affected CIs
var rel = new GlideRecord('cmdb_rel_ci');
rel.addQuery('child', this.incident.getValue('cmdb_ci'));
rel.addQuery('type.name', 'Depends on::Used by');
rel.query();
while (rel.next()) {
var svc = new GlideRecord('cmdb_ci_service');
if (svc.get(rel.getValue('parent'))) {
if (svc.getValue('owned_by')) {
users.push(svc.getValue('owned_by'));
}
}
}
return users;
},
type: 'MajorIncidentComms'
};
```
## Problem Management
### Problem Lifecycle
```
States:
1 - New (Open)
2 - Assessed / Known Error
3 - Root Cause Identified
4 - Fix in Progress
5 - Resolved
7 - Closed
Key fields:
- known_error: boolean (is this a known error?)
- workaround: text (temporary fix)
- root_cause: text (identified root cause)
- fix_notes: text (permanent fix details)
- related_incidents: M2M to incident table
```
### Problem Scripting
```javascript
// Auto-create problem from recurring incidents
// Scheduled Job: runs daily
(function() {
// Find categories with 5+ incidents in past 7 days
var ga = new GlideAggregate('incident');
ga.addQuery('sys_created_on', '>=', gs.daysAgoStart(7));
ga.addActiveQuery();
ga.groupBy('category');
ga.groupBy('cmdb_ci');
ga.addAggregate('COUNT');
ga.addHaving('COUNT', '>', 5);
ga.query();
while (ga.next()) {
var category = ga.getValue('category');
var ci = ga.getValue('cmdb_ci');
var count = ga.getAggregate('COUNT');
// Check if problem already exists
var existing = new GlideRecord('problem');
existing.addQuery('category', category);
existing.addQuery('cmdb_ci', ci);
existing.addActiveQuery();
existing.setLimit(1);
existing.query();
if (!existing.hasNext()) {
var prob = new GlideRecord('problem');
prob.initialize();
prob.setValue('short_description', 'Recurring incidents: ' + category +
' (' + count + ' in 7 days)');
prob.setValue('category', category);
prob.setValue('cmdb_ci', ci);
prob.setValue('description', count + ' incidents created in the past 7 days ' +
'for category ' + category + ' on CI ' + ci.getDisplayValue());
prob.insert();
gs.info('Auto-created problem: ' + prob.getValue('number'));
}
}
})();
```
### Known Error Database (KEDB)
```javascript
// Script Include: Search known errors for matching workarounds
var KEDBSearch = Class.create();
KEDBSearch.prototype = {
initialize: function() {},
findWorkaround: function(incidentGr) {
var results = [];
var prob = new GlideRecord('problem');
prob.addQuery('known_error', true);
prob.addQuery('workaround', '!=', '');
prob.addActiveQuery();
// Match on category + CI
var category = incidentGr.getValue('category');
var ci = incidentGr.getValue('cmdb_ci');
if (ci) {
prob.addQuery('cmdb_ci', ci);
} else {
prob.addQuery('category', category);
}
prob.query();
while (prob.next()) {
results.push({
number: prob.getValue('number'),
description: prob.getValue('short_description'),
workaround: prob.getValue('workaround'),
fix_planned: prob.getValue('fix_notes') != ''
});
}
return results;
},
type: 'KEDBSearch'
};
```
## Change Management
### Change Types
```
Standard Change:
- Pre-approved, low-risk
- Uses templates (std_change_proposal)
- No CAB approval needed
- Example: password reset, scheduled patch
Normal Change:
- Requires assessment and approval
- Goes through CAB (Change Advisory Board)
- Full risk assessment
- Example: server migration, app upgrade
Emergency Change:
- Expedited approval (post-implementation review)
- For critical production issues
- Retrospective documentation required
- Example: emergency security patch
```
### Change States
```
-5 - New
-4 - Assess
-3 - Authorize (approval phase)
-2 - Scheduled
-1 - Implement
0 - Review
3 - Closed
4 - Canceled
```
### Change Risk Assessment
```javascript
// Business Rule: Auto-calculate risk score
// Before Insert/Update on change_request
(function executeRule(current, previous) {
var riskScore = 0;
// Impact-based scoring
var impact = parseInt(current.getValue('impact'));
riskScore += (4 - impact) * 10; // High impact = more risk
// CI criticality
if (current.cmdb_ci) {
var ci = new GlideRecord('cmdb_ci');
if (ci.get(current.getValue('cmdb_ci'))) {
var criticality = ci.getValue('busines_criticality');
if (criticality == '1') riskScore += 30; // Most critical
else if (criticality == '2') riskScore += 20;
else riskScore += 10;
}
}
// Time-based risk
var plannedStart = new GlideDateTime(current.getValue('start_date'));
var dayOfWeek = plannedStart.getDayOfWeekLocalTime();
if (dayOfWeek >= 2 && dayOfWeek <= 6) { // Mon-Fri
riskScore += 15; // Business hours = higher risk
}
// Backout plan exists?
if (!current.getValue('backout_plan')) {
riskScore += 20;
}
// Test plan exists?
if (!current.getValue('test_plan')) {
riskScore += 15;
}
// Set risk level
if (riskScore >= 60) current.setValue('risk', '1'); // High
else if (riskScore >= 30) current.setValue('risk', '2'); // Moderate
else current.setValue('risk', '3'); // Low
current.setValue('risk_value', riskScore);
})(current, previous);
```
### Change Conflict Detection
```javascript
// Script Include: Check for scheduling conflicts
var ChangeConflict = Class.create();
ChangeConflict.prototype = {
initialize: function() {},
checkConflicts: function(changeGr) {
var conflicts = [];
var start = changeGr.getValue('start_date');
var end = changeGr.getValue('end_date');
var ci = changeGr.getValue('cmdb_ci');
// Check overlapping changes on same CI
var gr = new GlideRecord('change_request');
gr.addQuery('sys_id', '!=', changeGr.getUniqueValue());
gr.addQuery('cmdb_ci', ci);
gr.addQuery('state', 'IN', '-2,-1'); // Scheduled or Implement
gr.addQuery('start_date', '<=', end);
gr.addQuery('end_date', '>=', start);
gr.query();
while (gr.next()) {
conflicts.push({
number: gr.getValue('number'),
description: gr.getValue('short_description'),
window: gr.getValue('start_date') + ' to ' + gr.getValue('end_date')
});
}
// Check blackout windows
var blackout = new GlideRecord('change_blackout');
blackout.addActiveQuery();
blackout.addQuery('start_date', '<=', end);
blackout.addQuery('end_date', '>=', start);
blackout.query();
while (blackout.next()) {
conflicts.push({
number: 'BLACKOUT',
description: blackout.getValue('name'),
window: blackout.getValue('start_date') + ' to ' +
blackout.getValue('end_date')
});
}
return conflicts;
},
type: 'ChangeConflict'
};
```
## SLA Management
### SLA Definition
```
Navigate: Service Level Management > SLA > SLA Definitions
Table: contract_sla
Key fields:
- Name: descriptive (e.g., "P1 Incident Response - 15 min")
- Table: target table (incident, sn_customerservice_case, etc.)
- Type: Response, Resolution, or Custom
- Duration: SLA target time
- Schedule: business schedule (e.g., 24x7, 8x5)
- Conditions: when SLA applies
- Start condition: when SLA clock starts
- Pause condition: when clock pauses (e.g., On Hold)
- Stop condition: when SLA is achieved
- Reset condition: when SLA resets
```
### SLA Scripting
```javascript
// Script Include: SLA breach prediction
var SLAPredictor = Class.create();
SLAPredictor.prototype = {
initialize: function() {},
getAtRiskTasks: function(tableName, minutes) {
// Find tasks with SLA about to breach
var results = [];
var sla = new GlideRecord('task_sla');
sla.addQuery('task.sys_class_name', tableName);
sla.addQuery('active', true);
sla.addQuery('stage', 'in_progress');
// Calculate breach time threshold
var threshold = new GlideDateTime();
threshold.addSeconds(minutes * 60);
sla.addQuery('planned_end_time', '<=', threshold);
sla.query();
while (sla.next()) {
var remaining = GlideDateTime.subtract(
new GlideDateTime(),
new GlideDateTime(sla.getValue('planned_end_time'))
);
results.push({
task: sla.task.getDisplayValue(),
sla_name: sla.sla.getDisplayValue(),
breach_in: remaining.getDisplayValue(),
assigned_to: sla.task.assigned_to.getDisplayValue(),
priority: sla.task.getValue('priority')
});
}
return results;
},
type: 'SLAPredictor'
};
```
## Assignment Rules
### Auto-Assignment
```
Navigate: System Policy > Assignment
Assignment rule fields:
- Table: target table
- Conditions: when rule applies
- Group: target assignment group
- User: specific user (optional)
- Script: advanced assignment logic (optional)
- Order: evaluation priority (lower = first)
```
### Round-Robin Assignment
```javascript
// Script Include: Round-robin within a group
var RoundRobin = Class.create();
RoundRobin.prototype = {
initialize: function() {},
getNextAgent: function(groupSysId) {
// Get group members
var members = [];
var gm = new GlideRecord('sys_user_grmember');
gm.addQuery('group', groupSysId);
gm.query();
while (gm.next()) {
var user = gm.getValue('user');
// Check user is active and not on leave
var userGr = new GlideRecord('sys_user');
if (userGr.get(user) && userGr.getValue('active') == 'true') {
members.push(user);
}
}
if (members.length == 0) return null;
// Find member with fewest active assignments
var minCount = 999999;
var nextAgent = members[0];
for (var i = 0; i < members.length; i++) {
var ga = new GlideAggregate('incident');
ga.addQuery('assigned_to', members[i]);
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.query();
var count = 0;
if (ga.next()) {
count = parseInt(ga.getAggregate('COUNT'));
}
if (count < minCount) {
minCount = count;
nextAgent = members[i];
}
}
return nextAgent;
},
type: 'RoundRobin'
};
```
## Knowledge Management
### Knowledge Base Structure
```
Tables:
kb_knowledge_base — Knowledge Base container
kb_knowledge — Knowledge Articles
kb_category — Article categories
kb_feedback — Article feedback/ratings
Article workflow:
Draft → Review → Published → Retired
Key fields:
- Knowledge base, Category
- Short description, Text (article body)
- Valid to (expiration date)
- Workflow state
- Article type: text, wiki, external
```
### Knowledge API
```javascript
// Script Include: Search knowledge base
var KBSearch = Class.create();
KBSearch.prototype = {
initialize: function() {},
searchArticles: function(searchTerm, kbSysId, limit) {
var results = [];
var gr = new GlideRecord('kb_knowledge');
gr.addQuery('kb_knowledge_base', kbSysId);
gr.addQuery('workflow_state', 'published');
gr.addQuery('active', true);
// Full-text search
gr.addQuery('123TEXTQUERY321', searchTerm);
gr.setLimit(limit || 10);
gr.query();
while (gr.next()) {
results.push({
sys_id: gr.getUniqueValue(),
number: gr.getValue('number'),
title: gr.getValue('short_description'),
category: gr.category.getDisplayValue(),
views: gr.getValue('sys_view_count'),
rating: gr.getValue('rating')
});
}
return results;
},
// Link knowledge article to incident
linkToIncident: function(articleSysId, incidentSysId) {
var m2m = new GlideRecord('m2m_kb_task');
m2m.initialize();
m2m.setValue('kb_knowledge', articleSysId);
m2m.setValue('task', incidentSysId);
m2m.insert();
},
type: 'KBSearch'
};
```
## ITSM Reporting & Metrics
### Key ITSM Metrics
```
Incident:
- MTTR (Mean Time to Resolve) by priority
- First Contact Resolution rate
- Reopen rate
- SLA compliance % (response + resolution)
- Incidents by category/CI/group
- Backlog aging
Problem:
- Known Error count and resolution time
- Problems created from incidents (proactive %)
- Root cause categories
Change:
- Change success rate (vs failed/unauthorized)
- Emergency change % (should be <10%)
- Lead time (submit to implement)
- CAB approval time
- Changes causing incidents
Knowledge:
- Article usage (views, feedback score)
- Deflection rate (self-service resolution)
- Knowledge gap analysis
```
### Reporting Script
```javascript
// Script Include: ITSM Dashboard Metrics
var ITSMMetrics = Class.create();
ITSMMetrics.prototype = {
initialize: function() {},
getIncidentMetrics: function(daysBack) {
var metrics = {};
var startDate = gs.daysAgoStart(daysBack);
// Total incidents
var ga = new GlideAggregate('incident');
ga.addQuery('sys_created_on', '>=', startDate);
ga.addAggregate('COUNT');
ga.query();
metrics.total = ga.next() ? parseInt(ga.getAggregate('COUNT')) : 0;
// By priority
ga = new GlideAggregate('incident');
ga.addQuery('sys_created_on', '>=', startDate);
ga.groupBy('priority');
ga.addAggregate('COUNT');
ga.query();
metrics.by_priority = {};
while (ga.next()) {
metrics.by_priority[ga.getValue('priority')] =
parseInt(ga.getAggregate('COUNT'));
}
// MTTR (resolved incidents)
ga = new GlideAggregate('incident');
ga.addQuery('resolved_at', '>=', startDate);
ga.addQuery('resolved_at', 'ISNOTEMPTY', '');
ga.addAggregate('AVG', 'calendar_duration');
ga.query();
if (ga.next()) {
var avgSeconds = parseFloat(ga.getAggregate('AVG', 'calendar_duration'));
metrics.mttr_hours = Math.round(avgSeconds / 3600 * 10) / 10;
}
// SLA compliance
var slaGa = new GlideAggregate('task_sla');
slaGa.addQuery('task.sys_class_name', 'incident');
slaGa.addQuery('task.sys_created_on', '>=', startDate);
slaGa.addQuery('active', false);
slaGa.groupBy('has_breached');
slaGa.addAggregate('COUNT');
slaGa.query();
var met = 0, breached = 0;
while (slaGa.next()) {
if (slaGa.getValue('has_breached') == 'true') {
breached = parseInt(slaGa.getAggregate('COUNT'));
} else {
met = parseInt(slaGa.getAggregate('COUNT'));
}
}
metrics.sla_compliance = met + breached > 0 ?
Math.round(met / (met + breached) * 100) : 0;
return metrics;
},
type: 'ITSMMetrics'
};
```
## ITSM Best Practices
1. **Priority = Impact × Urgency** — never let users set priority directly
2. **Assignment groups before users** — assign to groups, let groups manage individual assignment
3. **Mandatory fields per state** — require resolution notes before closing, category before routing
4. **SLAs on everything** — response AND resolution, measured against business schedules
5. **Knowledge-centered service** — every resolved incident should check: can this be a KB article?
6. **Parent-child for major incidents** — track affected users/locations as child incidents
7. **Standard changes for repetitive work** — pre-approved templates reduce CAB load
8. **Post-incident review** — every P1/P2 should create a problem record
9. **Metrics-driven improvement** — track MTTR, reopen rate, SLA compliance monthly
10. **CMDB accuracy** — if your CMDB is wrong, impact analysis and assignment rules will fail

View File

@ -0,0 +1,332 @@
# Notifications
## Notification Types
```
Email Notifications (sysevent_email_action)
- Record-based: triggered by insert/update/delete on a table
- Event-based: triggered by system events (sysevent)
- Scheduled: sent on a schedule
Push Notifications (sys_push_notification)
- Mobile app push notifications
- Now Mobile, Agent Mobile
SMS Notifications (sys_sms_notification)
- Via SMS gateway integration
- Twilio, AT&T, custom providers
```
## Email Notifications
### Creating a Notification
```
Navigate: System Notification > Email > Notifications
Table: sysevent_email_action
Key fields:
- Name: descriptive name
- Table: target table (e.g., incident)
- When to send:
- Record inserted
- Record updated
- Record inserted or updated
- Event is fired
- Conditions: filter for when notification applies
- Who will receive:
- Users in fields (assigned_to, opened_by, etc.)
- Groups
- Users/Groups in related tables
- Event parm 1 / parm 2 recipients
- Explicit email addresses
- Weight: priority (higher weight = higher priority when throttled)
```
### Condition-Based Notification
```
Name: P1 Incident Assigned
Table: incident
When: Record updated
Conditions:
- Priority = 1 - Critical
- Assigned to changes
- Active = true
Send to:
- Users in fields: Assigned to
- Also CC: Assignment group manager
Subject: [${number}] Critical Incident Assigned to You: ${short_description}
```
### Event-Based Notification
```
Name: Password Reset Confirmation
Table: sys_user
When: Event is fired
Event name: user.password.reset
Send to:
- Event parm 1 (contains user sys_id)
Subject: Password Reset Successful
Body: Your password has been reset. If you did not request this, contact IT immediately.
```
### Firing Custom Events
```javascript
// Server script — fire a custom event
// Parameters: event_name, record, parm1, parm2
gs.eventQueue('custom.incident.escalated', current,
current.assigned_to.toString(), // parm1: recipient user sys_id
current.getValue('number') // parm2: additional context
);
```
## Email Templates
### Mail Script (Advanced)
```
Table: sys_script_email
Use: Dynamic content in notification body
Example: Include related change requests
```
```javascript
// Mail Script: include_related_changes
(function runMailScript(current, template, email, email_action, event) {
var changes = [];
var gr = new GlideRecord('change_request');
gr.addQuery('cmdb_ci', current.getValue('cmdb_ci'));
gr.addQuery('state', 'IN', '-5,-4,-3,-2,-1'); // Open states
gr.orderByDesc('sys_created_on');
gr.setLimit(5);
gr.query();
while (gr.next()) {
changes.push(gr.getValue('number') + ' - ' + gr.getValue('short_description'));
}
if (changes.length > 0) {
template.print('<h3>Related Open Changes:</h3><ul>');
for (var i = 0; i < changes.length; i++) {
template.print('<li>' + changes[i] + '</li>');
}
template.print('</ul>');
} else {
template.print('<p>No related open changes found.</p>');
}
})(current, template, email, email_action, event);
```
### Using Mail Scripts in Notifications
```html
<!-- In notification body (HTML) -->
<h2>Incident ${number} - ${short_description}</h2>
<p>Priority: ${priority}</p>
<p>State: ${state}</p>
<!-- Insert mail script output -->
${mail_script:include_related_changes}
<!-- Standard fields -->
<p>Assigned to: ${assigned_to.name}</p>
<p>Assignment group: ${assignment_group.name}</p>
<!-- Link to record -->
<p><a href="${URI_REF}">View Incident</a></p>
```
### Email Layout
```
Table: sysevent_email_template
Use: Consistent branding across all notifications
Structure:
- Header (logo, company name)
- Body (notification-specific content inserted here)
- Footer (unsubscribe link, legal text)
Key field: Message HTML with ${body} placeholder
```
## Notification Preferences
### User Preferences
```
Navigate: System Notification > Email > Notification Preferences
Users can:
- Opt out of specific notifications
- Change delivery channel (email, SMS, push)
- Set digest schedules (hourly, daily summary)
Admin controls:
- Mandatory notifications (users cannot opt out)
- Device-specific settings
```
### Digest Notifications
```
Navigate: System Notification > Email > Notifications > Digest tab
Configuration:
- Interval: Hourly, Daily, Weekly
- Send time: specific time of day
- Group by: table, category, or custom
Use case: Instead of 50 individual emails,
send one daily digest of all P3-P4 incident updates
```
## Inbound Email Actions
### Processing Incoming Emails
```
Table: sysevent_in_email_action
Use: Create/update records from incoming emails
Example: Create incident from email
Target table: incident
Conditions:
- Subject contains "URGENT" or "incident"
- From address is in sys_user
Action script:
```
```javascript
// Inbound email action script
(function runAction(current, event, email, logger) {
// current = target record (incident)
// email = incoming email object
current.setValue('short_description', email.subject);
current.setValue('description', email.body_text);
current.setValue('caller_id', email.from); // Auto-matches to sys_user
current.setValue('contact_type', 'email');
// Parse priority from subject
if (email.subject.indexOf('[P1]') > -1) {
current.setValue('priority', '1');
} else if (email.subject.indexOf('[P2]') > -1) {
current.setValue('priority', '2');
}
// Handle attachments
// Attachments are auto-attached to the record
current.insert();
// Reply to sender
email.setReplyTo(current); // Future replies thread to this record
})(current, event, email, logger);
```
### Reply Processing (Threading)
```javascript
// Inbound action for replies (updates existing record)
(function runAction(current, event, email, logger) {
// current = existing incident (matched by watermark in email)
// Add email body as work note
current.setValue('work_notes', 'Email from ' + email.from + ':\n' + email.body_text);
current.update();
})(current, event, email, logger);
```
## SMS Notifications
### Configuration
```
Navigate: System Notification > SMS > SMS Configuration
Setup:
1. Configure SMS provider (Twilio, etc.)
2. Set up SMS Notification records
3. Map phone numbers to user records
Twilio Integration:
- Account SID, Auth Token
- From Number
- REST endpoint: api.twilio.com
```
### SMS Notification Record
```
Table: sys_sms_notification
Similar to email notifications but:
- Character limit (160 for SMS, varies for MMS)
- No HTML formatting
- Phone number field required on user record
Message template:
"${number} ${priority} incident assigned to you: ${short_description}. Reply ACKNOWLEDGE to accept."
```
## Push Notifications
### Mobile Push Configuration
```
Navigate: System Notification > Push > Push Notifications
Setup:
- Firebase Cloud Messaging (FCM) for Android
- Apple Push Notification Service (APNs) for iOS
- ServiceNow Mobile App handles registration
Push Notification record:
- Table, Conditions (same as email)
- Title, Body (shorter than email)
- Deep link: opens specific record in mobile app
```
## Notification Troubleshooting
### Debug Notifications
```
Navigate: System Notification > Email > Notifications
Diagnostics:
1. System Logs > Email Log — see all sent/failed emails
2. System Diagnostics > Email — check SMTP connectivity
3. Notification Preview — test with a specific record
Common issues:
- SMTP not configured (System Mailboxes > Outbound)
- Email address missing on user record
- Notification inactive
- Conditions not met (test with a known record)
- Spam filter blocking ServiceNow domain
- Email throttled (check weight/throttle settings)
```
### Testing Notifications
```javascript
// Fix Script — manually trigger a notification for testing
var rec = new GlideRecord('incident');
if (rec.get('number', 'INC0012345')) {
// Fire event that notification listens to
gs.eventQueue('incident.assigned', rec,
rec.getValue('assigned_to'),
rec.getValue('number'));
}
// OR trigger notification directly
var notify = new SNCEmailSender();
notify.send(rec, 'your_notification_sys_id');
```
## Best Practices
1. **Event-based > direct** — decouple notification from transaction processing
2. **Digest for low-priority** — reduce email fatigue
3. **Mandatory for critical** — P1 incident notifications should not be opt-outable
4. **Mail scripts for dynamic content** — don't cram logic into notification body
5. **Test with Notification Preview** — before activating in production
6. **Monitor email logs** — failed deliveries often go unnoticed
7. **Unsubscribe links** — include in footer for GDPR/CAN-SPAM compliance
8. **Don't over-notify** — every unnecessary email erodes trust in the system
9. **Use weight field** — prioritize critical notifications during throttle periods
10. **Template your layouts** — consistent branding builds user confidence

View File

@ -0,0 +1,351 @@
# Now Experience / UI Builder
## UI Builder Overview
UI Builder is the visual editor for creating **Now Experience** pages — modern, responsive UIs that replace Service Portal for new development.
Navigate: **UI Builder** (All > Now Experience > UI Builder)
## Pages & Variants
### Page Structure
```
Page: Incident Overview
├── URL: /x/myapp/incident-overview
├── Variants:
│ ├── Default (desktop)
│ ├── Tablet variant (breakpoint: 768px)
│ └── Mobile variant (breakpoint: 480px)
├── Data Resources:
│ ├── Look Up incident record (GlideRecord)
│ └── Get user preferences (Script)
└── Components:
├── Header (Now Heading)
├── Record Details (Now Record Fields)
├── Activity Stream
└── Related Lists
```
### Creating a Page
1. Open UI Builder
2. Click **Create** → **Page**
3. Choose **Experience**: Workspace, Portal, or Custom App
4. Set URL path, name, description
5. Add components from the component palette
## Data Resources
### Record Data Resource
```
Type: Look up record
Table: incident
Conditions: sys_id = [Page Parameter: sys_id]
Fields: number, short_description, state, priority, assigned_to, caller_id
```
### GraphQL Data Resource
```
Type: GraphQL
Query:
query($sysId: String!) {
GlideRecord_Query {
incident(queryConditions: { sys_id: { _eq: $sysId } }) {
number { value displayValue }
short_description { value }
state { value displayValue }
assigned_to { value displayValue }
}
}
}
Variables:
sysId: [Page Parameter: sys_id]
```
### Script Data Resource
```javascript
// Server-side script data resource
(function(dataResourceParams) {
var result = {
incidents: [],
totalCount: 0
};
var ga = new GlideAggregate('incident');
ga.addQuery('assigned_to', gs.getUserID());
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
result.totalCount = parseInt(ga.getAggregate('COUNT'));
}
var gr = new GlideRecord('incident');
gr.addQuery('assigned_to', gs.getUserID());
gr.addActiveQuery();
gr.orderByDesc('priority');
gr.setLimit(10);
gr.query();
while (gr.next()) {
result.incidents.push({
sys_id: gr.getUniqueValue(),
number: gr.getValue('number'),
short_description: gr.getValue('short_description'),
priority: gr.getValue('priority')
});
}
return result;
})(dataResourceParams);
```
## Custom Components (Seismic Framework)
### Component Scaffold (CLI)
```bash
# Install Now CLI
npm install -g @servicenow/cli
# Create component project
snc ui-component create --name x-myapp-incident-card
cd x-myapp-incident-card
# Project structure:
# ├── src/
# │ ├── x-myapp-incident-card/
# │ │ ├── __tests__/
# │ │ ├── index.js ← main component
# │ │ ├── styles.scss ← component styles
# │ │ └── actionHandlers.js ← event handlers
# │ └── index.js
# ├── package.json
# └── now-ui.json ← component metadata
```
### Component Definition
```javascript
// src/x-myapp-incident-card/index.js
import { createCustomElement } from '@servicenow/ui-core';
import snabbdom from '@servicenow/ui-renderer-snabbdom';
import styles from './styles.scss';
const view = (state, { updateState, dispatch }) => {
const { incidents, loading } = state;
return (
<div className="incident-card">
{loading ? (
<now-loader label="Loading..." />
) : (
<div className="incident-list">
{incidents.map(inc => (
<div className="incident-item"
key={inc.sys_id}
on-click={() => dispatch('INCIDENT_SELECTED', { sys_id: inc.sys_id })}>
<span className="number">{inc.number}</span>
<span className="desc">{inc.short_description}</span>
<now-highlighted-value
label="Priority"
value={inc.priority}
variant={inc.priority === '1' ? 'critical' : 'normal'} />
</div>
))}
</div>
)}
</div>
);
};
createCustomElement('x-myapp-incident-card', {
renderer: { type: snabbdom },
view,
styles,
properties: {
assignedTo: { default: '' },
maxRecords: { default: 10 }
},
initialState: {
incidents: [],
loading: true
},
actionHandlers: {
'COMPONENT_RENDERED': ({ dispatch, properties }) => {
dispatch('FETCH_INCIDENTS', {
assignedTo: properties.assignedTo,
limit: properties.maxRecords
});
}
}
});
```
### Action Handlers (data fetching)
```javascript
// src/x-myapp-incident-card/actionHandlers.js
import { actionTypes } from '@servicenow/ui-core';
import { createHttpEffect } from '@servicenow/ui-effect-http';
export default {
'FETCH_INCIDENTS': createHttpEffect('/api/now/table/incident', {
method: 'GET',
queryParams: {
sysparm_query: ({ action }) =>
`assigned_to=${action.payload.assignedTo}^active=true`,
sysparm_limit: ({ action }) => action.payload.limit,
sysparm_fields: 'sys_id,number,short_description,priority,state'
},
successActionType: 'FETCH_INCIDENTS_SUCCESS',
errorActionType: 'FETCH_INCIDENTS_ERROR'
}),
'FETCH_INCIDENTS_SUCCESS': ({ action, updateState }) => {
updateState({
incidents: action.payload.result,
loading: false
});
},
'FETCH_INCIDENTS_ERROR': ({ updateState }) => {
updateState({ loading: false, incidents: [] });
}
};
```
### Deploy Component
```bash
# Build and deploy to instance
snc ui-component deploy --instance https://myinstance.service-now.com
# Development mode (live reload)
snc ui-component develop --instance https://myinstance.service-now.com
```
## Workspaces
### Agent Workspace Configuration
```
Navigate: Workspace Experience > Workspace Configuration
Key settings:
- Landing page: Dashboard or list view
- Record pages: Layout per table (incident, problem, change)
- Lists: Column configuration, filters
- Contextual sidebar: Related records, activity stream
- Playbook panel: Position and auto-open settings
```
### Workspace Record Page Layout
```
┌─────────────────────────────────────────────────┐
│ Header: Number | State | Priority | Actions │
├──────────────────────┬──────────────────────────┤
│ Form Fields │ Contextual Sidebar │
│ - Short Description │ - Activity Stream │
│ - Caller │ - Related Records │
│ - Category │ - Similar Incidents │
│ - Assignment Group │ - Playbook Activities │
│ - Assigned To │ │
│ - Description │ │
├──────────────────────┴──────────────────────────┤
│ Related Lists / Tabs │
│ [Tasks] [Worknotes] [Attachments] [Approvals] │
└─────────────────────────────────────────────────┘
```
## Declarative Actions
Replace UI Actions in workspaces:
```
Navigate: Now Experience > Declarative Actions
Action: Escalate Incident
Applicable to: incident table
Conditions: state != 6 AND state != 7 (not resolved/closed)
Roles: itil
Action type: Record action
Form: Form button
List: List action
Implementation:
1. Client action: Show confirmation dialog
2. Server action: Update record (priority=1, state=2)
3. Client action: Show success banner
```
## UX Page Properties & State
### Page Parameters
```
Define URL parameters that components can consume:
/incident-dashboard?group=SYS_ID&view=kanban
Components bind to page parameters:
Component property: group → Page parameter: group
Component property: viewType → Page parameter: view
```
### Client State Parameters
```
Share state between components on same page:
Component A dispatches: UPDATE_CLIENT_STATE_PARAM
key: selectedIncident
value: sys_id
Component B listens: CLIENT_STATE_PARAM_CHANGED
key: selectedIncident
→ Fetches and displays incident detail
```
## Theming & Branding
```
Navigate: Now Experience > Theme Management
Customizable:
- Primary/secondary colors
- Font family and sizes
- Logo and favicon
- Header/navigation colors
- Component-level overrides via CSS custom properties
CSS Variables:
--now-color--primary-1
--now-color--secondary-1
--now-font-family
--now-button--primary--background-color
```
## Mobile Development
### Configurable Workspace (mobile-responsive)
- Workspaces auto-adapt to mobile breakpoints
- Configure mobile-specific variants in UI Builder
- Simplified navigation: bottom tabs, swipe gestures
### Native Mobile (ServiceNow Mobile App)
```
Navigate: Now Mobile > Mobile App Builder
Applets:
- My Requests
- Approvals
- Agent (for ITSM agents)
- Custom applets via Mobile App Builder
Mobile Cards:
- Configurable list views
- Quick actions (swipe)
- Offline support (limited)
```
## Best Practices
1. **Use UI Builder for new development** — Service Portal is legacy
2. **GraphQL over REST** for data resources — better performance, field selection
3. **Component reuse** — build generic components, configure via properties
4. **Minimize data resources** — combine queries where possible
5. **Test at all breakpoints** — desktop, tablet, mobile
6. **Use Now Design System components** — consistent UX, accessibility
7. **Declarative Actions over UI Actions** for workspaces
8. **Cache static data** — reduce server round-trips
9. **Lazy load** — don't fetch data until component is visible
10. **Accessibility** — use ARIA attributes, keyboard navigation, screen reader support

View File

@ -0,0 +1,380 @@
# ServiceNow Performance Optimization
## GlideRecord Optimization
### Use setLimit() Always
```javascript
// BAD — fetches ALL records even if you need one
var gr = new GlideRecord('incident');
gr.addQuery('number', 'INC0010001');
gr.query();
if (gr.next()) { /* ... */ }
// GOOD — stops after first match
var gr = new GlideRecord('incident');
gr.addQuery('number', 'INC0010001');
gr.setLimit(1);
gr.query();
if (gr.next()) { /* ... */ }
// BEST — use get() for single record by sys_id
var gr = new GlideRecord('incident');
if (gr.get(sysId)) { /* ... */ }
```
### Use addEncodedQuery for Complex Filters
```javascript
// Encoded query is parsed once and sent to DB — faster than chained addQuery
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^priority=1^state!=6^assignment_groupISNOTEMPTY^sys_created_on>=javascript:gs.daysAgoStart(30)');
gr.query();
// vs chained (slightly slower, more overhead)
gr.addQuery('active', true);
gr.addQuery('priority', '1');
gr.addQuery('state', '!=', '6');
gr.addNotNullQuery('assignment_group');
gr.addQuery('sys_created_on', '>=', gs.daysAgoStart(30));
```
### Select Only Needed Fields
```javascript
// Only fetch fields you need — reduces data transfer
var gr = new GlideRecord('incident');
gr.addActiveQuery();
gr.setLimit(100);
gr.addQuery('priority', '1');
// Restrict returned fields
gr.chooseWindow(0, 100);
gr.query();
// Note: ServiceNow doesn't have a native "select fields" for GlideRecord
// but chooseWindow + setLimit reduces memory footprint
```
### Use GlideAggregate Instead of getRowCount()
```javascript
// BAD — loads ALL records into memory just to count
var gr = new GlideRecord('incident');
gr.addActiveQuery();
gr.query();
var count = gr.getRowCount(); // TERRIBLE for large tables
// GOOD — database-level count
var ga = new GlideAggregate('incident');
ga.addActiveQuery();
ga.addAggregate('COUNT');
ga.query();
var count = 0;
if (ga.next()) count = parseInt(ga.getAggregate('COUNT'));
```
## Avoiding N+1 Queries
```javascript
// BAD — N+1 pattern (1 query + N sub-queries)
var gr = new GlideRecord('incident');
gr.addActiveQuery();
gr.query();
while (gr.next()) {
var user = new GlideRecord('sys_user');
user.get(gr.getValue('assigned_to')); // query per incident!
gs.info(user.getValue('email'));
}
// GOOD — collect IDs, single query
var gr = new GlideRecord('incident');
gr.addActiveQuery();
gr.query();
var userIds = [];
var incidents = [];
while (gr.next()) {
var userId = gr.getValue('assigned_to');
if (userId && userIds.indexOf(userId) == -1) userIds.push(userId);
incidents.push({
number: gr.getValue('number'),
assigned_to: userId
});
}
// Single query for all users
var userMap = {};
if (userIds.length > 0) {
var ug = new GlideRecord('sys_user');
ug.addQuery('sys_id', 'IN', userIds.join(','));
ug.query();
while (ug.next()) {
userMap[ug.getUniqueValue()] = ug.getValue('email');
}
}
// Map results
incidents.forEach(function(inc) {
inc.email = userMap[inc.assigned_to] || 'unassigned';
});
// ALSO GOOD — use dot-walking (single query, ServiceNow handles join)
var gr = new GlideRecord('incident');
gr.addActiveQuery();
gr.query();
while (gr.next()) {
var email = gr.getValue('assigned_to.email'); // dot-walk, no extra query
}
```
## Indexing Strategies
### Check Existing Indexes
```
Navigate: System Definition > Tables > [table] > Table Indexes tab
Key indexed fields (incident):
- number (unique)
- sys_id (primary key)
- active + priority (composite)
- assigned_to
- assignment_group
- state
- sys_created_on
```
### Creating Custom Indexes
```
Navigate: System Definition > Tables > [table] > Table Indexes
New Index:
Table: u_my_custom_table
Fields: u_status, u_category (composite index)
Unique: false
// Composite index order matters:
// Index on (category, state) helps: WHERE category='x' AND state='y'
// Does NOT help: WHERE state='y' (wrong order)
```
### When to Add Indexes
- Fields frequently used in `addQuery()` / `addEncodedQuery()`
- Fields used in `orderBy()`
- Reference fields used in joins
- Fields in ACL conditions
- **Don't over-index** — each index slows writes
## Client Script Performance
### Always Use Asynchronous GlideAjax
```javascript
// BAD — blocks UI thread
var ga = new GlideAjax('MyUtil');
ga.addParam('sysparm_name', 'getData');
var result = ga.getXMLWait(); // FREEZES browser!
// GOOD — async callback
var ga = new GlideAjax('MyUtil');
ga.addParam('sysparm_name', 'getData');
ga.getXMLAnswer(function(answer) {
// Process result without blocking UI
g_form.setValue('field', answer);
});
```
### Batch GlideAjax Calls
```javascript
// BAD — 3 separate round-trips
var ga1 = new GlideAjax('Util');
ga1.addParam('sysparm_name', 'getA');
ga1.getXMLAnswer(function(a) {
var ga2 = new GlideAjax('Util');
ga2.addParam('sysparm_name', 'getB');
ga2.getXMLAnswer(function(b) {
// nested callback hell + 3 HTTP requests
});
});
// GOOD — single round-trip, return all data
var ga = new GlideAjax('Util');
ga.addParam('sysparm_name', 'getAllData');
ga.addParam('sysparm_incident_id', g_form.getUniqueValue());
ga.getXMLAnswer(function(answer) {
var data = JSON.parse(answer);
g_form.setValue('field_a', data.a);
g_form.setValue('field_b', data.b);
g_form.setValue('field_c', data.c);
});
```
### Minimize Client Script onLoad Work
```javascript
// BAD — heavy work on every form load
function onLoad() {
// Multiple GlideAjax calls
// DOM manipulation
// Complex calculations
}
// GOOD — use Display Business Rule + g_scratchpad
// Server-side (Display BR): pre-compute data
g_scratchpad.precomputedData = computeExpensiveData(current);
// Client-side (onLoad): just read cached data
function onLoad() {
if (g_scratchpad.precomputedData) {
g_form.setValue('field', g_scratchpad.precomputedData);
}
}
```
## Business Rule Performance
### Use Async for Heavy Operations
```javascript
// BEFORE: synchronous — blocks user's save
// After BR: Sync to external system (2-3 second API call)
// AFTER: async — user's save returns immediately
// After BR (async): Sync to external system
// The async BR runs in background worker thread
```
### Minimize Queries in Before Rules
```javascript
// BAD — query in every save
(function executeRule(current, previous) {
var gr = new GlideRecord('sys_user');
gr.get(current.getValue('assigned_to'));
var dept = gr.getValue('department');
// ... complex logic
})(current, previous);
// GOOD — only run when relevant field changes
// Set Condition: assigned_to changes
(function executeRule(current, previous) {
if (!current.assigned_to.changes()) return; // double-check
// ... only runs when needed
})(current, previous);
```
### Avoid current.update() in Business Rules
```javascript
// BAD — causes recursion, double save
(function executeRule(current, previous) {
current.setValue('description', 'updated');
current.update(); // triggers all BRs again!
})(current, previous);
// GOOD — in Before rule, just set values (auto-saved)
(function executeRule(current, previous) {
current.setValue('description', 'updated');
// No update() needed — system saves automatically
})(current, previous);
```
## Cache Management
```javascript
// Flush specific cache
gs.cacheFlush('sys_properties'); // property cache
gs.cacheFlush('sys_cache_flush'); // general cache flush
// System Properties for caching
// glide.cache.max_size — max cache entries
// glide.db.pooler.connections — DB connection pool size
```
### Script Include Caching Pattern
```javascript
var CachedLookup = Class.create();
CachedLookup.prototype = {
initialize: function() {
this.CACHE_KEY = 'x_myapp.cached_config';
},
getConfig: function() {
// Check cache first (using system property as simple cache)
var cached = gs.getProperty(this.CACHE_KEY);
if (cached) {
var data = JSON.parse(cached);
// Check TTL (5 minutes)
if (new Date().getTime() - data.timestamp < 300000) {
return data.value;
}
}
// Cache miss — query and store
var config = this._loadConfig();
gs.setProperty(this.CACHE_KEY, JSON.stringify({
value: config,
timestamp: new Date().getTime()
}));
return config;
},
_loadConfig: function() {
// expensive query here
return {};
},
type: 'CachedLookup'
};
```
## Semaphores & Mutual Exclusion
```javascript
// Prevent concurrent execution of critical sections
var sem = new GlideSemaphore();
var lockName = 'my_integration_lock';
if (sem.acquire(lockName, 30)) { // 30 second timeout
try {
// Critical section — only one thread at a time
processIntegration();
} finally {
sem.release(lockName);
}
} else {
gs.warn('Could not acquire lock: ' + lockName);
}
```
## Slow Query Analysis
```
Navigate: System Diagnostics > Slow Queries
Key metrics:
- Execution time > 1 second = investigate
- Full table scans = add index
- Missing indexes = sys_slow_query_log
// Enable slow query logging
System Property: glide.sql.debug = true (TEMP only!)
System Property: glide.slow_query_threshold = 1000 (ms)
```
## Database Views vs Joins
```javascript
// Database View — pre-joined virtual table (read-only, fast)
// Create via: System Definition > Database Views
// Good for: reporting, dashboards, repeated complex joins
// GlideRecord Join — runtime join (flexible but slower)
var gr = new GlideRecord('incident');
var join = gr.addJoinQuery('sys_user_grmember', 'assigned_to', 'user');
join.addCondition('group.name', 'Network');
gr.query();
// Use addJoinQuery sparingly — database views are faster for repeated patterns
```
## Performance Checklist
1. ✅ `setLimit()` on all queries
2. ✅ `GlideAggregate` for counts, not `getRowCount()`
3. ✅ Async GlideAjax (never `getXMLWait()`)
4. ✅ Async Business Rules for heavy operations
5. ✅ No `current.update()` in Before rules
6. ✅ Avoid N+1 queries — batch lookups
7. ✅ Use encoded queries for complex filters
8. ✅ Index frequently queried fields
9. ✅ Use Display BR + `g_scratchpad` over onLoad GlideAjax
10. ✅ Batch GlideAjax calls — single round-trip
11. ✅ No `eval()` anywhere
12. ✅ `setWorkflow(false)` for bulk data operations

View File

@ -0,0 +1,283 @@
# Playbooks & Process Automation Designer (PAD)
## What Are Playbooks?
Playbooks provide **guided, step-by-step resolution processes** for agents in Agent Workspace. They combine human tasks, automation, and decision logic into a structured workflow that agents follow to resolve issues consistently.
**Key concepts:**
- **Playbook** = overall process definition (like a runbook, but interactive)
- **Activity** = individual step in a playbook (instruction, automation, decision, approval)
- **Lane** = who performs the activity (agent, system, requester, approver)
- **Process Automation Designer (PAD)** = the visual editor for building playbooks
## Where Playbooks Live
- **Agent Workspace** — primary consumption point
- **Process Automation Designer** — build/edit interface
- **Flow Designer** — automation activities use Flow actions
Navigate to: **Process Automation > Process Automation Designer**
## Playbook Structure
```
Playbook: Incident Resolution - Network
├── Stage 1: Triage
│ ├── Activity: Gather Information (Instruction)
│ │ └── "Ask caller for error message and affected systems"
│ ├── Activity: Check CMDB (Automation)
│ │ └── Look up affected CIs and recent changes
│ └── Activity: Classify Issue (Decision)
│ ├── → Hardware Failure → Stage 2A
│ └── → Software Issue → Stage 2B
├── Stage 2A: Hardware Resolution
│ ├── Activity: Create Change Request (Automation)
│ ├── Activity: Notify On-site Team (Automation)
│ └── Activity: Verify Resolution (Instruction)
├── Stage 2B: Software Resolution
│ ├── Activity: Apply Known Fix (Instruction)
│ ├── Activity: Test Connectivity (Automation)
│ └── Activity: Verify Resolution (Instruction)
└── Stage 3: Close
├── Activity: Document Resolution (Instruction)
└── Activity: Customer Satisfaction Survey (Automation)
```
## Activity Types
### Instruction Activity
Tells the agent what to do — human-driven step.
```
Name: Gather Caller Information
Type: Instruction
Assigned to: Agent Lane
Instructions: |
1. Confirm the caller's contact information
2. Ask for the specific error message
3. Determine when the issue started
4. Check if any recent changes were made
Required fields to update:
- Description (detailed symptoms)
- Category / Subcategory
- Configuration Item
```
### Automation Activity
Runs Flow Designer actions automatically.
```
Name: Look Up Recent Changes
Type: Automation
Action: Look Up Records
Table: change_request
Conditions:
cmdb_ci = [Playbook Context: cmdb_ci]
state = Closed
closed_at >= last 7 days
Output: Display results to agent in workspace
```
### Decision Activity
Branch the playbook based on conditions.
```
Name: Determine Resolution Path
Type: Decision
Condition: [Playbook Context: category]
Branches:
"network" → Go to Network Troubleshooting stage
"software" → Go to Software Fix stage
"hardware" → Go to Hardware Replacement stage
DEFAULT → Go to General Resolution stage
```
### Approval Activity
Request approval before proceeding.
```
Name: Manager Approval for Emergency Change
Type: Approval
Approver: [Record: opened_by.manager]
Due: 2 hours
On Approve: Continue to next activity
On Reject: Go to "Request Denied" stage
```
### Wait Activity
Pause until a condition is met.
```
Name: Wait for Vendor Response
Type: Wait
Condition: [Record: u_vendor_response] is not empty
Timeout: 48 hours
On Timeout: → Escalation stage
```
## Building ITSM Playbooks
### Incident Playbook Pattern
```
Trigger: Incident created with category = "network"
Context Record: incident
Stages:
1. Initial Assessment (2 activities)
- Auto-enrich: Pull CMDB data, recent incidents, known errors
- Triage: Agent reviews enrichment, sets priority/assignment
2. Diagnosis (3 activities)
- Run Diagnostics: Automated ping/trace tests via MID Server
- Check Known Errors: Auto-search knowledge base
- Decision: Known fix available? → Yes/No branch
3. Resolution (2 activities)
- Apply Fix: Instructions for agent OR automated remediation
- Verify: Confirm service restored (auto-check or agent confirm)
4. Closure (2 activities)
- Document: Agent fills resolution notes
- Close: Auto-update state, send satisfaction survey
```
### Problem Playbook Pattern
```
Trigger: Problem created
Context: problem
Stages:
1. Root Cause Investigation
- Gather related incidents (automation)
- Timeline analysis (instruction)
- 5-Why analysis template (instruction)
2. Solution Development
- Document root cause (instruction)
- Propose fix / workaround (instruction)
- Approval for change (approval activity)
3. Implementation
- Create Change Request (automation)
- Wait for Change completion (wait activity)
- Validate fix (instruction)
4. Knowledge
- Create KB article (automation + instruction)
- Link to related incidents (automation)
- Close problem (automation)
```
### Change Playbook Pattern
```
Trigger: Change Request state = "Assess"
Context: change_request
Stages:
1. Planning
- Risk assessment checklist (instruction)
- Auto-calculate risk score (automation)
- Implementation plan template (instruction)
2. Approval
- CAB approval (approval, group-based)
- Decision: Standard/Normal/Emergency path
3. Implementation
- Pre-implementation checklist (instruction)
- Execute change tasks (linked to change_task records)
- Post-implementation verification (instruction)
4. Review
- PIR (Post-Implementation Review) (instruction)
- Close change (automation)
```
## Custom Playbook Activities
### Creating a Custom Activity
```
Navigate: Process Automation > Activity Definitions
Name: Check Service Health
Category: Custom
Inputs:
- service_name (String)
- ci_sys_id (Reference: cmdb_ci)
Action Type: Subflow
→ Calls subflow: "Service Health Check"
→ Inputs mapped from activity inputs
Outputs:
- health_status (String: "healthy" | "degraded" | "down")
- details (String)
Display: Show outputs in Agent Workspace activity panel
```
### Script-Based Activity
```javascript
// Custom activity using Run Script action
(function execute(inputs, outputs) {
// Check if similar incidents exist
var ga = new GlideAggregate('incident');
ga.addQuery('cmdb_ci', inputs.ci_sys_id);
ga.addQuery('active', true);
ga.addQuery('sys_id', '!=', inputs.current_incident);
ga.addAggregate('COUNT');
ga.query();
outputs.similar_count = 0;
if (ga.next()) {
outputs.similar_count = parseInt(ga.getAggregate('COUNT'));
}
outputs.is_widespread = outputs.similar_count > 3;
outputs.recommendation = outputs.is_widespread ?
'Consider creating a Major Incident' :
'Isolated incident - proceed with standard resolution';
})(inputs, outputs);
```
## Playbook Analytics
### Key Metrics
- **Completion Rate** — % of playbooks completed vs abandoned
- **Average Duration** — time to complete each stage/activity
- **Bottleneck Analysis** — which activities take longest
- **Agent Compliance** — are agents following the playbook?
### Accessing Analytics
```
Navigate: Process Automation > Playbook Experience > Analytics
Reports: PA dashboards for playbook execution metrics
Custom: Create reports on sn_playbook_execution table
```
## Agent Workspace Integration
Playbooks appear in the **Activity Stream** panel:
- Current activity is highlighted
- Agent can see upcoming steps
- Automation results display inline
- Agent can add notes to each activity
- Mandatory fields enforce completion before advancing
### Configuring Workspace Display
```
Navigate: Workspace Experience > Workspace Configuration
Add: Playbook component to record page layout
Configure: Activity panel position, auto-expand settings
```
## Best Practices
1. **Keep playbooks focused** — one process, not a mega-workflow
2. **2-4 stages max** — too many stages overwhelm agents
3. **3-5 activities per stage** — granular but not micro-managed
4. **Automate what you can** — reduce agent manual steps
5. **Use decision branches** — not every incident follows the same path
6. **Include context** in instructions — link to KB articles, include screenshots
7. **Set timeouts** on wait activities — don't let playbooks stall forever
8. **Test with real agents** — get feedback on instructions clarity
9. **Version your playbooks** — use publish/draft lifecycle
10. **Monitor analytics** — iterate based on completion rates and duration data

View File

@ -0,0 +1,414 @@
# ServiceNow REST API Patterns
## Table API
### GET — Query Records
```bash
# Get active incidents, limit 10
GET /api/now/table/incident?sysparm_query=active=true&sysparm_limit=10&sysparm_fields=number,short_description,state,priority&sysparm_display_value=true
# Pagination
GET /api/now/table/incident?sysparm_query=active=true&sysparm_limit=100&sysparm_offset=200
# Order by
GET /api/now/table/incident?sysparm_query=active=true^ORDERBYDESCsys_created_on
```
### GET — Single Record
```bash
GET /api/now/table/incident/{sys_id}?sysparm_fields=number,short_description,state&sysparm_display_value=all
# display_value=all returns both value and display_value for each field
```
### POST — Create Record
```bash
POST /api/now/table/incident
Content-Type: application/json
{
"short_description": "Cannot access email",
"caller_id": "user_sys_id",
"category": "software",
"impact": "2",
"urgency": "2"
}
```
### PUT — Replace Record (all fields)
```bash
PUT /api/now/table/incident/{sys_id}
Content-Type: application/json
{
"state": "6",
"close_code": "Solved (Permanently)",
"close_notes": "Resolved by resetting credentials"
}
```
### PATCH — Update Specific Fields
```bash
PATCH /api/now/table/incident/{sys_id}
Content-Type: application/json
{
"assigned_to": "user_sys_id",
"state": "2"
}
```
### DELETE
```bash
DELETE /api/now/table/incident/{sys_id}
```
### Authentication
```bash
# Basic Auth from environment variables
curl --user "$SN_USER:$SN_PASS" \
https://instance.service-now.com/api/now/table/incident
# OAuth2 Bearer Token
curl -H "Authorization: Bearer $SN_ACCESS_TOKEN" \
https://instance.service-now.com/api/now/table/incident
# OAuth2 Token Request (prefer a secure client flow, not inline hardcoded secrets)
POST /oauth_token.do
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=$SN_CLIENT_ID&client_secret=$SN_CLIENT_SECRET
```
### Pagination Pattern (Python example)
```python
import os
import requests
base_url = "https://instance.service-now.com/api/now/table/incident"
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {os.environ['SN_ACCESS_TOKEN']}",
}
offset = 0
limit = 100
all_records = []
while True:
params = {
"sysparm_query": "active=true",
"sysparm_limit": limit,
"sysparm_offset": offset,
"sysparm_fields": "number,short_description,state"
}
resp = requests.get(base_url, headers=headers, params=params, timeout=30)
resp.raise_for_status()
data = resp.json()["result"]
if not data:
break
all_records.extend(data)
offset += limit
# Also check X-Total-Count header
total = int(resp.headers.get("X-Total-Count", 0))
if offset >= total:
break
```
## Scripted REST API
### Creating a Custom API Endpoint
1. Navigate to **System Web Services > Scripted REST APIs**
2. Create new API: Name, API ID (namespace), Base API path
### Resource Script (GET single record)
```javascript
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var id = request.pathParams.id;
// Validate input
if (gs.nil(id)) {
response.setStatus(400);
return { error: 'Missing required parameter: id' };
}
var gr = new GlideRecord('incident');
if (!gr.get(id)) {
response.setStatus(404);
return { error: 'Incident not found', id: id };
}
// Check permissions
if (!gr.canRead()) {
response.setStatus(403);
return { error: 'Insufficient permissions' };
}
return {
sys_id: gr.getUniqueValue(),
number: gr.getValue('number'),
short_description: gr.getValue('short_description'),
state: gr.getValue('state'),
priority: gr.getValue('priority'),
assigned_to: gr.getDisplayValue('assigned_to'),
created: gr.getValue('sys_created_on')
};
})(request, response);
```
### Resource Script (POST with validation)
```javascript
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var body = request.body.data;
// Validate required fields
var required = ['short_description', 'caller_id', 'category'];
var missing = [];
for (var i = 0; i < required.length; i++) {
if (gs.nil(body[required[i]])) missing.push(required[i]);
}
if (missing.length > 0) {
response.setStatus(400);
return { error: 'Missing required fields', fields: missing };
}
var gr = new GlideRecord('incident');
gr.initialize();
gr.setValue('short_description', body.short_description);
gr.setValue('caller_id', body.caller_id);
gr.setValue('category', body.category);
gr.setValue('description', body.description || '');
gr.setValue('impact', body.impact || '3');
gr.setValue('urgency', body.urgency || '3');
var sys_id = gr.insert();
if (sys_id) {
response.setStatus(201);
response.setHeader('Location', '/api/x_myapp/v1/incidents/' + sys_id);
return {
sys_id: sys_id,
number: gr.getValue('number')
};
} else {
response.setStatus(500);
return { error: 'Failed to create incident' };
}
})(request, response);
```
### Versioned API Pattern
```
Base path: /api/x_myapp/incidents
Resources:
/v1/incidents — GET (list), POST (create)
/v1/incidents/{id} — GET (read), PATCH (update), DELETE
/v2/incidents — GET (enhanced list with pagination metadata)
```
### Query Parameters in Scripted REST
```javascript
// Access query parameters
var limit = parseInt(request.queryParams.limit) || 20;
var offset = parseInt(request.queryParams.offset) || 0;
var query = request.queryParams.query || 'active=true';
// Access headers
var apiKey = request.getHeader('X-API-Key');
var contentType = request.getHeader('Content-Type');
```
## Outbound REST (RESTMessageV2)
### Basic GET Request
```javascript
try {
var rm = new sn_ws.RESTMessageV2('My REST Message', 'GET');
rm.setStringParameterNoEscape('endpoint', 'https://api.example.com/data');
rm.setRequestHeader('Accept', 'application/json');
var response = rm.execute();
var statusCode = response.getStatusCode();
var body = response.getBody();
if (statusCode == 200) {
var data = JSON.parse(body);
gs.info('Got ' + data.results.length + ' records');
} else {
gs.error('REST call failed: ' + statusCode + ' - ' + body);
}
} catch(e) {
gs.error('REST exception: ' + e.message);
}
```
### POST with OAuth2
```javascript
var rm = new sn_ws.RESTMessageV2();
rm.setEndpoint('https://api.example.com/create');
rm.setHttpMethod('POST');
// Preferred: configure auth on the REST Message method record.
// Alternate: keep the profile name in a property rather than hardcoding it.
var oauthProfile = gs.getProperty('x_scope.outbound.oauth_profile', 'x_scope.default_oauth_profile');
rm.setAuthenticationProfile('oauth2', oauthProfile);
rm.setRequestHeader('Content-Type', 'application/json');
rm.setRequestBody(JSON.stringify({
name: 'Test',
description: 'Created from ServiceNow'
}));
var response = rm.execute();
```
### Async REST (Mid Server)
```javascript
var rm = new sn_ws.RESTMessageV2('External API', 'GET');
rm.setMIDServer('my_mid_server');
rm.setEccTopic('MyIntegration');
// Execute asynchronously through MID
var response = rm.executeAsync();
var eccQueueId = response.getBody(); // ECC queue sys_id
// Response comes back via ECC queue — handle in a Business Rule on ecc_queue
```
### Retry Pattern
```javascript
function callWithRetry(rmName, method, maxRetries) {
var retries = 0;
while (retries < maxRetries) {
try {
var rm = new sn_ws.RESTMessageV2(rmName, method);
var response = rm.execute();
var code = response.getStatusCode();
if (code >= 200 && code < 300) return response;
if (code == 429 || code >= 500) {
retries++;
gs.sleep(1000 * retries); // exponential-ish backoff
continue;
}
return response; // 4xx — don't retry
} catch(e) {
retries++;
if (retries >= maxRetries) throw e;
gs.sleep(1000 * retries);
}
}
return null;
}
```
## Import Sets + Transform Maps via API
### POST to Import Set Table
```bash
POST /api/now/import/u_my_import_set
Content-Type: application/json
{
"u_name": "John Doe",
"u_email": "john@example.com",
"u_department": "IT"
}
# Response includes transform result
```
### Bulk Import via Attachment
```bash
# Upload CSV/Excel to import set
POST /api/now/attachment/upload
Content-Type: multipart/form-data
table_name=u_my_import_set&table_sys_id=...
# Then trigger transform via API
POST /api/now/table/sys_transform_map
```
## Batch API
```bash
POST /api/now/batch
Content-Type: application/json
{
"batch_request_id": "batch_001",
"rest_requests": [
{
"id": "req_1",
"method": "GET",
"url": "/api/now/table/incident/sys_id_1",
"headers": [{"name": "Accept", "value": "application/json"}]
},
{
"id": "req_2",
"method": "PATCH",
"url": "/api/now/table/incident/sys_id_2",
"headers": [{"name": "Content-Type", "value": "application/json"}],
"body": "{\"state\": \"2\"}"
}
]
}
```
## Error Handling Pattern (Scripted REST)
```javascript
(function process(request, response) {
try {
// ... main logic ...
return { result: data };
} catch(e) {
gs.error('API Error: ' + e.message + '\nStack: ' + e.stack);
response.setStatus(500);
return {
error: {
message: 'Internal server error',
detail: gs.getProperty('glide.appcreator.env') == 'development' ? e.message : undefined
}
};
}
})(request, response);
```
## Attachment API
```bash
# Upload attachment
POST /api/now/attachment/file?table_name=incident&table_sys_id={sys_id}&file_name=document.pdf
Content-Type: application/pdf
[binary data]
# Get attachment metadata
GET /api/now/attachment?sysparm_query=table_sys_id={sys_id}
# Download attachment
GET /api/now/attachment/{attachment_sys_id}/file
```
## CORS and Security
```javascript
// In Scripted REST API properties:
// - Requires authentication: true (default)
// - Requires ACL authorization: Check for row-level security
// - Allowed request headers: Authorization, Content-Type, Accept
// For CORS: System Properties
// glide.rest.cors.enabled = true
// glide.rest.cors.rule.* — configure per-origin rules
```

View File

@ -0,0 +1,161 @@
# SAM Pro
## Scope
Use this reference for Software Asset Management Professional workflows: normalization, software models, publishers, entitlements, installations, reclamation candidates, and license compliance analysis.
## Plugin and Role Assumptions
Clarify whether the environment uses:
- SAM Professional normalization and reconciliation
- usage metering for reclaim decisions
- publisher packs or custom normalization rules
- device, user, subscription, or core-based metrics
- procurement and contract integrations for entitlement sourcing
Typical stakeholders are SAM analysts, procurement teams, software owners, and platform admins supporting discovery quality.
## Core Tables
| Area | Table |
|------|-------|
| Software Entitlement | `alm_license` |
| Software Installation | `cmdb_sam_sw_install` |
| Software Model | `cmdb_software_product_model` |
| Discovery Model / Normalization Links | implementation-specific SAM tables |
| User Subscription / Allocation | depends on license metric and integration model |
## Core Concepts
Important distinctions:
- install data is not entitlement data
- normalized software is not the same as raw discovery strings
- publishers, products, versions, and editions matter
- reclaimable software depends on usage, allocation model, and business policy
## Installation Aggregation
```javascript
var ga = new GlideAggregate('cmdb_sam_sw_install');
ga.addQuery('active', true);
ga.groupBy('display_name');
ga.addAggregate('COUNT');
ga.query();
while (ga.next()) {
gs.info(ga.getValue('display_name') + ': ' + ga.getAggregate('COUNT'));
}
```
## Entitlement Lookup Pattern
```javascript
var ent = new GlideRecord('alm_license');
ent.addQuery('active', true);
ent.addQuery('software_model', softwareModelSysId);
ent.query();
while (ent.next()) {
gs.info(ent.getValue('number') + ' metric=' + ent.getValue('license_metric'));
}
```
## Effective License Position Check
Keep compliance math reviewable and model-specific.
```javascript
var installs = new GlideAggregate('cmdb_sam_sw_install');
installs.addQuery('active', true);
installs.addQuery('software_model', softwareModelSysId);
installs.addAggregate('COUNT');
installs.query();
if (installs.next()) {
gs.info('Install count for model ' + softwareModelSysId + ': ' + installs.getAggregate('COUNT'));
}
```
Use the output only as one input. Actual license position still depends on metric, rights, bundles, downgrade rules, and allocation model.
## Reclamation Candidate Pattern
Use reclamation only when the organization has a clear policy for inactivity windows and approval.
```javascript
var install = new GlideRecord('cmdb_sam_sw_install');
install.addQuery('last_used', '<', gs.daysAgoStart(90));
install.addQuery('active', true);
install.setLimit(100);
install.query();
while (install.next()) {
gs.info('Potential reclaim candidate: ' + install.getValue('display_name'));
}
```
## Normalization Guidance
Before giving compliance recommendations, call out:
- whether software discovery is normalized
- whether the publisher, product, and version mappings are trusted
- whether the customer uses device, user, core, or subscription metrics
If normalization confidence is poor, fix that first instead of pretending the compliance result is definitive.
## Workflow Example: Publisher Review Cycle
1. ingest discovery and usage data
2. normalize raw installs to software models
3. compare installed footprint to entitlement coverage
4. identify reclaim candidates and true deficits separately
5. route deficits to procurement and reclaims to software owners
6. confirm reclaimed installs or new purchases before updating compliance posture
## Practical Workflow
1. ingest discovery and usage data
2. normalize to software models
3. map installations to entitlements
4. identify surplus, deficit, and reclaim candidates
5. route actions to owners or procurement
6. verify remediation or reclaim completion
## Metrics
- effective license position by publisher
- reclaim candidates by inactivity window
- unnormalized software backlog
- top publishers by spend exposure
- installs without mapped entitlements
## Troubleshooting
Common failure modes:
- compliance numbers shift unexpectedly because normalization rules or publisher mappings changed
- reclaim candidates are invalid because usage signals are stale or not collected consistently
- license deficits are overstated because bundles, downgrade rights, or alternative entitlements are ignored
- discovery shows installs that should be retired because device or user decommission processes lag
- procurement records exist but are not linked cleanly to entitlement records
## Testing Guidance
Validate at least these scenarios:
- aggregate installs for a normalized software model and confirm counts match known sample data
- look up entitlements for a model with more than one license metric
- test reclamation windows for 30, 60, and 90 day inactivity rules
- simulate incomplete normalization and confirm the workflow flags the data-quality blocker
- verify purchase-linked entitlement records appear in the expected compliance workflow
## Best Practices
1. Never infer compliance from raw install counts alone.
2. Explain the license metric whenever recommending entitlement math.
3. Keep reclamation decisions reviewable and reversible.
4. Use software models and normalized data as the operating layer for automation.
5. Call out data-quality blockers, especially around publishers, versions, and inactive install signals.

View File

@ -0,0 +1,469 @@
# ServiceNow Scripting Patterns
## Business Rules
### When to Use Which Type
| Type | Fires | Use For |
|------|-------|---------|
| **Before** | Before DB write | Validation, field defaults, calculated fields, abort with `current.setAbortAction(true)` |
| **After** | After DB write | Creating related records, notifications, events, anything needing sys_id |
| **Async** | After, in background | Heavy processing, integrations, anything slow. Does NOT block user. |
| **Display** | Before form loads | Populating `g_scratchpad` for client scripts |
### Before Business Rule — Set defaults, validate
```javascript
// Name: Set Priority from Impact/Urgency
// Table: incident | When: before | Insert: true | Update: true
// Condition: current.impact.changes() || current.urgency.changes()
(function executeRule(current, previous) {
var impact = parseInt(current.getValue('impact'));
var urgency = parseInt(current.getValue('urgency'));
// Priority matrix
var priorityMap = {
'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;
current.setValue('priority', priorityMap[key] || '4');
})(current, previous);
```
### Before Business Rule — Validation with abort
```javascript
// Name: Validate Resolution
// Table: incident | When: before | Update: true
// Condition: current.state.changesTo('6')
(function executeRule(current, previous) {
if (gs.nil(current.getValue('close_code'))) {
current.setAbortAction(true);
gs.addErrorMessage('Close code is required when resolving an incident.');
return;
}
if (gs.nil(current.getValue('close_notes'))) {
current.setAbortAction(true);
gs.addErrorMessage('Close notes are required when resolving an incident.');
}
})(current, previous);
```
### After Business Rule — Create related record
```javascript
// Name: Create Problem from Major Incident
// Table: incident | When: after | Update: true
// Condition: current.priority == '1' && current.state.changesTo('6') && !current.problem_id
(function executeRule(current, previous) {
var prob = new GlideRecord('problem');
prob.initialize();
prob.setValue('short_description', 'Problem from ' + current.getValue('number'));
prob.setValue('description', current.getValue('description'));
prob.setValue('category', current.getValue('category'));
prob.setValue('assignment_group', current.getValue('assignment_group'));
var probId = prob.insert();
// Link back — use workflow false to avoid recursion
current.setValue('problem_id', probId);
current.setWorkflow(false);
current.update();
})(current, previous);
```
### Async Business Rule — Heavy processing
```javascript
// Name: Sync to External System
// Table: incident | When: async | Insert: true | Update: true
// Condition: current.assignment_group.name == 'External Team'
(function executeRule(current, previous) {
try {
var rm = new sn_ws.RESTMessageV2('External System', 'POST');
rm.setStringParameterNoEscape('incident_number', current.getValue('number'));
rm.setStringParameterNoEscape('description', current.getValue('short_description'));
var response = rm.execute();
var statusCode = response.getStatusCode();
if (statusCode != 200 && statusCode != 201) {
gs.error('External sync failed for ' + current.getValue('number') + ': ' + statusCode);
}
} catch(e) {
gs.error('External sync exception: ' + e.message);
}
})(current, previous);
```
### Display Business Rule — Pass data to client
```javascript
// Name: Pass User Info to Client
// Table: incident | When: display
(function executeRule(current, previous) {
g_scratchpad.isVIP = false;
if (!current.isNewRecord() && !gs.nil(current.getValue('caller_id'))) {
var caller = new GlideRecord('sys_user');
if (caller.get(current.getValue('caller_id'))) {
g_scratchpad.isVIP = caller.getValue('vip') == 'true';
g_scratchpad.callerLocation = caller.getDisplayValue('location');
}
}
})(current, previous);
```
### current vs previous
```javascript
// previous is only available in Update rules (before/after)
if (current.state.changes()) {
gs.info('State changed from ' + previous.state + ' to ' + current.state);
}
// previous is READ-ONLY — never modify it
// previous is NOT available in async rules after a short delay (data may change)
```
## Script Includes
### Standard Pattern (Class Prototype)
```javascript
var IncidentUtils = Class.create();
IncidentUtils.prototype = {
initialize: function() {
// constructor
},
getActiveCount: function(assignmentGroup) {
var ga = new GlideAggregate('incident');
ga.addActiveQuery();
ga.addQuery('assignment_group', assignmentGroup);
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) return parseInt(ga.getAggregate('COUNT'));
return 0;
},
reassignIncident: function(incidentId, newAssignee) {
var gr = new GlideRecord('incident');
if (gr.get(incidentId)) {
gr.setValue('assigned_to', newAssignee);
return gr.update();
}
return null;
},
type: 'IncidentUtils'
};
// Usage:
var util = new IncidentUtils();
var count = util.getActiveCount(groupSysId);
```
### Client-Callable Script Include (for GlideAjax)
```javascript
// Check "Client callable" checkbox in Script Include form
var ClientUtils = Class.create();
ClientUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
// Each method accessible via GlideAjax sysparm_name
validateEmail: function() {
var email = this.getParameter('sysparm_email');
var gr = new GlideRecord('sys_user');
gr.addQuery('email', email);
gr.setLimit(1);
gr.query();
return gr.hasNext().toString(); // must return string
},
getUserGroups: function() {
var userId = this.getParameter('sysparm_user_id');
var groups = [];
var gr = new GlideRecord('sys_user_grmember');
gr.addQuery('user', userId);
gr.query();
while (gr.next()) {
groups.push({
sys_id: gr.getValue('group'),
name: gr.getDisplayValue('group')
});
}
return JSON.stringify(groups);
},
type: 'ClientUtils'
});
```
### Extending Base Classes
```javascript
var CustomApprovalUtils = Class.create();
CustomApprovalUtils.prototype = Object.extendsObject(ApprovalUtils, {
initialize: function() {
ApprovalUtils.prototype.initialize.call(this);
this.customConfig = gs.getProperty('my_app.approval_config');
},
getApproversByAmount: function(amount) {
// Custom logic on top of base class
if (amount > 10000) return this._getVPApprovers();
return this._getManagerApprovers();
},
_getVPApprovers: function() {
// private method by convention (underscore prefix)
var approvers = [];
// ... logic
return approvers;
},
type: 'CustomApprovalUtils'
});
```
## Client Scripts
### onChange
```javascript
// Type: onChange | Table: incident | Field: category
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue == '') return;
// Clear subcategory when category changes
g_form.setValue('subcategory', '');
// Make certain fields mandatory based on category
if (newValue == 'network') {
g_form.setMandatory('cmdb_ci', true);
g_form.setVisible('u_network_segment', true);
} else {
g_form.setMandatory('cmdb_ci', false);
g_form.setVisible('u_network_segment', false);
}
}
```
### onLoad
```javascript
// Type: onLoad | Table: incident
function onLoad() {
// Access Display BR data via g_scratchpad
if (g_scratchpad.isVIP) {
g_form.showFieldMsg('caller_id', 'This is a VIP caller!', 'info');
g_form.setMandatory('priority', true);
}
// New record defaults
if (g_form.isNewRecord()) {
g_form.setValue('contact_type', 'self-service');
}
}
```
### onSubmit
```javascript
// Type: onSubmit | Table: incident
function onSubmit() {
var desc = g_form.getValue('short_description');
if (desc.length < 10) {
g_form.addErrorMessage('Short description must be at least 10 characters.');
return false; // prevent submission
}
// Confirm before submission
if (g_form.getValue('state') == '7') { // Closed
return confirm('Are you sure you want to close this incident?');
}
return true; // allow submission
}
```
### onCellEdit (list editing)
```javascript
// Type: onCellEdit | Table: incident
function onCellEdit(sysIDs, table, oldValues, newValue, callback) {
// sysIDs is array of selected record sys_ids
// Prevent bulk priority change to Critical
if (newValue == '1' && sysIDs.length > 1) {
callback({
status: 'error',
error_message: 'Cannot bulk-change priority to Critical'
});
return false;
}
callback({ status: 'success' });
return true;
}
```
## UI Policies
### Script-based UI Policy
```javascript
// Run script: true
// Execute on: onLoad, onChange
// Applies to field: short_description
function onCondition() {
// Dynamic condition beyond simple field checks
var priority = g_form.getValue('priority');
var category = g_form.getValue('category');
return (priority == '1' && category == 'network');
}
function onTrue() {
g_form.setMandatory('cmdb_ci', true);
g_form.setVisible('u_network_impact', true);
}
function onFalse() {
g_form.setMandatory('cmdb_ci', false);
g_form.setVisible('u_network_impact', false);
}
```
## UI Actions
### Form Button — Client + Server
```javascript
// Name: Escalate to Major
// Table: incident | Form button: true | List button: false
// Condition: current.priority != '1' && gs.hasRole('itil')
// Client Script (runs first):
function escalateToMajor() {
if (!confirm('Escalate this incident to Major Incident?')) return;
// Set fields before server script runs
g_form.setValue('priority', '1');
gsftSubmit(null, g_form.getFormElement(), 'escalate_major');
}
// Server Script (runs after client):
if (typeof RP != 'undefined' && RP.isDialog()) {
// Handle dialog context
}
current.setValue('priority', '1');
current.setValue('state', '2'); // In Progress
gs.eventQueue('incident.escalated', current, gs.getUserID(), current.getValue('number'));
gs.addInfoMessage('Incident escalated to Major Incident');
current.update();
action.setRedirectURL(current);
```
### List Context UI Action
```javascript
// Name: Assign to Me (List)
// Table: incident | Form button: false | List button: true | List choice: true
// Server Script:
var selected = RP.getParameterValue('sysparm_checked_items');
if (!gs.nil(selected)) {
var ids = selected.split(',');
for (var i = 0; i < ids.length; i++) {
var sysId = ids[i].split('.').pop(); // format: table.sys_id
var gr = new GlideRecord('incident');
if (gr.get(sysId)) {
gr.setValue('assigned_to', gs.getUserID());
gr.update();
}
}
gs.addInfoMessage(ids.length + ' incident(s) assigned to you');
}
action.setRedirectURL(current);
```
## Scheduled Jobs
```javascript
// Script field of Scheduled Job record
// Runs on schedule — no current/previous object
var cutoffDate = gs.daysAgoStart(30);
var gr = new GlideRecord('incident');
gr.addQuery('state', '6'); // Resolved
gr.addQuery('resolved_at', '<', cutoffDate);
gr.query();
var count = 0;
while (gr.next()) {
gr.setValue('state', '7'); // Closed
gr.setValue('close_notes', 'Auto-closed after 30 days in resolved state.');
gr.update();
count++;
}
gs.info('Auto-close job: closed ' + count + ' incidents');
```
## Fix Scripts (one-time data fixes)
```javascript
// Name: Migrate Category Values
// ALWAYS test in sub-prod first!
var gr = new GlideRecord('incident');
gr.addQuery('category', 'old_category');
gr.query();
var count = 0;
var batchSize = 500;
while (gr.next()) {
gr.setValue('category', 'new_category');
gr.setWorkflow(false); // don't trigger business rules
gr.autoSysFields(false); // don't update sys_updated_on
gr.update();
count++;
// Log progress for long-running scripts
if (count % batchSize === 0) {
gs.info('Migrated ' + count + ' records...');
}
}
gs.info('Migration complete: ' + count + ' records updated');
```
## Event-Driven Architecture
### Register Event (sys_event_register)
```
Event name: incident.escalated
Table: incident
Fired by: Business Rule / Script
```
### Script Action (fires on event)
```javascript
// Table: incident
// Event: incident.escalated
// Script:
var incident = event.GlideRecord;
var escalatedBy = event.parm1; // gs.getUserID() from eventQueue
var incNumber = event.parm2; // incident number from eventQueue
// Notify on-call manager
var mgr = new OnCallManager();
mgr.notifyManager(incident.getValue('assignment_group'),
'Major incident escalation: ' + incNumber);
// Create communication task
var task = new GlideRecord('sn_major_inc_comm_task');
task.initialize();
task.setValue('incident', incident.getUniqueValue());
task.setValue('short_description', 'Stakeholder communication for ' + incNumber);
task.insert();
```
### Firing Events
```javascript
// In Business Rule or Script Include:
gs.eventQueue('incident.escalated', current, gs.getUserID(), current.getValue('number'));
// Delayed event (fires after 60 seconds)
gs.eventQueueScheduled('incident.followup', current, '', '',
new GlideDateTime(gs.minutesAgo(-1)));
```

View File

@ -0,0 +1,163 @@
# Security Incident Response
## Scope
Use this reference for Security Incident Response workflows, security incident states, containment and eradication orchestration, response tasks, forensic handoff, and SOC-driven automation.
## Plugin and Role Assumptions
Clarify whether the environment uses:
- Security Incident Response only or broader SecOps orchestration
- SIEM, EDR, SOAR, threat intel, or case-management integrations
- product-native response tasks or custom task extensions
- forensic evidence workflows or external evidence stores
- manual approval gates before destructive containment actions
Typical stakeholders include SOC analysts, incident commanders, infrastructure responders, IAM teams, and legal or compliance stakeholders for severe events.
## Core Tables
| Area | Table |
|------|-------|
| Security Incident | `sn_si_incident` |
| Security Incident Task / Response Task | environment-specific task extensions depending on implementation |
| Related Risk / Exception / Evidence | depends on IRM and SecOps configuration |
| Indicator / Artifact / Observable | implementation-specific, often tied to integrations and response records |
## Lifecycle Model
Suggested phases:
1. Detection
2. Triage
3. Containment
4. Eradication
5. Recovery
6. Lessons learned / closure
If the customer already has a phase model, preserve it rather than forcing a custom one.
## Phase Transition Guard
```javascript
// Before update on sn_si_incident
(function executeRule(current, previous) {
if (current.state.changesTo('containment') && gs.nil(current.getValue('assignment_group'))) {
gs.addErrorMessage('Containment requires an assignment group.');
current.setAbortAction(true);
return;
}
if (current.state.changesTo('eradication') && gs.nil(current.getValue('work_notes'))) {
gs.addErrorMessage('Add eradication notes before moving to eradication.');
current.setAbortAction(true);
}
})(current, previous);
```
## Containment Workflow Pattern
Typical containment actions:
- isolate endpoint or CI
- disable account or credential
- block IOC at control point
- notify business owners
- create evidence-preservation tasks
Model these as separate actions or response tasks so the incident record remains auditable.
## Response Task Creation
```javascript
var task = new GlideRecord('task');
task.initialize();
task.setValue('parent', securityIncidentSysId);
task.setValue('short_description', 'Collect volatile evidence');
task.setValue('assignment_group', assignmentGroupSysId);
task.insert();
```
## Severity Routing Pattern
Make severity-based ownership transparent.
```javascript
// Before insert on sn_si_incident
(function executeRule(current, previous) {
if (current.getValue('priority') == '1' && gs.nil(current.getValue('assignment_group'))) {
current.setValue('assignment_group', highSeverityGroupSysId);
}
})(current, previous);
```
Document the fallback order if routing changes by geography, service owner, or incident type.
## Forensic Handoff
Capture:
- time of acquisition
- custodian or owner
- system or artifact source
- chain-of-custody notes
- approval or legal hold requirements
## Workflow Example: Destructive Containment Approval
For actions like disabling accounts or isolating endpoints:
1. triage confirms the incident is active and severe enough to justify containment
2. a response task captures the proposed action, target, and rollback path
3. a human approver or incident commander authorizes the action
4. the integration executes the action and records upstream identifiers
5. the incident and task capture notes, actor attribution, and timestamps
6. recovery includes validation that the action can be safely reversed
## Integration Guidance
When integrating with EDR, SIEM, SOAR, or case tools:
1. define source of truth for status
2. preserve external IDs
3. make retries idempotent
4. map severity and priority explicitly
5. separate detection events from response actions
## Metrics
- mean time to triage
- mean time to contain
- mean time to recover
- incidents by severity and vector
- open incidents without owner
- response tasks past due
## Troubleshooting
Common failure modes:
- incidents move into containment without an owner because routing depends on incomplete severity mappings
- response tasks are created but not linked cleanly to the parent incident or external action
- destructive integrations run without clear approval or rollback evidence
- recovery is marked complete while related tasks or evidence collection remain open
- SIEM or SOAR connectors overwrite local notes, actor attribution, or severity without governance
## Testing Guidance
Validate at least these scenarios:
- block a transition to containment when assignment is missing
- block a transition to eradication when required notes are missing
- create a high-severity incident and confirm routing sends it to the correct group
- simulate a destructive containment action and verify approval, execution, and rollback evidence
- test duplicate inbound detection events and confirm idempotent linking rather than duplicate cases
## Best Practices
1. Phase ownership should be explicit. Containment, eradication, and recovery often belong to different teams.
2. Use tasks or flows for operational work; keep the parent incident focused on governance and case state.
3. Preserve evidence and approval trails for high-severity incidents.
4. Avoid silent state transitions from integrations without notes, actor attribution, and external ID traceability.
5. If proposing automation, explain the rollback and human-approval gates for destructive containment actions.

View File

@ -0,0 +1,287 @@
# ServiceNow Security
## ACL Rules (Access Control)
### ACL Evaluation Order
1. Table-level ACL (e.g., `incident.*`)
2. Field-level ACL (e.g., `incident.short_description`)
3. Most restrictive wins — ALL applicable ACLs must pass
### Row-Level ACL (record access)
```
Name: incident (read)
Type: record
Operation: read
Table: incident
Condition: active=true
Script:
// Advanced condition — return true to allow access
answer = current.getValue('assignment_group') != '' &&
gs.getUser().isMemberOf(current.getValue('assignment_group'));
Roles: itil, incident_manager
// Role OR script must pass (depends on "Requires role" checkbox)
```
### Field-Level ACL
```
Name: incident.close_notes (write)
Type: record
Operation: write
Table: incident
Field: close_notes
Condition: state=6^ORstate=7
Script:
answer = gs.hasRole('itil');
```
### Common ACL Patterns
```javascript
// Only allow assigned group members to write
answer = gs.hasRole('admin') ||
gs.getUser().isMemberOf(current.getValue('assignment_group'));
// Only allow assignee or manager to close
answer = current.getValue('assigned_to') == gs.getUserID() ||
gs.hasRole('incident_manager');
// Restrict based on caller's company (multi-tenant)
answer = current.caller_id.company == gs.getUser().getCompanyID();
// Time-based restriction
var gdt = new GlideDateTime(current.getValue('sys_created_on'));
var now = new GlideDateTime();
var diff = GlideDateTime.subtract(gdt, now);
answer = diff.getNumericValue() < 86400000; // within 24 hours
```
### ACL Debugging
```
Navigate: System Security > Debugging
Enable: Session Debug > Security (ACL)
// In scripts:
gs.setProperty('glide.security.debug', true); // TEMP only!
// Check ACL in script
var gr = new GlideRecord('incident');
if (gr.get(sys_id)) {
gs.info('Can read: ' + gr.canRead());
gs.info('Can write: ' + gr.canWrite());
gs.info('Can delete: ' + gr.canDelete());
}
```
## Role Management
### Role Hierarchy
```
admin
├── security_admin
├── incident_manager
│ └── itil
│ └── sn_incident_read
├── problem_manager
│ └── itil
├── change_manager
│ └── itil
└── knowledge_manager
└── knowledge
```
### Creating Custom Roles
```
Navigate: User Administration > Roles
Name: x_myapp.incident_resolver
Description: Can resolve incidents in MyApp scope
Elevated privilege: false (check only for sensitive operations)
// Contains roles (inherited):
itil
// Contained by roles (grants this role):
x_myapp.admin
```
### Checking Roles in Scripts
```javascript
// Server-side
gs.hasRole('itil'); // exact role
gs.hasRole('itil,incident_manager'); // any of these roles
// Check role for specific user
var gr = new GlideRecord('sys_user_has_role');
gr.addQuery('user', userId);
gr.addQuery('role.name', 'itil');
gr.addQuery('state', 'active');
gr.query();
var hasRole = gr.hasNext();
// Client-side (via g_user)
var isAdmin = g_user.hasRole('admin');
var isItil = g_user.hasRole('itil');
```
## Scoped Application Security
### Access Control for Scoped Apps
```
Navigate: System Applications > Application Access
Application: My Custom App (x_myapp)
Settings:
- Can read: All application scopes
- Can create/update/delete: This application scope only
- Accessible from: All scopes / This scope only
- Caller Access: Restrict which scopes can call Script Includes
```
### Cross-Scope Access
```javascript
// Calling a Script Include from another scope
// The target Script Include must have "Accessible from: All application scopes"
// From scope x_app_a, calling x_app_b.UtilityClass:
var util = new x_app_b.UtilityClass(); // prefix with scope namespace
var result = util.myMethod(param);
// If Script Include is NOT accessible cross-scope:
// → Results in "Security restricted: Script Include not accessible" error
```
### Privileged Script Include
```javascript
// Mark as "Protection policy: Read-only" to prevent modification
// Mark as "Accessible from: All application scopes" for cross-scope use
// For accessing tables outside your scope:
var si = new global.SomeGlobalUtil(); // access global scope utilities
```
### Application Properties (scoped)
```javascript
// Define in: System Properties > Application Properties
// Name: x_myapp.max_records
// Value: 500
// Access (within scope):
var maxRec = gs.getProperty('x_myapp.max_records', '100');
// Other scopes can read if "Can read" is set to "All application scopes"
```
## Data Encryption
### Column-Level Encryption
```
Navigate: System Security > Column Level Security
Table: sys_user
Fields:
- social_security_number (Edge Encrypted)
- bank_account (Edge Encrypted)
Edge Encryption:
- Data encrypted at rest AND in transit
- Decrypted only at edge (browser)
- Not searchable when encrypted
- Requires Edge Encryption plugin
```
### Encryption in Scripts
```javascript
// Encrypt a value
var encryptor = new GlideEncrypter();
var encrypted = encryptor.encrypt('sensitive data');
// Decrypt
var decrypted = encryptor.decrypt(encrypted);
// For password fields (one-way hash)
var gr = new GlideRecord('my_table');
if (gr.get(sys_id)) {
gr.setValue('password_field', 'new_password'); // auto-hashed
gr.update();
}
```
## Security Best Practices
### Input Validation
```javascript
// NEVER trust user input in encoded queries
var userInput = request.queryParams.filter;
// BAD: gr.addEncodedQuery(userInput); // SQL injection risk!
// GOOD: Validate and sanitize
var allowedFields = ['number', 'state', 'priority'];
var field = allowedFields.indexOf(userInput) >= 0 ? userInput : 'number';
gr.addQuery(field, sanitizedValue);
```
### Prevent Script Injection
```javascript
// Never use eval()
// eval(userInput); // NEVER!
// Never construct queries from raw user input
// gr.addEncodedQuery(userInput); // DANGEROUS!
// Use parameterized approaches
var gr = new GlideRecord('incident');
gr.addQuery('number', userProvidedNumber); // safe — single field query
gr.query();
```
### Impersonation
```javascript
// Check if user is impersonating
var impersonating = gs.isInteractive() && gs.getImpersonatingUserName() != '';
// Impersonate in script (requires admin)
var impersonator = new GlideImpersonate();
impersonator.impersonate(targetUserSysId);
// ... perform actions as target user
impersonator.unimpersonate();
```
### Delegation
```
Navigate: System Security > Delegated Administration
// Delegate group management
// Delegate approval authority
// Time-bound delegation (start/end date)
// Delegate specific roles only
```
## Security Operations (SecOps) Integration
```
Key tables:
- sn_si_incident — Security Incidents
- sn_vul_vulnerable_item — Vulnerabilities
- sn_ti_indicator — Threat Indicators
Integration points:
- SIEM (Splunk, QRadar) → Security Incident Response
- Vulnerability scanners (Qualys, Tenable) → Vulnerability Response
- Threat intel feeds → Threat Intelligence
```
## Audit & Compliance
```javascript
// Check audit history
var gr = new GlideRecord('sys_audit');
gr.addQuery('tablename', 'incident');
gr.addQuery('documentkey', incidentSysId);
gr.orderByDesc('sys_created_on');
gr.setLimit(50);
gr.query();
while (gr.next()) {
gs.info(gr.getValue('fieldname') + ': ' +
gr.getValue('oldvalue') + ' → ' + gr.getValue('newvalue'));
}
```

View File

@ -0,0 +1,315 @@
# Service Catalog
## Catalog Structure
```
Service Catalog
├── Catalog Categories
│ ├── Hardware
│ ├── Software
│ ├── Access Requests
│ └── General Services
├── Catalog Items (sc_cat_item)
│ ├── Variables (item_option_new)
│ ├── Variable Sets (item_option_new_set)
│ ├── Catalog UI Policies
│ ├── Catalog Client Scripts
│ └── Workflows / Flows
├── Record Producers (sc_cat_item_producer)
├── Order Guides (sc_cat_item_guide)
└── Content Items (sc_cat_item_content)
```
## Catalog Items
### Creating a Standard Catalog Item
```
Table: sc_cat_item
Key fields:
- Name, Short description, Description
- Category (sc_category)
- Catalogs (many-to-many: sc_cat_item_catalog)
- Active, Desktop/Mobile/Service Portal
- Workflow or Flow
- Template (for task generation)
```
### Variables (Questions)
```
Table: item_option_new
Types:
- Single Line Text, Multi Line Text
- Select Box, Lookup Select Box
- Check Box, Multiple Choice
- Reference (links to any table)
- Date, Date/Time
- Macro (free-form HTML/UI Page)
- Label (display only)
- Container Start/End (grouping)
- Masked (password fields)
- Requested For (special user picker)
Key attributes:
- Name (internal), Question Text (display)
- Type, Default Value
- Mandatory (yes/no/on condition)
- Order (display sequence)
- Read only, Hidden
- Reference qualifier (for Reference type)
```
### Variable Sets (Reusable Variable Groups)
```
Table: item_option_new_set
Use case: Common questions shared across items
e.g., "Justification Set" with manager, reason, cost center
Creating:
1. Create Variable Set record
2. Add variables to the set
3. Associate set with catalog items (many-to-many)
Types:
- Single Row: variables appear inline
- Multi Row: tabular data entry (e.g., multiple users)
```
## Catalog Client Scripts
### onChange — Dynamic Variable Behavior
```javascript
// Catalog Client Script — onChange
// Applies to: Catalog Item or Variable Set
// Variable: department
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == '') return;
// Show/hide variable based on selection
if (newValue == 'IT') {
g_form.setDisplay('it_subcategory', true);
g_form.setMandatory('it_subcategory', true);
} else {
g_form.setDisplay('it_subcategory', false);
g_form.setMandatory('it_subcategory', false);
}
}
```
### onLoad — Initialize Form State
```javascript
// Catalog Client Script — onLoad
function onLoad() {
// Pre-populate variables based on current user
var user = g_user.userID;
g_form.setValue('requested_for', user);
// Hide variables initially
g_form.setDisplay('additional_details', false);
g_form.setDisplay('manager_approval_required', false);
}
```
### onSubmit — Validate Before Submission
```javascript
// Catalog Client Script — onSubmit
function onSubmit() {
var cost = parseFloat(g_form.getValue('estimated_cost'));
if (cost > 5000 && g_form.getValue('manager_justification') == '') {
g_form.addErrorMessage('Manager justification required for items over $5,000');
g_form.setMandatory('manager_justification', true);
return false; // Prevent submission
}
return true;
}
```
## Catalog UI Policies
```
Table: catalog_ui_policy
Use case: Show/hide/mandatory variables without scripting
Example: Show "Emergency Justification" when Priority = Critical
Conditions: priority == 1
Actions:
- Set "emergency_justification" visible = true
- Set "emergency_justification" mandatory = true
Reverse: When condition no longer met, reverse actions automatically
Order: Lower number = higher priority
```
## Record Producers
Generate task records directly from catalog submissions.
```
Table: sc_cat_item_producer
Key fields:
- Table name: target table (e.g., incident, hr_case, sc_task)
- Script: server-side processing after submission
Use cases:
- "Report an Incident" → creates incident record
- "HR Case" → creates hr_case record
- "Facility Request" → creates fm_request record
Server Script (runs after record creation):
```
```javascript
// Record Producer script
// 'current' = the newly created target record
// 'producer' = the catalog item
// Variables accessible via: producer.getVariable('var_name')
current.setValue('short_description', producer.getVariable('issue_summary'));
current.setValue('description', producer.getVariable('detailed_description'));
current.setValue('category', producer.getVariable('category'));
current.setValue('cmdb_ci', producer.getVariable('affected_ci'));
current.setValue('impact', producer.getVariable('impact'));
current.setValue('urgency', producer.getVariable('urgency'));
// Set assignment based on category
var category = current.getValue('category');
if (category == 'network') {
current.assignment_group.setDisplayValue('Network Operations');
} else if (category == 'database') {
current.assignment_group.setDisplayValue('Database Administration');
}
```
## Order Guides
Multi-item ordering with a wizard-style interface.
```
Table: sc_cat_item_guide
Use case: "New Employee Onboarding" bundles:
- Laptop request
- Software bundle
- Building access
- Email account
- Phone setup
Structure:
1. Create Order Guide record
2. Define Rule Base (conditions for including items)
3. Optional: Script to cascade variables to child items
Cascade Script:
```
```javascript
// Order Guide cascade script
// Pass selected department to all child items
var items = current.guide_items;
for (var i = 0; i < items.length; i++) {
items[i].setVariable('department', current.getVariable('department'));
items[i].setVariable('location', current.getVariable('office_location'));
}
```
## Fulfillment Workflows
### Flow Designer (Modern)
```
Trigger: Service Catalog item requested
→ Approval (if cost > threshold)
→ Create catalog tasks (parallel or sequential)
→ Notify fulfillment groups
→ Wait for task completion
→ Close RITM
```
### Execution Plans
```
Table: sc_cat_item_delivery_plan
Use case: Multi-step fulfillment without Flow/Workflow
Delivery Plan:
Task 1: "Provision Account" → Assignment: IT Ops
Task 2: "Ship Hardware" → Assignment: Logistics (after Task 1)
Task 3: "Configure Software" → Assignment: Desktop Support (after Task 1)
Task 4: "User Training" → Assignment: Training Team (after Task 2 & 3)
```
## Pricing & Recurring Costs
```
Catalog Item fields:
- Price: one-time cost
- Recurring price: monthly/annual
- Recurring frequency: monthly, quarterly, annually
Variable pricing (dynamic):
Use pricing implications on variables
e.g., Laptop model selection changes price
Script-based pricing:
```
```javascript
// Pricing script on catalog item
// Runs when item is added to cart
(function calculatePrice(item) {
var basePrice = 500;
var ram = item.getVariable('ram_size');
if (ram == '32GB') basePrice += 200;
if (ram == '64GB') basePrice += 500;
var monitor = item.getVariable('external_monitor');
if (monitor == 'true') basePrice += 350;
item.price = basePrice;
return basePrice;
})(current);
```
## Service Portal Catalog
### Widget: SC Catalog Item
```javascript
// Server script — custom catalog widget
(function() {
var catItemId = $sp.getParameter('sys_id');
var gr = new GlideRecord('sc_cat_item');
if (gr.get(catItemId)) {
data.item = {
name: gr.getValue('name'),
short_description: gr.getValue('short_description'),
description: gr.getValue('description'),
price: gr.getValue('price'),
picture: gr.getValue('picture')
};
}
// Get variables
data.variables = [];
var vars = new GlideRecord('item_option_new');
vars.addQuery('cat_item', catItemId);
vars.addActiveQuery();
vars.orderBy('order');
vars.query();
while (vars.next()) {
data.variables.push({
name: vars.getValue('name'),
question_text: vars.getValue('question_text'),
type: vars.getValue('type'),
mandatory: vars.getValue('mandatory') == 'true'
});
}
})();
```
## Catalog Best Practices
1. **Use Variable Sets** for common questions — DRY principle
2. **Catalog UI Policies > Client Scripts** when possible — no code maintenance
3. **Execution Plans** for simple multi-task — save Flow Designer for complex logic
4. **Test in Service Portal AND Agent Workspace** — behavior can differ
5. **Set fulfillment groups** via catalog tasks, not assignment rules
6. **Use record producers** for incident/case creation — better UX than raw forms
7. **Limit variables per item** to 10-15 — too many overwhelms users
8. **Category hierarchy** max 3 levels deep — navigation gets confusing beyond that
9. **Active = false** instead of deleting — preserve order history
10. **Approval policies** by dollar amount — automate low-cost approvals

View File

@ -0,0 +1,730 @@
# Service Portal
## Overview
Service Portal is ServiceNow's AngularJS-based framework for building user-facing portals. It provides a widget-based architecture for creating self-service experiences for employees, customers, and partners.
### Portal Structure
```
Service Portal
├── Portal Record (sp_portal)
│ ├── Theme (sp_theme)
│ ├── CSS Variables / SCSS
│ └── Logo, favicon, branding
├── Pages (sp_page)
│ ├── Containers (rows/columns)
│ └── Widget Instances (sp_instance)
├── Widgets (sp_widget)
│ ├── HTML Template
│ ├── Client Script (AngularJS controller)
│ ├── Server Script (Rhino/GlideRecord)
│ ├── CSS/SCSS
│ ├── Link function (advanced DOM)
│ └── Angular Providers (services/factories)
├── Headers & Footers (sp_header_footer)
├── Menus (sp_menu)
└── Search Sources (sp_search_source)
```
## Widget Architecture
### Widget Components
```
Every widget has up to 6 parts:
1. HTML Template — AngularJS template (HTML + ng-directives)
2. CSS — scoped styles (SCSS supported)
3. Client Script — AngularJS controller function
4. Server Script — runs on server, populates data object
5. Link Function — advanced DOM manipulation (rare)
6. Angular Providers — reusable services/factories
Data flow:
Server Script → data object → Client Script → HTML Template
Client Script → $http/spUtil → Server Script (via GlideAjax or widget server)
```
### Server Script
```javascript
// Server script — runs on page load
// Available objects: input, data, options, $sp
(function() {
// Query data for the widget
var gr = new GlideRecord('incident');
gr.addQuery('caller_id', gs.getUserID());
gr.addActiveQuery();
gr.orderByDesc('sys_created_on');
gr.setLimit(10);
gr.query();
data.incidents = [];
while (gr.next()) {
data.incidents.push({
sys_id: gr.getUniqueValue(),
number: gr.getValue('number'),
short_description: gr.getValue('short_description'),
state: gr.state.getDisplayValue(),
priority: gr.getValue('priority'),
opened_at: gr.getValue('opened_at')
});
}
data.count = data.incidents.length;
// Access widget options
data.title = options.title || 'My Incidents';
data.max_display = options.max_display || 5;
// Handle client-side actions (POST back)
if (input) {
if (input.action == 'close_incident') {
var inc = new GlideRecord('incident');
if (inc.get(input.sys_id)) {
inc.setValue('state', '7'); // Closed
inc.setValue('close_notes', input.close_notes || 'Closed via portal');
inc.update();
data.message = 'Incident ' + inc.getValue('number') + ' closed.';
}
}
}
})();
```
### Client Script (AngularJS Controller)
```javascript
// Client script — AngularJS controller
// Available: $scope, $http, spUtil, $location, $timeout, snRecordWatcher
api.controller = function($scope, spUtil, snRecordWatcher, $timeout) {
var c = this; // 'c' is the controller alias used in HTML
// Data from server script is available as c.data
c.incidents = c.data.incidents;
c.title = c.data.title;
// Client-side action — calls server script with input
c.closeIncident = function(sysId) {
c.data.action = 'close_incident';
c.data.sys_id = sysId;
c.data.close_notes = c.closeNotes;
// POST back to server script
c.server.update().then(function(response) {
// Server script runs again with input populated
c.incidents = response.data.incidents;
spUtil.addInfoMessage(response.data.message);
});
};
// Real-time updates using Record Watcher
snRecordWatcher.initList('incident', 'caller_id=' + window.NOW.user_id + '^active=true');
$scope.$on('record.updated', function(event, data) {
// Refresh widget when incident is updated
c.server.refresh().then(function(response) {
c.incidents = response.data.incidents;
});
});
// Navigate to a page
c.viewIncident = function(sysId) {
$location.url('?id=ticket&table=incident&sys_id=' + sysId);
};
// Use spUtil for common operations
c.showForm = function() {
spUtil.get('widget-form', {
table: 'incident',
sys_id: '-1', // New record
view: 'sp'
}).then(function(widget) {
c.formWidget = widget;
});
};
};
```
### HTML Template
```html
<!-- AngularJS template — uses 'c' as controller alias -->
<div class="panel panel-default">
<div class="panel-heading">
<h3>{{c.title}} ({{c.data.count}})</h3>
</div>
<div class="panel-body">
<!-- Empty state -->
<div ng-if="c.incidents.length == 0" class="text-center text-muted">
<i class="fa fa-check-circle fa-3x"></i>
<p>No open incidents. You're all set!</p>
</div>
<!-- Incident list -->
<div class="list-group">
<a ng-repeat="inc in c.incidents | limitTo:c.data.max_display"
ng-click="c.viewIncident(inc.sys_id)"
class="list-group-item"
ng-class="{'list-group-item-danger': inc.priority == '1',
'list-group-item-warning': inc.priority == '2'}">
<div class="row">
<div class="col-sm-3">
<strong>{{inc.number}}</strong>
</div>
<div class="col-sm-6">
{{inc.short_description}}
</div>
<div class="col-sm-3 text-right">
<span class="label"
ng-class="{'label-success': inc.state == 'Resolved',
'label-info': inc.state == 'In Progress',
'label-default': inc.state == 'New'}">
{{inc.state}}
</span>
</div>
</div>
</a>
</div>
<!-- Close incident modal -->
<div ng-if="c.showCloseModal" class="well">
<h4>Close Incident</h4>
<textarea ng-model="c.closeNotes"
class="form-control"
placeholder="Closure notes..."></textarea>
<br>
<button class="btn btn-primary"
ng-click="c.closeIncident(c.selectedIncident)">
Confirm Close
</button>
<button class="btn btn-default"
ng-click="c.showCloseModal = false">
Cancel
</button>
</div>
</div>
</div>
```
### Widget CSS (SCSS)
```scss
// Scoped to widget — won't leak to other widgets
.panel-heading h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.list-group-item {
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: $sp-navbar-divider-color; // Theme variable
}
}
.label {
font-size: 12px;
padding: 4px 8px;
}
// Responsive
@media (max-width: 768px) {
.row > div {
margin-bottom: 5px;
}
}
```
## $sp API (GlideSPScriptable)
### Server-Side $sp Methods
```javascript
// Available in widget server scripts as $sp
// Get current portal
var portal = $sp.getPortalRecord();
var portalUrl = portal.getValue('url_suffix');
// Get page parameters
var sysId = $sp.getParameter('sys_id');
var table = $sp.getParameter('table');
// Get current user
var userGr = $sp.getUser(); // Returns GlideRecord of sys_user
// Get widget by ID (embed another widget)
var widget = $sp.getWidget('widget-cool-clock', {
timezone: 'US/Central'
});
data.clockWidget = widget;
// Get catalog item for display
var catItem = $sp.getCatalogItem('sys_id_here');
data.catalogItem = catItem;
// Get Knowledge Base article
var article = $sp.getKBArticle('kb_sys_id');
data.article = article;
// Get record and check ACLs
var record = $sp.getRecord(); // Current page record
var canRead = $sp.canReadRecord('incident', 'sys_id_here');
var canWrite = GlideRecordSecure('incident'); // Respects ACLs
// Build URL
var url = $sp.getPortalUrl() + '?id=form&table=incident&sys_id=' + sysId;
```
## Angular Providers
### Creating a Reusable Service
```javascript
// Angular Provider — shared service across widgets
// Type: Factory
function myPortalService($http, spUtil) {
var service = {};
service.getRecords = function(table, query, limit) {
return $http.get('/api/now/table/' + table, {
params: {
sysparm_query: query,
sysparm_limit: limit || 20,
sysparm_display_value: true
}
}).then(function(response) {
return response.data.result;
});
};
service.createRecord = function(table, data) {
return $http.post('/api/now/table/' + table, data)
.then(function(response) {
spUtil.addInfoMessage('Record created: ' + response.data.result.number);
return response.data.result;
})
.catch(function(error) {
spUtil.addErrorMessage('Error creating record');
throw error;
});
};
service.formatDate = function(dateString) {
if (!dateString) return '';
var d = new Date(dateString);
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
};
return service;
}
// Register: paste into Angular Provider record
// Name: myPortalService
// Type: Factory
```
### Using Providers in Widgets
```javascript
// Client script — inject the provider
api.controller = function($scope, myPortalService) {
var c = this;
myPortalService.getRecords('incident', 'active=true', 10)
.then(function(records) {
c.incidents = records;
});
c.createIncident = function() {
myPortalService.createRecord('incident', {
short_description: c.description,
urgency: c.urgency
}).then(function(result) {
c.newIncidentNumber = result.number;
});
};
};
```
## Embedded Widgets
### Embedding Widgets in Other Widgets
```javascript
// Server script — get widget to embed
data.formWidget = $sp.getWidget('widget-form', {
table: 'incident',
sys_id: $sp.getParameter('sys_id'),
view: 'sp'
});
data.approvalWidget = $sp.getWidget('sc-approval-summary', {
sys_id: $sp.getParameter('sys_id')
});
```
```html
<!-- HTML Template — render embedded widget -->
<sp-widget widget="c.data.formWidget"></sp-widget>
<!-- Conditional embedding -->
<sp-widget ng-if="c.showApproval" widget="c.data.approvalWidget"></sp-widget>
```
## Theming & Branding
### Theme Configuration
```
Navigate: Service Portal > Themes (sp_theme)
Theme components:
- CSS Variables: brand colors, fonts, spacing
- Header/Footer: portal-wide header and footer widgets
- SCSS: global styles compiled into portal CSS
Key CSS variables:
$sp-navbar-bg — navbar background
$sp-navbar-text-color — navbar text
$sp-body-bg — page background
$sp-primary-color — primary brand color
$sp-link-color — link color
$sp-tagline-color — subtitle text
$sp-navbar-divider-color — divider lines
```
### Custom Theme SCSS
```scss
// Theme SCSS — applies globally to the portal
// Override Bootstrap variables
$brand-primary: #1a73e8;
$brand-success: #34a853;
$brand-danger: #ea4335;
$font-family-base: 'Poppins', 'Helvetica Neue', sans-serif;
$font-size-base: 14px;
// Custom portal styles
.navbar-default {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.panel {
border-radius: 8px;
border: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
// Catalog item cards
.sc-cat-item {
transition: transform 0.2s;
&:hover {
transform: translateY(-2px);
}
}
// Service Portal specific utility classes
.sp-hero {
background: linear-gradient(135deg, $brand-primary, darken($brand-primary, 15%));
color: white;
padding: 60px 0;
text-align: center;
}
```
## Pages & Navigation
### Page Configuration
```
Navigate: Service Portal > Pages (sp_page)
Page record fields:
- Title: page display name
- ID: URL identifier (?id=page_id)
- Internal: hide from public navigation
- CSS: page-specific styles
- Roles: restrict access
Page layout:
Container → Row → Column → Widget Instance
Column sizes: 1-12 (Bootstrap grid)
Row options: full-width, contained
```
### URL Parameters
```javascript
// Server script — access URL parameters
var pageId = $sp.getParameter('id');
var tableName = $sp.getParameter('table');
var recordSysId = $sp.getParameter('sys_id');
var customParam = $sp.getParameter('my_param');
// Client script — navigate with parameters
c.navigateToRecord = function(table, sysId) {
var url = '?id=form&table=' + table + '&sys_id=' + sysId;
window.location.href = url;
// OR using $location:
// $location.search({id: 'form', table: table, sys_id: sysId});
};
```
### Menu Configuration
```
Navigate: Service Portal > Menus (sp_menu)
Menu items:
- Label: display text
- Page: link to portal page
- URL: external link
- Sub-items: dropdown children
- Roles: visibility per role
- Order: menu position
- Glyph: Font Awesome icon
```
## Search Sources
### Custom Search Source
```
Navigate: Service Portal > Search Sources (sp_search_source)
Fields:
- Name: display name
- Table: source table
- Conditions: base filter
- Page: results page ID
- Fields to display: title, description, image
- Order: search priority
Script-based search:
```
```javascript
// Search source data fetch script
(function(query) {
var results = [];
var gr = new GlideRecord('kb_knowledge');
gr.addQuery('workflow_state', 'published');
gr.addQuery('123TEXTQUERY321', query);
gr.setLimit(10);
gr.query();
while (gr.next()) {
results.push({
primary: gr.getValue('short_description'),
secondary: gr.getValue('kb_knowledge_base').getDisplayValue(),
tertiary: gr.getValue('sys_view_count') + ' views',
url: '?id=kb_article&sys_id=' + gr.getUniqueValue(),
sys_id: gr.getUniqueValue()
});
}
return results;
})(query);
```
## spUtil — Client-Side Utility
```javascript
// spUtil — available in all widget client scripts
// Messages
spUtil.addInfoMessage('Record saved successfully');
spUtil.addErrorMessage('Something went wrong');
spUtil.addTrivialMessage('FYI: System maintenance tonight');
// Get a widget reference
spUtil.get('widget-id', {option: 'value'}).then(function(widget) {
c.embeddedWidget = widget;
});
// Update widget (calls server script with input)
spUtil.update($scope); // Shortcut for c.server.update()
// Record watcher — real-time updates
spUtil.recordWatch($scope, 'incident', 'active=true', function(name, data) {
// Called when record matching filter is inserted/updated
c.server.refresh();
});
// Create a modal
spUtil.createModal({
title: 'Confirm Action',
message: 'Are you sure you want to proceed?',
buttons: [
{label: 'Cancel', cancel: true},
{label: 'Confirm', primary: true}
]
}).then(function() {
// User clicked Confirm
}, function() {
// User clicked Cancel
});
```
## Performance Optimization
### Widget Performance
```
1. Minimize server script queries
- Use encoded queries, setLimit(), setFields()
- Cache data when possible (session/application cache)
2. Reduce client-side DOM operations
- Use ng-if over ng-show for heavy content (ng-if removes from DOM)
- Limit ng-repeat with | limitTo filter
- Use track by in ng-repeat: ng-repeat="item in items track by item.sys_id"
3. Avoid unnecessary server roundtrips
- Batch operations in single server.update() call
- Use Angular providers to cache shared data
- Client-side filtering when dataset is small
4. Image optimization
- Use thumbnails in lists, full image on detail
- Lazy load images below the fold
5. Use record watcher sparingly
- Each watcher = active WebSocket connection
- Unsubscribe when leaving page: $scope.$on('$destroy', ...)
```
### Caching Pattern
```javascript
// Server script — cache expensive queries
(function() {
var cacheKey = 'sp_catalog_categories_' + gs.getUserID();
var cached = gs.getSession().getClientData(cacheKey);
if (cached) {
data.categories = JSON.parse(cached);
return;
}
// Expensive query
var cats = [];
var gr = new GlideRecord('sc_category');
gr.addActiveQuery();
gr.orderBy('title');
gr.query();
while (gr.next()) {
cats.push({
sys_id: gr.getUniqueValue(),
title: gr.getValue('title'),
icon: gr.getValue('icon')
});
}
data.categories = cats;
gs.getSession().putClientData(cacheKey, JSON.stringify(cats));
})();
```
## Common Patterns
### Form Widget with Validation
```javascript
// Server script
(function() {
if (input && input.action == 'submit') {
// Validate
var errors = [];
if (!input.short_description) errors.push('Description is required');
if (!input.category) errors.push('Category is required');
if (errors.length > 0) {
data.errors = errors;
return;
}
// Create record
var gr = new GlideRecord('incident');
gr.initialize();
gr.setValue('short_description', input.short_description);
gr.setValue('description', input.description);
gr.setValue('category', input.category);
gr.setValue('caller_id', gs.getUserID());
gr.insert();
data.success = true;
data.incidentNumber = gr.getValue('number');
}
// Load categories for dropdown
data.categories = [];
var cat = new GlideRecord('sys_choice');
cat.addQuery('name', 'incident');
cat.addQuery('element', 'category');
cat.addActiveQuery();
cat.orderBy('sequence');
cat.query();
while (cat.next()) {
data.categories.push({
value: cat.getValue('value'),
label: cat.getValue('label')
});
}
})();
```
### Pagination Widget
```javascript
// Server script with pagination
(function() {
var pageSize = options.page_size || 10;
var currentPage = input && input.page ? input.page : 1;
var offset = (currentPage - 1) * pageSize;
// Get total count
var ga = new GlideAggregate('incident');
ga.addQuery('caller_id', gs.getUserID());
ga.addAggregate('COUNT');
ga.query();
data.totalCount = ga.next() ? parseInt(ga.getAggregate('COUNT')) : 0;
data.totalPages = Math.ceil(data.totalCount / pageSize);
data.currentPage = currentPage;
// Get page of records
var gr = new GlideRecord('incident');
gr.addQuery('caller_id', gs.getUserID());
gr.orderByDesc('sys_created_on');
gr.chooseWindow(offset, offset + pageSize);
gr.query();
data.records = [];
while (gr.next()) {
data.records.push({
sys_id: gr.getUniqueValue(),
number: gr.getValue('number'),
short_description: gr.getValue('short_description'),
state: gr.state.getDisplayValue()
});
}
})();
```
```javascript
// Client script — pagination controls
api.controller = function($scope) {
var c = this;
c.goToPage = function(page) {
if (page < 1 || page > c.data.totalPages) return;
c.data.page = page;
c.server.update().then(function(response) {
c.data.records = response.data.records;
c.data.currentPage = response.data.currentPage;
});
};
};
```
## Best Practices
1. **Server script for data, client script for interaction** — never query GlideRecord client-side
2. **Use `c.server.update()`** for client→server communication, not raw `$http` to ServiceNow APIs
3. **Always check ACLs** — use `GlideRecordSecure` or `$sp.canReadRecord()` before displaying data
4. **Scope your CSS** — widget CSS is auto-scoped, but theme CSS isn't
5. **Embed widgets** with `$sp.getWidget()` + `<sp-widget>` — don't duplicate code
6. **Record Watcher for real-time** — but limit active watchers (performance cost)
7. **Test mobile** — Service Portal is responsive but widgets may not be
8. **Options for configurability** — use widget instance options so the same widget works in multiple contexts
9. **Error handling** — always handle server.update() failures with `.catch()` or error state
10. **URL parameters** — use `$sp.getParameter()` server-side, `$location.search()` client-side

View File

@ -0,0 +1,174 @@
# Strategic Portfolio Management (SPM / PPM)
## Scope
Use this reference for demand management, project and portfolio management, resource plans, investment governance, roadmaps, and Agile 2.0 portfolio patterns.
## Plugin and Role Assumptions
Clarify whether the environment uses:
- classic PPM only
- Strategic Portfolio Management with investment funding and benefit tracking
- Agile 2.0 or hybrid delivery
- Resource Management with named or role-based plans
- custom PMO approval boards or stage-gate governance
The main stakeholders are PMO analysts, portfolio managers, project managers, resource managers, and finance partners.
## Core Tables
| Area | Table |
|------|-------|
| Demand | `dmn_demand` |
| Project | `pm_project` |
| Project Task | `pm_project_task` |
| Portfolio | `pm_portfolio` |
| Program | `pm_program` |
| Resource Plan | `resource_plan` |
| Benefit Plan | `pm_project_benefit` |
| Investment Funding | `fm_funding_allocation` |
## Demand to Project Lifecycle
Common path:
1. Demand intake
2. Qualification
3. Prioritization
4. Approval
5. Project creation
6. Resource planning
7. Execution and benefit tracking
## Project Creation Pattern
```javascript
// Convert approved demand into a project
var demand = new GlideRecord('dmn_demand');
if (demand.get(demandSysId) && demand.getValue('state') == 'approved') {
var project = new GlideRecord('pm_project');
project.initialize();
project.setValue('short_description', demand.getValue('short_description'));
project.setValue('description', demand.getValue('description'));
project.setValue('portfolio', demand.getValue('portfolio'));
project.setValue('business_case', demand.getUniqueValue());
project.insert();
}
```
## Approval Guard Pattern
If approval requires a portfolio and an owner, block promotion before the demand leaves intake.
```javascript
// Before update on dmn_demand
(function executeRule(current, previous) {
if (current.state.changesTo('approved')) {
if (gs.nil(current.getValue('portfolio')) || gs.nil(current.getValue('owner'))) {
gs.addErrorMessage('Approved demands must have a portfolio and owner.');
current.setAbortAction(true);
}
}
})(current, previous);
```
## Resource Planning
Track:
- requested role or named resource
- start and end dates
- capacity
- allocation percent or hours
- portfolio/program alignment
```javascript
var plan = new GlideRecord('resource_plan');
plan.initialize();
plan.setValue('task', projectSysId);
plan.setValue('resource_type', 'role');
plan.setValue('planned_hours', '80');
plan.setValue('start_date', startDate);
plan.setValue('end_date', endDate);
plan.insert();
```
## Portfolio Governance
Useful approval inputs:
- strategic alignment
- expected benefit
- delivery risk
- compliance or regulatory pressure
- available capacity
- funding status
Decision tables work well for consistent scoring and routing.
## Workflow Example: Demand Review Board
1. a new demand enters intake with a sponsor, portfolio, and business value statement
2. qualification checks confirm completeness, dependencies, and rough order of magnitude
3. a governance board scores the demand using strategic alignment, urgency, and risk inputs
4. approved work creates a project, program entry, or agile epic path depending on delivery model
5. resource plans and funding allocations are created before execution starts
6. benefits are reviewed after go-live instead of stopping at delivery closeout
## Benefit Tracking
Do not stop at project closure. Strong SPM guidance includes:
- expected vs actual benefit review
- funding variance
- schedule variance
- milestone health
- portfolio-level rollups
## Agile 2.0 / Hybrid Delivery Notes
SPM often spans project and agile data. When designing solutions:
- keep portfolio and product planning aligned
- clarify source of truth for delivery state
- avoid duplicate milestone tracking
- define when work stays in agile records vs project tasks
## Reporting KPIs
- demand throughput
- approval lead time
- projects by health
- budget variance
- benefit realization
- resource over-allocation
- portfolio risk exposure
## Troubleshooting
Common failure modes:
- approved demands create projects with missing portfolio or owner data
- resource plans do not reflect actual execution dates because plan creation is disconnected from governance milestones
- benefit reporting stops at project closure and never compares expected vs actual value
- hybrid delivery models duplicate progress across agile work and project tasks
- funding approvals live outside SPM and leave the record state out of sync with actual governance
## Testing Guidance
Validate at least these scenarios:
- approve a complete demand and confirm project creation includes the right portfolio and business-case linkage
- attempt approval without a portfolio or owner and verify the transition is blocked
- create resource plans for role-based and named-resource models
- test a rejected demand, a deferred demand, and a multi-stage approval path
- verify benefit review reminders still trigger after project closure
## Best Practices
1. Separate intake, governance, and execution records clearly.
2. Use reference relationships between demand, project, portfolio, and funding data rather than copying values blindly.
3. Resource planning logic should be transparent and reviewable by PMO stakeholders.
4. When generating automations, specify which approvals happen in Flow Designer vs native SPM workflow.
5. Include reporting and audit expectations whenever a solution changes demand or project state automatically.

View File

@ -0,0 +1,399 @@
# ServiceNow Testing & Debugging
## Automated Test Framework (ATF)
### Creating a Test
Navigate: **Automated Test Framework > Tests**
### Server-Side Test Steps
```
Test: Verify Incident Auto-Assignment
Step 1: Create Record
Table: incident
Values:
short_description: ATF Test Incident
category: network
caller_id: [ATF test user]
Save as: incident_record
Step 2: Assert Field Value
Record: {incident_record}
Field: assignment_group
Expected: Network Operations
Assertion: Equals
Step 3: Assert Field Value
Record: {incident_record}
Field: state
Expected: New
Assertion: Equals
Step 4: Update Record
Record: {incident_record}
Values:
state: In Progress
assigned_to: [ATF test user]
Step 5: Assert Condition
Condition: {incident_record}.state == 2
Step 6: Delete Record (cleanup)
Record: {incident_record}
```
### Parameterized Tests
```
Test: Verify Priority Matrix
Parameters:
| impact | urgency | expected_priority |
|--------|---------|-------------------|
| 1 | 1 | 1 |
| 1 | 2 | 2 |
| 2 | 1 | 2 |
| 2 | 2 | 3 |
| 3 | 3 | 5 |
Step 1: Create Record
Table: incident
Values:
impact: {parameter.impact}
urgency: {parameter.urgency}
short_description: Priority Matrix Test
Step 2: Assert Field Value
Field: priority
Expected: {parameter.expected_priority}
```
### Server Script Step
```javascript
// Custom assertion logic
(function(outputs, steps, params, stepResult, assertEqual) {
var gr = new GlideRecord('incident');
gr.addQuery('short_description', 'CONTAINS', 'ATF Test');
gr.addActiveQuery();
gr.query();
var count = 0;
while (gr.next()) count++;
assertEqual({
name: 'Active test incidents should be zero after cleanup',
shouldbe: 0,
value: count
});
stepResult.setOutputMessage('Found ' + count + ' remaining test records');
})(outputs, steps, params, stepResult, assertEqual);
```
### REST Step (test API endpoints)
```
Step: REST Request
Method: POST
URL: /api/now/table/incident
Body: {
"short_description": "ATF REST Test",
"category": "software"
}
Expected Status: 201
Step: Assert JSON Body
Response field: result.number
Assertion: Starts with "INC"
```
## Client-Side ATF Tests
### Testing Client Scripts
```
Step: Open Form
Table: incident
Record: {created_record}
View: Default
Step: Set Field Value
Field: category
Value: network
Step: Assert Field Property
Field: cmdb_ci
Property: mandatory
Expected: true
Step: Assert Field Property
Field: u_network_segment
Property: visible
Expected: true
Step: Set Field Value
Field: priority
Value: 1 - Critical
Step: Click UI Action
Button: Escalate to Major
Step: Assert Form Message
Type: info
Contains: "escalated"
```
### Client Script Step (custom client assertions)
```javascript
// Custom client-side test step
(function(outputs, steps, params, stepResult, assertEqual) {
var priority = g_form.getValue('priority');
var isMandatory = g_form.isMandatory('cmdb_ci');
assertEqual({
name: 'CI should be mandatory for P1',
shouldbe: true,
value: isMandatory
});
// Check field messages
var fieldMsg = document.querySelector('[data-field="caller_id"] .field-msg');
assertEqual({
name: 'VIP message should be displayed',
shouldbe: true,
value: fieldMsg !== null
});
})(outputs, steps, params, stepResult, assertEqual);
```
## Test Suites
### Creating a Test Suite
```
Navigate: Automated Test Framework > Suites
Name: ITSM Regression Suite
Tests:
1. Incident Auto-Assignment (order: 100)
2. Incident Priority Matrix (order: 200)
3. Incident Resolution Validation (order: 300)
4. Problem Creation from Incident (order: 400)
5. Change Request Approval Flow (order: 500)
Schedule: Weekly, Sunday 2:00 AM
Notification: Email on failure to dev-team@company.com
```
### Suite Properties
```
Run order: Ordered (respect test sequence) vs Unordered (parallel-friendly)
Abort on failure: true (stop suite if a test fails)
Browser: Chrome, Firefox (for client-side tests)
Test runner user: atf_test_user (dedicated service account)
```
## Mock Data & Test Data Management
### ATF Test Data
```javascript
// Setup script (runs before test)
(function(outputs, steps, params, stepResult) {
// Create test user
var user = new GlideRecord('sys_user');
user.initialize();
user.setValue('user_name', 'atf_test_user_' + gs.generateGUID());
user.setValue('first_name', 'ATF');
user.setValue('last_name', 'TestUser');
user.setValue('email', 'atf@test.com');
user.setValue('active', true);
outputs.test_user_id = user.insert();
// Create test group
var group = new GlideRecord('sys_user_group');
group.initialize();
group.setValue('name', 'ATF Test Group ' + gs.generateGUID());
outputs.test_group_id = group.insert();
// Add user to group
var member = new GlideRecord('sys_user_grmember');
member.initialize();
member.setValue('user', outputs.test_user_id);
member.setValue('group', outputs.test_group_id);
member.insert();
})(outputs, steps, params, stepResult);
```
### Cleanup Script (runs after test)
```javascript
(function(outputs, steps, params, stepResult) {
// Clean up in reverse order of creation
var tables = ['sys_user_grmember', 'sys_user_group', 'sys_user', 'incident'];
var fields = ['user', 'sys_id', 'sys_id', 'sys_id'];
var values = [
steps.setup.test_user_id,
steps.setup.test_group_id,
steps.setup.test_user_id,
steps.create_incident.sys_id
];
for (var i = 0; i < tables.length; i++) {
var gr = new GlideRecord(tables[i]);
gr.addQuery(fields[i], values[i]);
gr.deleteMultiple();
}
})(outputs, steps, params, stepResult);
```
## Instance Scan
### Running Instance Scan
```
Navigate: Instance Scan > Scan Results
Checks include:
- Performance: slow queries, missing indexes, N+1 patterns
- Security: hardcoded credentials, eval() usage, XSS risks
- Best practices: deprecated API usage, global scope violations
- Upgrade safety: customizations on OOB records
```
### Custom Scan Checks
```
Navigate: Instance Scan > Scan Checks
Name: Check for hardcoded sys_ids
Type: Script Include scan
Table: sys_script_include
Script:
(function(engine) {
var pattern = /[a-f0-9]{32}/g;
var script = engine.getValue('script');
var matches = script.match(pattern);
if (matches && matches.length > 0) {
engine.finding({
description: 'Found ' + matches.length + ' potential hardcoded sys_ids',
severity: 'warning'
});
}
})(engine);
```
## Debugging
### Session Debug Modules
```
Navigate: System Diagnostics > Session Debug
Enable (per session):
- Debug Business Rule — shows which BRs fire
- Debug Security — shows ACL evaluation
- Debug SQL — shows database queries
- Debug Workflow — shows workflow execution (legacy)
- Debug Flow — shows Flow Designer execution
Output: System Logs > Session Debug Log
```
### Script Debugger
```
Navigate: System Diagnostics > Script Debugger
Features:
- Set breakpoints in Business Rules, Script Includes
- Step through code line by line
- Inspect variables and GlideRecord values
- Watch expressions
- Call stack inspection
// Programmatic breakpoint:
gs.breakpoint(); // triggers debugger if session debug enabled
```
### Transaction Log
```
Navigate: System Logs > Transactions
Key fields:
- URL — what was accessed
- Response time — total ms
- SQL count — number of DB queries
- Business rule count — BRs executed
// Filter for slow transactions:
response_time > 5000 (> 5 seconds)
```
### Debugging Business Rules
```javascript
// Temporary debug logging (remove before prod!)
(function executeRule(current, previous) {
gs.info('[DEBUG] BR fired on ' + current.getValue('number'));
gs.info('[DEBUG] State change: ' + previous.getValue('state') + ' → ' + current.getValue('state'));
gs.info('[DEBUG] Changed fields: ' + current.getChangedFields());
// ... actual logic
})(current, previous);
```
### Debugging Client Scripts
```javascript
// Browser console (F12)
function onChange(control, oldValue, newValue, isLoading) {
jslog('onChange fired: ' + oldValue + ' → ' + newValue);
console.log('Debug: field value = ', g_form.getValue('state'));
// Check client-side errors in browser DevTools > Console
}
```
### Debugging REST API Calls
```javascript
// In Scripted REST API:
(function process(request, response) {
gs.info('[REST DEBUG] Method: ' + request.getHeader('X-HTTP-Method'));
gs.info('[REST DEBUG] Path params: ' + JSON.stringify(request.pathParams));
gs.info('[REST DEBUG] Query params: ' + JSON.stringify(request.queryParams));
gs.info('[REST DEBUG] Body: ' + JSON.stringify(request.body.data));
// ... process request
})(request, response);
```
## Performance Testing
### Load Testing Approach
```
1. Use ATF to create realistic test scenarios
2. Run multiple test suites in parallel
3. Monitor: System Diagnostics > Stats
4. Check: Transaction times, semaphore waits, query times
5. Identify bottlenecks: slow queries, BR execution time
// Key metrics to monitor:
// - Average transaction response time
// - p95 response time
// - Database query count per transaction
// - Business Rule execution time per transaction
// - Cache hit/miss ratio
```
## Testing Best Practices
1. **Isolate tests** — each test creates AND cleans up its own data
2. **Use dedicated ATF user** — don't run as admin (tests ACLs)
3. **Test Business Rules independently** — create record, assert result
4. **Test client scripts with ATF client steps** — not manual browser testing
5. **Run suites on schedule** — catch regressions early
6. **Test edge cases** — null values, empty strings, max lengths
7. **Test security** — impersonate non-admin user, verify ACL enforcement
8. **Version control tests** — include in scoped app / update set
9. **Don't test platform** — test YOUR customizations, not OOB behavior
10. **Clean up** — ALWAYS delete test data, even on failure (use finally blocks)

View File

@ -0,0 +1,316 @@
# Update Sets & CI/CD
## Update Sets
### What Are Update Sets?
Update Sets capture **configuration changes** (not data) made in an instance. They're the primary mechanism for moving customizations between instances (Dev → Test → Prod).
### Tracked vs Untracked Changes
```
Tracked (captured in update sets):
✅ Business Rules, Client Scripts, Script Includes
✅ UI Policies, UI Actions
✅ ACLs, Roles
✅ Forms, Lists, Related Lists
✅ Flows, Subflows
✅ Catalog Items, Variables
✅ System Properties
✅ Scheduled Jobs (definition)
✅ Notifications
✅ Application Files (scoped apps)
NOT tracked:
❌ Data records (incidents, users, etc.)
❌ Attachments (unless on tracked records)
❌ Homepages/dashboards (user-specific)
❌ Update Set records themselves
❌ Import Sets, Transform Maps (usually)
```
### Creating & Managing Update Sets
```
Navigate: System Update Sets > Local Update Sets
Best practices:
- One update set per feature/story
- Naming: "STRY0012345 - Add VIP flag to incident form"
- Never work in "Default" update set
- Close update set when feature is complete
- Review contents before promoting
```
### Retrieving Update Sets (Target Instance)
```
Navigate: System Update Sets > Retrieved Update Sets
Steps:
1. Source instance: Complete and close update set
2. Target instance: Navigate to Retrieved Update Sets
3. Click "Retrieve Update Sets" → select source
4. Preview (check for conflicts)
5. Resolve conflicts (if any)
6. Commit
Conflict types:
- Record exists in target with different update set
- Record deleted in target but modified in source
- Collision: same record modified in both
Resolution options:
- Accept remote (source wins)
- Accept local (target wins)
- Manual merge
```
### Batch Update Sets
```
Use case: Group related update sets for a single deployment
Creating:
1. Create Batch parent record
2. Add child update sets (in commit order)
3. Preview and commit the batch
Benefits:
- Atomic deployment (all or nothing)
- Controlled commit order
- Single preview/commit action
```
### Update Set Best Practices
```
1. One feature per update set — easy rollback
2. Never put data fixes in update sets — use Fix Scripts
3. Close update sets promptly — avoid capturing unintended changes
4. Preview before commit — always
5. Back out plan — know how to revert each update set
6. Don't modify OOB records when possible — use insert/override
7. Test in sub-prod first — never commit directly to Prod
8. Document the update set — description field exists, use it
```
## Source Control Integration
### Git Integration (App Repository)
```
Navigate: System Applications > Studio > Source Control
Setup:
1. Create Git repo (GitHub, GitLab, Bitbucket, Azure DevOps)
2. Generate credentials (SSH key or token)
3. In Studio: Source Control > Link to Source Control
4. Configure: URL, branch, credentials
Operations:
- Commit: Push app files to Git
- Apply Remote Changes: Pull from Git
- Stash: Temporary save (like git stash)
- Create Branch: Feature branching
- Switch Branch: Change active branch
```
### App Repository Structure in Git
```
my-scoped-app/
├── README.md
├── sn_source_control.properties
├── checksum.json
├── sys_app_<sys_id>.xml # App record
├── sys_script_include/ # Script Includes
│ ├── MyUtils_<sys_id>.xml
│ └── MyHelper_<sys_id>.xml
├── sys_script/ # Business Rules
│ ├── ValidateIncident_<sys_id>.xml
│ └── AutoAssign_<sys_id>.xml
├── sys_ui_policy/ # UI Policies
├── sys_client_script/ # Client Scripts
├── sys_security_acl/ # ACLs
├── sys_flow/ # Flows
└── sys_hub_action/ # Flow actions
```
## CI/CD with ServiceNow
### ServiceNow CI/CD API
```
Base URL: /api/sn_cicd/
Endpoints:
POST /app/install — Install app from source control
POST /app/publish — Publish app to app repo
POST /app/apply_changes — Apply remote source control changes
GET /progress/{result_id} — Check operation status
POST /testsuite/run — Run ATF test suite
GET /testsuite/results — Get test results
POST /app_repo/rollback — Rollback app version
POST /sc/apply_changes — Apply source control changes
POST /plugin/activate — Activate a plugin
POST /instance/scan — Run instance scan
```
### GitHub Actions Pipeline
```yaml
# .github/workflows/servicenow-cicd.yml
name: ServiceNow CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install App to Dev
uses: ServiceNow/sncicd-install-app@v2
with:
instanceUrl: ${{ secrets.SN_DEV_URL }}
appSysId: ${{ secrets.APP_SYS_ID }}
scope: ${{ secrets.APP_SCOPE }}
env:
snowUsername: ${{ secrets.SN_USERNAME }}
snowPassword: ${{ secrets.SN_PASSWORD }}
- name: Run ATF Tests
uses: ServiceNow/sncicd-tests-run@v2
with:
instanceUrl: ${{ secrets.SN_DEV_URL }}
testSuiteSysId: ${{ secrets.TEST_SUITE_ID }}
env:
snowUsername: ${{ secrets.SN_USERNAME }}
snowPassword: ${{ secrets.SN_PASSWORD }}
- name: Publish App
if: github.ref == 'refs/heads/main'
uses: ServiceNow/sncicd-publish-app@v2
with:
instanceUrl: ${{ secrets.SN_DEV_URL }}
appSysId: ${{ secrets.APP_SYS_ID }}
versionFormat: autodetect
env:
snowUsername: ${{ secrets.SN_USERNAME }}
snowPassword: ${{ secrets.SN_PASSWORD }}
- name: Install to Prod
if: github.ref == 'refs/heads/main'
uses: ServiceNow/sncicd-install-app@v2
with:
instanceUrl: ${{ secrets.SN_PROD_URL }}
appSysId: ${{ secrets.APP_SYS_ID }}
env:
snowUsername: ${{ secrets.SN_USERNAME }}
snowPassword: ${{ secrets.SN_PASSWORD }}
```
### Azure DevOps Pipeline
```yaml
# azure-pipelines.yml
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
jobs:
- job: ApplyChanges
steps:
- task: ServiceNow-CICD-App-Apply-Changes@1
inputs:
connectedServiceName: 'ServiceNow-Dev'
appScope: 'x_myco_myapp'
branch: 'main'
- stage: Test
dependsOn: Build
jobs:
- job: RunATF
steps:
- task: ServiceNow-CICD-Run-Test-Suite@1
inputs:
connectedServiceName: 'ServiceNow-Dev'
testSuiteSysId: '$(TEST_SUITE_ID)'
- stage: Deploy
dependsOn: Test
condition: succeeded()
jobs:
- deployment: Production
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: ServiceNow-CICD-App-Install@1
inputs:
connectedServiceName: 'ServiceNow-Prod'
appSysId: '$(APP_SYS_ID)'
```
### Jenkins Pipeline
```groovy
pipeline {
agent any
environment {
SN_CREDS = credentials('servicenow-credentials')
SN_DEV = 'https://dev12345.service-now.com'
SN_PROD = 'https://prod12345.service-now.com'
APP_SYS_ID = 'abc123...'
}
stages {
stage('Apply Source Control') {
steps {
snApplyChanges(
url: "${SN_DEV}",
credentialsId: 'servicenow-credentials',
appScope: 'x_myco_myapp',
branch: 'main'
)
}
}
stage('Run Tests') {
steps {
snRunTestSuite(
url: "${SN_DEV}",
credentialsId: 'servicenow-credentials',
testSuiteSysId: env.TEST_SUITE_ID
)
}
}
stage('Deploy to Prod') {
when { branch 'main' }
steps {
snInstallApp(
url: "${SN_PROD}",
credentialsId: 'servicenow-credentials',
appSysId: "${APP_SYS_ID}"
)
}
}
}
}
```
## Deployment Best Practices
1. **Dev → Test → Stage → Prod** — minimum 3 environments for enterprise
2. **Automated ATF** on every deployment — catch regressions early
3. **Instance Scan** before promoting — check for best practice violations
4. **Version your apps** semantically — major.minor.patch
5. **Never develop in Prod** — exception: emergency fix with immediate backport
6. **Branch strategy:** main = production, develop = current sprint, feature/* = individual stories
7. **Code review** via Git PRs — even for ServiceNow development
8. **Rollback plan** for every deployment — test rollback in sub-prod
9. **Freeze periods** — no deployments during critical business windows
10. **Change records** for every Prod deployment — automate creation via CI/CD

View File

@ -0,0 +1,473 @@
# Virtual Agent
## Overview
Virtual Agent is ServiceNow's AI-powered chatbot that handles employee and customer self-service through conversational interfaces. It integrates with NLU (Natural Language Understanding), the Service Catalog, Knowledge Base, and custom server-side scripts.
### Channels
```
Supported channels:
- ServiceNow Service Portal (web chat widget)
- Microsoft Teams
- Slack
- Web API (custom integrations)
- Mobile (Now Mobile app)
- Employee Center
- Customer Service Portal (CSM)
```
## NLU (Natural Language Understanding)
### NLU Models
```
Navigate: Virtual Agent > NLU Models
Model types:
- Intent-based: maps user utterances to intents
- ServiceNow NLU: built-in model (no external dependency)
- IBM Watson: external NLU engine
- Google Dialogflow: external NLU engine
- Microsoft LUIS: external NLU engine
Recommendation: Use ServiceNow NLU (simplest, no extra licensing)
```
### Creating Intents
```
Navigate: Virtual Agent > NLU > Intents
Intent structure:
- Name: descriptive (e.g., "Reset Password")
- Model: which NLU model
- Utterances: example phrases (minimum 15-20 for accuracy)
Example — Password Reset Intent:
Utterances:
- "I forgot my password"
- "Reset my password"
- "Can't log in to my account"
- "Password expired"
- "I need to change my password"
- "Locked out of my account"
- "Help me reset my credentials"
- "My password isn't working"
- "How do I reset my password"
- "I'm having trouble signing in"
- "Account locked"
- "Password doesn't work anymore"
- "Need a new password"
- "Can you help me with my login"
- "Unable to access my account"
Tips:
- Vary sentence structure and vocabulary
- Include misspellings users commonly make
- Add both formal and informal phrasing
- 15-20 utterances minimum per intent
- Avoid overlapping utterances between intents
```
### Entities
```
Navigate: Virtual Agent > NLU > Entities
Entity types:
- System entities: date, time, number, email (built-in)
- Custom entities: application-specific values
Example — Department Entity:
Name: department
Values:
- IT (synonyms: information technology, tech support)
- HR (synonyms: human resources, people team)
- Finance (synonyms: accounting, billing)
- Facilities (synonyms: building, office)
Usage in utterances:
"I need help from {department}"
"Connect me to {department}"
```
### Training & Testing
```
Navigate: Virtual Agent > NLU > Model Testing
Training:
1. Add utterances to intents
2. Click "Train" on the model
3. Wait for training to complete (1-5 minutes)
Testing:
- Test Utterances tab: type phrases, see predicted intent
- Confusion matrix: see where intents overlap
- Batch testing: upload CSV of test phrases
Accuracy targets:
- >90% intent classification accuracy
- <5% confusion between intents
- Test with 50+ phrases per intent for production
```
## Topics (Conversation Flows)
### Topic Structure
```
Navigate: Virtual Agent > Designer
Topic = a conversation flow that handles one user intent
Structure:
Topic: Password Reset
├── Greeting node
│ └── "I can help you reset your password."
├── User Input node (collect info)
│ └── "Which account needs a reset?"
│ ├── Choice: Network/AD account
│ └── Choice: Application-specific account
├── Script node (server-side logic)
│ └── Look up user, validate identity
├── Bot Response node
│ └── "I've sent a reset link to your email."
└── End node
└── "Is there anything else I can help with?"
```
### Topic Node Types
#### Greeting / Bot Response
```
Text output nodes:
- Static text: fixed message
- Dynamic text: include variables ${variable_name}
- Rich content: links, images, cards
- Carousel: multiple cards user can swipe
Example:
"Hi ${user.first_name}! I can see you're in the ${user.department} department.
How can I help you today?"
```
#### User Input
```
Input types:
- Text: free-form text entry
- Choice: buttons/quick replies (up to 10 options)
- Date/Time picker
- File upload
- Lookup: reference field search
- Number
- Email
Choice example:
Prompt: "What type of request?"
Choices:
- "New Software" → routes to software topic
- "Hardware Issue" → routes to hardware topic
- "Access Request" → routes to access topic
- "Other" → routes to general inquiry
```
#### Script Node (Server-Side)
```javascript
// Script node — execute server-side logic
(function execute() {
// Access topic variables
var userId = vaSystem.getUser();
var inputText = vaInputs.get('user_response');
// GlideRecord queries
var gr = new GlideRecord('sys_user');
if (gr.get(userId)) {
vaVars.user_name = gr.getValue('name');
vaVars.user_email = gr.getValue('email');
vaVars.user_department = gr.department.getDisplayValue();
vaVars.user_manager = gr.manager.getDisplayValue();
}
// Check for open incidents
var inc = new GlideRecord('incident');
inc.addQuery('caller_id', userId);
inc.addActiveQuery();
inc.addQuery('short_description', 'CONTAINS', inputText);
inc.setLimit(3);
inc.query();
var openIncidents = [];
while (inc.next()) {
openIncidents.push({
number: inc.getValue('number'),
description: inc.getValue('short_description'),
state: inc.state.getDisplayValue()
});
}
vaVars.has_existing = openIncidents.length > 0;
vaVars.existing_incidents = JSON.stringify(openIncidents);
})();
```
#### Decision Node (Branching)
```
Branch based on:
- Topic variable values
- Script output
- User role/group membership
- NLU entity values
Example:
Condition: vaVars.has_existing == true
→ Yes: "I found existing incidents. Would you like to check status?"
→ No: "Let me create a new incident for you."
```
#### Flow Trigger Node
```
Execute a Flow Designer flow from within a topic:
- Pass topic variables as flow inputs
- Receive flow outputs back into topic
- Wait for flow completion or run async
Use cases:
- Create catalog request
- Run approval workflow
- Execute automated remediation
- Look up complex data
```
#### Create Record Node
```
Create records directly from the topic:
- Table: incident, sc_req_item, hr_case, etc.
- Field mapping: topic variables → record fields
- Return: sys_id, number for confirmation
Example — Create Incident:
Table: incident
Mappings:
short_description = ${user_issue_summary}
description = ${user_detailed_description}
caller_id = ${sys_user}
category = ${selected_category}
urgency = ${selected_urgency}
Output variable: created_incident_number
```
#### Knowledge Search Node
```
Search knowledge base within conversation:
- Search term: from user input variable
- Knowledge base: specific or all
- Display: article cards with links
- Deflection: if article solves issue, close topic
Configuration:
Search query: ${user_issue_description}
Max results: 3
If user selects article → End topic (deflected)
If user says "not helpful" → Continue to create incident
```
### Topic Variables
```
Variable types:
- String, Integer, Boolean
- Reference (to any table)
- Date/Time
- JSON (complex objects)
System variables (auto-available):
- vaSystem.getUser() → current user sys_id
- vaSystem.getChannel() → chat channel name
- vaSystem.getLanguage() → user's language
- vaSystem.getTimezone() → user's timezone
Setting/Getting variables in scripts:
vaVars.my_variable = 'value'; // Set
var val = vaVars.my_variable; // Get
vaInputs.get('input_name'); // Get user input
vaOutputs.set('output_name', value); // Set output for next node
```
## Now Assist (GenAI) Integration
### Virtual Agent + Now Assist
```
Zurich+ features:
- Generative AI fallback: if no intent matches,
Now Assist generates a response from knowledge base
- Summarization: VA can summarize long KB articles
- Suggested intents: Now Assist suggests new intents
based on unmatched conversations
- Conversation analytics: AI-powered topic gap analysis
Configuration:
Navigate: Now Assist > Virtual Agent Settings
Enable: "Generative AI topic"
Knowledge sources: select knowledge bases for grounding
```
### AI Search in Topics
```
Node type: AI Search
- Searches across Knowledge, Service Catalog, Communities
- Returns AI-summarized answers
- Includes source links for verification
- Fallback if VA can't match intent
```
## Multi-Language Support
```
Navigate: Virtual Agent > Localization
Setup:
1. Enable languages in system settings
2. Create translated versions of topics
3. Map intents to language-specific NLU models
4. Translate bot responses, choices, prompts
Automatic detection:
- User's language preference (sys_user.preferred_language)
- Browser language header
- Manual language selection in chat widget
Translation approach:
- Separate NLU model per language (best accuracy)
- OR single model with multi-language utterances (simpler)
```
## Analytics & Improvement
### Key Metrics
```
Navigate: Virtual Agent > Analytics Dashboard
Metrics:
- Containment rate: % resolved without human agent
- Deflection rate: % handled by KB article
- Abandonment rate: % who leave mid-conversation
- Average conversation length (turns)
- Intent match rate: % of utterances matched
- Escalation rate: % transferred to live agent
- CSAT: post-conversation satisfaction score
Targets:
- Containment: >60% for common topics
- Intent match: >85%
- Abandonment: <15%
```
### Conversation Analysis
```
Navigate: Virtual Agent > Conversations
Review:
- Full conversation transcripts
- Unmatched utterances (intent gaps)
- Conversation paths (where users get stuck)
- Feedback analysis (thumbs up/down per topic)
Improvement workflow:
1. Review unmatched utterances weekly
2. Create new intents or add utterances to existing
3. Retrain NLU model
4. Analyze abandonment points in topics
5. Simplify or add branches where users get stuck
```
## Live Agent Handoff
### Escalation to Human Agent
```
Topic node: Transfer to Agent
Configuration:
- Queue: which assignment group receives the chat
- Context passing: include conversation history
- Wait message: "Connecting you to a live agent..."
- Estimated wait time display
- Fallback: if no agents available → create incident instead
Script for custom escalation:
```
```javascript
// Pre-escalation script — gather context
(function execute() {
// Package conversation context for the agent
var context = {
user_issue: vaVars.issue_summary,
attempted_solutions: vaVars.solutions_tried,
category: vaVars.category,
priority: vaVars.detected_priority,
conversation_turns: vaVars.turn_count
};
// Set agent-visible context
vaVars.escalation_context = JSON.stringify(context);
// Create interaction record for tracking
var interaction = new GlideRecord('interaction');
interaction.initialize();
interaction.setValue('type', 'chat');
interaction.setValue('opened_for', vaSystem.getUser());
interaction.setValue('short_description', vaVars.issue_summary);
interaction.insert();
vaVars.interaction_id = interaction.getUniqueValue();
})();
```
### Agent Workspace Chat
```
When escalated, agent sees:
- Full conversation transcript
- User details (name, department, location)
- Context variables from VA topic
- Suggested knowledge articles
- Quick actions (create incident, check status)
Configuration:
Navigate: Workspace Experience > Agent Chat
- Queue management
- Capacity rules (max concurrent chats per agent)
- Auto-accept settings
- Canned responses
```
## Virtual Agent Designer Tips
### Topic Design Patterns
```
1. Greeting → Identify Intent → Collect Info → Action → Confirm → End
2. Keep topics focused — one intent per topic
3. Max 7 conversation turns before resolution or escalation
4. Always offer escalation to human agent
5. Use quick replies over free text when possible
6. Confirm before taking action ("I'll create an incident. Proceed?")
7. Handle "I don't know" and "none of these" gracefully
8. End every topic with "Anything else I can help with?"
```
### Common Pitfalls
```
- Too many choices (>5 buttons) — users won't read them all
- Deep conversation trees — users abandon after 4-5 nodes
- No fallback for unmatched intents — user gets stuck
- Assuming user knows ServiceNow terms — use plain language
- Not testing on mobile — chat widget renders differently
- Ignoring analytics — launch and forget = declining usage
- Single-language only — limits adoption in global orgs
```
## Best Practices
1. **Start with top 10 IT requests** — password reset, software install, hardware request
2. **15+ utterances per intent** — less = poor recognition
3. **Test with real users** before production — their language differs from yours
4. **Measure containment** — if VA can't resolve, it's just an extra step before human
5. **Review unmatched weekly** — biggest source of improvement
6. **Quick replies > free text** — guided paths = higher completion
7. **Always offer human option** — users get frustrated when trapped in bot loops
8. **KB integration** — deflection through existing knowledge is highest ROI
9. **Channel-specific testing** — Teams/Slack render differently than portal
10. **Iterate monthly** — VA is never "done," always improving

View File

@ -0,0 +1,174 @@
# Vulnerability Response
## Scope
Use this reference for Vulnerability Response triage, vulnerable items, vulnerability groups, remediation workflows, exception handling, scanner integrations, and remediation metrics.
## Plugin and Role Assumptions
Clarify whether the environment uses:
- Vulnerability Response only or broader SecOps
- one or more scanner connectors
- CI ownership from CMDB, service ownership, or custom support models
- exception workflows tied to IRM records or custom waivers
- remediation through ITSM, FSM, or platform tasking
Key stakeholders are vulnerability analysts, infrastructure owners, application owners, and risk approvers.
## Core Tables
| Area | Table |
|------|-------|
| Vulnerable Item | `sn_vul_vulnerable_item` |
| Vulnerability Group | `sn_vul_group` |
| Vulnerability Entry | `sn_vul_entry` |
| Remediation Task | `sn_vul_remediation_task` |
| Exception / Risk Acceptance | instance-specific, often linked through risk/exception records |
## Typical Lifecycle
1. Ingest scanner finding
2. Match to CI or asset
3. Create or update vulnerable item
4. Group for remediation
5. Assign ownership
6. Remediate, mitigate, defer, or accept risk
7. Verify closure with scanner data
## Triage Inputs
Key triage factors:
- source scanner and connector health
- CI match confidence
- exploitability and severity
- environment or business criticality
- internet exposure
- exception posture
- ownership and remediation SLA
## Vulnerable Item Query Pattern
```javascript
var gr = new GlideRecord('sn_vul_vulnerable_item');
gr.addQuery('state', 'open');
gr.addQuery('risk_rating', '1'); // highest risk
gr.addQuery('assignment_group', assignmentGroupSysId);
gr.setLimit(50);
gr.query();
while (gr.next()) {
gs.info(gr.getValue('number') + ' ' + gr.getValue('source') + ' ' + gr.getValue('cmdb_ci'));
}
```
## Grouping Guidance
Group vulnerable items only when the grouping logic supports actionability. Common grouping dimensions:
- same CI
- same application service
- same patch or fix path
- same ownership path
- same scanner plugin and remediation action
Avoid grouping items with different remediation owners just to reduce queue volume.
## Auto-Assignment Pattern
```javascript
// Before insert/update on sn_vul_vulnerable_item
(function executeRule(current, previous) {
if (gs.nil(current.getValue('assignment_group')) && !gs.nil(current.getValue('cmdb_ci'))) {
var ci = new GlideRecord('cmdb_ci');
if (ci.get(current.getValue('cmdb_ci')) && !gs.nil(ci.getValue('assignment_group'))) {
current.setValue('assignment_group', ci.getValue('assignment_group'));
}
}
})(current, previous);
```
## Ownership Fallback Pattern
If CI ownership is inconsistent, make the fallback order explicit.
```javascript
var ci = new GlideRecord('cmdb_ci');
if (ci.get(ciSysId)) {
var owningGroup = ci.getValue('assignment_group') || ci.getValue('support_group');
if (!gs.nil(owningGroup)) {
gs.info('Resolved owning group: ' + owningGroup);
}
}
```
If the customer routes ownership from business services, service offerings, or custom CI fields, document that hierarchy instead of guessing.
## Exception Pattern
A strong exception workflow should capture:
- why remediation is deferred
- compensating controls
- approver and approval date
- expiry date
- review cadence
- linkage to the vulnerable item or group
Use Flow Designer or risk records for approval routing rather than hardcoded branching.
## Workflow Example: Exception Review
1. the analyst proposes a defer or accept-risk path on a vulnerable item or group
2. the request captures remediation blocker, compensating controls, business impact, and expiry date
3. an approver reviews severity, exploitability, and affected services
4. approval creates review reminders before expiry
5. denial returns the item to active remediation
6. closure requires scanner confirmation or approved compensating evidence
## Integration Pattern
For scanner or ticketing integrations:
1. keep external identifiers on the vulnerable item or related import records
2. treat scanner status as source data, not as a free-form user field
3. document retry and deduplication behavior
4. preserve auditability of state transitions
## Reporting KPIs
- open vulnerable items by risk
- overdue vulnerable items by group
- remediation lead time
- exception inventory by expiry
- stale scanner findings
- percent of findings with CI ownership gaps
## Troubleshooting
Common failure modes:
- vulnerable items remain unassigned because CI ownership is blank or mapped to the wrong class
- findings reopen repeatedly because closure is driven by tickets instead of scanner confirmation
- grouping hides ownership problems because unlike assets or services were combined for convenience
- exceptions never expire because the approval workflow does not create reminders or follow-up reviews
- connector failures leave stale records that look open even though the upstream tool changed state
## Testing Guidance
Validate at least these scenarios:
- create a vulnerable item with a mapped CI and confirm assignment resolves correctly
- test a vulnerable item with no CI owner and verify the documented fallback path
- process remediation closure with and without scanner confirmation
- submit an exception request with compensating controls and confirm expiry reminders are created
- simulate connector delay or duplicate findings and verify deduplication logic
## Best Practices
1. Keep grouping rules explicit and explain why a vulnerable item belongs to a group.
2. Do not close vulnerable items purely from ticket closure; require scanner confirmation or compensating workflow evidence.
3. Distinguish remediation, mitigation, accepted risk, and false positive states.
4. Call out plugin assumptions and scanner-source dependencies whenever proposing automation.
5. For high-volume environments, prefer batch-safe assignment and aggregation logic over per-record integrations.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Smoke tests for the ServiceNow helper and bundled references."""
from __future__ import annotations
import importlib.util
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
HELPER_PATH = ROOT / "scripts" / "snow_helper.py"
spec = importlib.util.spec_from_file_location("snow_helper", HELPER_PATH)
snow_helper = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = snow_helper
spec.loader.exec_module(snow_helper)
class QueryGenerationTests(unittest.TestCase):
def test_group_names_with_apostrophes_are_preserved(self) -> None:
query = snow_helper.generate_query("find all incidents assigned to group Bob's Team")
self.assertIn("assignment_group.name", query)
self.assertIn("Bob\\'s Team", query)
def test_last_n_days_query_uses_valid_add_query_signature(self) -> None:
query = snow_helper.generate_query("find all incidents created in the last 7 days")
self.assertIn("gr.addQuery('sys_created_on', '>=', gs.daysAgoStart(7));", query)
def test_count_query_uses_glide_aggregate(self) -> None:
query = snow_helper.generate_query("count active incidents")
self.assertIn("new GlideAggregate('incident')", query)
self.assertIn("ga.addAggregate('COUNT');", query)
def test_security_incident_mapping_is_available(self) -> None:
query = snow_helper.generate_query("find all security incidents in state containment")
self.assertIn("new GlideRecord('sn_si_incident')", query)
def test_sam_pro_mapping_is_available(self) -> None:
query = snow_helper.generate_query("find all software entitlements in state active")
self.assertIn("new GlideRecord('alm_license')", query)
class LintTests(unittest.TestCase):
def test_detects_direct_assignment_for_any_gliderecord_variable(self) -> None:
issues = snow_helper.lint_script(
"var ticket = new GlideRecord('incident');\n"
"ticket.short_description = 'Hello';\n"
)
self.assertTrue(any("Direct field assignment" in issue.message for issue in issues))
def test_detects_unfiltered_query(self) -> None:
issues = snow_helper.lint_script(
"var gr = new GlideRecord('incident');\n"
"gr.query();\n"
)
self.assertTrue(any("without any filter conditions" in issue.message for issue in issues))
def test_detects_add_aggregate_on_gliderecord(self) -> None:
issues = snow_helper.lint_script(
"var gr = new GlideRecord('incident');\n"
"gr.addAggregate('COUNT');\n"
)
self.assertTrue(any("addAggregate()" in issue.message for issue in issues))
def test_integrationhub_template_is_available(self) -> None:
class Args:
table = "incident"
template = snow_helper.template_integrationhub_action(Args())
self.assertIn("Connection & Credential Alias", template)
self.assertIn("RESTMessageV2", template)
class ValidationTests(unittest.TestCase):
def test_skill_validation_passes(self) -> None:
errors = snow_helper.validate_skill(str(ROOT))
self.assertEqual([], errors)
if __name__ == "__main__":
unittest.main()