scoped-apps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Scoped Application Development for ServiceNow

ServiceNow限定范围应用开发

Scoped applications provide isolation and portability for custom development in ServiceNow.
限定范围应用(Scoped applications)为ServiceNow中的自定义开发提供隔离性和可移植性。

Why Use Scoped Apps?

为何使用限定范围应用?

FeatureGlobal ScopeScoped App
Naming conflictsPossiblePrevented (x_prefix)
PortabilityDifficultEasy (Update Sets)
SecurityOpenControlled (Cross-scope)
Store publishingNoYes
DependenciesImplicitExplicit
特性全局作用域限定范围应用
命名冲突可能存在已避免(x_前缀)
可移植性困难容易(更新集)
安全性开放受控(跨作用域)
商店发布不支持支持
依赖关系隐式显式

Creating a Scoped Application

创建限定范围应用

Via Studio (Recommended)

通过Studio(推荐)

1. Navigate: System Applications > Studio
2. Click: Create Application
3. Enter:
   - Name: "My Custom App"
   - Scope: "x_mycom_myapp" (auto-generated)
   - Version: 1.0.0
4. Configure:
   - Runtime access: Check tables needing cross-scope access
1. 导航至:系统应用 > Studio
2. 点击:创建应用
3. 输入:
   - 名称:"My Custom App"
   - 作用域:"x_mycom_myapp"(自动生成)
   - 版本:1.0.0
4. 配置:
   - 运行时访问:勾选需要跨作用域访问的表

Via MCP

通过MCP

javascript
snow_create_application({
  name: "My Custom Application",
  scope: "x_mycom_custom",
  version: "1.0.0",
  description: "Custom application for..."
});
javascript
snow_create_application({
  name: "My Custom Application",
  scope: "x_mycom_custom",
  version: "1.0.0",
  description: "Custom application for..."
});

Scope Naming Convention

作用域命名规范

x_[vendor]_[app]

Examples:
- x_acme_hr          (ACME Corp HR App)
- x_mycom_inventory  (My Company Inventory)
- x_snc_global       (ServiceNow Global)
x_[供应商]_[应用]

示例:
- x_acme_hr          (ACME Corp HR应用)
- x_mycom_inventory  (我司库存管理应用)
- x_snc_global       (ServiceNow全局)

Table Naming

表命名规则

javascript
// Scoped tables are automatically prefixed
// Table name in Studio: "task_tracker"
// Actual table name: "x_mycom_myapp_task_tracker"

// Creating records
var gr = new GlideRecord('x_mycom_myapp_task_tracker');
gr.initialize();
gr.setValue('name', 'My Task');
gr.insert();
javascript
// 限定范围的表会自动添加前缀
// Studio中的表名:"task_tracker"
// 实际表名:"x_mycom_myapp_task_tracker"

// 创建记录
var gr = new GlideRecord('x_mycom_myapp_task_tracker');
gr.initialize();
gr.setValue('name', 'My Task');
gr.insert();

Script Include in Scoped App

限定范围应用中的脚本包含

javascript
var TaskManager = Class.create();
TaskManager.prototype = {
  initialize: function() {
    this.tableName = 'x_mycom_myapp_task_tracker';
  },

  createTask: function(name, description) {
    var gr = new GlideRecord(this.tableName);
    gr.initialize();
    gr.setValue('name', name);
    gr.setValue('description', description);
    return gr.insert();
  },

  // Mark as accessible from other scopes
  // Requires: "Accessible from: All application scopes"
  getTask: function(sysId) {
    var gr = new GlideRecord(this.tableName);
    if (gr.get(sysId)) {
      return {
        name: gr.getValue('name'),
        description: gr.getValue('description')
      };
    }
    return null;
  },

  type: 'TaskManager'
};
javascript
var TaskManager = Class.create();
TaskManager.prototype = {
  initialize: function() {
    this.tableName = 'x_mycom_myapp_task_tracker';
  },

  createTask: function(name, description) {
    var gr = new GlideRecord(this.tableName);
    gr.initialize();
    gr.setValue('name', name);
    gr.setValue('description', description);
    return gr.insert();
  },

  // 标记为可从其他作用域访问
  // 要求:"可访问范围:所有应用作用域"
  getTask: function(sysId) {
    var gr = new GlideRecord(this.tableName);
    if (gr.get(sysId)) {
      return {
        name: gr.getValue('name'),
        description: gr.getValue('description')
      };
    }
    return null;
  },

  type: 'TaskManager'
};

Cross-Scope Access

跨作用域访问

Calling Other Scope's Script Include

调用其他作用域的脚本包含

javascript
// From scope: x_mycom_otherapp
// Calling: x_mycom_myapp.TaskManager

// Option 1: Direct call (if accessible)
var tm = new x_mycom_myapp.TaskManager();
var task = tm.getTask(sysId);

// Option 2: GlideScopedEvaluator
var evaluator = new GlideScopedEvaluator();
evaluator.putVariable('sysId', sysId);
var result = evaluator.evaluateScript(
  'x_mycom_myapp',
  'new TaskManager().getTask(sysId)'
);
javascript
// 当前作用域:x_mycom_otherapp
// 调用:x_mycom_myapp.TaskManager

// 选项1:直接调用(如果允许访问)
var tm = new x_mycom_myapp.TaskManager();
var task = tm.getTask(sysId);

// 选项2:使用GlideScopedEvaluator
var evaluator = new GlideScopedEvaluator();
evaluator.putVariable('sysId', sysId);
var result = evaluator.evaluateScript(
  'x_mycom_myapp',
  'new TaskManager().getTask(sysId)'
);

Accessing Other Scope's Tables

访问其他作用域的表

javascript
// Check if cross-scope access is allowed
var gr = new GlideRecord('x_other_app_table');
if (!gr.isValid()) {
  gs.error('No access to x_other_app_table');
  return;
}

// If accessible, query normally
gr.addQuery('active', true);
gr.query();
javascript
// 检查是否允许跨作用域访问
var gr = new GlideRecord('x_other_app_table');
if (!gr.isValid()) {
  gs.error('No access to x_other_app_table');
  return;
}

// 如果允许,正常查询
gr.addQuery('active', true);
gr.query();

Application Properties

应用属性

Define Properties

定义属性

javascript
// In Application > Properties
// Name: x_mycom_myapp.default_priority
// Value: 3
// Type: string

// In Application > Modules
// Create "Properties" module pointing to:
// /sys_properties_list.do?sysparm_query=name=x_mycom_myapp
javascript
// 在 应用 > 属性 中
// 名称:x_mycom_myapp.default_priority
// 值:3
// 类型:字符串

// 在 应用 > 模块 中
// 创建指向以下地址的「属性」模块:
// /sys_properties_list.do?sysparm_query=name=x_mycom_myapp

Use Properties

使用属性

javascript
// Get property value
var defaultPriority = gs.getProperty('x_mycom_myapp.default_priority', '3');

// Set property value (requires admin)
gs.setProperty('x_mycom_myapp.default_priority', '2');
javascript
// 获取属性值
var defaultPriority = gs.getProperty('x_mycom_myapp.default_priority', '3');

// 设置属性值(需要管理员权限)
gs.setProperty('x_mycom_myapp.default_priority', '2');

Application Files Structure

应用文件结构

x_mycom_myapp/
├── Tables
│   ├── x_mycom_myapp_task
│   └── x_mycom_myapp_config
├── Script Includes
│   ├── TaskManager
│   └── ConfigUtils
├── Business Rules
│   └── Validate Task
├── UI Pages
│   └── task_dashboard
├── REST API
│   └── Task API
├── Scheduled Jobs
│   └── Daily Cleanup
└── Application Properties
    ├── default_priority
    └── enable_notifications
x_mycom_myapp/
├── 表
│   ├── x_mycom_myapp_task
│   └── x_mycom_myapp_config
├── 脚本包含
│   ├── TaskManager
│   └── ConfigUtils
├── 业务规则
│   └── 验证任务
├── UI页面
│   └── task_dashboard
├── REST API
│   └── 任务API
├── 计划任务
│   └── 每日清理
└── 应用属性
    ├── default_priority
    └── enable_notifications

REST API in Scoped App

限定范围应用中的REST API

Define Scripted REST API

定义脚本化REST API

javascript
// Resource: /api/x_mycom_myapp/tasks
// HTTP Method: GET

(function process(request, response) {
  var tasks = [];
  var gr = new GlideRecord('x_mycom_myapp_task_tracker');
  gr.addQuery('active', true);
  gr.query();

  while (gr.next()) {
    tasks.push({
      sys_id: gr.getUniqueValue(),
      name: gr.getValue('name'),
      status: gr.getValue('status')
    });
  }

  response.setBody({
    result: tasks,
    count: tasks.length
  });

})(request, response);
javascript
// 资源:/api/x_mycom_myapp/tasks
// HTTP方法:GET

(function process(request, response) {
  var tasks = [];
  var gr = new GlideRecord('x_mycom_myapp_task_tracker');
  gr.addQuery('active', true);
  gr.query();

  while (gr.next()) {
    tasks.push({
      sys_id: gr.getUniqueValue(),
      name: gr.getValue('name'),
      status: gr.getValue('status')
    });
  }

  response.setBody({
    result: tasks,
    count: tasks.length
  });

})(request, response);

Calling the API

调用API

bash
curl -X GET \
  "https://instance.service-now.com/api/x_mycom_myapp/tasks" \
  -H "Authorization: Bearer token"
bash
curl -X GET \
  "https://instance.service-now.com/api/x_mycom_myapp/tasks" \
  -H "Authorization: Bearer token"

Application Dependencies

应用依赖

Declare Dependencies

声明依赖

Application > Dependencies
Add:
  - sn_hr_core (HR Core)
  - sn_cmdb (CMDB)
应用 > 依赖关系
添加:
  - sn_hr_core (HR核心)
  - sn_cmdb (配置管理数据库)

Check Dependencies in Code

在代码中检查依赖

javascript
// Check if plugin is active
if (GlidePluginManager.isActive('com.snc.hr.core')) {
  // HR Core is available
  var hrCase = new sn_hr_core.hr_case();
}
javascript
// 检查插件是否激活
if (GlidePluginManager.isActive('com.snc.hr.core')) {
  // HR核心可用
  var hrCase = new sn_hr_core.hr_case();
}

Publishing to Store

发布至商店

Checklist Before Publishing

发布前检查清单

□ All tables have proper ACLs
□ No hard-coded sys_ids
□ No hard-coded instance URLs
□ All dependencies declared
□ Properties have default values
□ Documentation complete
□ Test cases pass
□ No global scope modifications
□ Update Set tested on clean instance
□ 所有表都配置了合适的ACL
□ 没有硬编码的sys_id
□ 没有硬编码的实例URL
□ 已声明所有依赖
□ 属性都有默认值
□ 文档完整
□ 测试用例通过
□ 未修改全局作用域
□ 更新集已在干净实例上测试

Version Management

版本管理

Major.Minor.Patch
1.0.0 - Initial release
1.1.0 - New feature added
1.1.1 - Bug fix
2.0.0 - Breaking change
主版本.次版本.补丁版本
1.0.0 - 初始版本
1.1.0 - 新增功能
1.1.1 - 修复Bug
2.0.0 - 破坏性变更

Common Mistakes

常见错误

MistakeProblemSolution
Global modificationsWon't deploy cleanlyKeep changes in scope
Hard-coded sys_idsFails on other instancesUse properties or lookups
Missing ACLsSecurity vulnerabilitiesCreate ACLs for all tables
No error handlingSilent failuresAdd try/catch, logging
Accessing global tables directlyUpgrade conflictsUse references, not copies
错误问题解决方案
修改全局作用域无法干净部署将变更限定在自身作用域内
硬编码sys_id在其他实例上运行失败使用属性或查找方式
缺少ACL安全漏洞为所有表创建ACL
无错误处理静默失败添加try/catch和日志
直接访问全局表升级时冲突使用引用而非复制

Best Practices

最佳实践

  1. Single Responsibility - One app per business function
  2. Explicit Dependencies - Declare all requirements
  3. Property-Driven - Configurable without code changes
  4. Defensive Coding - Check access before operations
  5. Documentation - Include README, release notes
  6. Testing - Automated tests for critical functions
  7. Versioning - Semantic versioning for updates
  1. 单一职责 - 一个应用对应一个业务功能
  2. 显式依赖 - 声明所有依赖项
  3. 属性驱动 - 无需修改代码即可配置
  4. 防御式编码 - 操作前检查访问权限
  5. 文档完善 - 包含README和发布说明
  6. 测试充分 - 为关键功能编写自动化测试
  7. 版本规范 - 使用语义化版本管理更新