snow-flow 10.0.1-dev.389 → 10.0.1-dev.391
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,10 +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
|
-
*
|
|
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.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js';
|
|
@@ -27,6 +31,320 @@ function isSysId(value: string): boolean {
|
|
|
27
31
|
return /^[a-f0-9]{32}$/.test(value);
|
|
28
32
|
}
|
|
29
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 (query by api_id, more reliable than name)
|
|
251
|
+
var checkResp = await client.get('/api/now/table/sys_ws_definition', {
|
|
252
|
+
params: {
|
|
253
|
+
sysparm_query: 'api_id=' + FLOW_FACTORY_API_ID,
|
|
254
|
+
sysparm_fields: 'sys_id,api_id,namespace',
|
|
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 = resolveNamespaceFromRecord(existing);
|
|
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 (do NOT set namespace — let ServiceNow assign it)
|
|
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
|
+
});
|
|
276
|
+
|
|
277
|
+
var apiSysId = apiResp.data.result?.sys_id;
|
|
278
|
+
if (!apiSysId) {
|
|
279
|
+
throw new Error('Failed to create Scripted REST API definition — no sys_id returned');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 5. Deploy the POST /create resource
|
|
283
|
+
try {
|
|
284
|
+
await client.post('/api/now/table/sys_ws_operation', {
|
|
285
|
+
web_service_definition: apiSysId,
|
|
286
|
+
http_method: 'POST',
|
|
287
|
+
name: 'create',
|
|
288
|
+
active: true,
|
|
289
|
+
relative_path: '/create',
|
|
290
|
+
short_description: 'Create a flow or subflow with GlideRecord (triggers all BRs + version record)',
|
|
291
|
+
operation_script: FLOW_FACTORY_SCRIPT,
|
|
292
|
+
requires_authentication: true,
|
|
293
|
+
enforce_acl: 'no'
|
|
294
|
+
});
|
|
295
|
+
} catch (opError: any) {
|
|
296
|
+
// Cleanup the API definition if operation creation fails
|
|
297
|
+
try { await client.delete('/api/now/table/sys_ws_definition/' + apiSysId); } catch (_) {}
|
|
298
|
+
throw new Error('Failed to create Scripted REST operation: ' + (opError.message || opError));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 6. Read back the actual namespace ServiceNow assigned
|
|
302
|
+
var nsResp = await client.get('/api/now/table/sys_ws_definition/' + apiSysId, {
|
|
303
|
+
params: { sysparm_fields: 'sys_id,api_id,namespace', sysparm_display_value: 'all' }
|
|
304
|
+
});
|
|
305
|
+
var resolvedNs = resolveNamespaceFromRecord(nsResp.data.result || {});
|
|
306
|
+
|
|
307
|
+
_flowFactoryCache = { apiSysId: apiSysId, namespace: resolvedNs, timestamp: Date.now() };
|
|
308
|
+
return { namespace: resolvedNs, apiSysId: apiSysId };
|
|
309
|
+
|
|
310
|
+
} finally {
|
|
311
|
+
_bootstrapPromise = null;
|
|
312
|
+
}
|
|
313
|
+
})();
|
|
314
|
+
|
|
315
|
+
return _bootstrapPromise;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Extract the URL namespace from a sys_ws_definition record.
|
|
320
|
+
* The namespace field can be a string, a reference object, or empty.
|
|
321
|
+
* For global scope APIs the namespace is typically the instance company prefix or 'now'.
|
|
322
|
+
*/
|
|
323
|
+
function resolveNamespaceFromRecord(record: any): string {
|
|
324
|
+
var ns = record.namespace;
|
|
325
|
+
if (!ns) return FLOW_FACTORY_NAMESPACE;
|
|
326
|
+
|
|
327
|
+
// display_value / value pair (sysparm_display_value=all)
|
|
328
|
+
if (typeof ns === 'object') {
|
|
329
|
+
// display_value is the scope name like "Global" or "x_snflw" — use value for sys_id, display_value for name
|
|
330
|
+
var dv = ns.display_value || '';
|
|
331
|
+
// If display_value looks like a scope namespace (x_something, sn_something, now, global), use it
|
|
332
|
+
if (dv && dv !== 'Global' && dv !== 'global') return dv;
|
|
333
|
+
// For Global scope, fall through to default
|
|
334
|
+
}
|
|
335
|
+
if (typeof ns === 'string' && ns.length > 0 && ns.length < 100) {
|
|
336
|
+
return ns;
|
|
337
|
+
}
|
|
338
|
+
return FLOW_FACTORY_NAMESPACE;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Invalidate the Flow Factory cache (e.g. on 404 when API was deleted externally).
|
|
343
|
+
*/
|
|
344
|
+
function invalidateFlowFactoryCache(): void {
|
|
345
|
+
_flowFactoryCache = null;
|
|
346
|
+
}
|
|
347
|
+
|
|
30
348
|
/**
|
|
31
349
|
* Resolve a flow name to its sys_id. If the value is already a 32-char hex
|
|
32
350
|
* string it is returned as-is.
|
|
@@ -202,7 +520,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
202
520
|
switch (action) {
|
|
203
521
|
|
|
204
522
|
// ────────────────────────────────────────────────────────────────
|
|
205
|
-
// CREATE
|
|
523
|
+
// CREATE (Scripted REST API → Table API fallback)
|
|
206
524
|
// ────────────────────────────────────────────────────────────────
|
|
207
525
|
case 'create':
|
|
208
526
|
case 'create_subflow': {
|
|
@@ -223,7 +541,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
223
541
|
var outputsArg = args.outputs || [];
|
|
224
542
|
var shouldActivate = args.activate !== false;
|
|
225
543
|
|
|
226
|
-
// Build flow_definition JSON
|
|
544
|
+
// Build flow_definition JSON (shared by both methods)
|
|
227
545
|
var flowDefinition: any = {
|
|
228
546
|
name: flowName,
|
|
229
547
|
description: flowDescription,
|
|
@@ -261,166 +579,263 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
261
579
|
version: '1.0'
|
|
262
580
|
};
|
|
263
581
|
|
|
264
|
-
// Remove trigger block for subflows
|
|
265
582
|
if (isSubflow) {
|
|
266
583
|
delete flowDefinition.trigger;
|
|
267
584
|
}
|
|
268
585
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
validated: true,
|
|
278
|
-
type: isSubflow ? 'subflow' : 'flow',
|
|
279
|
-
flow_definition: JSON.stringify(flowDefinition),
|
|
280
|
-
latest_snapshot: JSON.stringify(flowDefinition)
|
|
281
|
-
};
|
|
586
|
+
// ── Try Scripted REST API (Flow Factory) first ──────────────
|
|
587
|
+
var flowSysId: string | null = null;
|
|
588
|
+
var usedMethod = 'table_api';
|
|
589
|
+
var versionCreated = false;
|
|
590
|
+
var factoryWarnings: string[] = [];
|
|
591
|
+
var triggerCreated = false;
|
|
592
|
+
var actionsCreated = 0;
|
|
593
|
+
var varsCreated = 0;
|
|
282
594
|
|
|
283
|
-
|
|
284
|
-
|
|
595
|
+
try {
|
|
596
|
+
var factory = await ensureFlowFactoryAPI(client);
|
|
285
597
|
|
|
286
|
-
|
|
287
|
-
|
|
598
|
+
var factoryPayload = {
|
|
599
|
+
name: flowName,
|
|
600
|
+
description: flowDescription,
|
|
601
|
+
type: isSubflow ? 'subflow' : 'flow',
|
|
602
|
+
category: flowCategory,
|
|
603
|
+
run_as: flowRunAs,
|
|
604
|
+
activate: shouldActivate,
|
|
605
|
+
trigger_type: triggerType,
|
|
606
|
+
trigger_table: flowTable,
|
|
607
|
+
trigger_condition: triggerCondition,
|
|
608
|
+
activities: activitiesArg.map(function (act: any, idx: number) {
|
|
609
|
+
return { name: act.name, type: act.type || 'script', inputs: act.inputs || {}, order: (idx + 1) * 100 };
|
|
610
|
+
}),
|
|
611
|
+
inputs: inputsArg,
|
|
612
|
+
outputs: outputsArg,
|
|
613
|
+
flow_definition: flowDefinition
|
|
614
|
+
};
|
|
288
615
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (!isSubflow && triggerType !== 'manual') {
|
|
292
|
-
try {
|
|
293
|
-
// Lookup trigger action type
|
|
294
|
-
var triggerTypeLookup: Record<string, string> = {
|
|
295
|
-
'record_created': 'sn_fd.trigger.record_created',
|
|
296
|
-
'record_updated': 'sn_fd.trigger.record_updated',
|
|
297
|
-
'scheduled': 'sn_fd.trigger.scheduled'
|
|
298
|
-
};
|
|
299
|
-
var triggerInternalName = triggerTypeLookup[triggerType] || '';
|
|
616
|
+
var factoryEndpoint = '/api/' + factory.namespace + '/' + FLOW_FACTORY_API_ID + '/create';
|
|
617
|
+
var factoryResp: any;
|
|
300
618
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
action_type: triggerDefId,
|
|
315
|
-
name: triggerType,
|
|
316
|
-
order: 0,
|
|
317
|
-
active: true
|
|
318
|
-
};
|
|
319
|
-
if (flowTable) {
|
|
320
|
-
triggerData.table = flowTable;
|
|
321
|
-
}
|
|
322
|
-
if (triggerCondition) {
|
|
323
|
-
triggerData.condition = triggerCondition;
|
|
324
|
-
}
|
|
619
|
+
try {
|
|
620
|
+
factoryResp = await client.post(factoryEndpoint, factoryPayload);
|
|
621
|
+
} catch (callError: any) {
|
|
622
|
+
// 404 = API was deleted externally → invalidate cache, retry once
|
|
623
|
+
if (callError.response?.status === 404) {
|
|
624
|
+
invalidateFlowFactoryCache();
|
|
625
|
+
var retryFactory = await ensureFlowFactoryAPI(client);
|
|
626
|
+
var retryEndpoint = '/api/' + retryFactory.namespace + '/' + FLOW_FACTORY_API_ID + '/create';
|
|
627
|
+
factoryResp = await client.post(retryEndpoint, factoryPayload);
|
|
628
|
+
} else {
|
|
629
|
+
throw callError;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
325
632
|
|
|
326
|
-
|
|
327
|
-
|
|
633
|
+
var factoryResult = factoryResp.data?.result || factoryResp.data;
|
|
634
|
+
if (factoryResult && factoryResult.success && factoryResult.flow_sys_id) {
|
|
635
|
+
flowSysId = factoryResult.flow_sys_id;
|
|
636
|
+
usedMethod = 'scripted_rest_api';
|
|
637
|
+
versionCreated = !!factoryResult.version_created;
|
|
638
|
+
|
|
639
|
+
// Extract step details
|
|
640
|
+
var steps = factoryResult.steps || {};
|
|
641
|
+
if (steps.trigger) {
|
|
642
|
+
triggerCreated = !!steps.trigger.success;
|
|
643
|
+
if (!steps.trigger.success && steps.trigger.error) {
|
|
644
|
+
factoryWarnings.push('Trigger: ' + steps.trigger.error);
|
|
328
645
|
}
|
|
329
646
|
}
|
|
330
|
-
|
|
331
|
-
|
|
647
|
+
if (steps.actions) {
|
|
648
|
+
actionsCreated = steps.actions.created || 0;
|
|
649
|
+
}
|
|
650
|
+
if (steps.variables) {
|
|
651
|
+
varsCreated = steps.variables.created || 0;
|
|
652
|
+
}
|
|
653
|
+
if (steps.version && !steps.version.success) {
|
|
654
|
+
factoryWarnings.push('Version record: ' + (steps.version.error || 'creation failed'));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} catch (factoryError: any) {
|
|
658
|
+
// Flow Factory unavailable — fall through to Table API
|
|
659
|
+
var statusCode = factoryError.response?.status;
|
|
660
|
+
if (statusCode !== 403) {
|
|
661
|
+
// Log non-permission errors as warnings (403 = silently skip)
|
|
662
|
+
factoryWarnings.push('Flow Factory unavailable (' + (statusCode || factoryError.message || 'unknown') + '), using Table API fallback');
|
|
332
663
|
}
|
|
333
664
|
}
|
|
334
665
|
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
});
|
|
666
|
+
// ── Table API fallback (existing logic) ─────────────────────
|
|
667
|
+
if (!flowSysId) {
|
|
668
|
+
var flowData: any = {
|
|
669
|
+
name: flowName,
|
|
670
|
+
description: flowDescription,
|
|
671
|
+
active: shouldActivate,
|
|
672
|
+
internal_name: sanitizeInternalName(flowName),
|
|
673
|
+
category: flowCategory,
|
|
674
|
+
run_as: flowRunAs,
|
|
675
|
+
status: shouldActivate ? 'published' : 'draft',
|
|
676
|
+
validated: true,
|
|
677
|
+
type: isSubflow ? 'subflow' : 'flow',
|
|
678
|
+
flow_definition: JSON.stringify(flowDefinition),
|
|
679
|
+
latest_snapshot: JSON.stringify(flowDefinition)
|
|
680
|
+
};
|
|
351
681
|
|
|
352
|
-
|
|
353
|
-
|
|
682
|
+
var flowResponse = await client.post('/api/now/table/sys_hub_flow', flowData);
|
|
683
|
+
var createdFlow = flowResponse.data.result;
|
|
684
|
+
flowSysId = createdFlow.sys_id;
|
|
685
|
+
|
|
686
|
+
// Create sys_hub_flow_version via Table API (critical for Flow Designer UI)
|
|
687
|
+
try {
|
|
688
|
+
var versionData: any = {
|
|
354
689
|
flow: flowSysId,
|
|
355
|
-
name:
|
|
356
|
-
|
|
357
|
-
|
|
690
|
+
name: '1.0',
|
|
691
|
+
version: '1.0',
|
|
692
|
+
state: shouldActivate ? 'published' : 'draft',
|
|
693
|
+
active: true,
|
|
694
|
+
flow_definition: JSON.stringify(flowDefinition)
|
|
358
695
|
};
|
|
359
|
-
|
|
360
|
-
|
|
696
|
+
var versionResp = await client.post('/api/now/table/sys_hub_flow_version', versionData);
|
|
697
|
+
var versionSysId = versionResp.data.result?.sys_id;
|
|
698
|
+
if (versionSysId) {
|
|
699
|
+
versionCreated = true;
|
|
700
|
+
// Link flow to its version
|
|
701
|
+
try {
|
|
702
|
+
await client.patch('/api/now/table/sys_hub_flow/' + flowSysId, {
|
|
703
|
+
latest_version: versionSysId
|
|
704
|
+
});
|
|
705
|
+
} catch (linkError) {
|
|
706
|
+
// Version exists but link failed — still better than no version
|
|
707
|
+
factoryWarnings.push('Version created but latest_version link failed');
|
|
708
|
+
}
|
|
361
709
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
actionsCreated++;
|
|
365
|
-
} catch (actError) {
|
|
366
|
-
// Action creation is best-effort
|
|
710
|
+
} catch (verError: any) {
|
|
711
|
+
factoryWarnings.push('sys_hub_flow_version creation failed: ' + (verError.message || verError));
|
|
367
712
|
}
|
|
368
|
-
}
|
|
369
713
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (isSubflow) {
|
|
373
|
-
for (var vi = 0; vi < inputsArg.length; vi++) {
|
|
374
|
-
var inp = inputsArg[vi];
|
|
714
|
+
// Create trigger instance (non-manual flows only)
|
|
715
|
+
if (!isSubflow && triggerType !== 'manual') {
|
|
375
716
|
try {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
717
|
+
var triggerTypeLookup: Record<string, string> = {
|
|
718
|
+
'record_created': 'sn_fd.trigger.record_created',
|
|
719
|
+
'record_updated': 'sn_fd.trigger.record_updated',
|
|
720
|
+
'scheduled': 'sn_fd.trigger.scheduled'
|
|
721
|
+
};
|
|
722
|
+
var triggerInternalName = triggerTypeLookup[triggerType] || '';
|
|
723
|
+
|
|
724
|
+
if (triggerInternalName) {
|
|
725
|
+
var triggerDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
|
|
726
|
+
params: {
|
|
727
|
+
sysparm_query: 'internal_name=' + triggerInternalName,
|
|
728
|
+
sysparm_fields: 'sys_id',
|
|
729
|
+
sysparm_limit: 1
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
var triggerDefId = triggerDefResp.data.result?.[0]?.sys_id;
|
|
734
|
+
if (triggerDefId) {
|
|
735
|
+
var triggerData: any = {
|
|
736
|
+
flow: flowSysId,
|
|
737
|
+
action_type: triggerDefId,
|
|
738
|
+
name: triggerType,
|
|
739
|
+
order: 0,
|
|
740
|
+
active: true
|
|
741
|
+
};
|
|
742
|
+
if (flowTable) triggerData.table = flowTable;
|
|
743
|
+
if (triggerCondition) triggerData.condition = triggerCondition;
|
|
744
|
+
|
|
745
|
+
await client.post('/api/now/table/sys_hub_trigger_instance', triggerData);
|
|
746
|
+
triggerCreated = true;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
} catch (triggerError) {
|
|
387
750
|
// Best-effort
|
|
388
751
|
}
|
|
389
752
|
}
|
|
390
|
-
|
|
391
|
-
|
|
753
|
+
|
|
754
|
+
// Create action instances
|
|
755
|
+
for (var ai = 0; ai < activitiesArg.length; ai++) {
|
|
756
|
+
var activity = activitiesArg[ai];
|
|
392
757
|
try {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
758
|
+
var actionTypeName = activity.type || 'script';
|
|
759
|
+
var actionTypeQuery = 'internal_nameLIKE' + actionTypeName + '^ORnameLIKE' + actionTypeName;
|
|
760
|
+
|
|
761
|
+
var actionDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
|
|
762
|
+
params: {
|
|
763
|
+
sysparm_query: actionTypeQuery,
|
|
764
|
+
sysparm_fields: 'sys_id,name,internal_name',
|
|
765
|
+
sysparm_limit: 1
|
|
766
|
+
}
|
|
399
767
|
});
|
|
400
|
-
|
|
401
|
-
|
|
768
|
+
|
|
769
|
+
var actionDefId = actionDefResp.data.result?.[0]?.sys_id;
|
|
770
|
+
var instanceData: any = {
|
|
771
|
+
flow: flowSysId,
|
|
772
|
+
name: activity.name,
|
|
773
|
+
order: (ai + 1) * 100,
|
|
774
|
+
active: true
|
|
775
|
+
};
|
|
776
|
+
if (actionDefId) instanceData.action_type = actionDefId;
|
|
777
|
+
|
|
778
|
+
await client.post('/api/now/table/sys_hub_action_instance', instanceData);
|
|
779
|
+
actionsCreated++;
|
|
780
|
+
} catch (actError) {
|
|
402
781
|
// Best-effort
|
|
403
782
|
}
|
|
404
783
|
}
|
|
405
|
-
}
|
|
406
784
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
785
|
+
// Create flow variables (subflows)
|
|
786
|
+
if (isSubflow) {
|
|
787
|
+
for (var vi = 0; vi < inputsArg.length; vi++) {
|
|
788
|
+
var inp = inputsArg[vi];
|
|
789
|
+
try {
|
|
790
|
+
await client.post('/api/now/table/sys_hub_flow_variable', {
|
|
791
|
+
flow: flowSysId,
|
|
792
|
+
name: inp.name,
|
|
793
|
+
label: inp.label || inp.name,
|
|
794
|
+
type: inp.type || 'string',
|
|
795
|
+
mandatory: inp.mandatory || false,
|
|
796
|
+
default_value: inp.default_value || '',
|
|
797
|
+
variable_type: 'input'
|
|
798
|
+
});
|
|
799
|
+
varsCreated++;
|
|
800
|
+
} catch (varError) { /* best-effort */ }
|
|
801
|
+
}
|
|
802
|
+
for (var vo = 0; vo < outputsArg.length; vo++) {
|
|
803
|
+
var out = outputsArg[vo];
|
|
804
|
+
try {
|
|
805
|
+
await client.post('/api/now/table/sys_hub_flow_variable', {
|
|
806
|
+
flow: flowSysId,
|
|
807
|
+
name: out.name,
|
|
808
|
+
label: out.label || out.name,
|
|
809
|
+
type: out.type || 'string',
|
|
810
|
+
variable_type: 'output'
|
|
811
|
+
});
|
|
812
|
+
varsCreated++;
|
|
813
|
+
} catch (varError) { /* best-effort */ }
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Best-effort snapshot
|
|
818
|
+
try {
|
|
819
|
+
await client.post('/api/sn_flow_designer/flow/snapshot', { flow_id: flowSysId });
|
|
820
|
+
} catch (snapError) { /* may not exist */ }
|
|
414
821
|
}
|
|
415
822
|
|
|
416
|
-
// Build summary
|
|
823
|
+
// ── Build summary ───────────────────────────────────────────
|
|
824
|
+
var methodLabel = usedMethod === 'scripted_rest_api'
|
|
825
|
+
? 'Scripted REST API (GlideRecord)'
|
|
826
|
+
: 'Table API' + (factoryWarnings.length > 0 ? ' (fallback)' : '');
|
|
827
|
+
|
|
417
828
|
var createSummary = summary()
|
|
418
829
|
.success('Created ' + (isSubflow ? 'subflow' : 'flow') + ': ' + flowName)
|
|
419
|
-
.field('sys_id', flowSysId)
|
|
830
|
+
.field('sys_id', flowSysId!)
|
|
420
831
|
.field('Type', isSubflow ? 'Subflow' : 'Flow')
|
|
421
832
|
.field('Category', flowCategory)
|
|
422
833
|
.field('Status', shouldActivate ? 'Published (active)' : 'Draft')
|
|
423
|
-
.field('Method',
|
|
834
|
+
.field('Method', methodLabel);
|
|
835
|
+
|
|
836
|
+
if (versionCreated) {
|
|
837
|
+
createSummary.field('Version', 'v1.0 created');
|
|
838
|
+
}
|
|
424
839
|
|
|
425
840
|
if (!isSubflow && triggerType !== 'manual') {
|
|
426
841
|
createSummary.field('Trigger', triggerType + (triggerCreated ? ' (created)' : ' (best-effort)'));
|
|
@@ -432,10 +847,14 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
432
847
|
if (varsCreated > 0) {
|
|
433
848
|
createSummary.field('Variables', varsCreated + ' created');
|
|
434
849
|
}
|
|
850
|
+
for (var wi = 0; wi < factoryWarnings.length; wi++) {
|
|
851
|
+
createSummary.warning(factoryWarnings[wi]);
|
|
852
|
+
}
|
|
435
853
|
|
|
436
854
|
return createSuccessResult({
|
|
437
855
|
created: true,
|
|
438
|
-
method:
|
|
856
|
+
method: usedMethod,
|
|
857
|
+
version_created: versionCreated,
|
|
439
858
|
flow: {
|
|
440
859
|
sys_id: flowSysId,
|
|
441
860
|
name: flowName,
|
|
@@ -452,7 +871,8 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
452
871
|
} : null,
|
|
453
872
|
activities_created: actionsCreated,
|
|
454
873
|
activities_requested: activitiesArg.length,
|
|
455
|
-
variables_created: varsCreated
|
|
874
|
+
variables_created: varsCreated,
|
|
875
|
+
warnings: factoryWarnings.length > 0 ? factoryWarnings : undefined
|
|
456
876
|
}, {}, createSummary.build());
|
|
457
877
|
}
|
|
458
878
|
|
|
@@ -805,5 +1225,5 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
805
1225
|
}
|
|
806
1226
|
}
|
|
807
1227
|
|
|
808
|
-
export const version = '
|
|
1228
|
+
export const version = '2.0.0';
|
|
809
1229
|
export const author = 'Snow-Flow Team';
|