Merge 77db996045 into f6656c1256
This commit is contained in:
commit
46c59ade78
|
|
@ -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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)));
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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'));
|
||||
}
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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()
|
||||
Loading…
Reference in New Issue