snow-flow 10.0.1-dev.388 → 10.0.1-dev.390
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
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* snow_manage_flow - Complete Flow Designer lifecycle management
|
|
3
3
|
*
|
|
4
4
|
* Create, list, get, update, activate, deactivate, delete and publish
|
|
5
|
-
* Flow Designer flows and subflows
|
|
5
|
+
* Flow Designer flows and subflows.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* For create/create_subflow: uses a bootstrapped Scripted REST API
|
|
8
|
+
* ("Flow Factory") that runs GlideRecord server-side, ensuring
|
|
9
|
+
* sys_hub_flow_version records are created and all Business Rules fire.
|
|
10
|
+
* Falls back to Table API if the factory is unavailable.
|
|
11
|
+
*
|
|
12
|
+
* All other actions (list, get, update, activate, etc.) use the Table API.
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js';
|
|
@@ -28,6 +31,306 @@ function isSysId(value: string): boolean {
|
|
|
28
31
|
return /^[a-f0-9]{32}$/.test(value);
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
// ── Flow Factory (Scripted REST API bootstrap) ──────────────────────
|
|
35
|
+
|
|
36
|
+
var FLOW_FACTORY_API_NAME = 'Snow-Flow Flow Factory';
|
|
37
|
+
var FLOW_FACTORY_API_ID = 'flow_factory';
|
|
38
|
+
var FLOW_FACTORY_NAMESPACE = 'x_snflw';
|
|
39
|
+
var FLOW_FACTORY_CACHE_TTL = 300000; // 5 minutes
|
|
40
|
+
|
|
41
|
+
var _flowFactoryCache: { apiSysId: string; namespace: string; timestamp: number } | null = null;
|
|
42
|
+
var _bootstrapPromise: Promise<{ namespace: string; apiSysId: string }> | null = null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* ES5 GlideRecord script deployed as a Scripted REST API resource.
|
|
46
|
+
* This runs server-side on ServiceNow and triggers all Business Rules,
|
|
47
|
+
* unlike direct Table API inserts which skip sys_hub_flow_version creation.
|
|
48
|
+
*/
|
|
49
|
+
var FLOW_FACTORY_SCRIPT = [
|
|
50
|
+
'(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {',
|
|
51
|
+
' var body = request.body.data;',
|
|
52
|
+
' var result = { success: false, steps: {} };',
|
|
53
|
+
'',
|
|
54
|
+
' try {',
|
|
55
|
+
' var flowName = body.name || "Unnamed Flow";',
|
|
56
|
+
' var isSubflow = body.type === "subflow";',
|
|
57
|
+
' var flowDesc = body.description || flowName;',
|
|
58
|
+
' var flowCategory = body.category || "custom";',
|
|
59
|
+
' var runAs = body.run_as || "user";',
|
|
60
|
+
' var shouldActivate = body.activate !== false;',
|
|
61
|
+
' var triggerType = body.trigger_type || "manual";',
|
|
62
|
+
' var triggerTable = body.trigger_table || "";',
|
|
63
|
+
' var triggerCondition = body.trigger_condition || "";',
|
|
64
|
+
' var activities = body.activities || [];',
|
|
65
|
+
' var inputs = body.inputs || [];',
|
|
66
|
+
' var outputs = body.outputs || [];',
|
|
67
|
+
'',
|
|
68
|
+
' // Step 1: Create sys_hub_flow via GlideRecord (triggers all BRs)',
|
|
69
|
+
' var flow = new GlideRecord("sys_hub_flow");',
|
|
70
|
+
' flow.initialize();',
|
|
71
|
+
' flow.setValue("name", flowName);',
|
|
72
|
+
' flow.setValue("description", flowDesc);',
|
|
73
|
+
' flow.setValue("internal_name", flowName.toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, ""));',
|
|
74
|
+
' flow.setValue("category", flowCategory);',
|
|
75
|
+
' flow.setValue("run_as", runAs);',
|
|
76
|
+
' flow.setValue("active", shouldActivate);',
|
|
77
|
+
' flow.setValue("status", shouldActivate ? "published" : "draft");',
|
|
78
|
+
' flow.setValue("validated", true);',
|
|
79
|
+
' flow.setValue("type", isSubflow ? "subflow" : "flow");',
|
|
80
|
+
'',
|
|
81
|
+
' if (body.flow_definition) {',
|
|
82
|
+
' flow.setValue("flow_definition", typeof body.flow_definition === "string" ? body.flow_definition : JSON.stringify(body.flow_definition));',
|
|
83
|
+
' flow.setValue("latest_snapshot", typeof body.flow_definition === "string" ? body.flow_definition : JSON.stringify(body.flow_definition));',
|
|
84
|
+
' }',
|
|
85
|
+
'',
|
|
86
|
+
' var flowSysId = flow.insert();',
|
|
87
|
+
' if (!flowSysId) {',
|
|
88
|
+
' result.error = "Failed to insert sys_hub_flow record";',
|
|
89
|
+
' response.setStatus(500);',
|
|
90
|
+
' response.setBody(result);',
|
|
91
|
+
' return;',
|
|
92
|
+
' }',
|
|
93
|
+
' result.steps.flow = { success: true, sys_id: flowSysId + "" };',
|
|
94
|
+
'',
|
|
95
|
+
' // Step 2: Create sys_hub_flow_version (this is what Table API misses!)',
|
|
96
|
+
' try {',
|
|
97
|
+
' var ver = new GlideRecord("sys_hub_flow_version");',
|
|
98
|
+
' ver.initialize();',
|
|
99
|
+
' ver.setValue("flow", flowSysId);',
|
|
100
|
+
' ver.setValue("name", "1.0");',
|
|
101
|
+
' ver.setValue("version", "1.0");',
|
|
102
|
+
' ver.setValue("state", shouldActivate ? "published" : "draft");',
|
|
103
|
+
' ver.setValue("active", true);',
|
|
104
|
+
' if (body.flow_definition) {',
|
|
105
|
+
' ver.setValue("flow_definition", typeof body.flow_definition === "string" ? body.flow_definition : JSON.stringify(body.flow_definition));',
|
|
106
|
+
' }',
|
|
107
|
+
' var verSysId = ver.insert();',
|
|
108
|
+
' if (verSysId) {',
|
|
109
|
+
' result.steps.version = { success: true, sys_id: verSysId + "" };',
|
|
110
|
+
' // Update flow to point to latest version',
|
|
111
|
+
' var flowUpd = new GlideRecord("sys_hub_flow");',
|
|
112
|
+
' if (flowUpd.get(flowSysId)) {',
|
|
113
|
+
' flowUpd.setValue("latest_version", verSysId);',
|
|
114
|
+
' flowUpd.update();',
|
|
115
|
+
' }',
|
|
116
|
+
' } else {',
|
|
117
|
+
' result.steps.version = { success: false, error: "Insert returned empty sys_id" };',
|
|
118
|
+
' }',
|
|
119
|
+
' } catch (verErr) {',
|
|
120
|
+
' result.steps.version = { success: false, error: verErr.getMessage ? verErr.getMessage() : verErr + "" };',
|
|
121
|
+
' }',
|
|
122
|
+
'',
|
|
123
|
+
' // Step 3: Create trigger instance (non-manual, non-subflow only)',
|
|
124
|
+
' if (!isSubflow && triggerType !== "manual") {',
|
|
125
|
+
' try {',
|
|
126
|
+
' var triggerMap = {',
|
|
127
|
+
' "record_created": "sn_fd.trigger.record_created",',
|
|
128
|
+
' "record_updated": "sn_fd.trigger.record_updated",',
|
|
129
|
+
' "scheduled": "sn_fd.trigger.scheduled"',
|
|
130
|
+
' };',
|
|
131
|
+
' var triggerIntName = triggerMap[triggerType] || "";',
|
|
132
|
+
' if (triggerIntName) {',
|
|
133
|
+
' var trigDef = new GlideRecord("sys_hub_action_type_definition");',
|
|
134
|
+
' trigDef.addQuery("internal_name", triggerIntName);',
|
|
135
|
+
' trigDef.query();',
|
|
136
|
+
' if (trigDef.next()) {',
|
|
137
|
+
' var trigInst = new GlideRecord("sys_hub_trigger_instance");',
|
|
138
|
+
' trigInst.initialize();',
|
|
139
|
+
' trigInst.setValue("flow", flowSysId);',
|
|
140
|
+
' trigInst.setValue("action_type", trigDef.getUniqueValue());',
|
|
141
|
+
' trigInst.setValue("name", triggerType);',
|
|
142
|
+
' trigInst.setValue("order", 0);',
|
|
143
|
+
' trigInst.setValue("active", true);',
|
|
144
|
+
' if (triggerTable) trigInst.setValue("table", triggerTable);',
|
|
145
|
+
' if (triggerCondition) trigInst.setValue("condition", triggerCondition);',
|
|
146
|
+
' var trigSysId = trigInst.insert();',
|
|
147
|
+
' result.steps.trigger = { success: !!trigSysId, sys_id: trigSysId + "" };',
|
|
148
|
+
' } else {',
|
|
149
|
+
' result.steps.trigger = { success: false, error: "Trigger type definition not found: " + triggerIntName };',
|
|
150
|
+
' }',
|
|
151
|
+
' }',
|
|
152
|
+
' } catch (trigErr) {',
|
|
153
|
+
' result.steps.trigger = { success: false, error: trigErr.getMessage ? trigErr.getMessage() : trigErr + "" };',
|
|
154
|
+
' }',
|
|
155
|
+
' }',
|
|
156
|
+
'',
|
|
157
|
+
' // Step 4: Create action instances',
|
|
158
|
+
' var actionsCreated = 0;',
|
|
159
|
+
' for (var ai = 0; ai < activities.length; ai++) {',
|
|
160
|
+
' try {',
|
|
161
|
+
' var act = activities[ai];',
|
|
162
|
+
' var actTypeName = act.type || "script";',
|
|
163
|
+
' var actDef = new GlideRecord("sys_hub_action_type_definition");',
|
|
164
|
+
' actDef.addQuery("internal_name", "CONTAINS", actTypeName);',
|
|
165
|
+
' actDef.addOrCondition("name", "CONTAINS", actTypeName);',
|
|
166
|
+
' actDef.query();',
|
|
167
|
+
' var actInst = new GlideRecord("sys_hub_action_instance");',
|
|
168
|
+
' actInst.initialize();',
|
|
169
|
+
' actInst.setValue("flow", flowSysId);',
|
|
170
|
+
' actInst.setValue("name", act.name || ("Action " + (ai + 1)));',
|
|
171
|
+
' actInst.setValue("order", (ai + 1) * 100);',
|
|
172
|
+
' actInst.setValue("active", true);',
|
|
173
|
+
' if (actDef.next()) {',
|
|
174
|
+
' actInst.setValue("action_type", actDef.getUniqueValue());',
|
|
175
|
+
' }',
|
|
176
|
+
' if (actInst.insert()) actionsCreated++;',
|
|
177
|
+
' } catch (actErr) {',
|
|
178
|
+
' // Best-effort per action',
|
|
179
|
+
' }',
|
|
180
|
+
' }',
|
|
181
|
+
' result.steps.actions = { success: true, created: actionsCreated, requested: activities.length };',
|
|
182
|
+
'',
|
|
183
|
+
' // Step 5: Create flow variables (subflows)',
|
|
184
|
+
' var varsCreated = 0;',
|
|
185
|
+
' if (isSubflow) {',
|
|
186
|
+
' for (var vi = 0; vi < inputs.length; vi++) {',
|
|
187
|
+
' try {',
|
|
188
|
+
' var inp = inputs[vi];',
|
|
189
|
+
' var fv = new GlideRecord("sys_hub_flow_variable");',
|
|
190
|
+
' fv.initialize();',
|
|
191
|
+
' fv.setValue("flow", flowSysId);',
|
|
192
|
+
' fv.setValue("name", inp.name);',
|
|
193
|
+
' fv.setValue("label", inp.label || inp.name);',
|
|
194
|
+
' fv.setValue("type", inp.type || "string");',
|
|
195
|
+
' fv.setValue("mandatory", inp.mandatory || false);',
|
|
196
|
+
' fv.setValue("default_value", inp.default_value || "");',
|
|
197
|
+
' fv.setValue("variable_type", "input");',
|
|
198
|
+
' if (fv.insert()) varsCreated++;',
|
|
199
|
+
' } catch (vErr) { /* best-effort */ }',
|
|
200
|
+
' }',
|
|
201
|
+
' for (var vo = 0; vo < outputs.length; vo++) {',
|
|
202
|
+
' try {',
|
|
203
|
+
' var out = outputs[vo];',
|
|
204
|
+
' var ov = new GlideRecord("sys_hub_flow_variable");',
|
|
205
|
+
' ov.initialize();',
|
|
206
|
+
' ov.setValue("flow", flowSysId);',
|
|
207
|
+
' ov.setValue("name", out.name);',
|
|
208
|
+
' ov.setValue("label", out.label || out.name);',
|
|
209
|
+
' ov.setValue("type", out.type || "string");',
|
|
210
|
+
' ov.setValue("variable_type", "output");',
|
|
211
|
+
' if (ov.insert()) varsCreated++;',
|
|
212
|
+
' } catch (vErr) { /* best-effort */ }',
|
|
213
|
+
' }',
|
|
214
|
+
' }',
|
|
215
|
+
' result.steps.variables = { success: true, created: varsCreated };',
|
|
216
|
+
'',
|
|
217
|
+
' result.success = true;',
|
|
218
|
+
' result.flow_sys_id = flowSysId + "";',
|
|
219
|
+
' result.version_created = !!(result.steps.version && result.steps.version.success);',
|
|
220
|
+
' response.setStatus(201);',
|
|
221
|
+
'',
|
|
222
|
+
' } catch (e) {',
|
|
223
|
+
' result.success = false;',
|
|
224
|
+
' result.error = e.getMessage ? e.getMessage() : e + "";',
|
|
225
|
+
' response.setStatus(500);',
|
|
226
|
+
' }',
|
|
227
|
+
'',
|
|
228
|
+
' response.setBody(result);',
|
|
229
|
+
'})(request, response);'
|
|
230
|
+
].join('\n');
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Ensure the Flow Factory Scripted REST API exists on the ServiceNow instance.
|
|
234
|
+
* Idempotent — checks cache first, then instance, deploys only if missing.
|
|
235
|
+
* Uses a concurrency lock to prevent duplicate bootstrap calls.
|
|
236
|
+
*/
|
|
237
|
+
async function ensureFlowFactoryAPI(client: any): Promise<{ namespace: string; apiSysId: string }> {
|
|
238
|
+
// 1. Check in-memory cache
|
|
239
|
+
if (_flowFactoryCache && (Date.now() - _flowFactoryCache.timestamp) < FLOW_FACTORY_CACHE_TTL) {
|
|
240
|
+
return { namespace: _flowFactoryCache.namespace, apiSysId: _flowFactoryCache.apiSysId };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 2. Concurrency lock — reuse in-flight bootstrap
|
|
244
|
+
if (_bootstrapPromise) {
|
|
245
|
+
return _bootstrapPromise;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_bootstrapPromise = (async () => {
|
|
249
|
+
try {
|
|
250
|
+
// 3. Check if API already exists on instance
|
|
251
|
+
var checkResp = await client.get('/api/now/table/sys_ws_definition', {
|
|
252
|
+
params: {
|
|
253
|
+
sysparm_query: 'name=' + FLOW_FACTORY_API_NAME,
|
|
254
|
+
sysparm_fields: 'sys_id,namespace.name',
|
|
255
|
+
sysparm_limit: 1
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (checkResp.data.result && checkResp.data.result.length > 0) {
|
|
260
|
+
var existing = checkResp.data.result[0];
|
|
261
|
+
var ns = (existing['namespace.name'] || existing.namespace?.display_value || FLOW_FACTORY_NAMESPACE);
|
|
262
|
+
_flowFactoryCache = { apiSysId: existing.sys_id, namespace: ns, timestamp: Date.now() };
|
|
263
|
+
return { namespace: ns, apiSysId: existing.sys_id };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 4. Deploy the Scripted REST API
|
|
267
|
+
var apiResp = await client.post('/api/now/table/sys_ws_definition', {
|
|
268
|
+
name: FLOW_FACTORY_API_NAME,
|
|
269
|
+
api_id: FLOW_FACTORY_API_ID,
|
|
270
|
+
active: true,
|
|
271
|
+
short_description: 'Bootstrapped by Snow-Flow MCP for reliable Flow Designer creation via GlideRecord',
|
|
272
|
+
is_versioned: false,
|
|
273
|
+
enforce_acl: 'no',
|
|
274
|
+
requires_authentication: true,
|
|
275
|
+
namespace: FLOW_FACTORY_NAMESPACE
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
var apiSysId = apiResp.data.result?.sys_id;
|
|
279
|
+
if (!apiSysId) {
|
|
280
|
+
throw new Error('Failed to create Scripted REST API definition — no sys_id returned');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 5. Deploy the POST /create resource
|
|
284
|
+
try {
|
|
285
|
+
await client.post('/api/now/table/sys_ws_operation', {
|
|
286
|
+
web_service_definition: apiSysId,
|
|
287
|
+
http_method: 'POST',
|
|
288
|
+
name: 'create',
|
|
289
|
+
active: true,
|
|
290
|
+
relative_path: '/create',
|
|
291
|
+
short_description: 'Create a flow or subflow with GlideRecord (triggers all BRs + version record)',
|
|
292
|
+
operation_script: FLOW_FACTORY_SCRIPT,
|
|
293
|
+
requires_authentication: true,
|
|
294
|
+
enforce_acl: 'no'
|
|
295
|
+
});
|
|
296
|
+
} catch (opError: any) {
|
|
297
|
+
// Cleanup the API definition if operation creation fails
|
|
298
|
+
try { await client.delete('/api/now/table/sys_ws_definition/' + apiSysId); } catch (_) {}
|
|
299
|
+
throw new Error('Failed to create Scripted REST operation: ' + (opError.message || opError));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Resolve actual namespace — may differ from requested
|
|
303
|
+
var nsResp = await client.get('/api/now/table/sys_ws_definition/' + apiSysId, {
|
|
304
|
+
params: { sysparm_fields: 'sys_id,namespace' }
|
|
305
|
+
});
|
|
306
|
+
var resolvedNs = FLOW_FACTORY_NAMESPACE;
|
|
307
|
+
if (nsResp.data.result?.namespace) {
|
|
308
|
+
var nsVal = nsResp.data.result.namespace;
|
|
309
|
+
if (typeof nsVal === 'object' && nsVal.display_value) {
|
|
310
|
+
resolvedNs = nsVal.display_value;
|
|
311
|
+
} else if (typeof nsVal === 'string' && nsVal.length > 0) {
|
|
312
|
+
resolvedNs = nsVal;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_flowFactoryCache = { apiSysId: apiSysId, namespace: resolvedNs, timestamp: Date.now() };
|
|
317
|
+
return { namespace: resolvedNs, apiSysId: apiSysId };
|
|
318
|
+
|
|
319
|
+
} finally {
|
|
320
|
+
_bootstrapPromise = null;
|
|
321
|
+
}
|
|
322
|
+
})();
|
|
323
|
+
|
|
324
|
+
return _bootstrapPromise;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Invalidate the Flow Factory cache (e.g. on 404 when API was deleted externally).
|
|
329
|
+
*/
|
|
330
|
+
function invalidateFlowFactoryCache(): void {
|
|
331
|
+
_flowFactoryCache = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
31
334
|
/**
|
|
32
335
|
* Resolve a flow name to its sys_id. If the value is already a 32-char hex
|
|
33
336
|
* string it is returned as-is.
|
|
@@ -203,7 +506,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
203
506
|
switch (action) {
|
|
204
507
|
|
|
205
508
|
// ────────────────────────────────────────────────────────────────
|
|
206
|
-
// CREATE
|
|
509
|
+
// CREATE (Scripted REST API → Table API fallback)
|
|
207
510
|
// ────────────────────────────────────────────────────────────────
|
|
208
511
|
case 'create':
|
|
209
512
|
case 'create_subflow': {
|
|
@@ -223,9 +526,8 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
223
526
|
var inputsArg = args.inputs || [];
|
|
224
527
|
var outputsArg = args.outputs || [];
|
|
225
528
|
var shouldActivate = args.activate !== false;
|
|
226
|
-
var usedBackgroundScript = false;
|
|
227
529
|
|
|
228
|
-
// Build flow_definition JSON
|
|
530
|
+
// Build flow_definition JSON (shared by both methods)
|
|
229
531
|
var flowDefinition: any = {
|
|
230
532
|
name: flowName,
|
|
231
533
|
description: flowDescription,
|
|
@@ -263,222 +565,235 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
263
565
|
version: '1.0'
|
|
264
566
|
};
|
|
265
567
|
|
|
266
|
-
// Remove trigger block for subflows
|
|
267
568
|
if (isSubflow) {
|
|
268
569
|
delete flowDefinition.trigger;
|
|
269
570
|
}
|
|
270
571
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
validated: true,
|
|
280
|
-
type: isSubflow ? 'subflow' : 'flow',
|
|
281
|
-
flow_definition: JSON.stringify(flowDefinition),
|
|
282
|
-
latest_snapshot: JSON.stringify(flowDefinition)
|
|
283
|
-
};
|
|
572
|
+
// ── Try Scripted REST API (Flow Factory) first ──────────────
|
|
573
|
+
var flowSysId: string | null = null;
|
|
574
|
+
var usedMethod = 'table_api';
|
|
575
|
+
var versionCreated = false;
|
|
576
|
+
var factoryWarnings: string[] = [];
|
|
577
|
+
var triggerCreated = false;
|
|
578
|
+
var actionsCreated = 0;
|
|
579
|
+
var varsCreated = 0;
|
|
284
580
|
|
|
285
|
-
// 1. Create flow record via Table API (with 403 fallback)
|
|
286
|
-
var flowResponse: any;
|
|
287
581
|
try {
|
|
288
|
-
|
|
289
|
-
} catch (tableError: any) {
|
|
290
|
-
if (tableError.response?.status === 403) {
|
|
291
|
-
usedBackgroundScript = true;
|
|
292
|
-
var createScript = [
|
|
293
|
-
"var gr = new GlideRecord('sys_hub_flow');",
|
|
294
|
-
"gr.initialize();",
|
|
295
|
-
"gr.name = " + JSON.stringify(flowName) + ";",
|
|
296
|
-
"gr.description = " + JSON.stringify(flowDescription) + ";",
|
|
297
|
-
"gr.active = " + shouldActivate + ";",
|
|
298
|
-
"gr.internal_name = " + JSON.stringify(sanitizeInternalName(flowName)) + ";",
|
|
299
|
-
"gr.category = " + JSON.stringify(flowCategory) + ";",
|
|
300
|
-
"gr.run_as = " + JSON.stringify(flowRunAs) + ";",
|
|
301
|
-
"gr.status = " + JSON.stringify(shouldActivate ? 'published' : 'draft') + ";",
|
|
302
|
-
"gr.validated = true;",
|
|
303
|
-
"gr.type = " + JSON.stringify(isSubflow ? 'subflow' : 'flow') + ";",
|
|
304
|
-
"gr.flow_definition = " + JSON.stringify(JSON.stringify(flowDefinition)) + ";",
|
|
305
|
-
"gr.latest_snapshot = " + JSON.stringify(JSON.stringify(flowDefinition)) + ";",
|
|
306
|
-
"var sysId = gr.insert();",
|
|
307
|
-
"if (sysId) {",
|
|
308
|
-
" gs.print(JSON.stringify({ sys_id: sysId, name: gr.name.toString() }));",
|
|
309
|
-
"} else {",
|
|
310
|
-
" gs.error('Failed to create flow');",
|
|
311
|
-
"}"
|
|
312
|
-
].join('\n');
|
|
313
|
-
|
|
314
|
-
var scriptResp = await client.post('/api/now/table/sys_script_execution', {
|
|
315
|
-
script: createScript,
|
|
316
|
-
description: 'Create Flow: ' + flowName
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
var scriptOutput = scriptResp.data.result?.output || '';
|
|
320
|
-
var scriptMatch = scriptOutput.match(/\{[^}]+\}/);
|
|
321
|
-
if (scriptMatch) {
|
|
322
|
-
flowResponse = { data: { result: JSON.parse(scriptMatch[0]) } };
|
|
323
|
-
} else {
|
|
324
|
-
throw new Error('Failed to create flow via background script. Output: ' + scriptOutput);
|
|
325
|
-
}
|
|
326
|
-
} else {
|
|
327
|
-
throw tableError;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
var createdFlow = flowResponse.data.result;
|
|
332
|
-
var flowSysId = createdFlow.sys_id;
|
|
333
|
-
|
|
334
|
-
// 2. Create trigger instance (non-manual flows only)
|
|
335
|
-
var triggerCreated = false;
|
|
336
|
-
if (!isSubflow && triggerType !== 'manual') {
|
|
337
|
-
try {
|
|
338
|
-
// Lookup trigger action type
|
|
339
|
-
var triggerTypeLookup: Record<string, string> = {
|
|
340
|
-
'record_created': 'sn_fd.trigger.record_created',
|
|
341
|
-
'record_updated': 'sn_fd.trigger.record_updated',
|
|
342
|
-
'scheduled': 'sn_fd.trigger.scheduled'
|
|
343
|
-
};
|
|
344
|
-
var triggerInternalName = triggerTypeLookup[triggerType] || '';
|
|
582
|
+
var factory = await ensureFlowFactoryAPI(client);
|
|
345
583
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
584
|
+
var factoryPayload = {
|
|
585
|
+
name: flowName,
|
|
586
|
+
description: flowDescription,
|
|
587
|
+
type: isSubflow ? 'subflow' : 'flow',
|
|
588
|
+
category: flowCategory,
|
|
589
|
+
run_as: flowRunAs,
|
|
590
|
+
activate: shouldActivate,
|
|
591
|
+
trigger_type: triggerType,
|
|
592
|
+
trigger_table: flowTable,
|
|
593
|
+
trigger_condition: triggerCondition,
|
|
594
|
+
activities: activitiesArg.map(function (act: any, idx: number) {
|
|
595
|
+
return { name: act.name, type: act.type || 'script', inputs: act.inputs || {}, order: (idx + 1) * 100 };
|
|
596
|
+
}),
|
|
597
|
+
inputs: inputsArg,
|
|
598
|
+
outputs: outputsArg,
|
|
599
|
+
flow_definition: flowDefinition
|
|
600
|
+
};
|
|
354
601
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
var triggerData: any = {
|
|
358
|
-
flow: flowSysId,
|
|
359
|
-
action_type: triggerDefId,
|
|
360
|
-
name: triggerType,
|
|
361
|
-
order: 0,
|
|
362
|
-
active: true
|
|
363
|
-
};
|
|
364
|
-
if (flowTable) {
|
|
365
|
-
triggerData.table = flowTable;
|
|
366
|
-
}
|
|
367
|
-
if (triggerCondition) {
|
|
368
|
-
triggerData.condition = triggerCondition;
|
|
369
|
-
}
|
|
602
|
+
var factoryEndpoint = '/api/' + factory.namespace + '/' + FLOW_FACTORY_API_ID + '/create';
|
|
603
|
+
var factoryResp: any;
|
|
370
604
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
605
|
+
try {
|
|
606
|
+
factoryResp = await client.post(factoryEndpoint, factoryPayload);
|
|
607
|
+
} catch (callError: any) {
|
|
608
|
+
// 404 = API was deleted externally → invalidate cache, retry once
|
|
609
|
+
if (callError.response?.status === 404) {
|
|
610
|
+
invalidateFlowFactoryCache();
|
|
611
|
+
var retryFactory = await ensureFlowFactoryAPI(client);
|
|
612
|
+
var retryEndpoint = '/api/' + retryFactory.namespace + '/' + FLOW_FACTORY_API_ID + '/create';
|
|
613
|
+
factoryResp = await client.post(retryEndpoint, factoryPayload);
|
|
614
|
+
} else {
|
|
615
|
+
throw callError;
|
|
374
616
|
}
|
|
375
|
-
} catch (triggerError) {
|
|
376
|
-
// Trigger creation is best-effort
|
|
377
617
|
}
|
|
378
|
-
}
|
|
379
618
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
var
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
sysparm_fields: 'sys_id,name,internal_name',
|
|
393
|
-
sysparm_limit: 1
|
|
619
|
+
var factoryResult = factoryResp.data?.result || factoryResp.data;
|
|
620
|
+
if (factoryResult && factoryResult.success && factoryResult.flow_sys_id) {
|
|
621
|
+
flowSysId = factoryResult.flow_sys_id;
|
|
622
|
+
usedMethod = 'scripted_rest_api';
|
|
623
|
+
versionCreated = !!factoryResult.version_created;
|
|
624
|
+
|
|
625
|
+
// Extract step details
|
|
626
|
+
var steps = factoryResult.steps || {};
|
|
627
|
+
if (steps.trigger) {
|
|
628
|
+
triggerCreated = !!steps.trigger.success;
|
|
629
|
+
if (!steps.trigger.success && steps.trigger.error) {
|
|
630
|
+
factoryWarnings.push('Trigger: ' + steps.trigger.error);
|
|
394
631
|
}
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
var actionDefId = actionDefResp.data.result?.[0]?.sys_id;
|
|
398
|
-
var instanceData: any = {
|
|
399
|
-
flow: flowSysId,
|
|
400
|
-
name: activity.name,
|
|
401
|
-
order: (ai + 1) * 100,
|
|
402
|
-
active: true
|
|
403
|
-
};
|
|
404
|
-
if (actionDefId) {
|
|
405
|
-
instanceData.action_type = actionDefId;
|
|
406
632
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
633
|
+
if (steps.actions) {
|
|
634
|
+
actionsCreated = steps.actions.created || 0;
|
|
635
|
+
}
|
|
636
|
+
if (steps.variables) {
|
|
637
|
+
varsCreated = steps.variables.created || 0;
|
|
638
|
+
}
|
|
639
|
+
if (steps.version && !steps.version.success) {
|
|
640
|
+
factoryWarnings.push('Version record: ' + (steps.version.error || 'creation failed'));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} catch (factoryError: any) {
|
|
644
|
+
// Flow Factory unavailable — fall through to Table API
|
|
645
|
+
var statusCode = factoryError.response?.status;
|
|
646
|
+
if (statusCode !== 403) {
|
|
647
|
+
// Log non-permission errors as warnings (403 = silently skip)
|
|
648
|
+
factoryWarnings.push('Flow Factory unavailable (' + (statusCode || factoryError.message || 'unknown') + '), using Table API fallback');
|
|
412
649
|
}
|
|
413
650
|
}
|
|
414
651
|
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
652
|
+
// ── Table API fallback (existing logic) ─────────────────────
|
|
653
|
+
if (!flowSysId) {
|
|
654
|
+
var flowData: any = {
|
|
655
|
+
name: flowName,
|
|
656
|
+
description: flowDescription,
|
|
657
|
+
active: shouldActivate,
|
|
658
|
+
internal_name: sanitizeInternalName(flowName),
|
|
659
|
+
category: flowCategory,
|
|
660
|
+
run_as: flowRunAs,
|
|
661
|
+
status: shouldActivate ? 'published' : 'draft',
|
|
662
|
+
validated: true,
|
|
663
|
+
type: isSubflow ? 'subflow' : 'flow',
|
|
664
|
+
flow_definition: JSON.stringify(flowDefinition),
|
|
665
|
+
latest_snapshot: JSON.stringify(flowDefinition)
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
var flowResponse = await client.post('/api/now/table/sys_hub_flow', flowData);
|
|
669
|
+
var createdFlow = flowResponse.data.result;
|
|
670
|
+
flowSysId = createdFlow.sys_id;
|
|
671
|
+
|
|
672
|
+
// Create trigger instance (non-manual flows only)
|
|
673
|
+
if (!isSubflow && triggerType !== 'manual') {
|
|
420
674
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
675
|
+
var triggerTypeLookup: Record<string, string> = {
|
|
676
|
+
'record_created': 'sn_fd.trigger.record_created',
|
|
677
|
+
'record_updated': 'sn_fd.trigger.record_updated',
|
|
678
|
+
'scheduled': 'sn_fd.trigger.scheduled'
|
|
679
|
+
};
|
|
680
|
+
var triggerInternalName = triggerTypeLookup[triggerType] || '';
|
|
681
|
+
|
|
682
|
+
if (triggerInternalName) {
|
|
683
|
+
var triggerDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
|
|
684
|
+
params: {
|
|
685
|
+
sysparm_query: 'internal_name=' + triggerInternalName,
|
|
686
|
+
sysparm_fields: 'sys_id',
|
|
687
|
+
sysparm_limit: 1
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
var triggerDefId = triggerDefResp.data.result?.[0]?.sys_id;
|
|
692
|
+
if (triggerDefId) {
|
|
693
|
+
var triggerData: any = {
|
|
694
|
+
flow: flowSysId,
|
|
695
|
+
action_type: triggerDefId,
|
|
696
|
+
name: triggerType,
|
|
697
|
+
order: 0,
|
|
698
|
+
active: true
|
|
699
|
+
};
|
|
700
|
+
if (flowTable) triggerData.table = flowTable;
|
|
701
|
+
if (triggerCondition) triggerData.condition = triggerCondition;
|
|
702
|
+
|
|
703
|
+
await client.post('/api/now/table/sys_hub_trigger_instance', triggerData);
|
|
704
|
+
triggerCreated = true;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} catch (triggerError) {
|
|
432
708
|
// Best-effort
|
|
433
709
|
}
|
|
434
710
|
}
|
|
435
|
-
|
|
436
|
-
|
|
711
|
+
|
|
712
|
+
// Create action instances
|
|
713
|
+
for (var ai = 0; ai < activitiesArg.length; ai++) {
|
|
714
|
+
var activity = activitiesArg[ai];
|
|
437
715
|
try {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
716
|
+
var actionTypeName = activity.type || 'script';
|
|
717
|
+
var actionTypeQuery = 'internal_nameLIKE' + actionTypeName + '^ORnameLIKE' + actionTypeName;
|
|
718
|
+
|
|
719
|
+
var actionDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
|
|
720
|
+
params: {
|
|
721
|
+
sysparm_query: actionTypeQuery,
|
|
722
|
+
sysparm_fields: 'sys_id,name,internal_name',
|
|
723
|
+
sysparm_limit: 1
|
|
724
|
+
}
|
|
444
725
|
});
|
|
445
|
-
|
|
446
|
-
|
|
726
|
+
|
|
727
|
+
var actionDefId = actionDefResp.data.result?.[0]?.sys_id;
|
|
728
|
+
var instanceData: any = {
|
|
729
|
+
flow: flowSysId,
|
|
730
|
+
name: activity.name,
|
|
731
|
+
order: (ai + 1) * 100,
|
|
732
|
+
active: true
|
|
733
|
+
};
|
|
734
|
+
if (actionDefId) instanceData.action_type = actionDefId;
|
|
735
|
+
|
|
736
|
+
await client.post('/api/now/table/sys_hub_action_instance', instanceData);
|
|
737
|
+
actionsCreated++;
|
|
738
|
+
} catch (actError) {
|
|
447
739
|
// Best-effort
|
|
448
740
|
}
|
|
449
741
|
}
|
|
450
|
-
}
|
|
451
742
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
743
|
+
// Create flow variables (subflows)
|
|
744
|
+
if (isSubflow) {
|
|
745
|
+
for (var vi = 0; vi < inputsArg.length; vi++) {
|
|
746
|
+
var inp = inputsArg[vi];
|
|
747
|
+
try {
|
|
748
|
+
await client.post('/api/now/table/sys_hub_flow_variable', {
|
|
749
|
+
flow: flowSysId,
|
|
750
|
+
name: inp.name,
|
|
751
|
+
label: inp.label || inp.name,
|
|
752
|
+
type: inp.type || 'string',
|
|
753
|
+
mandatory: inp.mandatory || false,
|
|
754
|
+
default_value: inp.default_value || '',
|
|
755
|
+
variable_type: 'input'
|
|
756
|
+
});
|
|
757
|
+
varsCreated++;
|
|
758
|
+
} catch (varError) { /* best-effort */ }
|
|
759
|
+
}
|
|
760
|
+
for (var vo = 0; vo < outputsArg.length; vo++) {
|
|
761
|
+
var out = outputsArg[vo];
|
|
762
|
+
try {
|
|
763
|
+
await client.post('/api/now/table/sys_hub_flow_variable', {
|
|
764
|
+
flow: flowSysId,
|
|
765
|
+
name: out.name,
|
|
766
|
+
label: out.label || out.name,
|
|
767
|
+
type: out.type || 'string',
|
|
768
|
+
variable_type: 'output'
|
|
769
|
+
});
|
|
770
|
+
varsCreated++;
|
|
771
|
+
} catch (varError) { /* best-effort */ }
|
|
772
|
+
}
|
|
462
773
|
}
|
|
463
|
-
}
|
|
464
774
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
});
|
|
470
|
-
} catch (snapError) {
|
|
471
|
-
// Snapshot API may not exist in all instances
|
|
775
|
+
// Best-effort snapshot
|
|
776
|
+
try {
|
|
777
|
+
await client.post('/api/sn_flow_designer/flow/snapshot', { flow_id: flowSysId });
|
|
778
|
+
} catch (snapError) { /* may not exist */ }
|
|
472
779
|
}
|
|
473
780
|
|
|
474
|
-
// Build summary
|
|
781
|
+
// ── Build summary ───────────────────────────────────────────
|
|
782
|
+
var methodLabel = usedMethod === 'scripted_rest_api'
|
|
783
|
+
? 'Scripted REST API (GlideRecord)'
|
|
784
|
+
: 'Table API' + (factoryWarnings.length > 0 ? ' (fallback)' : '');
|
|
785
|
+
|
|
475
786
|
var createSummary = summary()
|
|
476
787
|
.success('Created ' + (isSubflow ? 'subflow' : 'flow') + ': ' + flowName)
|
|
477
|
-
.field('sys_id', flowSysId)
|
|
788
|
+
.field('sys_id', flowSysId!)
|
|
478
789
|
.field('Type', isSubflow ? 'Subflow' : 'Flow')
|
|
479
790
|
.field('Category', flowCategory)
|
|
480
791
|
.field('Status', shouldActivate ? 'Published (active)' : 'Draft')
|
|
481
|
-
.field('Method',
|
|
792
|
+
.field('Method', methodLabel);
|
|
793
|
+
|
|
794
|
+
if (versionCreated) {
|
|
795
|
+
createSummary.field('Version', 'v1.0 created');
|
|
796
|
+
}
|
|
482
797
|
|
|
483
798
|
if (!isSubflow && triggerType !== 'manual') {
|
|
484
799
|
createSummary.field('Trigger', triggerType + (triggerCreated ? ' (created)' : ' (best-effort)'));
|
|
@@ -490,10 +805,14 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
490
805
|
if (varsCreated > 0) {
|
|
491
806
|
createSummary.field('Variables', varsCreated + ' created');
|
|
492
807
|
}
|
|
808
|
+
for (var wi = 0; wi < factoryWarnings.length; wi++) {
|
|
809
|
+
createSummary.warning(factoryWarnings[wi]);
|
|
810
|
+
}
|
|
493
811
|
|
|
494
812
|
return createSuccessResult({
|
|
495
813
|
created: true,
|
|
496
|
-
method:
|
|
814
|
+
method: usedMethod,
|
|
815
|
+
version_created: versionCreated,
|
|
497
816
|
flow: {
|
|
498
817
|
sys_id: flowSysId,
|
|
499
818
|
name: flowName,
|
|
@@ -510,7 +829,8 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
510
829
|
} : null,
|
|
511
830
|
activities_created: actionsCreated,
|
|
512
831
|
activities_requested: activitiesArg.length,
|
|
513
|
-
variables_created: varsCreated
|
|
832
|
+
variables_created: varsCreated,
|
|
833
|
+
warnings: factoryWarnings.length > 0 ? factoryWarnings : undefined
|
|
514
834
|
}, {}, createSummary.build());
|
|
515
835
|
}
|
|
516
836
|
|
|
@@ -863,5 +1183,5 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
863
1183
|
}
|
|
864
1184
|
}
|
|
865
1185
|
|
|
866
|
-
export const version = '
|
|
1186
|
+
export const version = '2.0.0';
|
|
867
1187
|
export const author = 'Snow-Flow Team';
|