snow-flow 10.0.197 → 10.0.199

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.197",
3
+ "version": "10.0.199",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -1,25 +1,37 @@
1
1
  /**
2
2
  * snow_order_catalog_item - Order catalog item
3
3
  *
4
- * Orders a catalog item programmatically, creating a request (RITM) with specified variable values.
4
+ * Orders a catalog item programmatically, creating a request (RITM) with
5
+ * specified variable values.
6
+ *
7
+ * Operation order (avoids race condition with flows):
8
+ * 1. Fetch variable definitions (item_option_new)
9
+ * 2. Create sc_request
10
+ * 3. Pre-create sc_item_option records in parallel (no RITM needed yet)
11
+ * 4. Short delay to ensure DB commit
12
+ * 5. Create sc_req_item (RITM) — flow triggers here with variables ready
13
+ * 6. Create sc_item_option_mtom links in parallel
14
+ * 7. Two PATCHs to trigger Business Rules that associate the flow context
15
+ *
16
+ * Based on contribution by @frodoyoraul (PR #83).
5
17
  */
6
18
 
7
19
  import { MCPToolDefinition, ServiceNowContext, ToolResult } from "../../shared/types.js"
8
20
  import { getAuthenticatedClient } from "../../shared/auth.js"
9
21
  import { createSuccessResult, createErrorResult } from "../../shared/error-handler.js"
10
22
 
23
+ // Delay between sc_item_option creation and RITM insert to ensure DB commit
24
+ const VARIABLE_COMMIT_DELAY_MS = 1000
25
+
11
26
  export const toolDefinition: MCPToolDefinition = {
12
27
  name: "snow_order_catalog_item",
13
28
  description: "Orders a catalog item programmatically, creating a request (RITM) with specified variable values.",
14
- // Metadata for tool discovery (not sent to LLM)
15
29
  category: "itsm",
16
30
  subcategory: "service-catalog",
17
31
  use_cases: ["ordering", "automation", "ritm"],
18
32
  complexity: "intermediate",
19
33
  frequency: "high",
20
34
 
21
- // Permission enforcement
22
- // Classification: WRITE - Write operation based on name pattern
23
35
  permission: "write",
24
36
  allowedRoles: ["developer", "admin"],
25
37
  inputSchema: {
@@ -36,46 +48,89 @@ export const toolDefinition: MCPToolDefinition = {
36
48
  },
37
49
  }
38
50
 
51
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
52
+
39
53
  export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
40
54
  const { cat_item, requested_for, variables, quantity = 1, delivery_address, special_instructions } = args
41
55
 
42
56
  try {
43
57
  const client = await getAuthenticatedClient(context)
44
58
 
45
- // Create service catalog request
59
+ // 1. Fetch variable definitions before creating the RITM
60
+ const varDefsResponse = await client.get("/api/now/table/item_option_new", {
61
+ params: {
62
+ sysparm_query: "cat_item=" + cat_item,
63
+ sysparm_fields: "sys_id,name",
64
+ sysparm_limit: 100,
65
+ },
66
+ })
67
+ const varDefs = varDefsResponse.data.result || []
68
+ const varMap = new Map<string, string>()
69
+ varDefs.forEach((def: any) => {
70
+ if (def.name) varMap.set(def.name, def.sys_id)
71
+ })
72
+
73
+ // 2. Create sc_request
46
74
  const requestData: any = {
47
75
  requested_for,
48
76
  opened_by: requested_for,
49
77
  }
50
-
51
78
  if (special_instructions) requestData.special_instructions = special_instructions
52
79
 
53
80
  const requestResponse = await client.post("/api/now/table/sc_request", requestData)
54
81
  const requestId = requestResponse.data.result.sys_id
55
82
 
56
- // Create requested item (RITM)
83
+ // 3. Pre-create sc_item_option records in parallel (no RITM needed yet)
84
+ const varEntries = variables ? Object.entries(variables).filter(([name]) => varMap.has(name)) : []
85
+ const skippedVars = variables ? Object.keys(variables).filter((name) => !varMap.has(name)) : []
86
+
87
+ const optionSysIds: Array<{ optionSysId: string }> = await Promise.all(
88
+ varEntries.map(async ([varName, varValue]) => {
89
+ const defSysId = varMap.get(varName)!
90
+ const optResp = await client.post("/api/now/table/sc_item_option", {
91
+ item_option_new: defSysId,
92
+ value: varValue,
93
+ })
94
+ return { optionSysId: optResp.data.result.sys_id }
95
+ }),
96
+ )
97
+
98
+ // 4. Wait for sc_item_option records to be committed in DB
99
+ if (optionSysIds.length > 0) {
100
+ await sleep(VARIABLE_COMMIT_DELAY_MS)
101
+ }
102
+
103
+ // 5. Create sc_req_item (RITM) — flow triggers here with variables already ready
57
104
  const ritmData: any = {
58
105
  request: requestId,
59
106
  cat_item,
60
107
  requested_for,
61
108
  quantity,
62
109
  }
63
-
64
110
  if (delivery_address) ritmData.delivery_address = delivery_address
65
111
 
66
112
  const ritmResponse = await client.post("/api/now/table/sc_req_item", ritmData)
67
113
  const ritmId = ritmResponse.data.result.sys_id
68
114
  const ritmNumber = ritmResponse.data.result.number
69
115
 
70
- // Set variable values if provided
71
- if (variables) {
72
- for (const [varName, varValue] of Object.entries(variables)) {
73
- await client.post("/api/now/table/sc_item_option_mtom", {
116
+ // 6. Create sc_item_option_mtom links in parallel
117
+ await Promise.all(
118
+ optionSysIds.map(({ optionSysId }) =>
119
+ client.post("/api/now/table/sc_item_option_mtom", {
74
120
  request_item: ritmId,
75
- name: varName,
76
- value: varValue,
77
- })
78
- }
121
+ sc_item_option: optionSysId,
122
+ }),
123
+ ),
124
+ )
125
+
126
+ // 7. Two PATCHs to trigger Business Rules that associate the flow context.
127
+ // sys_mod_count is auto-incremented by ServiceNow (the value we send is
128
+ // ignored), but the PATCH itself is a real update that fires BR triggers.
129
+ // Two updates are needed: the first associates the flow, the second
130
+ // ensures the flow context is fully propagated.
131
+ if (optionSysIds.length > 0) {
132
+ await client.patch("/api/now/table/sc_req_item/" + ritmId, { sys_mod_count: "1" })
133
+ await client.patch("/api/now/table/sc_req_item/" + ritmId, { sys_mod_count: "1" })
79
134
  }
80
135
 
81
136
  return createSuccessResult(
@@ -85,6 +140,8 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
85
140
  ritm_id: ritmId,
86
141
  ritm_number: ritmNumber,
87
142
  quantity,
143
+ variables_processed: varEntries.length,
144
+ variables_skipped: skippedVars.length > 0 ? skippedVars : undefined,
88
145
  },
89
146
  {
90
147
  operation: "order_catalog_item",
@@ -97,5 +154,5 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
97
154
  }
98
155
  }
99
156
 
100
- export const version = "1.0.0"
101
- export const author = "Snow-Flow SDK Migration"
157
+ export const version = "2.0.0"
158
+ export const author = "Snow-Flow"
@@ -8,6 +8,7 @@
8
8
  import { MCPToolDefinition, ServiceNowContext, ToolResult } from "../../shared/types.js"
9
9
  import { getAuthenticatedClient } from "../../shared/auth.js"
10
10
  import { createSuccessResult, createErrorResult } from "../../shared/error-handler.js"
11
+ import { linkUiPolicyReference, verifyUiPolicyLink } from "../ui-policies/link-ui-policy.js"
11
12
 
12
13
  /**
13
14
  * Extract sys_id from API response - handles both string and object formats
@@ -129,6 +130,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
129
130
 
130
131
  // Create UI Policy Actions
131
132
  const createdActions = []
133
+ const unlinkableActions: string[] = []
132
134
  for (const action of actions) {
133
135
  const actionData: any = {
134
136
  ui_policy: policySysId,
@@ -140,10 +142,20 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
140
142
  }
141
143
 
142
144
  const actionResponse = await client.post("/api/now/table/sys_ui_policy_action", actionData)
143
- createdActions.push(actionResponse.data.result)
145
+ const actionResult = actionResponse.data.result
146
+ const actionSysId = extractSysId(actionResult.sys_id)
147
+
148
+ // Verify ui_policy link; if POST didn't set it, use fallback chain
149
+ const alreadyLinked = await verifyUiPolicyLink(client, actionSysId, policySysId)
150
+ if (!alreadyLinked) {
151
+ const linked = await linkUiPolicyReference(client, actionSysId, policySysId)
152
+ if (!linked) unlinkableActions.push(action.field_name)
153
+ }
154
+
155
+ createdActions.push(actionResult)
144
156
  }
145
157
 
146
- return createSuccessResult({
158
+ const result: Record<string, any> = {
147
159
  created: true,
148
160
  ui_policy: {
149
161
  sys_id: policySysId,
@@ -161,7 +173,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
161
173
  mandatory: action.mandatory === "true",
162
174
  readonly: action.readonly === "true",
163
175
  cleared: action.cleared === "true",
164
- ui_policy_reference: policySysId,
176
+ ui_policy_linked: !unlinkableActions.includes(action.field),
165
177
  })),
166
178
  total_actions: createdActions.length,
167
179
  best_practices: [
@@ -171,7 +183,16 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
171
183
  "Order matters when multiple policies affect same field",
172
184
  "Test with different user roles and data",
173
185
  ],
174
- })
186
+ }
187
+
188
+ if (unlinkableActions.length > 0) {
189
+ result.warning =
190
+ "Some actions could not be linked to the UI policy: " +
191
+ unlinkableActions.join(", ") +
192
+ ". Link them manually in the ServiceNow UI."
193
+ }
194
+
195
+ return createSuccessResult(result)
175
196
  } catch (error: any) {
176
197
  return createErrorResult(error.message)
177
198
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Shared helper to link a sys_ui_policy_action record to its parent UI policy.
3
+ *
4
+ * ServiceNow's REST Table API POST silently ignores the "ui_policy" reference
5
+ * field on sys_ui_policy_action during creation. This module provides a
6
+ * cascading fallback (PATCH → PUT → GlideRecord) to set the reference after
7
+ * the record has been created.
8
+ */
9
+
10
+ import type { AxiosInstance } from "axios"
11
+
12
+ /**
13
+ * Verify that the ui_policy reference field is set on a sys_ui_policy_action record.
14
+ */
15
+ export async function verifyUiPolicyLink(
16
+ client: AxiosInstance,
17
+ actionSysId: string,
18
+ expectedSysId: string,
19
+ ): Promise<boolean> {
20
+ try {
21
+ const res = await client.get(
22
+ "/api/now/table/sys_ui_policy_action/" + actionSysId + "?sysparm_fields=ui_policy",
23
+ )
24
+ const ref = res.data.result?.ui_policy
25
+ const val = typeof ref === "object" && ref !== null ? ref.value : ref
26
+ return !!val && val !== "" && val === expectedSysId
27
+ } catch {
28
+ return false
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Attempt to link a sys_ui_policy_action record to its parent UI policy using
34
+ * a cascading fallback chain:
35
+ * Tier 1 — PATCH (standard Table API update)
36
+ * Tier 2 — PUT (full record update)
37
+ * Tier 3 — GlideRecord via Scripted REST endpoint (last resort)
38
+ *
39
+ * Returns true if the link was successfully set and verified.
40
+ */
41
+ export async function linkUiPolicyReference(
42
+ client: AxiosInstance,
43
+ actionSysId: string,
44
+ uiPolicySysId: string,
45
+ ): Promise<boolean> {
46
+ const url = "/api/now/table/sys_ui_policy_action/" + actionSysId
47
+ const body = { ui_policy: uiPolicySysId }
48
+
49
+ // Tier 1: PATCH
50
+ try {
51
+ await client.patch(url, body)
52
+ if (await verifyUiPolicyLink(client, actionSysId, uiPolicySysId)) return true
53
+ } catch {
54
+ // fall through
55
+ }
56
+
57
+ // Tier 2: PUT
58
+ try {
59
+ await client.put(url, body)
60
+ if (await verifyUiPolicyLink(client, actionSysId, uiPolicySysId)) return true
61
+ } catch {
62
+ // fall through
63
+ }
64
+
65
+ // Tier 3: GlideRecord via Scripted REST endpoint
66
+ try {
67
+ const script =
68
+ "var gr = new GlideRecord('sys_ui_policy_action');\n" +
69
+ "gr.get('" + actionSysId + "');\n" +
70
+ "gr.setValue('ui_policy', '" + uiPolicySysId + "');\n" +
71
+ "gr.update();\n" +
72
+ "gs.print(gr.getValue('ui_policy'));"
73
+
74
+ await client.post("/api/snow_flow_exec/execute", {
75
+ script,
76
+ execution_id: "link_ui_policy_" + actionSysId,
77
+ })
78
+ if (await verifyUiPolicyLink(client, actionSysId, uiPolicySysId)) return true
79
+ } catch {
80
+ // all tiers exhausted
81
+ }
82
+
83
+ return false
84
+ }
@@ -10,18 +10,15 @@
10
10
  * - "disabled" is the actual column name for "Read only" in the UI
11
11
  * - "table" is auto-derived from the parent UI policy but must be sent for field validation
12
12
  *
13
- * Platform limitation:
14
- * The "ui_policy" reference field on sys_ui_policy_action cannot be set via
15
- * the REST Table API (POST/PUT/PATCH all silently ignore it). This is a known
16
- * ServiceNow platform restriction for parent-child reference fields.
17
- * Workaround: after Table API creation, a direct XML POST is attempted to set
18
- * the ui_policy reference. If the XML POST also fails, the action is created
19
- * without the reference — it can be linked manually in the UI.
13
+ * The "ui_policy" reference field is set via POST during creation. If the
14
+ * platform silently ignores it, a cascading fallback (PATCH → PUT → GlideRecord)
15
+ * ensures the link is established.
20
16
  */
21
17
 
22
18
  import type { MCPToolDefinition, ServiceNowContext, ToolResult } from "../../shared/types.js"
23
19
  import { getAuthenticatedClient } from "../../shared/auth.js"
24
20
  import { createSuccessResult, createErrorResult } from "../../shared/error-handler.js"
21
+ import { linkUiPolicyReference, verifyUiPolicyLink } from "./link-ui-policy.js"
25
22
 
26
23
  export const toolDefinition: MCPToolDefinition = {
27
24
  name: "snow_create_ui_policy_action",
@@ -113,24 +110,9 @@ export async function execute(args: Record<string, unknown>, context: ServiceNow
113
110
  const action = response.data.result
114
111
  const actionSysId = action.sys_id?.value || action.sys_id
115
112
 
116
- const linked = await (async () => {
117
- try {
118
- const xmlBody =
119
- "<record>" + "<sys_id>" + actionSysId + "</sys_id>" + "<ui_policy>" + uid + "</ui_policy>" + "</record>"
120
- await client.post("/sys_ui_policy_action.do?XML&sys_id=" + actionSysId, xmlBody, {
121
- headers: { "Content-Type": "application/xml" },
122
- })
123
-
124
- const verify = await client.get(
125
- "/api/now/table/sys_ui_policy_action/" + actionSysId + "?sysparm_fields=ui_policy",
126
- )
127
- const ref = verify.data.result?.ui_policy
128
- const val = typeof ref === "object" && ref !== null ? ref.value : ref
129
- return !!val && val !== ""
130
- } catch (_e) {
131
- return false
132
- }
133
- })()
113
+ // Check if the POST already set the reference; if not, use the fallback chain
114
+ const alreadyLinked = await verifyUiPolicyLink(client, actionSysId, uid)
115
+ const linked = alreadyLinked || (await linkUiPolicyReference(client, actionSysId, uid))
134
116
 
135
117
  const refVal = action.ui_policy
136
118
  const resolvedRef = typeof refVal === "object" && refVal !== null ? refVal.value : refVal