mcpField
W
workspace-builder

workspace-builder

github.com/serac-labs开发7759 次安装9 周前更新
代码仓库

关于 workspace-builder

面向 ServiceNow 的技能,用于构建范围应用和配置工作区,包括 UI Builder 体验所需的关联记录。内容说明创建顺序、权限与限制,并建议在 UI Builder 中编排页面布局。

主要功能

  1. 01创建范围应用和表
  2. 02配置工作区列表
  3. 03构建关联的 UI Builder 记录
  4. 04了解创建顺序和角色要求
  5. 05在 UI Builder 中编排页面

适用场景

  1. 01为应用设置自定义工作区
  2. 02创建 UI Builder 体验所需的记录链
  3. 03配置工作区前核对所需角色

MCP 相关

介绍用于创建和检查工作区及 UI Builder 记录的 ServiceNow MCP 工具,包括按顺序构建从体验到路由的记录链。还说明创建孤立页面记录的工具限制,并建议使用 UI Builder 编排页面内容。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/serac-labs/serac --skill workspace-builder

安装后在 Claude Code 中输入/workspace-builder确认已加载。

详细介绍

ServiceNow App Engine Studio 和 Workspace Builder

App Engine Studio(AES)通过自定义工作区支持低代码应用开发。

AES 架构

Application (sys_scope)
    ├── Tables & Forms
    ├── Workflows
    ├── Workspaces (sys_aw_workspace)
    │   ├── Lists
    │   ├── Forms
    │   └── Dashboards
    └── Portals

关键表

表用途
sys_scope应用范围
sys_app应用记录
sys_aw_workspaceAgent Workspace 定义
sys_ux_experienceNext Experience 工作区——UI Builder 链的顶层
sys_ux_app_config工作区设置;关联到体验
sys_ux_macroponent页面内容和自定义组件
sys_ux_page_registry将宏组件注册为应用配置中的页面
sys_ux_app_route可访问已注册页面的 URL 路径
sys_ux_pageUI Builder 页面行——读取即可,切勿直接插入

应用开发(ES5)

创建作用域应用
// Create scoped application (ES5 ONLY!)
var app = new GlideRecord("sys_scope")
app.initialize()

// Basic info
app.setValue("name", "IT Asset Tracker")
app.setValue("scope", "x_myco_asset_track")
app.setValue("short_description", "Track IT assets across the organization")
app.setValue("version", "1.0.0")

// Vendor
app.setValue("vendor", "My Company")
app.setValue("vendor_prefix", "x_myco")

// License
app.setValue("licensable", true)

app.insert()
创建应用表
// Create table in scoped app (ES5 ONLY!)
function createAppTable(scope, tableDef) {
  var table = new GlideRecord("sys_db_object")
  table.initialize()

  table.setValue("name", scope + "_" + tableDef.name)
  table.setValue("label", tableDef.label)
  table.setValue("super_class", tableDef.extends || "task")

  // Scope assignment
  table.setValue("sys_scope", getAppSysId(scope))

  // Options
  table.setValue("is_extendable", tableDef.extendable || false)
  table.setValue("create_access_controls", true)

  table.insert()

  // Create fields
  if (tableDef.fields) {
    for (var i = 0; i < tableDef.fields.length; i++) {
      createField(scope + "_" + tableDef.name, tableDef.fields[i])
    }
  }

  return table.getUniqueValue()
}

// Example
createAppTable("x_myco_asset_track", {
  name: "asset_item",
  label: "Asset Item",
  extends: "cmdb_ci",
  fields: [
    { name: "u_purchase_date", label: "Purchase Date", type: "glide_date" },
    { name: "u_warranty_end", label: "Warranty End", type: "glide_date" },
    { name: "u_assigned_user", label: "Assigned User", type: "reference", reference: "sys_user" },
  ],
})

工作区配置(ES5)

创建自定义工作区
// Create workspace (ES5 ONLY!)
var workspace = new GlideRecord("sys_aw_workspace")
workspace.initialize()

workspace.setValue("name", "asset_tracker_workspace")
workspace.setValue("title", "Asset Tracker")
workspace.setValue("description", "Workspace for IT asset management")

// Primary table
workspace.setValue("primary_table", "x_myco_asset_track_asset_item")

// URL
workspace.setValue("url", "asset-tracker")

// Branding
workspace.setValue("icon", "laptop")
workspace.setValue("color", "#2E7D32")

// App scope
workspace.setValue("sys_scope", appScopeSysId)

// Features
workspace.setValue("agent_assist_enabled", false)
workspace.setValue("contextual_side_panel_enabled", true)

workspace.insert()
配置工作区列表
// Create workspace list (ES5 ONLY!)
function createWorkspaceList(workspaceSysId, listDef) {
  var list = new GlideRecord("sys_aw_list")
  list.initialize()

  list.setValue("workspace", workspaceSysId)
  list.setValue("name", listDef.name)
  list.setValue("table", listDef.table)

  // Filter
  list.setValue("filter", listDef.filter || "")

  // Columns
  list.setValue("columns", listDef.columns.join(","))

  // Sorting
  if (listDef.orderBy) {
    list.setValue("order_by", listDef.orderBy)
    list.setValue("order_by_desc", listDef.orderDesc || false)
  }

  // Grouping
  if (listDef.groupBy) {
    list.setValue("group_by", listDef.groupBy)
  }

  list.insert()

  return list.getUniqueValue()
}

// Example lists
createWorkspaceList(workspaceSysId, {
  name: "My Assets",
  table: "x_myco_asset_track_asset_item",
  filter: "u_assigned_user=javascript:gs.getUserID()",
  columns: ["number", "name", "u_purchase_date", "u_warranty_end", "state"],
})

createWorkspaceList(workspaceSysId, {
  name: "Expiring Warranties",
  table: "x_myco_asset_track_asset_item",
  filter: "u_warranty_endBETWEENjavascript:gs.daysAgoStart(0)@javascript:gs.daysAgoEnd(-30)",
  columns: ["number", "name", "u_assigned_user", "u_warranty_end"],
  orderBy: "u_warranty_end",
})

UI Builder 页面和工作区

上面的章节通过后台脚本写入普通表。本节并非如此。

Next Experience 工作区是一组相互关联记录构成的图,而不是单条记录。单独插入一行—— new GlideRecord("sys_ux_page"),也就是本指南早期版本曾建议的做法——虽然会成功, 返回一个 sys_id,却会留下一个孤立记录:没有体验拥有该页面,没有应用配置将其注册,也没有路由 能访问它。这一行本身不会显示缺少了什么。单独插入 sys_ux_macroponent 也是同样的情况。

此服务中的两个工具会通过 Table API POST 一个 sys_ux_page 行,但都不会构建图中的其余部分。 snow_artifact_manage (action='create', type='uib_page') 会写入 name、title 和 description,然后就此停止。snow_github_deploy 会将 target_type: "uib_page" 和 "sys_ux_page" 都映射到同一张表,默认将 upsert 设为 true,并在找不到匹配项时插入新行 ——因此,拼错 target_identifier 也会产生同样的孤立记录,而且不会提示问题。两者都会报告成功。 对 UI Builder 页面使用 snow_artifact_manage 执行 get、find、export 和 verify,而不是 create;为 snow_github_deploy 提供 target_sys_id,或使用已从实例读取的标识符。

除此之外,workspace 和 ui-builder 域中的 sys_ux_page 工具只能读取和删除。唯一的例外是 snow_uib_page_manage (action='create'),它根本不会操作这张表——请参阅脚本的边界。

记录链
步骤表工具传递内容
1sys_ux_experiencesnow_create_ux_experienceexperience_sys_id
2sys_ux_app_configsnow_create_ux_app_configapp_config_sys_id
3sys_ux_macroponentsnow_create_ux_page_macroponentmacroponent_sys_id
4sys_ux_page_registrysnow_create_ux_page_registrysys_name
5sys_ux_app_routesnow_create_ux_app_routename
6sys_ux_app_config (更新)snow_update_ux_app_config_landing_page—

此顺序是强制的,并非建议。若无法读回步骤 1 创建的体验,步骤 2 就会拒绝执行。步骤 4 会在写入前 GET sys_ux_app_config 和 sys_ux_macroponent,如果其中任一项缺失,就会拒绝执行。步骤 5 通过 page_sys_name 关联步骤 4:根据步骤 5 自身的架构,该值是注册表的 sys_name 字符串,而不是其 sys_id。步骤 6 最容易被跳过:步骤 5 的响应会提示你运行它,在运行之前,应用配置没有 landing_page。

步骤 5 会返回两个看起来可以互换、实际却不能互换的键。它的 name 输入是路由名称,最终会成为 URL 路径段;route 是路径,默认值为 /<name>。步骤 6 需要的是 name: snow_update_ux_app_config_landing_page 接受 route_name,并将其直接 PATCH 到 landing_page。如果传入步骤 5 的 route,而路由名称是 home,那么你设置的着陆页就会是 /home。

步骤 1 只需要一个名称。除非你传入 path,否则它会根据名称生成 path(转为小写,并将空格替换为连字符);homepage 默认为 home。它还会自行查询 sys_ux_macroponent,查找 name=uib_app_shell^category=app_shell,并在找不到时回退到 name=x_snc_app_shell_uib_app_shell^category=app_shell,以解析应用外壳。如果实例中两条记录都不存在,任何内容写入前就会返回 Could not find appShellUI macroponent (uib_app_shell) on this instance。

Builder Toolkit 依赖项

步骤 1 并非通过 Table API 写入。它会 POST 到 /api/sn_uibtk_api/buildertoolkit/experience。这是一个特定作用域的端点,在任何可访问的 ServiceNow REST 文档中都找不到——作用域名称看起来像是 UI Builder 自带的工具包,但这只是推测,并非契约;不要指望它在每次 family 版本发布后仍然可用。步骤 2 到 6 则通过普通的 /api/now/table/... API 写入。

如果实例中没有 sn_uibtk_api 作用域,步骤 1 会失败,并返回 Builder Toolkit API not available - UI Builder plugin may not be installed,同时指出插件 com.snc.ui_builder_toolkit。这是工具正常工作时的表现,并不表示工具有问题。

陷阱在步骤 2。 在写入任何内容之前,snow_create_ux_app_config 会 GET /api/sn_uibtk_api/buildertoolkit/experience,以确认体验存在,并吞掉这次 GET 的所有错误。在没有该工具包的实例上,此检查会返回 404,工具则报告 Experience '<sys_id>' not found or not accessible。此消息中的 sys_id 没问题;缺少的是工具包。请先检查步骤 1 的结果和插件,再考虑重新复制 sys_id。

角色

MCP 包中的 sn-roles.manifest.json 记录了对一个运行中实例的 ACL 探测结果,整条链上的创建权限并不一致:

表创建需要以下角色之一
sys_ux_app_configdelegated_developer、ui_builder_admin
sys_ux_macroponentdelegated_developer、ui_builder_admin
sys_ux_page_registrycanvas_admin、maint、ui_builder_admin、uxframework_designer
sys_ux_app_routeui_builder_admin
sys_ux_data_brokerui_builder_admin
sys_ux_page_elementui_builder_admin、uxframework_designer

因此,delegated_developer 可以通过步骤 2 和 3,然后在步骤 4 失败,此时已经写入了三条记录。ui_builder_admin 可以完成整条链;清单显示 admin 在所有位置都足够。

ACL 拒绝写入时,失败提示比你预想的更不明显。此服务的 HTTP 客户端会将 ServiceNow 错误正文重新抛出为 ServiceNow: <message>,并在 message 存在时丢弃正文的 detail——而指出表和操作的通常正是这部分。如果某个步骤失败,而提示信息没有提供任何线索,请查询该表中的记录,而不是猜测。

此服务提供三种不同的链

它们彼此并不一致。请确认你调用的是哪一种。

  • 上述六个步骤。 Experience、应用配置、macroponent、页面注册表、路由、落地页。不包含列表。每条记录都可以查看和验证。
  • snow_create_complete_workspace。 Experience、应用配置,然后通过工具包的 /page 端点创建一个页面,接着——仅当你传入 tables 时——创建一个 sys_ux_list_menu_config、每个表对应一个 sys_ux_list_category,以及每个类别对应一个 sys_ux_list。它不会写入 sys_ux_page_registry 或 sys_ux_app_route 记录,因为工具包的 /page 调用应当在服务端生成屏幕和路由。是否确实如此,需要你自行检查;该工具不会报告。
  • snow_create_workspace(属于 ui-builder 域,而非 workspace)。Experience、应用配置、路由、列表——完全不创建页面,因此什么内容都不会显示在任何页面上。

如果工作区必须正确,请优先使用六个步骤,因为你可以检查每条记录。如果你本来就打算随后打开 UI Builder,则可以使用 snow_create_complete_workspace。不要使用 snow_create_workspace 来构建用户将要打开的内容。

这三种方式都不会从实例读取 URL。snow_create_ux_app_route 会打印 /now/experience<route>,snow_create_complete_workspace 会打印 /now/<path>,而 snow_create_workspace 会打印 <instance_url>/workspace/<path>——这些都是工具根据你传入的字符串拼接而成的。若要获取实例实际识别的路径,请调用 snow_discover_all_workspaces,它会从工具包读取 Experience 列表——但要检查其输出,而不只是看状态。工具包调用失败时,工具本身不会失败:它会将 {type: "UX Experience", error: "Builder Toolkit API not available"} 加入列表并返回成功。在没有工具包的实例上,这会得到一个绿色的成功结果,但其中没有任何 Experience。

还有一个路由写入工具,但它的名字会误导你。snow_create_uib_page_registry 属于 ui-builder 域,实际上完全不会操作 sys_ux_page_registry:它会向 sys_ux_app_route 发送 POST 请求,并带上 page 列,以及 roles、public 和 active。这与步骤 5 的结构不同,后者通过 app_config 和 page_sys_name 建立关联。两个工具、同一张表,却对路由如何找到页面有两种互不兼容的设想——不要在同一个工作区中混用,也不要因为它的名字而把它当作步骤 4 来用。

脚本的边界

这条链会为你创建五行数据,它们会像这些工具设计的那样相互关联。但这是否足以生成 UI Builder 能打开的页面,本仓库并未展示,现有可访问文档也无法确定——请实际打开体验进行验证,不要只看成功响应。可以确定的是,这条链不会生成任何包含内容的页面。

snow_create_ux_page_macroponent 会将你传入的 composition 对象以 JSON 字符串的形式写入该列;如果你不传任何内容,它会写入 {"layout":"single-column","components":[]}。这是该工具的占位默认值。本仓库并未证明它或任何手动编写的 composition 是 UI Builder 运行时可渲染的结构,本指南也不会凭空编造格式。

在 UI Builder 中布局页面。 在 All 菜单中搜索 UI Builder,打开体验,再打开页面。这是编辑组件树的受支持编辑器,并且会写入运行时所需的 composition。用脚本创建链;用 UI Builder 编写页面。

snow_uib_page_manage (action='add_element') 看起来像是绕过上述限制的方法,而且它对自己的操作说明得很清楚:它会将 page、component、position 和 JSON properties 字符串 POST 到 sys_ux_page_element,这是一张真实的表,也有真实的 ACL。尚未证实的是两者之间的关联。它的 page_id 是 sys_ux_page 的 sys_id,而这条六步链从未创建 sys_ux_page——因此,目前没有证据表明可以将链创建的页面与通过这种方式添加的元素关联起来。未先在 UI Builder 中打开结果之前,不要用它来构建布局。

snow_uib_page_manage (action='create') 是唯一可能补上缺失的 sys_ux_page 的候选工具。它接收 experience_id 和名称,并将请求 POST 到工具包的 /page 端点——snow_create_complete_workspace 也会调用同一端点——然后返回工具包提供的 macroponent、screen 和 route ID。也就是说,实际由服务创建页面;这就是它有可能同时创建真实页面和 route,而 Table API route 无法做到的原因。使用前请注意两点:它需要工具包,而且返回的 ID 来自工具包,因此在将 ID 传给 add_element 前,请确认确实存在一行 sys_ux_page。结论不变——在 UI Builder 中打开页面后,再在其上进行构建。

该服务通过工具包而非 Table API 编辑的唯一 macroponent 内部内容是客户端状态:snow_create_uib_client_state 会 GET /api/sn_uibtk_api/buildertoolkit/macroponent,向获取到的 state_properties 追加内容,然后 PATCH 回整个数组。它需要 macroponent sys_id。这个读取-修改-写入过程没有任何保护——没有版本检查,也没有乐观锁——所以两个 Agent 同时向同一页面添加状态,正是典型的更新丢失场景。这一后果是从代码中推断出来的,并非在实例上实测所得——每次只让一个调用方添加状态,就不必验证它是否会发生。

验证链条

snow_validate_workspace_configuration 在 workspace_type: "ux_experience" 时不会检查出问题的部分。它会评估四项内容:体验的 active 标志、是否有任何 sys_ux_app_config 的 experience_assoc 指向该体验、该配置是否有 list_config_id,以及是否有任何 sys_ux_page_property 行引用了它。它不会读取 sys_ux_macroponent、sys_ux_page_registry 或 sys_ux_app_route,因此没有页面和路由的工作区也可能返回 passed。

请自行检查最后两项:

// The page must be registered against the app config from step 2
await snow_query_table({
  table: "sys_ux_page_registry",
  query: "app_config=" + appConfigSysId,
  fields: ["sys_name", "macroponent", "active"],
})

// ...and a route must name that same sys_name
await snow_query_table({
  table: "sys_ux_app_route",
  query: "app_config=" + appConfigSysId,
  fields: ["name", "route", "page_sys_name", "active"],
})

两项都应返回一行,并且路由的 page_sys_name 应与注册表的 sys_name 相等。

对待这项检查时,也要像看待下文的数据代理字段那样保持怀疑。sys_ux_page_registry.app_config、sys_ux_page_registry.sys_name 和 sys_ux_app_route.page_sys_name 是第 4 步和第 5 步执行器写入的列;本仓库中的任何内容都无法确认这些列与 ServiceNow 自身的文档一致,而且在可访问的文档中找不到 sys_ux_* 表。因此,空结果有两种解释——行不存在,或者列名错误——而 sysparm_query 不会告诉你是哪一种,因为编码查询中的无法识别字段不会引发错误。在相信空结果之前,请使用 sys_created_onONToday@javascript:gs.beginningOfToday()@javascript:gs.endOfToday() 查询同一张表,不指定 fields 列表,并读取链刚刚写入的原始行。如果该行存在,但列名不同,那么链运行成功了,只是这份操作说明已经过时。

不要使用 snow_uib_discover 来做这项检查。它的 pages 操作会读取链从未创建过的 sys_ux_page,而它的 routes 操作会读取按 experience=<sys_id> 筛选的 sys_ux_page_registry——但链是通过 app_config 关联注册表的。这两个操作都不会按六个步骤写入的任何内容进行筛选,因此无论它们返回什么,都无法说明你的工作区情况。使用它查看在 UI Builder 中创建的页面,并使用 snow_discover_all_workspaces 枚举体验。

使用 snow_query_table 查找宏组件时,有一个细节很重要:truncate_output 默认为 true,会将内容截断至 200 个字符,并附加 ... [truncated, N chars total]。它会根据字段名决定截断哪些字符串。如果字段名包含某个大内容子串——script、description、content、body、json、html、xml、template、comments 等约十个子串——超过 200 个字符的部分都会被截断。其他字段会先保留至 400 个字符。composition 不在此列表中,因此会保留至 400 个字符,而实际内容远长于此;short_description 包含 description,因此会在 200 个字符处截断。比较值时请传入 truncate_output: false,否则你比较的只是两个截断后的内容,并得出它们相同的结论。

数据资源(数据代理)

边界相同,规则也相同。不要从 sys_ux_data_broker 上的后台脚本创建数据代理。本指南早期版本在此处使用的字段集——type: "script" 加上 script 列——并未得到本仓库中任何内容的证实。

两个工具会写入此表:

  • snow_create_uib_data_broker 会 POST page、name、table、query 和 limit,以及你传入的 fields(以逗号连接)和 order_by。
  • snow_configure_uib_data_broker 会使用 query、refresh_interval、enable_caching、cache_duration、parameters 和 filters 中的任意字段 PATCH 现有行——最后两个字段会转换为 JSON 字符串。

这些列名是此服务对 sys_ux_data_broker 的假设,并非本仓库经过真实数据代理验证的结果。不要把成功响应当作字段已写入的证明:使用 snow_query_table 直接读回该行,并与同一实例中通过 UI Builder 创建的数据代理进行比较。如果两者结构不同,请在 UI Builder 中创建,仅使用这些工具读取。

数据代理脚本正文和 GraphQL 数据代理查询结构属于 ui-builder-patterns 指南。与本文件中的其他内容一样,它们在服务端运行,并使用 ES5。

这里唯一会遍历关联对象的操作是删除:snow_uib_page_manage (action='delete') 默认会先删除 page 指向该页面的 sys_ux_page_element、sys_ux_data_broker、sys_ux_client_script 和 sys_ux_app_route 行,然后再删除页面本身。传入 delete_dependencies: false 会将这些行全部保留——只会在你想清理的表中留下更多孤立记录。

应用部署(ES5)

创建更新集
// Create update set for app deployment (ES5 ONLY!)
function createAppUpdateSet(appName, description) {
  var updateSet = new GlideRecord("sys_update_set")
  updateSet.initialize()
  updateSet.setValue("name", appName + " - " + new GlideDateTime().getDate())
  updateSet.setValue("description", description)
  updateSet.setValue("application", getAppSysId(appName))
  updateSet.setValue("state", "in progress")
  return updateSet.insert()
}
导出应用
// Prepare app for export (ES5 ONLY!)
function prepareAppExport(appScope) {
  // Validate all components
  var issues = []

  // Check for missing dependencies
  var dependency = new GlideRecord("sys_app_dependency")
  dependency.addQuery("app.scope", appScope)
  dependency.query()

  while (dependency.next()) {
    if (!isDependencyInstalled(dependency.getValue("dependency"))) {
      issues.push("Missing dependency: " + dependency.dependency.getDisplayValue())
    }
  }

  // Validate update sets
  var updateSet = new GlideRecord("sys_update_set")
  updateSet.addQuery("application.scope", appScope)
  updateSet.addQuery("state", "in progress")
  updateSet.query()

  while (updateSet.next()) {
    issues.push("Open update set: " + updateSet.getValue("name"))
  }

  return {
    ready: issues.length === 0,
    issues: issues,
  }
}

MCP 工具集成

此文件 frontmatter 中的 tools: 列表就是完整集合。这里有意不再列出第二份清单:原本放在此处的摘要表在加入 UI Builder 链路后立刻过时了;与只保留一份清单相比,这种情况更糟。

示例工作流
// 1. Query applications
await snow_query_table({
  table: "sys_scope",
  query: "scopeSTARTSWITHx_",
  fields: "name,scope,version,vendor",
})

// 2. Find app tables
await snow_query_table({
  table: "sys_db_object",
  query: "nameSTARTSWITHx_myco",
  fields: "name,label,super_class",
})

// 3. Get workspace configs
await snow_query_table({
  table: "sys_aw_workspace",
  query: "sys_scope.scopeSTARTSWITHx_",
  fields: "name,title,primary_table,url",
})

最佳实践

  1. 命名约定 - 使用一致的前缀
  2. 作用域应用 - 使用作用域隔离
  3. 可复用组件 - 采用模块化设计
  4. 数据代理 - 高效获取数据
  5. 工作区设计 - 以用户为中心的布局
  6. 测试 - 为应用编写 ATF 测试
  7. 文档 - 编写应用文档
  8. 仅使用 ES5 - 不使用现代 JavaScript 语法

评论

支持 Markdown