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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.1-dev.389",
3
+ "version": "10.0.1-dev.391",
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,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 via Table API.
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) to manage flows directly.
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
- var flowData: any = {
270
- name: flowName,
271
- description: flowDescription,
272
- active: shouldActivate,
273
- internal_name: sanitizeInternalName(flowName),
274
- category: flowCategory,
275
- run_as: flowRunAs,
276
- status: shouldActivate ? 'published' : 'draft',
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
- // 1. Create flow record via Table API
284
- var flowResponse = await client.post('/api/now/table/sys_hub_flow', flowData);
595
+ try {
596
+ var factory = await ensureFlowFactoryAPI(client);
285
597
 
286
- var createdFlow = flowResponse.data.result;
287
- var flowSysId = createdFlow.sys_id;
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
- // 2. Create trigger instance (non-manual flows only)
290
- var triggerCreated = false;
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
- if (triggerInternalName) {
302
- var triggerDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
303
- params: {
304
- sysparm_query: 'internal_name=' + triggerInternalName,
305
- sysparm_fields: 'sys_id',
306
- sysparm_limit: 1
307
- }
308
- });
309
-
310
- var triggerDefId = triggerDefResp.data.result?.[0]?.sys_id;
311
- if (triggerDefId) {
312
- var triggerData: any = {
313
- flow: flowSysId,
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
- await client.post('/api/now/table/sys_hub_trigger_instance', triggerData);
327
- triggerCreated = true;
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
- } catch (triggerError) {
331
- // Trigger creation is best-effort
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
- // 3. Create action instances
336
- var actionsCreated = 0;
337
- for (var ai = 0; ai < activitiesArg.length; ai++) {
338
- var activity = activitiesArg[ai];
339
- try {
340
- // Lookup action type definition
341
- var actionTypeName = activity.type || 'script';
342
- var actionTypeQuery = 'internal_nameLIKE' + actionTypeName + '^ORnameLIKE' + actionTypeName;
343
-
344
- var actionDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
345
- params: {
346
- sysparm_query: actionTypeQuery,
347
- sysparm_fields: 'sys_id,name,internal_name',
348
- sysparm_limit: 1
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
- var actionDefId = actionDefResp.data.result?.[0]?.sys_id;
353
- var instanceData: any = {
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: activity.name,
356
- order: (ai + 1) * 100,
357
- active: true
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
- if (actionDefId) {
360
- instanceData.action_type = actionDefId;
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
- await client.post('/api/now/table/sys_hub_action_instance', instanceData);
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
- // 4. Create flow variables (for subflows with inputs/outputs)
371
- var varsCreated = 0;
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
- await client.post('/api/now/table/sys_hub_flow_variable', {
377
- flow: flowSysId,
378
- name: inp.name,
379
- label: inp.label || inp.name,
380
- type: inp.type || 'string',
381
- mandatory: inp.mandatory || false,
382
- default_value: inp.default_value || '',
383
- variable_type: 'input'
384
- });
385
- varsCreated++;
386
- } catch (varError) {
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
- for (var vo = 0; vo < outputsArg.length; vo++) {
391
- var out = outputsArg[vo];
753
+
754
+ // Create action instances
755
+ for (var ai = 0; ai < activitiesArg.length; ai++) {
756
+ var activity = activitiesArg[ai];
392
757
  try {
393
- await client.post('/api/now/table/sys_hub_flow_variable', {
394
- flow: flowSysId,
395
- name: out.name,
396
- label: out.label || out.name,
397
- type: out.type || 'string',
398
- variable_type: 'output'
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
- varsCreated++;
401
- } catch (varError) {
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
- // 5. Best-effort snapshot
408
- try {
409
- await client.post('/api/sn_flow_designer/flow/snapshot', {
410
- flow_id: flowSysId
411
- });
412
- } catch (snapError) {
413
- // Snapshot API may not exist in all instances
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', 'Table API');
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: 'table_api',
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 = '1.0.0';
1228
+ export const version = '2.0.0';
809
1229
  export const author = 'Snow-Flow Team';