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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.1-dev.388",
3
+ "version": "10.0.1-dev.390",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -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 via Table API with background script fallback.
5
+ * Flow Designer flows and subflows.
6
6
  *
7
- * ServiceNow does NOT expose a public Flow Designer creation API.
8
- * This tool uses the Table API (sys_hub_flow) with a GlideRecord fallback
9
- * when the caller lacks direct table write permissions (403).
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
- var flowData: any = {
272
- name: flowName,
273
- description: flowDescription,
274
- active: shouldActivate,
275
- internal_name: sanitizeInternalName(flowName),
276
- category: flowCategory,
277
- run_as: flowRunAs,
278
- status: shouldActivate ? 'published' : 'draft',
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
- flowResponse = await client.post('/api/now/table/sys_hub_flow', flowData);
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
- if (triggerInternalName) {
347
- var triggerDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
348
- params: {
349
- sysparm_query: 'internal_name=' + triggerInternalName,
350
- sysparm_fields: 'sys_id',
351
- sysparm_limit: 1
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
- var triggerDefId = triggerDefResp.data.result?.[0]?.sys_id;
356
- if (triggerDefId) {
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
- await client.post('/api/now/table/sys_hub_trigger_instance', triggerData);
372
- triggerCreated = true;
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
- // 3. Create action instances
381
- var actionsCreated = 0;
382
- for (var ai = 0; ai < activitiesArg.length; ai++) {
383
- var activity = activitiesArg[ai];
384
- try {
385
- // Lookup action type definition
386
- var actionTypeName = activity.type || 'script';
387
- var actionTypeQuery = 'internal_nameLIKE' + actionTypeName + '^ORnameLIKE' + actionTypeName;
388
-
389
- var actionDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
390
- params: {
391
- sysparm_query: actionTypeQuery,
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
- await client.post('/api/now/table/sys_hub_action_instance', instanceData);
409
- actionsCreated++;
410
- } catch (actError) {
411
- // Action creation is best-effort
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
- // 4. Create flow variables (for subflows with inputs/outputs)
416
- var varsCreated = 0;
417
- if (isSubflow) {
418
- for (var vi = 0; vi < inputsArg.length; vi++) {
419
- var inp = inputsArg[vi];
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
- await client.post('/api/now/table/sys_hub_flow_variable', {
422
- flow: flowSysId,
423
- name: inp.name,
424
- label: inp.label || inp.name,
425
- type: inp.type || 'string',
426
- mandatory: inp.mandatory || false,
427
- default_value: inp.default_value || '',
428
- variable_type: 'input'
429
- });
430
- varsCreated++;
431
- } catch (varError) {
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
- for (var vo = 0; vo < outputsArg.length; vo++) {
436
- var out = outputsArg[vo];
711
+
712
+ // Create action instances
713
+ for (var ai = 0; ai < activitiesArg.length; ai++) {
714
+ var activity = activitiesArg[ai];
437
715
  try {
438
- await client.post('/api/now/table/sys_hub_flow_variable', {
439
- flow: flowSysId,
440
- name: out.name,
441
- label: out.label || out.name,
442
- type: out.type || 'string',
443
- variable_type: 'output'
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
- varsCreated++;
446
- } catch (varError) {
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
- // 5. Activate if needed (and not already active from creation)
453
- if (shouldActivate && usedBackgroundScript) {
454
- try {
455
- await client.patch('/api/now/table/sys_hub_flow/' + flowSysId, {
456
- active: true,
457
- status: 'published',
458
- validated: true
459
- });
460
- } catch (activateError) {
461
- // Best-effort
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
- // 6. Best-effort snapshot
466
- try {
467
- await client.post('/api/sn_flow_designer/flow/snapshot', {
468
- flow_id: flowSysId
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', usedBackgroundScript ? 'Background script' : 'Table API');
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: usedBackgroundScript ? 'background_script' : 'table_api',
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 = '1.0.0';
1186
+ export const version = '2.0.0';
867
1187
  export const author = 'Snow-Flow Team';